Subject: bug#4787: 23.1; in Customize, after showing Lisp expression, how to see normal?

From: Mauro Aranda <[email-masked]>
Date: Fri, 2 Oct 2020 12:24:19 -0300

"Drew Adams" <[email-masked]> writes:

> emacs -Q
> M-x customize-face default
> In Customize, click State > Show Lisp Expression.
>
> It now seems to be impossible to get back the normal display, i.e., to
> not see the Lisp expression anymore.
>
> In fact, the 3 menu items For Current Display, For All Displays, and
> Show Lisp Expression are 3 _alternatives_. They should be radio-button
> menu items, not just normal menu items.

I think it would be nice to display them as radio buttons.  But, this
menus in Custom are displayed with `widget-choose', and `widget-choose'
doesn't support menus in the so-called extended menu format.

So I took a look to see if that would be easy to do.  I attach a patch for
review.
If the feature is accepted, then it shouldn't be too hard to change
Custom to pass a menu with radio-buttons for these options.

> And the item names should be parallel. There is no understandable
> relation between "For Current Display", for instance, and "Show Lisp
> Expression". Those don't seem to have anything to do with each
> other. There is no way to guess that these are alternatives.

Perhaps "Customize Face For Current Display" and "Customize Face For All
Kinds of Display" is better? Kinda long, though.

Anyway, I attach a patch to add support for extended menus in
widget-choose, as a first step to add radio buttons for related options
in these Custom menus.
From e7422bf114c0cb1896f6c587420d49e9d0feedd3 Mon Sep 17 00:00:00 2001
From: Mauro Aranda <[email-masked]>
Date: Fri, 2 Oct 2020 11:56:57 -0300
Subject: [PATCH] Support extended menus in widget-choose

* lisp/wid-edit.el (widget--simplify-menu): New function, to convert
extended menus into simplified menus when using the menu to prompt
through the minibuffer.
(widget-choose): Accept a keymap menu.  When not using x-popup-menu,
simplify the menu with widget--simplify-menu.  Document the changes in
the docstring.

* doc/misc/widget.texi (Utilities): Document widget-choose.

* etc/NEWS: Document the feature.
---
 doc/misc/widget.texi | 22 +++++++++++
 etc/NEWS             |  5 +++
 lisp/wid-edit.el     | 89 ++++++++++++++++++++++++++++++++++++++++----
 3 files changed, 109 insertions(+), 7 deletions(-)

diff --git a/doc/misc/widget.texi b/doc/misc/widget.texi
index bccfa0ab6c..3ce27a12a0 100644
--- a/doc/misc/widget.texi
+++ b/doc/misc/widget.texi
@@ -1740,6 +1740,28 @@ Utilities
 This is only meaningful for radio buttons or checkboxes in a list.
 @end defun
 
+@defun widget-choose title items &optional event
+Prompt the user to choose an item from a list of options.
+
+@var{title} is the name of the list of options.  @var{items} should be
+a menu, with its items in the simple format or in the extended format.
+@xref{Defining Menus,, Defining Menus, elisp, the Emacs Lisp Reference
+Manual}.  Independently of the format, you don't have to provide a
+title for the menu, just pass the desired title in @var{title}.  The
+optional @var{event} is an input event.  If @var{event} is a mouse
+event and the number of elements in @var{items} is less than the user
+option @code{widget-menu-max-size}, then @code{widget-choose} uses a
+popup menu to prompt the user.  Otherwise, @code{widget-choose} uses
+the minibuffer.
+
+When @var{items} is a keymap menu, the returned value is the symbol in
+the key vector, as in the argument of @code{define-key}
+(@pxref{Changing Key Bindings,,,elisp, the Emacs Lisp Reference
+Manual}).  When @var{items} is a list whose selectable items are of
+the form (@var{name} . @var{value}) (i.e., the simplified format),
+then the return value is the @var{value} of the chosen element.
+@end defun
+
 @node  Widget Wishlist
 @chapter Wishlist
 @cindex todo
diff --git a/etc/NEWS b/etc/NEWS
index d5b1496bba..418fe25b21 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1086,6 +1086,11 @@ based on the current window size.  In previous versions of Emacs, this
 was always done (and that could lead to odd displays when resizing the
 window after starting).  This variable defaults to nil.
 
+** Widget
+
++++
+*** widget-choose now supports menus in extended format.
+
 ** Miscellaneous
 
 ---
diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index 0a2ddb0ea1..6568cd2c8f 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -203,27 +203,100 @@ widget-menu-minibuffer-flag
   :group 'widgets
   :type 'boolean)
 
