PreferencesFX自定义Setting的实现案例

2021-01-17
PreferencesFX其实对常见类型的Setting有默认支持,比如字符串,数字,选择列表等, 甚至于也支持File/Directory类型的Setting, 允许我们使用FileChooser来选择和设定对应的Setting状态与展示, 但是,这几天想添加一个字体的Setting配置项, 发现默认的搞不定,但还想继续沿用PreferencesFX的基础设施,所以研究了下如何自定义PreferencesFX的Setting。
PreferencesFX其实提供了两种自定义Setting的扩展机制:

Setting.of(Node)
Setting.of(description, Field, Property)

首先,我们的Font类型的自定义Setting在展示的时候, 预期的展示是这样的: 一个TextField作为字体类型, 大小和风格的选择结果展示, 一个Button, 当点击的时候,则打开一个字体选择对话框(我们使用ControlsFX的DialogSeletorDialog), 用户选择了相应字体之后, 则将所选择的字体信息格式化之后设置给TextField并更新对应的Property, 至于Setting的显示名称,则直接使用传入的description即可。
在这个前提下,我们首先得先定义一个SimpleControl, 下面是我们的实现:

import com.dlsc.formsfx.model.structure.StringField
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl
import com.keevol.keenotes.desk.utils.FontStringConverter
import javafx.geometry.Insets
import javafx.scene.control.{Button, TextField}
import javafx.scene.layout.{HBox, Priority, StackPane}
import javafx.scene.text.Font
import org.controlsfx.dialog.FontSelectorDialog

/**
 * a custom simple control for font with foot chooser
 *
 * @author fq@keevol.com
 */
class SimpleFontControl extends SimpleControl[StringField, StackPane] {

  var textField: TextField = _
  var fontChooseButton: Button = _

  val fontStringConverter = new FontStringConverter()

  override def initializeParts(): Unit = {
    super.initializeParts()

    node = new StackPane()

    textField = new TextField()
    textField.setEditable(false)
    fontChooseButton = new Button("Choose Font")
    fontChooseButton.setOnAction(e => {
      val dialog = new FontSelectorDialog(Font.getDefault)
      val p = dialog.showAndWait()
      if (p.isPresent) {
        val font = p.get()
        println("font.toString: " + font.toString)
        textField.setText(fontStringConverter.toString(font))
      }
    })

    val hbox = new HBox(10)
    hbox.setPadding(new Insets(3))
    hbox.getChildren.addAll(textField, fontChooseButton)
    HBox.setHgrow(textField, Priority.ALWAYS)

    node.getChildren.add(hbox)

  }

  override def layoutParts(): Unit = {

  }

  override def setupBindings(): Unit = {
    super.setupBindings()
    // without this, PreferencesFX will throw exception.
    if (field.valueProperty.get == "null" || field.valueProperty.get == null) field.valueProperty.set("")

    field.valueProperty().bindBidirectional(textField.textProperty())
  }
}

这几个override的方法原则上 layoutParts()
是必需的, 但我们把这个方法的一些逻辑直接合并到了initializeParts()方法中(即组件的初始化和layout放一起了)。
因为Font和String类型差异,我们将Font到String的格式化逻辑以及从String创建Font的逻辑抽象封装到了FontStringConverter(一个StringConverter实现):

package com.keevol.keenotes.desk.utils

import javafx.scene.text.{Font, FontWeight}
import javafx.util.StringConverter
import org.apache.commons.lang3.StringUtils

class FontStringConverter extends StringConverter[Font] {
  override def toString(font: Font): String = s"${font.getFamily}, ${font.getSize}, ${font.getStyle}"

  override def fromString(fontString: String): Font = {
    val fontFamily = StringUtils.substringBefore(fontString, ",")
    val fontSize = StringUtils.substringBetween(fontString, ", ", ", ")
    val fontStyle = StringUtils.substringAfterLast(fontString, ", ")

    val f = if (StringUtils.contains(fontStyle.toLowerCase, "bold")) {
      Font.font(fontFamily, FontWeight.BOLD, fontSize.toDouble)
    } else {
      Font.font(fontFamily, fontSize.toDouble)
    }
    f
  }
}

有了这些之后, 我们就可以添加Font类型的自定义Setting到PreferencesFX了:

val fontProperty = new SimpleStringProperty("Serif")
...
Setting.of("Font", Field.ofStringType(fontProperty).render(new SimpleFontControl()), fontProperty)

现在, 我们的主程序就可以基于这个Setting做初始化了:

  def tile(channel: String, content: String, dt: Date = new Date()) = {
    val card = new KeeNoteCard
    card.title.setText(channel + s"@${DateFormatUtils.format(dt, "yyyy-MM-dd HH:mm:ss")}")
    card.content.setText(content)
    Bindings.bindBidirectional(settings.fontProperty, card.content.fontProperty(), new FontStringConverter())
    card
  }

card.content是一个Label,所以它的fontProperty()就是Font类型的ObjectProperty, 因为与Settings中的fontProperty(SimpleStringProperty类型)类型不同,所以我们使用了Bindings.bindBidirectional配合FontStringConverter完成了设定与应用组件之间的状态绑定。
当然,这种方法不一定是最好或者最符合PreferencesFX设计哲学的方式, 但却是最符合我编码胃口的方式,起码SimpleFontControl和FontStringConverter把通用逻辑都封装的差不多了。