Subject: bug#42887: 28.0.50; Customize Group doesn't show/hide option on repeated clicking

From: Mauro Aranda <[email-masked]>
Date: Sat, 22 Mar 2025 05:41:14 -0300

On 14/3/25 08:55, Mauro Aranda wrote:
 >
 > I've improved my fix for this bug, so that it doesn't produce weird
 > highlights when double clicking a button.
 >
 > The patch touches a very tricky part of wid-edit.el.  I've been testing
 > it for a while now, mostly with Customize and some packages of my own.
 > I haven't found many problems, but I figure it might need some testing
 > in the outside world.
 >

I've added documentation changes.  New patch attached.

From 4fc341a5a48f743b25edfc5a1facff080f2beaad Mon Sep 17 00:00:00 2001
From: Mauro Aranda <[email-masked]>
Date: Tue, 11 Mar 2025 08:26:11 -0300
Subject: [PATCH] Improve click handling in buffers with widgets

This change improves handling of double-click and triple-click
events on widgets.  (Bug#42887)
It also attempts to separate the handling of down-mouse events
from the up mouse events.  This is desired, and has been
attempted before.  (Bug#19185, Bug#20398, Bug#23571)

* lisp/wid-edit.el (widget-down-mouse): New function, to handle
down-mouse-1 and down-mouse-2 events.
(widget-button-release-event-p): Consider double and triple
modifiers as release events too.
(widget-button-click): Adapt it so that it only handles up
events.
(widget-button--check-and-call-button): Do not run the
mouse-down-action function here, since this is a helper for
widget-button-click, which now only handles up events.
(widget-keymap): Update bindings.

* doc/misc/widget.texi (Widgets and the Buffer): Adapt
documentation of widget-button-click to reflect the changes.
Document widget-down-mouse.
(User Interface): Mention widget-down-mouse.
---
 doc/misc/widget.texi |  30 ++++++---
 lisp/wid-edit.el     | 147 ++++++++++++++++++++++++++++++-------------
 2 files changed, 126 insertions(+), 51 deletions(-)

diff --git a/doc/misc/widget.texi b/doc/misc/widget.texi
index 864489726c8..10c67d2ea56 100644
--- a/doc/misc/widget.texi
+++ b/doc/misc/widget.texi
@@ -219,11 +219,11 @@ User Interface
 @cindex widget buttons
 @cindex button widgets
 Some portions of the buffer have an associated @dfn{action}, which can
-be @dfn{invoked} by a standard key or mouse command.  These portions
-are called @dfn{buttons}.  The default commands for activating a button
-are @code{widget-button-press} and @code{widget-button-click}.  The
-user typically interacts with the buttons with a key, like @key{RET},
-or with the mouse buttons.
+be @dfn{invoked} by a standard key or mouse command.  These portions are
+called @dfn{buttons}.  The default commands for activating a button are
+@code{widget-button-press}, @code{widget-button-click} and
+@code{widget-down-mouse}.  The user typically interacts with the buttons
+with a key, like @key{RET}, or with the mouse buttons.
 
 There are several different kind of buttons, all of which are present in
 the example:
@@ -765,8 +765,9 @@ Widgets and the Buffer
 
 Binds @key{TAB} to @code{widget-forward} and both @kbd{S-@key{TAB}} and
 @kbd{M-@key{TAB}} to @code{widget-backward}.  It also binds @key{RET} to
-@code{widget-button-press} and both @kbd{down-mouse-1} and
-@kbd{down-mouse-2} to @code{widget-button-click}.
+@code{widget-button-press}, both @kbd{mouse-1} and @kbd{mouse-2} to
+@code{widget-button-click} and both @kbd{down-mouse-1} and
+@kbd{down-mouse-2} to @code{widget-down-mouse}.
 @end defvar
 
 There's also a keymap for events that the Widget library doesn't need
@@ -847,15 +848,26 @@ Widgets and the Buffer
 
 @kindex mouse-2 @r{(on button widgets})
 @item mouse-2
+@itemx mouse-1
 @findex widget-button-click
 @deffn Command widget-button-click @var{event}
 Invoke the button at the location of the mouse pointer.
 
 If the mouse pointer is located in an editable text field, invoke the
 binding in @code{widget-global-map} (by default the global map).
+@end deffn
 
-In case the mouse-click is on a widget, calls the function stored in
-the @code{:mouse-down-action} property.
+@kindex down-mouse-1 @r{(on button widgets)}
+@item down-mouse-1
+@itemx down-mouse-2
+@findex widget-down-mouse
+@deffn Command widget-down-mouse @var{event}
+React to the down-mouse event @var{event}.
+
+If the down-mouse event happens on a button, run its
+@code{:mouse-down-action} function.  If not, run the command that
+@var{event} would run in any other case, by looking it up in
+@code{widget-global-map}.
 @end deffn
 @end table
 
diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index db241ca914a..6d619e8937f 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -72,8 +72,9 @@ widget-button-release-event-p
   "Non-nil if EVENT is a mouse-button-release event object."
   (and (eventp event)
        (memq (event-basic-type event) '(mouse-1 mouse-2 mouse-3))
-       (or (memq 'click (event-modifiers event))
-	   (memq  'drag (event-modifiers event)))))
+       (seq-some (lambda (el)
+                   (memq el '(click drag triple double)))
+                 (event-modifiers event))))
 
 ;;; Customization.
 
@@ -1102,8 +1103,10 @@ widget-keymap
     (define-key map [(shift tab)] 'widget-backward)
     (put 'widget-backward :advertised-binding [(shift tab)])
     (define-key map [backtab] 'widget-backward)
-    (define-key map [down-mouse-2] 'widget-button-click)
-    (define-key map [down-mouse-1] 'widget-button-click)
+    (define-key map [mouse-2] 'widget-button-click)
+    (define-key map [mouse-1] 'widget-button-click)
+    (define-key map [down-mouse-1] 'widget-down-mouse)
+    (define-key map [down-mouse-2] 'widget-down-mouse)
     (define-key map [touchscreen-begin] 'widget-button-click)
     ;; The following definition needs to avoid using escape sequences that
     ;; might get converted to ^M when building loaddefs.el
@@ -1208,33 +1211,31 @@ widget-button--check-and-call-button
                                 ;; Report the current position of point
                                 ;; to the catch block.
                                 (throw 'button-press-cancelled (point))))
-                          (unless (widget-apply button :mouse-down-action event)
-                            (let ((track-mouse t))
-                              (while (not (widget-button-release-event-p event))
-                                (setq event (read--potential-mouse-event))
-                                (when (and mouse-1 (mouse-movement-p event))
-                                  (push event unread-command-events)
-                                  (setq event oevent)
-                                  (throw 'button-press-cancelled nil))
-                                (unless (or (integerp event)
-                                            (memq (car event)
-                                                  '(switch-frame select-window))
-                                            (eq (car event)
-                                                'scroll-bar-movement))
-                                  (setq pos (widget-event-point event))
-                                  (if (and pos
-                                           (eq (get-char-property pos 'button)
-                                               button))
-                                      (when face
-                                        (overlay-put overlay
-                                                     'face pressed-face)
-                                        (overlay-put overlay
-                                                     'mouse-face pressed-face))
-                                    (overlay-put overlay
-                                                 'face face)
-                                    (overlay-put overlay
-                                                 'mouse-face mouse-face)))))))
-
+                          (let ((track-mouse t))
+                            (while (not (widget-button-release-event-p event))
+                              (setq event (read--potential-mouse-event))
+                              (when (and mouse-1 (mouse-movement-p event))
+                                (push event unread-command-events)
+                                (setq event oevent)
+                                (throw 'button-press-cancelled nil))
+                              (unless (or (integerp event)
+                                          (memq (car event)
+                                                '(switch-frame select-window))
+                                          (eq (car event)
+                                              'scroll-bar-movement))
+                                (setq pos (widget-event-point event))
+                                (if (and pos
+                                         (eq (get-char-property pos 'button)
+                                             button))
+                                    (when face
+                                      (overlay-put overlay
+                                                   'face pressed-face)
+                                      (overlay-put overlay
+                                                   'mouse-face pressed-face))
+                                  (overlay-put overlay
+                                               'face face)
+                                  (overlay-put overlay
+                                               'mouse-face mouse-face))))))
 		        ;; When mouse is released over the button, run
 		        ;; its action function.
 		        (when (and pos (eq (get-char-property pos 'button)
@@ -1259,7 +1260,7 @@ widget-button-click
   "Invoke the button that the mouse is pointing at."
   (interactive "e")
   (if (widget-event-point event)
-      (let* ((mouse-1 (memq (event-basic-type event) '(mouse-1 down-mouse-1)))
+      (let* ((mouse-1 (eq (event-basic-type event) 'mouse-1))
 	     (pos (widget-event-point event))
 	     (start (event-start event))
              (button (get-char-property
@@ -1276,16 +1277,8 @@ widget-button-click
             (cond
              ((eq (car event) 'touchscreen-begin)
               (setq command 'touch-screen-handle-touch))
-             (mouse-1 (cond ((setq command	;down event
-                                   (lookup-key widget-global-map [down-mouse-1]))
-                             (setq up nil))
-                            ((setq command	;up event
-                                   (lookup-key widget-global-map [mouse-1])))))
-             (t (cond ((setq command	;down event
-                             (lookup-key widget-global-map [down-mouse-2]))
-                       (setq up nil))
-                      ((setq command	;up event
-                             (lookup-key widget-global-map [mouse-2]))))))
+             (mouse-1 (setq command (lookup-key widget-global-map [mouse-1])))
+             (t (setq command (lookup-key widget-global-map [mouse-2]))))
 	    (when up
 	      ;; Don't execute up events twice.
 	      (while (not (and (widget-button-release-event-p event)))
@@ -1294,6 +1287,76 @@ widget-button-click
 	      (call-interactively command)))))
     (message "You clicked somewhere weird.")))
 
+(defun widget-down-mouse (event)
+  "React to down-mouse event EVENT.
+
+If down-mouse is on a button, change its face and run its :mouse-down-action
+function.
+
+If down-mouse is not on a button, run the command EVENT would run in any other
+case."
+  (interactive "e")
+  (if (widget-event-point event)
+      (let* ((mouse-1 (memq (event-basic-type event) '(mouse-1 down-mouse-1)))
+             (pos (widget-event-point event))
+             (start (event-start event))
+             (button (get-char-property
+                      pos 'button (and (windowp (posn-window start))
+                                       (window-buffer (posn-window start))))))
+        (save-selected-window
+          (select-window (posn-window (event-start event)))
+          (when button
+            (let* ((ov (widget-get button :button-overlay))
+                   (pressed-face (or (widget-get button :pressed-face)
+                                     widget-button-pressed-face))
+                   (face (overlay-get ov 'face))
+                   (mouse-face (overlay-get ov 'mouse-face))
+                   (oevent event)
+                   canceled)
+              (unwind-protect
+                  (progn
+                    (when face
+                      (overlay-put ov 'face pressed-face)
+                      (overlay-put ov 'mouse-face pressed-face))
+                    (unless (widget-apply button :mouse-down-action event)
+                      (let ((track-mouse t))
+                        (while (not (widget-button-release-event-p event))
+                          (setq event (read--potential-mouse-event))
+                          (when (and mouse-1 (mouse-movement-p event)
+                                     (not canceled))
+                            (push event unread-command-events)
+                            (setq event oevent)
+                            (setq canceled t))
+                          (unless (or canceled
+                                      (integerp event)
+                                      (memq (car event)
+                                            '(switch-frame select-window))
+                                      (eq (car event) 'scroll-bar-movement))
+                            (setq pos (widget-event-point event))
+                            (if (and pos
+                                     (eq (get-char-property pos 'button)
+                                         button))
+                                (when face
+                                  (overlay-put ov 'face pressed-face)
+                                  (overlay-put ov 'mouse-face pressed-face))
+                              (overlay-put ov 'face face)
+                              (overlay-put ov 'mouse-face mouse-face)))))))
+                (overlay-put ov 'face face)
+                (overlay-put ov 'mouse-face mouse-face))
+              (when (and (memq (event-basic-type event) '(mouse-1 mouse-2))
+                         (not (memq 'down (event-modifiers event))))
+                (push event unread-command-events))))
+          (let (command)
+            (cond
+             ((eq (car event) 'touchscreen-begin)
+              (setq command 'touch-screen-handle-touch))
+             (mouse-1 (setq command
+                            (lookup-key widget-global-map [down-mouse-1])))
+             (t (setq command (lookup-key widget-global-map [down-mouse-2]))))
+            (when command
+              (call-interactively command)))))
+    (message "You clicked somewhere weird.")))
+
 ;; Make sure `touch-screen-handle-touch' abstains from emulating
 ;; down-mouse-1 events for `widget-button-click'.
 
-- 
2.34.1