Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working draft of a filter dialog #2549

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import biz.ganttproject.app.RootLocalizer
import biz.ganttproject.core.option.ValidationException
import biz.ganttproject.core.option.Completion
import biz.ganttproject.storage.db.Tables
import net.sourceforge.ganttproject.GPLogger
import net.sourceforge.ganttproject.storage.ColumnConsumer
import net.sourceforge.ganttproject.storage.ProjectDatabase
import net.sourceforge.ganttproject.storage.ProjectDatabaseException
Expand All @@ -38,6 +39,7 @@ sealed class CalculationMethodImpl(override val propertyId: String, override val
*/
class SimpleSelect(propertyId: String,
val selectExpression: String = "id",
val whereExpression: String? = null,
resultClass: Class<*>) : CalculationMethodImpl(propertyId, resultClass)

class CalculationMethodValidator(private val projectDatabase: ProjectDatabase) {
Expand All @@ -47,6 +49,7 @@ class CalculationMethodValidator(private val projectDatabase: ProjectDatabase) {
try {
projectDatabase.validateColumnConsumer(ColumnConsumer(calculationMethod) {_,_->})
} catch (ex: ProjectDatabaseException) {
//GPLogger.create("ProjectDatabase").error("calculation method validation failed: ${ex.message}", ex)
throw ValidationException(RootLocalizer.formatText("option.customPropertyDialog.expression.validation.syntax"))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import biz.ganttproject.customproperty.*
import javafx.beans.property.BooleanProperty
import javafx.beans.property.SimpleBooleanProperty
import javafx.collections.FXCollections
import javafx.collections.MapChangeListener
import javafx.collections.ObservableList
import net.sourceforge.ganttproject.language.GanttLanguage
import net.sourceforge.ganttproject.storage.ProjectDatabase
import net.sourceforge.ganttproject.undo.GPUndoManager
Expand Down Expand Up @@ -55,16 +53,13 @@ class ColumnManager(
newItemFactory = {
ColumnAsListItem(null, isVisible = true, isCustom = true, customColumnsManager, {customPropertyEditor.updateVisibility(it)})
},
selection = {
dialogPane.listView.selectionModel.selectedItems
}
ourLocalizer
)

private val customPropertyEditor: CustomPropertyEditor = CustomPropertyEditor(
selectedItemProperty = selectedItem,
btnDeleteController = dialogModel.btnDeleteController,
escCloseEnabled = escCloseEnabled,
listItems = listItems,
model = EditorModel(
calculationMethodValidator = calculationMethodValidator,
expressionAutoCompletion = expressionAutoCompletion,
Expand All @@ -73,7 +68,8 @@ class ColumnManager(
},
localizer = ourEditorLocalizer
),
errorUi = { dialogPane.onError(it) })
dialogModel = dialogModel
)

private val mergedColumns: MutableList<ColumnList.Column> = mutableListOf()
internal val dialogPane = ItemListDialogPane<ColumnAsListItem>(
Expand All @@ -99,7 +95,6 @@ class ColumnManager(
listItems.add(ColumnAsListItem(columnStub, columnStub.isVisible, true, customColumnsManager, customPropertyEditor::updateVisibility))
}
}

}

internal fun onApply() {
Expand Down Expand Up @@ -131,7 +126,7 @@ class ColumnManager(
mergedColumns.add(ColumnList.ColumnStub(def.id, def.name, true, mergedColumns.size, 50))
}
if (columnItem.isCalculated) {
def.calculationMethod = SimpleSelect(def.id, columnItem.expression, def.propertyClass.javaClass)
def.calculationMethod = SimpleSelect(propertyId = def.id, selectExpression = columnItem.expression, resultClass = def.propertyClass.javaClass)
}
}
}
Expand Down Expand Up @@ -184,7 +179,7 @@ internal fun CustomPropertyDefinition.importColumnItem(item: ColumnAsListItem) {
}
this.propertyClass = item.type.getCustomPropertyClass()
if (item.isCalculated) {
this.calculationMethod = SimpleSelect(this.id, item.expression, this.propertyClass.javaClass)
this.calculationMethod = SimpleSelect(propertyId = this.id, selectExpression = item.expression, resultClass = this.propertyClass.javaClass)
} else {
this.calculationMethod = null
}
Expand Down Expand Up @@ -215,7 +210,7 @@ internal class EditorModel(
}
value
})
val typeOption = ObservableEnum(id ="type", initValue = PropertyType.STRING, allValues = PropertyType.values())
val typeOption = ObservableEnum(id = "type", initValue = PropertyType.STRING, allValues = PropertyType.values())
val defaultValueOption = ObservableString(
id = "defaultValue",
initValue = "",
Expand All @@ -237,7 +232,7 @@ internal class EditorModel(
if (it.isNotBlank()) {
calculationMethodValidator.validate(
// Incomplete instance just for validation purposes
SimpleSelect("", it, typeOption.value.getCustomPropertyClass().javaClass)
SimpleSelect(propertyId = "", selectExpression = it, resultClass = typeOption.value.getCustomPropertyClass().javaClass)
)
it
} else {
Expand All @@ -256,39 +251,29 @@ internal class EditorModel(
*/
internal class CustomPropertyEditor(
private val model: EditorModel,
private val selectedItemProperty: ObservableProperty<ColumnAsListItem?>,
dialogModel: ItemListDialogModel<ColumnAsListItem>,
selectedItemProperty: ObservableProperty<ColumnAsListItem?>,
private val btnDeleteController: BtnController<Unit>,
escCloseEnabled: BooleanProperty,
private val listItems: ObservableList<ColumnAsListItem>,
private val errorUi: (String?) -> Unit
) : ItemEditorPane<ColumnAsListItem?>(selectedItemProperty, model.allOptions, ourEditorLocalizer) {
) : ItemEditorPane<ColumnAsListItem>(model.allOptions, selectedItemProperty, dialogModel, ourEditorLocalizer) {

init {
escCloseEnabled.bind(propertySheet.isEscCloseEnabled)
selectedItemProperty.addWatcher {
if (it.trigger != this) {
selectedItem = it.newValue
}
}
}

private var isPropertyChangeIgnored = false
private var selectedItem: ColumnAsListItem?
get() = selectedItemProperty.value
set(value) {
isPropertyChangeIgnored = true
if (value != null) {
model.nameOption.set(value.title)
model.typeOption.set(value.type)
model.defaultValueOption.set(value.defaultValue)
visibilityToggle.isSelected = value.isVisible

if (value.isCustom) {
override fun loadData(item: ColumnAsListItem?) {
if (item != null) {
model.nameOption.set(item.title)
model.typeOption.set(item.type)
model.defaultValueOption.set(item.defaultValue)
visibilityToggle.isSelected = item.isVisible

if (item.isCustom) {
propertySheetLabel.text = ourLocalizer.formatText("propertyPane.title.custom")
propertySheet.isDisable = false
btnDeleteController.isDisabled.value = false
model.isCalculatedOption.set(value.isCalculated)
model.expressionOption.set(value.expression)
model.isCalculatedOption.set(item.isCalculated)
model.expressionOption.set(item.expression)
} else {
btnDeleteController.isDisabled.value = true
propertySheetLabel.text = ourLocalizer.formatText("propertyPane.title.builtin")
Expand All @@ -297,7 +282,15 @@ internal class CustomPropertyEditor(
model.expressionOption.set("")
}
}
isPropertyChangeIgnored = false
}

override fun saveData(item: ColumnAsListItem) {
item.isVisible = visibilityToggle.isSelected
item.title = model.nameOption.value ?: ""
item.type = model.typeOption.value
item.defaultValue = model.defaultValueOption.value ?: ""
item.isCalculated = model.isCalculatedOption.value
item.expression = model.expressionOption.value ?: ""
}

init {
Expand All @@ -306,37 +299,13 @@ internal class CustomPropertyEditor(
visibilityToggle.selectedProperty().addListener { _, _, _ ->
onEdit()
}
propertySheet.validationErrors.addListener(MapChangeListener {
if (propertySheet.validationErrors.isEmpty()) {
errorUi(null)
listItems.replaceAll { if (it != selectedItem?.cloneOf) it else selectedItem }
} else {
errorUi(propertySheet.validationErrors.values.joinToString(separator = "\n"))
}
})
}

internal fun updateVisibility(item: ColumnAsListItem) {
selectedItem?.let {
if (it.column?.id == item.column?.id) {
if (it.isVisible != item.isVisible) {
it.isVisible = item.isVisible
visibilityToggle.isSelected = item.isVisible
}
}
}

}
override fun onEdit() {
if (!isPropertyChangeIgnored) {
selectedItem?.clone()?.let {selected ->
selected.isVisible = visibilityToggle.isSelected
selected.title = model.nameOption.value ?: ""
selected.type = model.typeOption.value
selected.defaultValue = model.defaultValueOption.value ?: ""
selected.isCalculated = model.isCalculatedOption.value
selected.expression = model.expressionOption.value ?: ""
selectedItemProperty.set(selected, trigger = this)
editItem.value?.let {
if (it.column?.id == item.column?.id && it.isVisible != item.isVisible) {
it.isVisible = item.isVisible
visibilityToggle.isSelected = item.isVisible
}
}
}
Expand Down Expand Up @@ -477,6 +446,7 @@ internal val ourEditorLocalizer = run {
RootLocalizer.create(key)
}
it == "columnExists" -> RootLocalizer.create(it)
it == "addItem" -> RootLocalizer.create("addCustomColumn")
else -> null
}
}
Expand Down
146 changes: 94 additions & 52 deletions ganttproject/src/main/java/biz/ganttproject/ganttview/FilterDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,76 +18,118 @@
*/
package biz.ganttproject.ganttview

import biz.ganttproject.app.MappingLocalizer
import biz.ganttproject.app.RootLocalizer
import biz.ganttproject.app.dialog
import biz.ganttproject.core.option.Completion
import biz.ganttproject.core.option.DefaultBooleanOption
import biz.ganttproject.core.option.ObservableObject
import biz.ganttproject.core.option.ObservableProperty
import biz.ganttproject.core.option.ObservableString
import biz.ganttproject.lib.fx.VBoxBuilder
import biz.ganttproject.core.option.ValidationException
import biz.ganttproject.customproperty.CalculationMethodValidator
import biz.ganttproject.customproperty.CustomPropertyClass
import biz.ganttproject.customproperty.ExpressionAutoCompletion
import biz.ganttproject.customproperty.SimpleSelect
import javafx.collections.FXCollections
import javafx.geometry.Pos
import javafx.scene.control.ListView
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.util.Callback
import net.sourceforge.ganttproject.action.CancelAction
import net.sourceforge.ganttproject.action.OkAction
import net.sourceforge.ganttproject.storage.ProjectDatabase

fun showFilterDialog(filterManager: TaskFilterManager) {
dialog(title = "Filter Dialog") { dlg ->
dlg.addStyleClass("dlg-list-view-editor")
dlg.addStyleSheet("/biz/ganttproject/ganttview/ListViewEditorDialog.css")
dlg.setHeader(
VBoxBuilder("header").apply {
addTitle("Task Filters").also { hbox ->
hbox.alignment = Pos.CENTER_LEFT
hbox.isFillHeight = true
}
}.vbox
/**
* Shows a dialog that allows for creating custom task filters.
*/
fun showFilterDialog(filterManager: TaskFilterManager, projectDatabase: ProjectDatabase) {
dialog(title = i18n.formatText("title")) { dlg ->
val listItems = FXCollections.observableArrayList(filterManager.filters)
val editItem = ObservableObject<TaskFilter?>("", null)
val editorModel = FilterEditorModel(editItem, CalculationMethodValidator(projectDatabase), ExpressionAutoCompletion()::complete)
val dialogModel = ItemListDialogModel<TaskFilter>(
listItems,
newItemFactory = {
TaskFilter("", "", DefaultBooleanOption("", false), { _, _ -> false})
},
i18n
)
dialogModel.btnApplyController.onAction = {
filterManager.importFilters(listItems)
}
val editor = FilterEditor(editorModel, editItem, dialogModel)
val dialogPane = ItemListDialogPane<TaskFilter>(
listItems,
editItem,
{ filter -> ShowHideListItem(filter.title, filter.isEnabledProperty) },
dialogModel,
editor,
i18n
)
dialogPane.build(dlg)
}
}

val editItem = ObservableObject<TaskFilter?>("", null)
val model = FilterEditorModel(editItem)
val editor = FilterEditor(editItem, model.fields)
internal class FilterEditorModel(
editItem: ObservableObject<TaskFilter?>,
calculationMethodValidator: CalculationMethodValidator,
expressionAutoCompletion: (String, Int) -> List<Completion>) {

val listView = ListView<TaskFilter>().apply {
cellFactory = Callback { ShowHideListCell { filter ->
ShowHideListItem(filter.title, filter.isEnabledProperty)
} }
items = FXCollections.observableArrayList(filterManager.filters)
selectionModel.selectedItemProperty().addListener { _, _, newValue ->
if (newValue != null) {
editItem.set(newValue, this@apply)
val nameField = ObservableString(id="name", "")
val descriptionField = ObservableString(id="description", "")
val expressionField = ObservableString(id="expression", initValue = "",
validator = {
if (editItem.value?.isBuiltIn == true) {
""
} else {
if (it.isNotBlank()) {
calculationMethodValidator.validate(
// Incomplete instance just for validation purposes
SimpleSelect("", "num", whereExpression = it, CustomPropertyClass.INTEGER.javaClass)
)
it
} else {
throw ValidationException(i18n.formatText("expression.validation.empty"))
}
}
selectionModel.select(0)
}
).also {
it.completions = expressionAutoCompletion
}

val fields = listOf(nameField, descriptionField, expressionField)
}

val content = HBox().also {
it.children.addAll(listView, editor.node)
HBox.setHgrow(editor.node, Priority.ALWAYS)
internal class FilterEditor(
private val editorModel: FilterEditorModel, editItem: ObservableObject<TaskFilter?>, model: ItemListDialogModel<TaskFilter>)
: ItemEditorPane<TaskFilter>(
editorModel.fields, editItem, model, i18n
) {
override fun loadData(item: TaskFilter?) {
if (item != null) {
editorModel.nameField.set(item.title)
editorModel.descriptionField.set(item.description)
editorModel.expressionField.set(item.expression)
propertySheet.isDisable = item.isBuiltIn
} else {
editorModel.nameField.set("")
editorModel.descriptionField.set("")
editorModel.expressionField.set("")
}
dlg.setContent(content)
}

dlg.setupButton(OkAction.create("ok") {})
dlg.setupButton(CancelAction.create("cancel") {})
override fun saveData(item: TaskFilter) {
item.title = editorModel.nameField.value ?: ""
item.description = editorModel.descriptionField.value ?: ""
item.expression = editorModel.expressionField.value ?: ""
}
}

internal class FilterEditorModel(editItem: ObservableObject<TaskFilter?>) {
val nameField = ObservableString("name", "")
val descriptionField = ObservableString("description", "")
val fields = listOf(nameField)

init {
editItem.addWatcher {
if (it.trigger != this) {
nameField.set(it.newValue?.title)
descriptionField.set(it.newValue?.description)
private val i18n = run {
val fallback1 = MappingLocalizer(mapOf()) {
when {
it.endsWith(".label") -> {
val key = it.split('.', limit = 2)[0]
RootLocalizer.create(key)
}
it == "columnExists" -> RootLocalizer.create(it)
else -> null
}
}

val fallback2 = RootLocalizer.createWithRootKey("taskTable.filterDialog", fallback1)
RootLocalizer.createWithRootKey("", fallback2)
}
internal class FilterEditor(editItem: ObservableProperty<TaskFilter?>, fields: List<ObservableProperty<*>>): ItemEditorPane<TaskFilter?>(
editItem = editItem, fields = fields, ourEditorLocalizer
)
Loading
Loading