From 0219a41ba26662d0d1ffdd274fa71e70138836b3 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 09:12:34 +0100 Subject: [PATCH 01/21] Beautify Scale example --- examples/widgets/scale.nim | 43 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/widgets/scale.nim b/examples/widgets/scale.nim index 71a1439d..3969584e 100644 --- a/examples/widgets/scale.nim +++ b/examples/widgets/scale.nim @@ -44,29 +44,30 @@ method view(app: AppState): Widget = result = gui: Window(): title = "Scale Example" - defaultSize = (800, 600) + defaultSize = (800, 150) HeaderBar() {.addTitlebar.}: insert(app.toAutoFormMenu()) {.addRight.} - Scale: - min = app.min - max = app.max - value = app.value - marks = app.marks - inverted = app.inverted - showValue = app.showValue - stepSize = app.stepSize - pageSize = app.pageSize - orient = app.orient - showFillLevel = app.showFillLevel - precision = app.precision - valuePosition = app.valuePosition - sensitive = app.sensitive - tooltip = app.tooltip - sizeRequest = app.sizeRequest - - proc valueChanged(newValue: float64) = - app.value = newValue - echo "New value from Scale is ", $newValue + Box(orient = OrientY): + Scale {.expand: false.}: + min = app.min + max = app.max + value = app.value + marks = app.marks + inverted = app.inverted + showValue = app.showValue + stepSize = app.stepSize + pageSize = app.pageSize + orient = app.orient + showFillLevel = app.showFillLevel + precision = app.precision + valuePosition = app.valuePosition + sensitive = app.sensitive + tooltip = app.tooltip + sizeRequest = app.sizeRequest + + proc valueChanged(newValue: float64) = + app.value = newValue + echo "New value from Scale is ", $newValue adw.brew(gui(App())) From e60b67b57129e7288d1950a706f13931fd387a92 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 09:14:30 +0100 Subject: [PATCH 02/21] Remove unused import --- examples/widgets/action_bar.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/widgets/action_bar.nim b/examples/widgets/action_bar.nim index 6d18ba65..7b8190ee 100644 --- a/examples/widgets/action_bar.nim +++ b/examples/widgets/action_bar.nim @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import std/[sequtils] import owlkettle, owlkettle/[dataentries, playground, adw] viewable App: From 3c510f6c011b6214d1649a5d8aec5b480967a770 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 09:27:59 +0100 Subject: [PATCH 03/21] Remove unused imports from example --- examples/widgets/editable_label.nim | 1 - examples/widgets/fixed.nim | 2 +- examples/widgets/picture.nim | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/widgets/editable_label.nim b/examples/widgets/editable_label.nim index 4a0a61f3..79fa4fb2 100644 --- a/examples/widgets/editable_label.nim +++ b/examples/widgets/editable_label.nim @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import std/[sequtils] import owlkettle, owlkettle/[dataentries, playground, adw] viewable App: diff --git a/examples/widgets/fixed.nim b/examples/widgets/fixed.nim index 07b7a4ba..caf91d4b 100644 --- a/examples/widgets/fixed.nim +++ b/examples/widgets/fixed.nim @@ -21,7 +21,7 @@ # SOFTWARE import std/random -import owlkettle, owlkettle/[adw, dataentries] +import owlkettle, owlkettle/[adw] type FixedItem = ref object name: string diff --git a/examples/widgets/picture.nim b/examples/widgets/picture.nim index c29a3fb3..5626e382 100644 --- a/examples/widgets/picture.nim +++ b/examples/widgets/picture.nim @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import std/[asyncfutures] import owlkettle, owlkettle/[playground, adw] const APP_NAME = "Image Example" From 0d3bb3d6b6987674f3574fdf67317aec9daf7931 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 09:30:17 +0100 Subject: [PATCH 04/21] Refactor playground to not require toListFormField This changes how this all works. Previously we generated expressions that act directly on `state` to manipulate its values. Now we fetch a pointer to each field on state or each field of an instance in a seq and act on that. This gets rid entirely of "toFormField", which can now be used for an individual field as well as a field of an instance in a seq. --- owlkettle/playground.nim | 174 ++++++++++----------------------------- 1 file changed, 45 insertions(+), 129 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 41057ea4..99645f9c 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import std/[options, times, macros, strformat, strutils, sequtils, typetraits] +import std/[options, times, macros, strformat, sugar, strutils, sequtils, typetraits] import ./dataentries import ./adw import ./widgetutils @@ -32,71 +32,72 @@ macro getField*(someType: untyped, fieldName: static string): untyped = nnkDotExpr.newTree(someType, ident(fieldName)) # Default `toFormField` implementations -proc toFormField[T: SomeNumber](state: auto, fieldName: static string, typ: typedesc[T]): Widget = +proc toFormField(state: auto, field: ptr SomeNumber, fieldName: static string): Widget = ## Provides a form field for all number times in SomeNumber return gui: ActionRow: title = fieldName FormulaEntry() {.addSuffix.}: - value = state.getField(fieldName).float + value = field[].float xAlign = 1.0 maxWidth = 8 proc changed(value: float) = - state.getField(fieldName) = value.T + field[] = type(field[])(value) -proc toFormField(state: auto, fieldName: static string, typ: typedesc[string]): Widget = +proc toFormField(state: auto, field: ptr string, fieldName: static string): Widget = ## Provides a form field for string return gui: ActionRow: title = fieldName Entry() {.addSuffix.}: - text = state.getField(fieldName) + text = field[] proc changed(text: string) = - state.getField(fieldName) = text + field[] = text -proc toFormField(state: auto, fieldName: static string, typ: typedesc[bool]): Widget = +proc toFormField(state: auto, field: ptr bool, fieldName: static string): Widget = ## Provides a form field for bool return gui: ActionRow: title = fieldName Box() {.addSuffix.}: Switch() {.vAlign: AlignCenter, expand: false.}: - state = state.getField(fieldName) + state = field[] proc changed(newVal: bool) = - state.getField(fieldName) = newVal + field[] = newVal -proc toFormField(state: auto, fieldName: static string, typ: typedesc[auto]): Widget = +proc toFormField(state: auto, field: ptr auto, fieldName: static string): Widget = ## Provides a dummy field as a fallback for any type without a `toFormField`. + const typeName: string = $field[].type return gui: ActionRow: title = fieldName Label(): - text = fmt"Override `toFormField` for '{$typ.type}'" + text = fmt"Override `toFormField` for '{typeName}'" tooltip = fmt""" - The type '{$typ.type}' must implement a `toFormField` proc: + The type '{typeName}' must implement a `toFormField` proc: `toFormField( state: auto, + field: ptr {typeName} fieldName: static string, - typ: typedesc[{$typ.type}] ): Widget` state: The State + field: The field's value to assign to/get the value from fieldName: The name of the field on `state` for which the current form field is being generated - typ: The type for which `toListFormField` shall function. Implementing the proc will override this dummy Widget. See the playground module for examples. """ -proc toFormField[T: enum](state: auto, fieldName: static string, typ: typedesc[T]): Widget = +proc toFormField(state: auto, field: ptr enum, fieldName: static string): Widget = ## Provides a form field for enums - let options: seq[string] = T.items.toSeq().mapIt($it) + let options: seq[string] = type(field[]).items.toSeq().mapIt($it) return gui: ComboRow: title = fieldName items = options - selected = ord(state.getField(fieldName)) + selected = ord(field[]) proc select(enumIndex: int) = - state.getField(fieldName) = enumIndex.T + field[] = type(field[])(enumIndex) viewable DateDialog: date: DateTime = now() @@ -123,165 +124,79 @@ method view (state: DateDialogState): Widget = proc select(date: DateTime) = state.date = date -proc toFormField(state: auto, fieldName: static string, typ: typedesc[DateTime]): Widget = +proc toFormField(state: auto, field: ptr DateTime, fieldName: static string): Widget = ## Provides a form field for DateTime return gui: ActionRow: title = fieldName - subtitle = $state.getField(fieldName).inZone(local()) + subtitle = $(field[]).inZone(local()) Button {.addSuffix.}: icon = "x-office-calendar-symbolic" text = "Select" proc clicked() = let (res, dialogState) = state.open(gui(DateDialog())) if res.kind == DialogAccept: - state.getField(fieldName) = DateDialogState(dialogState).date + field[] = DateDialogState(dialogState).date -proc toFormField(state: auto, fieldName: static string, typ: typedesc[tuple[x,y: int]]): Widget = +proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: static string): Widget = ## Provides a form field for the tuple type of sizeRequest + let tup = field[] return gui: ActionRow: title = fieldName NumberEntry() {.addSuffix.}: - value = state.getField(fieldName)[0].float + value = tup[0].float xAlign = 1.0 maxWidth = 8 proc changed(value: float) = - state.getField(fieldName)[0] = value.int + field[][0] = value.int NumberEntry() {.addSuffix.}: - value = state.getField(fieldName)[1].float + value = tup[1].float xAlign = 1.0 maxWidth = 8 proc changed(value: float) = - state.getField(fieldName)[1] = value.int + field[][1] = value.int - -## Default `toListFormField` implementations -proc toListFormField[T: SomeNumber](state: auto, fieldName: static string, index: int, typ: typedesc[T]): Widget = - ## Provides a form to display a single entry of any number in a list of number entries. - return gui: - ActionRow: - title = fieldName & $index - - FormulaEntry() {.addSuffix.}: - value = state.getField(fieldName)[index].float - xAlign = 1.0 - maxWidth = 8 - proc changed(value: float) = - state.getField(fieldName)[index] = value.T - -proc toListFormField(state: auto, fieldName: static string, index: int, typ: typedesc[string]): Widget = - ## Provides a form to display a single entry of type `string` in a list of `string` entries. - return gui: - ActionRow: - title = fieldName - Entry() {.addSuffix.}: - text = state.getField(fieldName)[index] - proc changed(text: string) = - state.getField(fieldName)[index] = text - -proc toListFormField(state: auto, fieldName: static string, index: int, typ: typedesc[bool]): Widget = - ## Provides a form to display a single entry of type `bool` in a list of `bool` entries. - return gui: - ActionRow: - title = fieldName - Box() {.addSuffix.}: - Switch() {.vAlign: AlignCenter, expand: false.}: - state = state.getField(fieldName)[index] - proc changed(newVal: bool) = - state.getField(fieldName)[index] = newVal - -proc toListFormField(state: auto, fieldName: static string, index: int, typ: typedesc[DateTime]): Widget = - ## Provides a form to display a single entry of type `DateTime` in a list of `DateTime` entries. - return gui: - ActionRow: - title = fieldName - subtitle = $state.getField(fieldName)[index].inZone(local()) - Button {.addSuffix.}: - icon = "x-office-calendar-symbolic" - text = "Select" - proc clicked() = - let (res, dialogState) = state.open(gui(DateDialog())) - if res.kind == DialogAccept: - state.getField(fieldName)[index] = DateDialogState(dialogState).date - -proc toListFormField[T: enum](state: auto, fieldName: static string, index: int, typ: typedesc[T]): Widget = - ## Provides a form to display a single entry of an enum in a list of enum entries. - let options: seq[string] = T.items.toSeq().mapIt($it) - return gui: - ComboRow: - title = fieldName - items = options - selected = ord(state.getField(fieldName)[index]) - proc select(enumIndex: int) = - state.getField(fieldName)[index] = enumIndex.T - -proc toListFormField(state: auto, fieldName: static string, index: int, typ: typedesc[auto]): Widget = - ## Provides a dummy row widget for displaying an entry in a list of any type without its own `toListFormField` - return gui: - ActionRow: - title = fieldName - Label(): - text = fmt"Override `toListFormField` for '{$typ.type}'" - tooltip = fmt""" - The type '{$typ.type}' must implement a `toListFormField` proc: - `toListFormField( - state: auto, - fieldName: static string, - index: int, - typ: typedesc[{$typ.type}] - ): Widget` - state: The State - fieldName: The name of the field on `state` that contains the seq for which the current form-row is being generated - index: The index on the list of entries in `state.` - typ: The type for which `toListFormField` shall function. - - Implementing the proc will override this dummy Widget. - See the playground module for examples. - """ - -proc toListFormField(state: auto, fieldName: static string, index: int, typ: typedesc[ScaleMark]): Widget = +proc toFormField(state: auto, field: ptr ScaleMark, fieldName: static string): Widget = ## Provides a form to display a single entry of type `ScaleMark` in a list of `ScaleMark` entries. - let mark: ScaleMark = state.getField(fieldName)[index] return gui: ActionRow: - title = fieldName & $index + title = fieldName FormulaEntry {.addSuffix.}: - value = mark.value + value = field[].value xAlign = 1.0 maxWidth = 8 proc changed(value: float) = - state.getField(fieldName)[index].value = value + field[].value = value DropDown {.addSuffix.}: items = ScalePosition.items.toSeq().mapIt($it) - selected = mark.position.int + selected = field[].position.int proc select(enumIndex: int) = - state.getField(fieldName)[index].position = enumIndex.ScalePosition - - Button {.addSuffix.}: - icon = "user-trash-symbolic" - proc clicked() = - state.getField(fieldName).delete(index) + field[].position = enumIndex.ScalePosition -proc toFormField[T](state: auto, fieldName: static string, typ: typedesc[seq[T]]): Widget = +proc toFormField[T](state: auto, field: ptr seq[T], fieldName: static string): Widget = ## Provides a form field for any field on `state` with a seq type. ## Displays a dummy widget if there is no `toListFormField` implementation for type T. + let formFields = collect(newSeq): + for index, value in field[]: + toFormField(state, field[][index].addr, fieldName) + return gui: ExpanderRow: title = fieldName - for index, num in state.getField(fieldName): - insert(toListFormField(state, fieldName, index, T)){.addRow.} + for index, formField in formFields: + insert(formField){.addRow.} ListBoxRow {.addRow.}: Button: icon = "list-add-symbolic" style = [ButtonFlat] proc clicked() = - state.getField(fieldName).add(default(T)) + field[].add(default(T)) proc toAutoFormMenu*[T](app: T, sizeRequest: tuple[x,y: int] = (400, 700), ignoreFields: static seq[string]): Widget = ## Provides a form for every field in a given `app` instance. @@ -292,7 +207,8 @@ proc toAutoFormMenu*[T](app: T, sizeRequest: tuple[x,y: int] = (400, 700), ignor var fieldWidgets: seq[Widget] = @[] for name, value in app[].fieldPairs: when name notin ["app", "viewed"] and name notin ignoreFields: - let fieldWidget = app.toFormField(name, value.type) + let field: ptr = value.addr + let fieldWidget = app.toFormField(field, name) fieldWidgets.add(fieldWidget) result = gui: From 1a8924a2fcfb0ef118c5e566dda0db304c4a0b5e Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 13:48:32 +0100 Subject: [PATCH 05/21] Fix toFormfield signature for enums Nim 1.6.12 appears to not like the `ptr enum` type. ptr[enum] seems okay though, so it just doesn't like the notation. --- owlkettle/playground.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 99645f9c..021dc0f9 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -88,7 +88,7 @@ proc toFormField(state: auto, field: ptr auto, fieldName: static string): Widget See the playground module for examples. """ -proc toFormField(state: auto, field: ptr enum, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr[enum] , fieldName: static string): Widget = ## Provides a form field for enums let options: seq[string] = type(field[]).items.toSeq().mapIt($it) return gui: From 93a6eb85358e02126e804decddbfd182fdfb3b38 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 14:42:44 +0100 Subject: [PATCH 06/21] Remove requirement for fieldnames to be static in playground --- owlkettle/playground.nim | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 021dc0f9..e37990f9 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -28,11 +28,11 @@ import ./guidsl import ./widgetdef import ./widgets -macro getField*(someType: untyped, fieldName: static string): untyped = +macro getField*(someType: untyped, fieldName: string): untyped = nnkDotExpr.newTree(someType, ident(fieldName)) # Default `toFormField` implementations -proc toFormField(state: auto, field: ptr SomeNumber, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number times in SomeNumber return gui: ActionRow: @@ -44,7 +44,7 @@ proc toFormField(state: auto, field: ptr SomeNumber, fieldName: static string): proc changed(value: float) = field[] = type(field[])(value) -proc toFormField(state: auto, field: ptr string, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr string, fieldName: string): Widget = ## Provides a form field for string return gui: ActionRow: @@ -54,7 +54,7 @@ proc toFormField(state: auto, field: ptr string, fieldName: static string): Widg proc changed(text: string) = field[] = text -proc toFormField(state: auto, field: ptr bool, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr bool, fieldName: string): Widget = ## Provides a form field for bool return gui: ActionRow: @@ -65,7 +65,7 @@ proc toFormField(state: auto, field: ptr bool, fieldName: static string): Widget proc changed(newVal: bool) = field[] = newVal -proc toFormField(state: auto, field: ptr auto, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr auto, fieldName: string): Widget = ## Provides a dummy field as a fallback for any type without a `toFormField`. const typeName: string = $field[].type return gui: @@ -78,7 +78,7 @@ proc toFormField(state: auto, field: ptr auto, fieldName: static string): Widget `toFormField( state: auto, field: ptr {typeName} - fieldName: static string, + fieldName: string, ): Widget` state: The State field: The field's value to assign to/get the value from @@ -88,7 +88,7 @@ proc toFormField(state: auto, field: ptr auto, fieldName: static string): Widget See the playground module for examples. """ -proc toFormField(state: auto, field: ptr[enum] , fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr[enum] , fieldName: string): Widget = ## Provides a form field for enums let options: seq[string] = type(field[]).items.toSeq().mapIt($it) return gui: @@ -124,7 +124,7 @@ method view (state: DateDialogState): Widget = proc select(date: DateTime) = state.date = date -proc toFormField(state: auto, field: ptr DateTime, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr DateTime, fieldName: string): Widget = ## Provides a form field for DateTime return gui: ActionRow: @@ -138,7 +138,7 @@ proc toFormField(state: auto, field: ptr DateTime, fieldName: static string): Wi if res.kind == DialogAccept: field[] = DateDialogState(dialogState).date -proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: string): Widget = ## Provides a form field for the tuple type of sizeRequest let tup = field[] return gui: @@ -158,7 +158,7 @@ proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: static str proc changed(value: float) = field[][1] = value.int -proc toFormField(state: auto, field: ptr ScaleMark, fieldName: static string): Widget = +proc toFormField(state: auto, field: ptr ScaleMark, fieldName: string): Widget = ## Provides a form to display a single entry of type `ScaleMark` in a list of `ScaleMark` entries. return gui: ActionRow: @@ -177,7 +177,7 @@ proc toFormField(state: auto, field: ptr ScaleMark, fieldName: static string): W proc select(enumIndex: int) = field[].position = enumIndex.ScalePosition -proc toFormField[T](state: auto, field: ptr seq[T], fieldName: static string): Widget = +proc toFormField[T](state: auto, field: ptr seq[T], fieldName: string): Widget = ## Provides a form field for any field on `state` with a seq type. ## Displays a dummy widget if there is no `toListFormField` implementation for type T. let formFields = collect(newSeq): From 7d2eec08d1624120e3d652f64a85a8bc4c245d12 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 15:08:32 +0100 Subject: [PATCH 07/21] Add percent formfield --- owlkettle/playground.nim | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index e37990f9..c12ff17f 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -28,9 +28,6 @@ import ./guidsl import ./widgetdef import ./widgets -macro getField*(someType: untyped, fieldName: string): untyped = - nnkDotExpr.newTree(someType, ident(fieldName)) - # Default `toFormField` implementations proc toFormField(state: auto, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number times in SomeNumber @@ -44,6 +41,19 @@ proc toFormField(state: auto, field: ptr SomeNumber, fieldName: string): Widget proc changed(value: float) = field[] = type(field[])(value) +# Default `toFormField` implementations +proc toFormField(state: auto, field: ptr range[0.0..1.0], fieldName: string): Widget = + ## Provides a form field for a range of float numbers between 0.0 and 1.0 + return gui: + ActionRow: + title = fieldName + FormulaEntry() {.addSuffix.}: + value = field[] + xAlign = 1.0 + maxWidth = 8 + proc changed(value: float) = + field[] = value + proc toFormField(state: auto, field: ptr string, fieldName: string): Widget = ## Provides a form field for string return gui: @@ -189,7 +199,8 @@ proc toFormField[T](state: auto, field: ptr seq[T], fieldName: string): Widget = title = fieldName for index, formField in formFields: - insert(formField){.addRow.} + insert(formField){.addRow.}: + Button(text = "lala") {.addSuffix.} ListBoxRow {.addRow.}: Button: From a6e8c82ae8c83b60db628476d427bff7e9d9770b Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Wed, 25 Oct 2023 15:14:49 +0100 Subject: [PATCH 08/21] Add delete button to seq formfield --- owlkettle/playground.nim | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index c12ff17f..1e68b319 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -199,8 +199,13 @@ proc toFormField[T](state: auto, field: ptr seq[T], fieldName: string): Widget = title = fieldName for index, formField in formFields: - insert(formField){.addRow.}: - Button(text = "lala") {.addSuffix.} + Box(orient = OrientX) {.addRow.}: + insert(formField) + Button() {.expand: false.}: + icon = "user-trash-symbolic" + style = [ButtonDestructive] + proc clicked() = + field[].delete(index) ListBoxRow {.addRow.}: Button: From 106ed806478d3e897e226daecb1decb3d98eb02c Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 11:03:27 +0200 Subject: [PATCH 09/21] Move generics over to use Viewable This required moving open to widgets.nim as due to being part of playground that proc was not available there (?). --- owlkettle.nim | 36 ------------------------------------ owlkettle/playground.nim | 22 +++++++++++----------- owlkettle/widgets.nim | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/owlkettle.nim b/owlkettle.nim index 725f76b2..30f5fb3e 100644 --- a/owlkettle.nim +++ b/owlkettle.nim @@ -134,42 +134,6 @@ proc remove*(event: EventDescriptor) = if g_source_remove(cuint(event)) == 0: raise newException(IoError, "Unable to remove " & $event) -proc open*(app: Viewable, widget: Widget): tuple[res: DialogResponse, state: WidgetState] = - let - state = widget.build() - dialogState = state.unwrapRenderable() - window = app.unwrapInternalWidget() - dialog = state.unwrapInternalWidget() - gtk_window_set_transient_for(dialog, window) - gtk_window_set_modal(dialog, cbool(bool(true))) - gtk_window_present(dialog) - - proc destroy(dialog: GtkWidget, closed: ptr bool) {.cdecl.} = - closed[] = true - - var closed = false - discard g_signal_connect(dialog, "destroy", destroy, closed.addr) - - if dialogState of DialogState or dialogState of BuiltinDialogState: - proc response(dialog: GtkWidget, responseId: cint, res: ptr cint) {.cdecl.} = - res[] = responseId - - var res = low(cint) - discard g_signal_connect(dialog, "response", response, res.addr) - while res == low(cint): - discard g_main_context_iteration(nil.GMainContext, cbool(ord(true))) - - state.read() - if not closed: - gtk_window_destroy(dialog) - result = (toDialogResponse(res), state) - else: - while not closed: - discard g_main_context_iteration(nil.GMainContext, cbool(ord(true))) - - state.read() - result = (DialogResponse(), state) - proc respond*(state: WidgetState, response: DialogResponse) = let widget = state.unwrapInternalWidget() diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 1e68b319..ac49adaa 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -29,7 +29,7 @@ import ./widgetdef import ./widgets # Default `toFormField` implementations -proc toFormField(state: auto, field: ptr SomeNumber, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number times in SomeNumber return gui: ActionRow: @@ -42,7 +42,7 @@ proc toFormField(state: auto, field: ptr SomeNumber, fieldName: string): Widget field[] = type(field[])(value) # Default `toFormField` implementations -proc toFormField(state: auto, field: ptr range[0.0..1.0], fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr range[0.0..1.0], fieldName: string): Widget = ## Provides a form field for a range of float numbers between 0.0 and 1.0 return gui: ActionRow: @@ -54,7 +54,7 @@ proc toFormField(state: auto, field: ptr range[0.0..1.0], fieldName: string): Wi proc changed(value: float) = field[] = value -proc toFormField(state: auto, field: ptr string, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr string, fieldName: string): Widget = ## Provides a form field for string return gui: ActionRow: @@ -64,7 +64,7 @@ proc toFormField(state: auto, field: ptr string, fieldName: string): Widget = proc changed(text: string) = field[] = text -proc toFormField(state: auto, field: ptr bool, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget = ## Provides a form field for bool return gui: ActionRow: @@ -75,7 +75,7 @@ proc toFormField(state: auto, field: ptr bool, fieldName: string): Widget = proc changed(newVal: bool) = field[] = newVal -proc toFormField(state: auto, field: ptr auto, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr auto, fieldName: string): Widget = ## Provides a dummy field as a fallback for any type without a `toFormField`. const typeName: string = $field[].type return gui: @@ -86,7 +86,7 @@ proc toFormField(state: auto, field: ptr auto, fieldName: string): Widget = tooltip = fmt""" The type '{typeName}' must implement a `toFormField` proc: `toFormField( - state: auto, + state: Viewable, field: ptr {typeName} fieldName: string, ): Widget` @@ -98,7 +98,7 @@ proc toFormField(state: auto, field: ptr auto, fieldName: string): Widget = See the playground module for examples. """ -proc toFormField(state: auto, field: ptr[enum] , fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr[enum] , fieldName: string): Widget = ## Provides a form field for enums let options: seq[string] = type(field[]).items.toSeq().mapIt($it) return gui: @@ -134,7 +134,7 @@ method view (state: DateDialogState): Widget = proc select(date: DateTime) = state.date = date -proc toFormField(state: auto, field: ptr DateTime, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr DateTime, fieldName: string): Widget = ## Provides a form field for DateTime return gui: ActionRow: @@ -148,7 +148,7 @@ proc toFormField(state: auto, field: ptr DateTime, fieldName: string): Widget = if res.kind == DialogAccept: field[] = DateDialogState(dialogState).date -proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget = ## Provides a form field for the tuple type of sizeRequest let tup = field[] return gui: @@ -168,7 +168,7 @@ proc toFormField(state: auto, field: ptr tuple[x, y: int], fieldName: string): W proc changed(value: float) = field[][1] = value.int -proc toFormField(state: auto, field: ptr ScaleMark, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget = ## Provides a form to display a single entry of type `ScaleMark` in a list of `ScaleMark` entries. return gui: ActionRow: @@ -187,7 +187,7 @@ proc toFormField(state: auto, field: ptr ScaleMark, fieldName: string): Widget = proc select(enumIndex: int) = field[].position = enumIndex.ScalePosition -proc toFormField[T](state: auto, field: ptr seq[T], fieldName: string): Widget = +proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget = ## Provides a form field for any field on `state` with a seq type. ## Displays a dummy widget if there is no `toListFormField` implementation for type T. let formFields = collect(newSeq): diff --git a/owlkettle/widgets.nim b/owlkettle/widgets.nim index ef429583..d061df26 100644 --- a/owlkettle/widgets.nim +++ b/owlkettle/widgets.nim @@ -3407,6 +3407,43 @@ renderable BuiltinDialog of BaseWidget: adder addButton +proc open*(app: Viewable, widget: Widget): tuple[res: DialogResponse, state: WidgetState] = + let + state = widget.build() + dialogState = state.unwrapRenderable() + window = app.unwrapInternalWidget() + dialog = state.unwrapInternalWidget() + gtk_window_set_transient_for(dialog, window) + gtk_window_set_modal(dialog, cbool(bool(true))) + gtk_window_present(dialog) + + proc destroyDialog(dialog: GtkWidget, closed: ptr bool) {.cdecl.} = + closed[] = true + + var closed = false + discard g_signal_connect(dialog, "destroy", destroyDialog , closed.addr) + + if dialogState of DialogState or dialogState of BuiltinDialogState: + proc response(dialog: GtkWidget, responseId: cint, res: ptr cint) {.cdecl.} = + res[] = responseId + + var res = low(cint) + discard g_signal_connect(dialog, "response", response, res.addr) + while res == low(cint): + discard g_main_context_iteration(nil.GMainContext, cbool(ord(true))) + + state.read() + if not closed: + gtk_window_destroy(dialog) + result = (toDialogResponse(res), state) + else: + while not closed: + discard g_main_context_iteration(nil.GMainContext, cbool(ord(true))) + + state.read() + result = (DialogResponse(), state) + + proc addButton*(dialog: BuiltinDialog, button: DialogButton) = dialog.hasButtons = true dialog.valButtons.add(button) From 25ede6ad73aae99b1810ef2317a864de77d63028 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 11:29:09 +0200 Subject: [PATCH 10/21] Make range toFormField generic over all ranges This requires introducing a `Range` concept as `range` is not a valid typeclass to use as a generic parameter. --- owlkettle/playground.nim | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index ac49adaa..9be3dcb8 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -28,7 +28,6 @@ import ./guidsl import ./widgetdef import ./widgets -# Default `toFormField` implementations proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number times in SomeNumber return gui: @@ -41,9 +40,11 @@ proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Wid proc changed(value: float) = field[] = type(field[])(value) -# Default `toFormField` implementations -proc toFormField(state: Viewable, field: ptr range[0.0..1.0], fieldName: string): Widget = - ## Provides a form field for a range of float numbers between 0.0 and 1.0 +type Range = concept r # Necessary as there is no range typeclass *for parameters*. So `field: ptr range` is not a valid parameter. + r is range + +proc toFormField(state: Viewable, field: ptr Range, fieldName: string): Widget = + ## Provides a form field for any range return gui: ActionRow: title = fieldName @@ -75,7 +76,7 @@ proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget = proc changed(newVal: bool) = field[] = newVal -proc toFormField(state: Viewable, field: ptr auto, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = ## Provides a dummy field as a fallback for any type without a `toFormField`. const typeName: string = $field[].type return gui: From fa85850792febeefeec60e93d71b0408177b3df9 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 12:41:33 +0200 Subject: [PATCH 11/21] Make toFormField doc comments more uniform --- owlkettle/playground.nim | 43 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 9be3dcb8..e82c4a7a 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -29,7 +29,7 @@ import ./widgetdef import ./widgets proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = - ## Provides a form field for all number times in SomeNumber + ## Provides a form field for all number types in SomeNumber return gui: ActionRow: title = fieldName @@ -40,11 +40,8 @@ proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Wid proc changed(value: float) = field[] = type(field[])(value) -type Range = concept r # Necessary as there is no range typeclass *for parameters*. So `field: ptr range` is not a valid parameter. - r is range - proc toFormField(state: Viewable, field: ptr Range, fieldName: string): Widget = - ## Provides a form field for any range + ## Provides a form field for all range types return gui: ActionRow: title = fieldName @@ -56,7 +53,7 @@ proc toFormField(state: Viewable, field: ptr Range, fieldName: string): Widget = field[] = value proc toFormField(state: Viewable, field: ptr string, fieldName: string): Widget = - ## Provides a form field for string + ## Provides a form field for strings return gui: ActionRow: title = fieldName @@ -66,7 +63,7 @@ proc toFormField(state: Viewable, field: ptr string, fieldName: string): Widget field[] = text proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget = - ## Provides a form field for bool + ## Provides a form field for booleans return gui: ActionRow: title = fieldName @@ -76,31 +73,8 @@ proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget = proc changed(newVal: bool) = field[] = newVal -proc toFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = - ## Provides a dummy field as a fallback for any type without a `toFormField`. - const typeName: string = $field[].type - return gui: - ActionRow: - title = fieldName - Label(): - text = fmt"Override `toFormField` for '{typeName}'" - tooltip = fmt""" - The type '{typeName}' must implement a `toFormField` proc: - `toFormField( - state: Viewable, - field: ptr {typeName} - fieldName: string, - ): Widget` - state: The State - field: The field's value to assign to/get the value from - fieldName: The name of the field on `state` for which the current form field is being generated - - Implementing the proc will override this dummy Widget. - See the playground module for examples. - """ - proc toFormField(state: Viewable, field: ptr[enum] , fieldName: string): Widget = - ## Provides a form field for enums + ## Provides a form field for all enum types let options: seq[string] = type(field[]).items.toSeq().mapIt($it) return gui: ComboRow: @@ -136,7 +110,7 @@ method view (state: DateDialogState): Widget = state.date = date proc toFormField(state: Viewable, field: ptr DateTime, fieldName: string): Widget = - ## Provides a form field for DateTime + ## Provides a form field for DateTimes return gui: ActionRow: title = fieldName @@ -170,7 +144,7 @@ proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string field[][1] = value.int proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget = - ## Provides a form to display a single entry of type `ScaleMark` in a list of `ScaleMark` entries. + ## Provides a form field for the type ScaleMark return gui: ActionRow: title = fieldName @@ -189,8 +163,7 @@ proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widg field[].position = enumIndex.ScalePosition proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget = - ## Provides a form field for any field on `state` with a seq type. - ## Displays a dummy widget if there is no `toListFormField` implementation for type T. + ## Provides a form field for any seq type let formFields = collect(newSeq): for index, value in field[]: toFormField(state, field[][index].addr, fieldName) From ede376e84f2dfb731df0ebc76ae42e62dad9c4bb Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 12:42:02 +0200 Subject: [PATCH 12/21] Add index to form field label of sequences --- owlkettle/playground.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index e82c4a7a..aef29af7 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -166,7 +166,7 @@ proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widg ## Provides a form field for any seq type let formFields = collect(newSeq): for index, value in field[]: - toFormField(state, field[][index].addr, fieldName) + toFormField(state, field[][index].addr, fmt"{fieldName} {index}") return gui: ExpanderRow: From caf7295c843fd18e85c657a5cacac90609db3413 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 12:43:47 +0200 Subject: [PATCH 13/21] Add forward declarations for all toFormField procs This is done in order to avoid any of the following proc bodies accidentally missing out on a proc that was provided befor e it. This helps with not needing to care about the order that the procs are defined in. --- owlkettle/playground.nim | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index aef29af7..33a7ece6 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -28,6 +28,22 @@ import ./guidsl import ./widgetdef import ./widgets +type Range = concept r # Necessary as there is no range typeclass *for parameters*. So `field: ptr range` is not a valid parameter. + r is range + +# Forward Declarations so order of procs below doesn't matter +proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr Range, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr string, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr[enum], fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr DateTime, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget +proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr auto, fieldName: string): Widget + + proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number types in SomeNumber return gui: From 580ce73969c107e31b0d4e1a353a4bf1d29ce91c Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 12:44:19 +0200 Subject: [PATCH 14/21] Add toFormField proc to fully generate form for any type --- owlkettle/playground.nim | 53 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 33a7ece6..ec549db3 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import std/[options, times, macros, strformat, sugar, strutils, sequtils, typetraits] +import std/[options, times, macros, strformat, sugar, strutils, sequtils, typetraits, tables] import ./dataentries import ./adw import ./widgetutils @@ -204,6 +204,57 @@ proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widg proc clicked() = field[].add(default(T)) +template getIterator*(a: typed): untyped = + ## Provides a fieldPairs iterator for both ref-types and value-types + when a is ref: + a[].fieldPairs + + else: + a.fieldPairs + +proc toFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = + ## Provides a fallback form field for any type that does not match any of the types above. + ## If the type has fields (e.g. object or tuple) it will generate a form with a formfield + ## for each field on the type. + ## If the type does not have fields it will generate a dummy-field with text informing the user to + ## define their own `toFormField` proc. + const hasFields = field is ptr object or field is ptr ref object or field is ptr tuple or field is ptr ref tuple + when hasFields: + var subFieldWidgets: Table[string, Widget] = initTable[string, Widget]() + for subFieldName, subFieldValue in field[].getIterator(): + let subField: ptr = subFieldValue.addr + let subFieldWidget = state.toFormField(subField, subFieldName) + subFieldWidgets[subFieldName] = subFieldWidget + + return gui: + ExpanderRow: + title = fieldName + + for subFieldName in subFieldWidgets.keys: + insert(subFieldWidgets[subFieldName]) {.addRow.} + + else: + const typeName: string = $field[].type + return gui: + ActionRow: + title = fieldName + Label(): + text = fmt"Override `toFormField` for '{typeName}'" + tooltip = fmt""" + The type '{typeName}' must implement a `toFormField` proc: + `toFormField( + state: Viewable, + field: ptr {typeName} + fieldName: string, + ): Widget` + state: The State + field: The field's value to assign to/get the value from + fieldName: The name of the field on `state` for which the current form field is being generated + + Implementing the proc will override this dummy Widget. + See the playground module for examples. + """ + proc toAutoFormMenu*[T](app: T, sizeRequest: tuple[x,y: int] = (400, 700), ignoreFields: static seq[string]): Widget = ## Provides a form for every field in a given `app` instance. ## The form is provided as a Popover that can be activated via a Menu-Button From cdf614ce8f32ed21e555aae25faf7e37a413ec61 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Thu, 26 Oct 2023 13:20:36 +0200 Subject: [PATCH 15/21] Add toFormField proc for any distinct type --- owlkettle/playground.nim | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index ec549db3..1ae59d6e 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -38,6 +38,7 @@ proc toFormField(state: Viewable, field: ptr string, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr bool, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr[enum], fieldName: string): Widget proc toFormField(state: Viewable, field: ptr DateTime, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr[distinct], fieldName: string): Widget proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget @@ -139,6 +140,13 @@ proc toFormField(state: Viewable, field: ptr DateTime, fieldName: string): Widge if res.kind == DialogAccept: field[] = DateDialogState(dialogState).date + +proc toFormField(state: Viewable, field: ptr[distinct], fieldName: string): Widget = + ## Provides a form field for any distinct type. + ## The form field provided is the same that the base-type of the distinct type would have. + let baseField = field[].distinctBase.addr + toFormField(state, baseField, fieldName) + proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget = ## Provides a form field for the tuple type of sizeRequest let tup = field[] From a6d243ad92f0ef176fd5772e596464ebc2c59e40 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sat, 4 Nov 2023 19:56:07 +0100 Subject: [PATCH 16/21] Add back removed import This import is relevant when -d:pixbufAsync is set --- examples/widgets/picture.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/widgets/picture.nim b/examples/widgets/picture.nim index 5626e382..1feb7549 100644 --- a/examples/widgets/picture.nim +++ b/examples/widgets/picture.nim @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import std/asyncfutures import owlkettle, owlkettle/[playground, adw] const APP_NAME = "Image Example" From de3ef3aad460148f36afa69f7dca9a91084a7eca Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sun, 5 Nov 2023 11:32:57 +0100 Subject: [PATCH 17/21] Add Variant toFormField overload This adds a new overload for toFormField to specifically handle object variants. Object Variants are hard to handle and thus by default shall receive the placeholder field that reminds the user to define their own toFormField proc for their variant. The Overload works via having a ObjectVariant concept, which catches all toFormField invocations for object variants. It simply checks on a technicality if a type-field can be assigned to itself. This is never true for kind fields on object variants as it results in compiletime errors. --- owlkettle/playground.nim | 73 +++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 1ae59d6e..088eb6f9 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -31,6 +31,23 @@ import ./widgets type Range = concept r # Necessary as there is no range typeclass *for parameters*. So `field: ptr range` is not a valid parameter. r is range +proc selfAssign[T](v: var T) = v = v +proc isObjectVariant(Obj: typedesc[object]): bool = + ## Checks if a given typedesc is of an object variant + ## by checking if you can assign to a field. This is a + ## compile-time error when doing this with the fieldPairs iterator + ## to an object variant "kind" field. + var obj = default Obj + for name, field in obj.fieldpairs: + when not compiles(selfAssign field): + return true + + return false + +type ObjectVariant = concept type V + ## A concept covering all object variant types + isObjectVariant(V) + # Forward Declarations so order of procs below doesn't matter proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr Range, fieldName: string): Widget @@ -42,9 +59,9 @@ proc toFormField(state: Viewable, field: ptr[distinct], fieldName: string): Widg proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr ObjectVariant, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr auto, fieldName: string): Widget - proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = ## Provides a form field for all number types in SomeNumber return gui: @@ -220,13 +237,42 @@ template getIterator*(a: typed): untyped = else: a.fieldPairs +proc toPlaceHolderFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = + ## Provides a placeholder form field informing the user to implement their own + ## `toFormField` proc, as none of the existing `toFormField` overloads could be applied. + const typeName: string = $field[].type + return gui: + ActionRow: + title = fieldName + Label(): + text = fmt"Override `toFormField` for '{typeName}'" + tooltip = fmt""" + The type '{typeName}' must implement a `toFormField` proc: + `toFormField( + state: Viewable, + field: ptr {typeName} + fieldName: string, + ): Widget` + state: The State + field: The field's value to assign to/get the value from + fieldName: The name of the field on `state` for which the current form field is being generated + + Implementing the proc will override this dummy Widget. + See the playground module for examples. + """ + +proc toFormField(state: Viewable, field: ptr ObjectVariant, fieldName: string): Widget = + ## Provides a form field for any object variant type + return state.toPlaceHolderFormField(field, fieldName) + proc toFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = - ## Provides a fallback form field for any type that does not match any of the types above. + ## Provides a fallback form field for any type that does not match any of the other `toFormField` overloads. ## If the type has fields (e.g. object or tuple) it will generate a form with a formfield ## for each field on the type. ## If the type does not have fields it will generate a dummy-field with text informing the user to ## define their own `toFormField` proc. - const hasFields = field is ptr object or field is ptr ref object or field is ptr tuple or field is ptr ref tuple + const hasFields = field is ptr object or field is ptr ref object or field is ptr tuple or field is ptr ref tuple + when hasFields: var subFieldWidgets: Table[string, Widget] = initTable[string, Widget]() for subFieldName, subFieldValue in field[].getIterator(): @@ -242,26 +288,7 @@ proc toFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = insert(subFieldWidgets[subFieldName]) {.addRow.} else: - const typeName: string = $field[].type - return gui: - ActionRow: - title = fieldName - Label(): - text = fmt"Override `toFormField` for '{typeName}'" - tooltip = fmt""" - The type '{typeName}' must implement a `toFormField` proc: - `toFormField( - state: Viewable, - field: ptr {typeName} - fieldName: string, - ): Widget` - state: The State - field: The field's value to assign to/get the value from - fieldName: The name of the field on `state` for which the current form field is being generated - - Implementing the proc will override this dummy Widget. - See the playground module for examples. - """ + return state.toPlaceHolderFormField(field, fieldName) proc toAutoFormMenu*[T](app: T, sizeRequest: tuple[x,y: int] = (400, 700), ignoreFields: static seq[string]): Widget = ## Provides a form for every field in a given `app` instance. From 93afe60d2677540a5f24bd9e7377adb1666adf84 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sun, 5 Nov 2023 11:48:55 +0100 Subject: [PATCH 18/21] Change name of ObjectVariant concept The new name is ObjectVariantType. This serves as an important distinction that his is a concept that covers **types**, not **instances**. The doc comment was adjusted to clarify this as well. --- owlkettle/playground.nim | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 088eb6f9..4b7ebd81 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -32,7 +32,7 @@ type Range = concept r # Necessary as there is no range typeclass *for parameter r is range proc selfAssign[T](v: var T) = v = v -proc isObjectVariant(Obj: typedesc[object]): bool = +proc isObjectVariantType(Obj: typedesc[object]): bool = ## Checks if a given typedesc is of an object variant ## by checking if you can assign to a field. This is a ## compile-time error when doing this with the fieldPairs iterator @@ -44,9 +44,11 @@ proc isObjectVariant(Obj: typedesc[object]): bool = return false -type ObjectVariant = concept type V - ## A concept covering all object variant types - isObjectVariant(V) +type ObjectVariantType = concept type V + ## A concept covering all object variant types. + ## This concept is **not** intended for use with object variant **instances**, + ## as its implementation relies on type. + isObjectVariantType(V) # Forward Declarations so order of procs below doesn't matter proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget @@ -59,7 +61,7 @@ proc toFormField(state: Viewable, field: ptr[distinct], fieldName: string): Widg proc toFormField(state: Viewable, field: ptr tuple[x, y: int], fieldName: string): Widget proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widget proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget -proc toFormField(state: Viewable, field: ptr ObjectVariant, fieldName: string): Widget +proc toFormField(state: Viewable, field: ptr ObjectVariantType, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr auto, fieldName: string): Widget proc toFormField(state: Viewable, field: ptr SomeNumber, fieldName: string): Widget = @@ -261,7 +263,7 @@ proc toPlaceHolderFormField(state: Viewable, field: ptr[auto], fieldName: string See the playground module for examples. """ -proc toFormField(state: Viewable, field: ptr ObjectVariant, fieldName: string): Widget = +proc toFormField(state: Viewable, field: ptr ObjectVariantType, fieldName: string): Widget = ## Provides a form field for any object variant type return state.toPlaceHolderFormField(field, fieldName) From 5fcc0e551a4d5e297743eca7a0c588e686b38f39 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sun, 5 Nov 2023 15:36:37 +0100 Subject: [PATCH 19/21] Fix deletion not working --- owlkettle/playground.nim | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 4b7ebd81..2693a5d6 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -207,6 +207,9 @@ proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widg proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget = ## Provides a form field for any seq type + ## Enables adding new entries and deleting existing entries. + ## Note that deletion of an entry is only enabled if the `toFormField` proc for + ## the given field's type `T` is an ActionRow widget. let formFields = collect(newSeq): for index, value in field[]: toFormField(state, field[][index].addr, fmt"{fieldName} {index}") @@ -216,13 +219,14 @@ proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widg title = fieldName for index, formField in formFields: - Box(orient = OrientX) {.addRow.}: - insert(formField) - Button() {.expand: false.}: - icon = "user-trash-symbolic" - style = [ButtonDestructive] - proc clicked() = - field[].delete(index) + ActionRow() {.addRow.}: + insert(formField) {.addSuffix.} + if formField of ActionRow: # GTK allows only removing ActionRow Widgets from ExpanderRow + Button() {.expand: false.}: + icon = "user-trash-symbolic" + style = [ButtonDestructive] + proc clicked() = + field[].delete(index) ListBoxRow {.addRow.}: Button: @@ -261,6 +265,8 @@ proc toPlaceHolderFormField(state: Viewable, field: ptr[auto], fieldName: string Implementing the proc will override this dummy Widget. See the playground module for examples. + Note: You should prefer returning ActionRow, ExpanderRow, EntryRow or other + PreferencesRow-based Widgets. """ proc toFormField(state: Viewable, field: ptr ObjectVariantType, fieldName: string): Widget = From a5d55031e0f40fb37363a3037574c431b2f85dbc Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sun, 5 Nov 2023 16:01:04 +0100 Subject: [PATCH 20/21] Beautify how delete button gets added --- owlkettle/playground.nim | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 2693a5d6..1ec883da 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -205,28 +205,37 @@ proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widg proc select(enumIndex: int) = field[].position = enumIndex.ScalePosition +proc addDeleteButton(formField: Widget, value: ptr seq[auto], index: int) = + let button = gui: + Button(): + icon = "user-trash-symbolic" + style = [ButtonDestructive] + proc clicked() = + value[].delete(index) + + if formField of ActionRow: + ActionRow(formField).addSuffix(button) + elif formField of ExpanderRow: + ExpanderRow(formField).addRow(button) + proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widget = ## Provides a form field for any seq type ## Enables adding new entries and deleting existing entries. ## Note that deletion of an entry is only enabled if the `toFormField` proc for - ## the given field's type `T` is an ActionRow widget. + ## the given field's type `T` is an ActionRow or ExpanderRow widget. let formFields = collect(newSeq): for index, value in field[]: - toFormField(state, field[][index].addr, fmt"{fieldName} {index}") - + let formField = toFormField(state, field[][index].addr, fmt"{fieldName} {index}") + formField.addDeleteButton(field, index) + formField + return gui: ExpanderRow: title = fieldName for index, formField in formFields: - ActionRow() {.addRow.}: - insert(formField) {.addSuffix.} - if formField of ActionRow: # GTK allows only removing ActionRow Widgets from ExpanderRow - Button() {.expand: false.}: - icon = "user-trash-symbolic" - style = [ButtonDestructive] - proc clicked() = - field[].delete(index) + insert(formField) {.addRow.} + ListBoxRow {.addRow.}: Button: From 32901440555d16266e6e5cd5157f977de2917411 Mon Sep 17 00:00:00 2001 From: Philipp Doerner Date: Sun, 5 Nov 2023 17:42:02 +0100 Subject: [PATCH 21/21] Add support for ref object variants To do so the proc that checks it just needs to also accept ref objects and use the `getIterator` template to decide on whether or not to deref before using fieldPairs. --- owlkettle/playground.nim | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim index 1ec883da..8be597d4 100644 --- a/owlkettle/playground.nim +++ b/owlkettle/playground.nim @@ -31,19 +31,27 @@ import ./widgets type Range = concept r # Necessary as there is no range typeclass *for parameters*. So `field: ptr range` is not a valid parameter. r is range +template getIterator*(a: typed): untyped = + ## Provides a fieldPairs iterator for both ref-types and value-types + when a is ref: + a[].fieldPairs + + else: + a.fieldPairs + proc selfAssign[T](v: var T) = v = v -proc isObjectVariantType(Obj: typedesc[object]): bool = +proc isObjectVariantType(Obj: typedesc[object | ref object]): bool = ## Checks if a given typedesc is of an object variant ## by checking if you can assign to a field. This is a ## compile-time error when doing this with the fieldPairs iterator ## to an object variant "kind" field. var obj = default Obj - for name, field in obj.fieldpairs: + for name, field in obj.getIterator(): when not compiles(selfAssign field): return true return false - + type ObjectVariantType = concept type V ## A concept covering all object variant types. ## This concept is **not** intended for use with object variant **instances**, @@ -244,14 +252,6 @@ proc toFormField[T](state: Viewable, field: ptr seq[T], fieldName: string): Widg proc clicked() = field[].add(default(T)) -template getIterator*(a: typed): untyped = - ## Provides a fieldPairs iterator for both ref-types and value-types - when a is ref: - a[].fieldPairs - - else: - a.fieldPairs - proc toPlaceHolderFormField(state: Viewable, field: ptr[auto], fieldName: string): Widget = ## Provides a placeholder form field informing the user to implement their own ## `toFormField` proc, as none of the existing `toFormField` overloads could be applied.