Pointful? : A mixin+interface for finding identified children

I'm running into a challenge with the GUI, I have a solution that I am considering putting into a module and posting on the package server. I'd like some feedback on whether this seems useful or if there's a better way.

tl;dr: Widgets get created in a specific order, which means that the ones created earlier cannot easily talk to the ones created later. I'm considering a mixin that allows asking a container for a specified child.

Long version:

I'm building a dialog with multiple controls on it, and I find myself wanting to let the various controls interact -- for example, the validator for the text field should be able to enable/disable the OK button and the OK button should be able to retrieve the value of the text field. This produces dependencies based on the order in which the various controls are instantiated, for example:

#lang racket/gui
(define diag (new dialog% ...))
(define text-fld
   (new text-field% 
     [parent diag]
     [callback (lambda (fld e) 
                 (send ok-btn enable #t))]  ; NOPE!
     ...)

(define ok-btn
  (new button% 
    [parent diag]
    [callback (lambda (btn e) 
                (displayln (send text-fld get-value)))] ; Ok
    ...)

The 'NOPE!' line is a compile time error because ok-btn is still an undefined identifier at that point. Reversing the order of the declarations doesn't help, it simply means that the ok button's callback is invalid instead of the text-field's.

I have come up with two ways to solve this. The first is an ugly hack:

#lang racket/gui

(define ok-btn-forward-reference #f)

(define diag (new dialog% [parent #f] [label "ok"]))
(define text-fld
  (new text-field%
       [parent diag]
       [label "text field"]
       [callback (lambda (fld e)
                   (display "before setting, ok enabled? ")
                   (displayln (send ok-btn-forward-reference is-enabled?))
                   (send ok-btn-forward-reference enable #t)
                   (display "after setting, ok enabled? ")
                   (displayln (send ok-btn-forward-reference is-enabled?)))]))

(define ok-btn
  (new button%
       [parent diag]
       [enabled #f]
       [label "Ok"]
       [callback (lambda (btn e)
                   (display "text field value is: ")
                   (displayln (send text-fld get-value))
                   (send diag show #f))]))
(set! ok-btn-forward-reference ok-btn)
(send diag show #t)

Here, I create an invalid forward reference that the text field's callback can use, then later set! the reference to be valid. It works, but it's ugly.

Another solution would be to create a subclass of dialog% that can locate and return the needed child:

#lang racket/gui

(define child-finder-dialog%
  (class dialog%
    (super-new)
    (define/public (get-ok-button)
      (define kids (send this get-children))
      (list-ref kids 1))))

(define diag
  (new child-finder-dialog%
   [parent #f]
   [label "ok"]))

(define text-fld
  (new text-field%
       [parent diag]
       [label "text field"]
       [callback (lambda (fld e)
                   (define ok-btn (send diag get-ok-button))
                   (display "before setting, ok enabled? ")
                   (displayln (send ok-btn is-enabled?))
                   (send ok-btn enable #t)
                   (display "after setting, ok enabled? ")
                   (displayln (send ok-btn is-enabled?)))]))

(define ok-btn
  (new button%
       [parent diag]
       [enabled #f]
       [label "Ok"]
       [callback (lambda (btn e)
                   (display "text field value is: ")
                   (displayln (send text-fld get-value))
                   (send diag show #f))]))

(send diag show #t)

Obviously, if we were going to do this then it would be better to use a generic get-specific-child method that can return any desired child instead of only the second one. Maybe its argument could be a predicate and it returns the first child that matches the predicate, the same way findf works on lists. Or maybe there should be a mixin that can be applied to any class in order to add a "name-tag" field which the get-specific-child method can search for. (Calling it 'name-tag' instead of 'id' in order to make it less likely to clash with existing fields.)

Also, it would be better if this was a mixin so as to make it easier to use. Perhaps with an interface such as child-finder<%> that makes it easy to tell if the method is available.

What do people think? Is this a sensible thing to do?

EDIT: Another issue would be whether the search for the relevant child should be single-level, depth-first, or breadth-first.

Could you bind text-fld and ok-btn in a letrec or similar? I
haven't tried this myself but it seems plausible:

(define-values (text-field ok-button)
  (letrec ([text-fld …] [ok-btn …])
    (values text-fld ok-btn)))

I used define-values to bind them at the top-level if you needed that.

Now I'm wondering how gui-easy would handle this… if the dependency
were data driven, a shared piece of state (an observable) that could
be read or written might work. With gui-easy one often constructs
components via functions that take callbacks to invoke, so you might
have

(define/obs @button-enabled? #f)
(define/obs @input "")
(dialog
  (text-field @input #:on-input (lambda (inp) (:= @input inp))
#:callback (lambda () (:= @button-enabled? #t)))
  (ok-button #:enabled? @button-enabled? (lambda () (displayln
(obs-peek @input)))))

(pseudo-code written in an email, so please forgive typos)

Oh! Interesting. Yes, letrec works fine for this case. I didn't think of that.

With the immediate problem solved, does it seem like the 'find desired child' functionality would be useful enough to be worth creating for general use?

EDIT: Assuming I'm understanding it correctly, it looks like the gui-easy code you're showing above does what I showed in the first example -- it creates a pair of forward references and then later it set!s those references.

What am I missing? The following compiles just fine for me:

#lang racket/gui

(define diag (new dialog%
                  [label "foo"]))

(define text-fld
   (new text-field% 
        [parent diag]
        [label "text"]
        [callback (lambda (fld e) 
                    (send ok-btn enable #t))]))

(define ok-btn
  (new button% 
       [parent diag]
       [label "button"]
       [callback (lambda (btn e) 
                   (displayln (send text-fld get-value)))]))

What.

goes off and tries it

...

Okay... I swear that I was having the issue I described, and now I'm not. What the hell? Apparently I'm losing my mind.

Well, thank you for setting me straight.

No worries! I was confused when I saw this because I write similar code all the time without any problems. At first I thought that perhaps it was a top-level issue, but apparently not.

1 Like

RE: gui-easy, I think you're correct, but the "effects" are slightly hidden through the "observable" abstraction, which is more than just "mutable state" (since the mutations can be observed and reacted to).

The original example that you posted does not have a compiler error, as @jjsimpso showed, but a small variation of it does not work:

#lang racket/gui
(define diag (new dialog%  [label "foo"]))

(define text-fld
   (new text-field% 
        [parent diag]
        [label "text"]
        [init-value (format "OK Button is ~a" (if (send ok-btn is-enabled?) "enabled" "disabled"))]
        [callback (lambda (fld e) 
                    (send ok-btn enable #t))]))

(define ok-btn
  (new button% 
       [parent diag]
       [label "button"]
       [enabled #f]
       [callback (lambda (btn e) 
                   (displayln (send text-fld get-value)))]))

To provide an initial value of the text field based on the state of ok-btn, you need to do something like this:

#lang racket/gui
(define diag (new dialog%  [label "foo"]))

(define text-fld
   (new text-field% 
        [parent diag]
        [label "text"]
        [callback (lambda (fld e) 
                    (send ok-btn enable #t))]))

(define ok-btn
  (new button% 
       [parent diag]
       [label "button"]
       [enabled #f]
       [callback (lambda (btn e) 
                   (displayln (send text-fld get-value)))]))

(send text-fld set-value (format "OK Button is ~a" (if (send ok-btn is-enabled?) "enabled" "disabled")))

I did run into initialization/declaration order problems when creating GUI controls before, and the solution usually involves creating the control than modifying some of its parameters later.

Alex.

1 Like