+(defun widget--simplify-menu (extended)
+  "Convert the EXTENDED menu into a menu composed of simple menu items.
+
+Each item in the simplified menu is of the form (ITEM-STRING . REAL-BINDING),
+where both elements are taken from the EXTENDED MENU.  ITEM-STRING is the
+correspondent ITEM-NAME in the menu-item entry:
+ (menu-item ITEM-NAME REAL-BINDING . ITEM-PROPERTY-LIST), and REAL-BINDING is
+the symbol in the key vector, as in `define-key'.
+ (See `(elisp)Defining Menus' for more information.)
+
+Only visible, enabled and meaningful menu items make their way into
+the returned simplified menu.  That is:
+For the menu item to be visible, it has to either lack a :visible form in its
+item-property-list, or the :visible form has to evaluate to a non-nil value.
+For the menu item to be enabled, it has to either lack a :enabled form in its
+item-property-list, or the :enable form has to evaluate to a non-nil value.
+Additionally, if the menu item is a radio button, then its selected form has
+to evaluate to nil for the menu item to be meaningful."
+  (let (simplified)
+    (map-keymap (lambda (ev def)
+                  (when (and (eq (nth 0 def) 'menu-item)
+                             (nth 2 def)) ; Only menu-items with a real binding.
+                    ;; Loop through the item-property-list, looking for
+                    ;; :visible, :enable (or :active) and :button properties.
+                    (let ((plist (nthcdr 3 def))
+                          (enable t) ; Enabled by default.
+                          (visible t) ; Visible by default.
+                          selected keyword value)
+                      (while (and plist (cdr plist)
+                                  (keywordp (setq keyword (car plist))))
+                        (setq value (cadr plist))
+                        (cond ((memq keyword '(:visible :included))
+                               (setq visible value))
+                              ((memq keyword '(:enable :active))
+                               (setq enable value))
+                              ((and (eq keyword :button)
+                                    (eq (car value) :radio))
+                               (setq selected (cdr value))))
+                        (setq plist (cddr plist)))
+                      (when (and (eval visible)
+                                 (eval enable)
+                                 (or (not selected)
+                                     (not (eval selected))))
+                        (push (cons (nth 1 def) ev) simplified)))))
+                extended)
+    (reverse simplified)))
+
 (defun widget-choose (title items &optional event)
   "Choose an item from a list.
 
 First argument TITLE is the name of the list.
-Second argument ITEMS is a list whose members are either
+Second argument ITEMS should be a menu, either with simple item definitions,
+or with extended item definitions.
+When ITEMS has simple item definitions, it is a list whose members are either
  (NAME . VALUE), to indicate selectable items, or just strings to
  indicate unselectable items.
+
+When ITEMS is a menu that uses an extended format, then ITEMS should be a
+keymap, and each binding should look like this:
+ (menu-item ITEM-NAME REAL-BINDING . ITEM-PROPERTY-LIST)
+or like this: (menu-item ITEM-NAME) to indicate a non-selectable item.
+REAL-BINDING should be a symbol, and should not be a keymap, because submenus
+are not supported.
+
 Optional third argument EVENT is an input event.
 
-The user is asked to choose between each NAME from the items alist,
-and the VALUE of the chosen element will be returned.  If EVENT is a
-mouse event, and the number of elements in items is less than
+If EVENT is a mouse event, and the number of elements in items is less than
 `widget-menu-max-size', a popup menu will be used, otherwise the
-minibuffer."
+minibuffer.
+
+The user is asked to choose between each NAME from ITEMS.
+If ITEMS has simple item definitions, then this function returns the VALUE of
+the chosen element.  If ITEMS is a keymap, then the return value is the symbol
+in the key vector, as in the argument of `define-key'."
   (cond ((and (< (length items) widget-menu-max-size)
 	      event (display-popup-menus-p))
 	 ;; Mouse click.
-	 (x-popup-menu event
-		       (list title (cons "" items))))
+         (if (keymapp items)
+             ;; Modify the keymap prompt, and then restore the old one, if any.
+             (let ((prompt (keymap-prompt items)))
+               (unwind-protect
+                   (progn
+                     (setq items (delete prompt items))
+                     (push title (cdr items))
+                     ;; Return just the first element of the list of events.
+                     (car (x-popup-menu event items)))
+                 (setq items (delete title items))
+                 (when prompt
+                   (push prompt (cdr items)))))
+	   (x-popup-menu event (list title (cons "" items)))))
 	((or widget-menu-minibuffer-flag
 	     (> (length items) widget-menu-max-shortcuts))
+         (when (keymapp items)
+           (setq items (widget--simplify-menu items)))
 	 ;; Read the choice of name from the minibuffer.
 	 (setq items (cl-remove-if 'stringp items))
 	 (let ((val (completing-read (concat title ": ") items nil t)))
@@ -233,6 +306,8 @@ widget-choose
 		   (setq val try))
 		 (cdr (assoc val items))))))
 	(t
+         (when (keymapp items)
+           (setq items (widget--simplify-menu items)))
 	 ;; Construct a menu of the choices
 	 ;; and then use it for prompting for a single character.
 	 (let* ((next-digit ?0)
-- 
2.28.0