3

So, I’m trying to add Customize support to packages that use a lot of plists, and where the value of pretty much any option can be either a single item or a list. I could have every single plist option’s value-type wrapped in a choice between “Single item” and “Multiple items,” but on an interface level that is cumbersome and counterintuitive. What I need is something like the specification (plist :options ((:meow (repeat (const purr))))), but: while multiple entries in the repeat will yield the overall value (:meow (purr purr …)), a single entry in the repeat should yield (:meow purr) instead of (:meow (purr)).

I tried making a simple version of this:

(defun tina/oom-value-set (widget value)
  (widget-put widget :inline (if (cdr value) nil t))
  (widget-default-value-set (widget value)))

(define-widget 'one-or-more 'repeat
  "Like ‘repeat’, but becomes inline when set with only one value.
Becomes non-inline when set with multiple values."
  :value-set 'tina/oom-value-set)

(defcustom tina/oom-test '(:meow)
  "Test my custom widget"
  :type '(plist :options ((:meow (one-or-more (const purr))))))

But, to my sadness, after customizing tina/oom-test, adding one “purr”, and saving it, the variable is now bound to (:meow (purr)), instead of (:meow purr) as I intended.

How can I get this working?

1

This doesn't take your approach to defining the widget, but if what you want is either the symbol meow or a list (meow purr...), with at least one purr, then I'd do it this way:

(defcustom cat1 'meow
  "..."
  :type '(choice
          (const :tag "Plain: meow" meow)
          (cons :tag "Happy: (meow purr...)" (const meow)
                (cons :tag "..." (const purr)
                      (repeat :tag "..." (const purr))))))

Or if you want to define a cat widget, then:

(define-widget 'cat 'lazy
  "..."
  :type '(choice
          (const :tag "Plain: meow" meow)
          (cons :tag "Happy: (meow purr...)" (const meow)
                (cons :tag "..." (const purr)
                      (repeat :tag "..." (const purr))))))

(defcustom cat1 'meow
  "..."
  :type 'cat)
  • Awwww, that is a nice example ☺ What I need is something more like either (meow purr), or (meow (purr purr)), (meow (purr purr purr)), etc. (Well, these values are going to be in a plist, so I guess it’s more like (:meow purr :woof (bark bark)), etc.) – Tina Russell Nov 27 '19 at 6:32
  • Really? I thought you said the exact opposite: "the variable is now bound to (meow (purr)), instead of (meow purr) as I intended." Based on that spec I produced (meow purr purr) instead of (meow (purr purr)). You indicated that you had already produce the latter but you wanted the former. So the question now seems unclear as stated. – Drew Nov 27 '19 at 15:48
  • It should be simple to do what you want, once it becomes clear what that is. You can probably change my code to do it, yourself. If, e.g., the base case should be (meow purr) instead of meow, and the next case should be (meow (purr purr)) instead of (meow purr purr), that's easy to fix. But so far, it's unclear what you're asking. – Drew Nov 27 '19 at 15:52
  • Okay, I should probably re-word it then. But yeah, I was hoping to do something like what you’re saying, but without having to offer a choice between “just one purr” and “multiple purrs”, and instead have the change happen automatically when you insert another purr. – Tina Russell Nov 27 '19 at 17:25
  • Also, (define-widget 'cat 'lazy is high-class programmer humor 😃 – Tina Russell Nov 27 '19 at 17:26
1

So, the problem with my implementation up there is that tina/oom-value-set never gets run, because the one-or-more widget’s :value-set function never gets run when setting the value of the custom variable. However, its :value-get function does get run during that process, so this actually works:

That didn’t actually work, because the :value-get function is used by code that sets up the customization widget as well as code that sets the custom variable itself. Either one or the other would get confused by a “list” that isn’t a list. So, this is where :value-to-internal and :value-to-external come in handy. Now, the code setting up the widget will see (:meow purr) as (:meow (purr)) and set up the widget appropriately, while the code setting the variable will see (:meow (purr)) as (:meow purr) and set the variable accordingly.

But wait! There’s more! We need a :match function, or else Customize will think purr is an invalid value for :meow. I made one that is pretty simple: if the match fails, it tries the match again, but with the value in a list. Now it all really, totally works. For sure.

(defun tina/oom-value-to-internal (_widget value)
  (if (listp value)
      value
    (list value)))

(defun tina/oom-value-to-external (_widget value)
  (if (and (listp value) (not (cdr value)))
      (car value)
    value))

(defun tina/oom-match (widget value)
  (or (widget-editable-list-match widget value)
      (widget-editable-list-match widget (list value))))

(define-widget 'one-or-more 'repeat
  "Like `repeat', but becomes inline when set with only one value.
Becomes non-inline when set with multiple values."
  :value-to-internal 'tina/oom-value-to-internal
  :value-to-external 'tina/oom-value-to-external
  :match 'tina/oom-match)

(defcustom tina/oom-test '(:meow nil)
  "Test my custom widget"
  :type '(plist :options ((:meow (one-or-more (const purr))))))

Now, if you add one purr and save the variable, the result is (:meow purr), and if you add more, the result is (:meow (purr purr)), (:meow (purr purr purr)), and so on.

Class dismissed!

  • You can accept your own answer, BTW. – Drew Nov 27 '19 at 21:49
  • I tried, but it says “you can accept your own answer tomorrow.” I guess there’s a waiting period, now – Tina Russell Nov 27 '19 at 23:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.