Browse Source

Add `org-toggle-radio-button' and related minor mode

* doc/org-manual.org (Checkboxes): Document the new minor mode
and command.

* lisp/org-keys.el (org-mode-map): Bind C-c C-x C-r to
`org-toggle-radio-button'.

* lisp/org-list.el (org-list-checkbox-radio-mode): New minor
mode to let C-c C-c call `org-toggle-radio-button' instead of
`org-toggle-checkbox'.
(org-toggle-radio-button): New command.

* lisp/org.el (org-ctrl-c-ctrl-c): Use `org-toggle-radio-button'.

* etc/ORG-NEWS: Document the new minor mode and command.

Thanks to Phil Sainty for sharing this idea and links to similar
implementations.
Bastien 1 year ago
parent
commit
561feb128d
5 changed files with 135 additions and 67 deletions
  1. 15 0
      doc/org-manual.org
  2. 10 0
      etc/ORG-NEWS
  3. 2 0
      lisp/org-keys.el
  4. 35 0
      lisp/org-list.el
  5. 73 67
      lisp/org.el

+ 15 - 0
doc/org-manual.org

@@ -4531,6 +4531,21 @@ The following commands work with checkboxes:
 
   - If there is no active region, just toggle the checkbox at point.
 
+- {{{kbd(C-c C-x C-r)}}} (~org-toggle-radio-button~) ::
+
+  #+kindex: C-c C-x C-r
+  #+findex: org-toggle-radio-button
+  #+cindex: radio button, checkbox as
+  Toggle checkbox status by using the checkbox of the item at point as
+  a radio button: when turned on, all other checkboxes on the same
+  level will be turned off.  With a universal prefix argument, toggle
+  the presence of the checkbox.  With double prefix argument, set it
+  to =[-]=.
+
+  #+findex: org-list-checkbox-radio-mode
+  {{{kdb(C-c C-c)}}} can be told to consider checkboxes as radio buttons
+  by calling {{{kbd(M-x org-list-checkbox-radio-mode)}}}, as minor mode.
+
 - {{{kbd(M-S-RET)}}} (~org-insert-todo-heading~) ::
 
   #+kindex: M-S-RET

+ 10 - 0
etc/ORG-NEWS

@@ -44,6 +44,16 @@ You can activate this minor mode by default by setting the option
 ~org-table-header-line-p~ to =t=.  You can also change the face for
 the header line by customizing the ~org-table-header~ face.
 
+*** New minor mode ~org-list-checkbox-radio-mode~
+
+When this minor mode is on, checkboxes behave as radio buttons: if a
+checkbox is turned on, other checkboxes at the same level are turned
+off.
+
+If you want to occasionally toggle a checkbox as a radio button
+without turning this minor mode on, you can use =<C-c C-x C-r>= to
+call ~org-toggle-radio-button~.
+
 *** Looping agenda commands over headlines
 
 ~org-agenda-loop-over-headlines-in-active-region~ allows you to loop

+ 2 - 0
lisp/org-keys.el

@@ -196,6 +196,7 @@
 (declare-function org-todo "org" (&optional arg1))
 (declare-function org-toggle-archive-tag "org" (&optional find-done))
 (declare-function org-toggle-checkbox "org" (&optional toggle-presence))
+(declare-function org-toggle-radio-button "org" (&optional arg))
 (declare-function org-toggle-comment "org" ())
 (declare-function org-toggle-fixed-width "org" ())
 (declare-function org-toggle-inline-images "org" (&optional include-linked))
@@ -658,6 +659,7 @@ COMMANDS is a list of alternating OLDDEF NEWDEF command names."
 (org-defkey org-mode-map (kbd "C-c C-x C-M-v") #'org-redisplay-inline-images)
 (org-defkey org-mode-map (kbd "C-c C-x \\") #'org-toggle-pretty-entities)
 (org-defkey org-mode-map (kbd "C-c C-x C-b") #'org-toggle-checkbox)
+(org-defkey org-mode-map (kbd "C-c C-x C-r") #'org-toggle-radio-button)
 (org-defkey org-mode-map (kbd "C-c C-x p") #'org-set-property)
 (org-defkey org-mode-map (kbd "C-c C-x P") #'org-set-property-and-value)
 (org-defkey org-mode-map (kbd "C-c C-x e") #'org-set-effort)

+ 35 - 0
lisp/org-list.el

@@ -2302,6 +2302,41 @@ is an integer, 0 means `-', 1 means `+' etc.  If WHICH is
         (org-list-struct-fix-ind struct parents)
         (org-list-struct-apply-struct struct old-struct)))))
 
+;;;###autoload
+(define-minor-mode org-list-checkbox-radio-mode
+  "When turned on, use list checkboxes as radio buttons."
+  nil " CheckBoxRadio" nil
+  (unless (eq major-mode 'org-mode)
+    (user-error "Cannot turn this mode outside org-mode buffers")))
+
+(defun org-toggle-radio-button (&optional arg)
+  "Toggle off all checkboxes and toggle on the one at point."
+  (interactive "P")
+  (if (not (org-at-item-p))
+      (user-error "Cannot toggle checkbox outside of a list")
+    (let* ((cpos (org-in-item-p))
+	   (struct (org-list-struct))
+	   (orderedp (org-entry-get nil "ORDERED"))
+	   (parents (org-list-parents-alist struct))
+	   (old-struct (copy-tree struct))
+	   (cbox (org-list-get-checkbox cpos struct))
+           (prevs (org-list-prevs-alist struct))
+	   (start (org-list-get-list-begin (point-at-bol) struct prevs))
+	   (new (unless (and cbox (equal arg '(4)) (equal start cpos))
+		  "[ ]")))
+      (dolist (pos (org-list-get-all-items
+		    start struct (org-list-prevs-alist struct)))
+	(org-list-set-checkbox pos struct new))
+      (when new
+	(org-list-set-checkbox
+	 cpos struct
+	 (cond ((equal arg '(4)) (unless cbox "[ ]"))
+	       ((equal arg '(16)) (unless cbox "[-]"))
+	       (t (if (equal cbox "[X]") "[ ]" "[X]")))))
+      (org-list-struct-fix-box struct parents prevs orderedp)
+      (org-list-struct-apply-struct struct old-struct)
+      (org-update-checkbox-count-maybe))))
+
 (defun org-toggle-checkbox (&optional toggle-presence)
   "Toggle the checkbox in the current line.
 

+ 73 - 67
lisp/org.el

@@ -17217,39 +17217,79 @@ This command does many different things, depending on context:
 	 ;; unconditionally, whereas `C-u' will toggle its presence.
 	 ;; Without a universal argument, if the item has a checkbox,
 	 ;; toggle it.  Otherwise repair the list.
-	 (let* ((box (org-element-property :checkbox context))
-		(struct (org-element-property :structure context))
-		(old-struct (copy-tree struct))
-		(parents (org-list-parents-alist struct))
-		(prevs (org-list-prevs-alist struct))
-		(orderedp (org-not-nil (org-entry-get nil "ORDERED"))))
-	   (org-list-set-checkbox
-	    (org-element-property :begin context) struct
-	    (cond ((equal arg '(16)) "[-]")
-		  ((and (not box) (equal arg '(4))) "[ ]")
-		  ((or (not box) (equal arg '(4))) nil)
-		  ((eq box 'on) "[ ]")
-		  (t "[X]")))
-	   ;; Mimic `org-list-write-struct' but with grabbing a return
-	   ;; value from `org-list-struct-fix-box'.
-	   (org-list-struct-fix-ind struct parents 2)
-	   (org-list-struct-fix-item-end struct)
-	   (org-list-struct-fix-bul struct prevs)
-	   (org-list-struct-fix-ind struct parents)
-	   (let ((block-item
-		  (org-list-struct-fix-box struct parents prevs orderedp)))
-	     (if (and box (equal struct old-struct))
-		 (if (equal arg '(16))
-		     (message "Checkboxes already reset")
-		   (user-error "Cannot toggle this checkbox: %s"
-			       (if (eq box 'on)
-				   "all subitems checked"
-				 "unchecked subitems")))
-	       (org-list-struct-apply-struct struct old-struct)
-	       (org-update-checkbox-count-maybe))
-	     (when block-item
-	       (message "Checkboxes were removed due to empty box at line %d"
-			(org-current-line block-item))))))
+	 (if (and (boundp org-list-checkbox-radio-mode)
+		  org-list-checkbox-radio-mode)
+	     (org-toggle-radio-button arg)
+	   (let* ((box (org-element-property :checkbox context))
+		  (struct (org-element-property :structure context))
+		  (old-struct (copy-tree struct))
+		  (parents (org-list-parents-alist struct))
+		  (prevs (org-list-prevs-alist struct))
+		  (orderedp (org-not-nil (org-entry-get nil "ORDERED"))))
+	     (org-list-set-checkbox
+	      (org-element-property :begin context) struct
+	      (cond ((equal arg '(16)) "[-]")
+		    ((and (not box) (equal arg '(4))) "[ ]")
+		    ((or (not box) (equal arg '(4))) nil)
+		    ((eq box 'on) "[ ]")
+		    (t "[X]")))
+	     ;; Mimic `org-list-write-struct' but with grabbing a return
+	     ;; value from `org-list-struct-fix-box'.
+	     (org-list-struct-fix-ind struct parents 2)
+	     (org-list-struct-fix-item-end struct)
+	     (org-list-struct-fix-bul struct prevs)
+	     (org-list-struct-fix-ind struct parents)
+	     (let ((block-item
+		    (org-list-struct-fix-box struct parents prevs orderedp)))
+	       (if (and box (equal struct old-struct))
+		   (if (equal arg '(16))
+		       (message "Checkboxes already reset")
+		     (user-error "Cannot toggle this checkbox: %s"
+				 (if (eq box 'on)
+				     "all subitems checked"
+				   "unchecked subitems")))
+		 (org-list-struct-apply-struct struct old-struct)
+		 (org-update-checkbox-count-maybe))
+	       (when block-item
+		 (message "Checkboxes were removed due to empty box at line %d"
+			  (org-current-line block-item)))))))
+	(`plain-list
+	 ;; At a plain list, with a double C-u argument, set
+	 ;; checkboxes of each item to "[-]", whereas a single one
+	 ;; will toggle their presence according to the state of the
+	 ;; first item in the list.  Without an argument, repair the
+	 ;; list.
+	 (if (and (boundp org-list-checkbox-radio-mode)
+		  org-list-checkbox-radio-mode)
+	     (org-toggle-radio-button arg)
+	   (let* ((begin (org-element-property :contents-begin context))
+		  (struct (org-element-property :structure context))
+		  (old-struct (copy-tree struct))
+		  (first-box (save-excursion
+			       (goto-char begin)
+			       (looking-at org-list-full-item-re)
+			       (match-string-no-properties 3)))
+		  (new-box (cond ((equal arg '(16)) "[-]")
+				 ((equal arg '(4)) (unless first-box "[ ]"))
+				 ((equal first-box "[X]") "[ ]")
+				 (t "[X]"))))
+	     (cond
+	      (arg
+	       (dolist (pos
+			(org-list-get-all-items
+			 begin struct (org-list-prevs-alist struct)))
+		 (org-list-set-checkbox pos struct new-box)))
+	      ((and first-box (eq (point) begin))
+	       ;; For convenience, when point is at bol on the first
+	       ;; item of the list and no argument is provided, simply
+	       ;; toggle checkbox of that item, if any.
+	       (org-list-set-checkbox begin struct new-box)))
+	     (when (equal
+		    (org-list-write-struct
+		     struct (org-list-parents-alist struct) old-struct)
+		    old-struct)
+	       (message "Cannot update this checkbox"))
+	     (org-update-checkbox-count-maybe))))
 	(`keyword
 	 (let ((org-inhibit-startup-visibility-stuff t)
 	       (org-startup-align-all-tables nil))
@@ -17258,40 +17298,6 @@ This command does many different things, depending on context:
 	     (setq org-table-coordinate-overlays nil))
 	   (org-save-outline-visibility 'use-markers (org-mode-restart)))
 	 (message "Local setup has been refreshed"))
-	(`plain-list
-	 ;; At a plain list, with a double C-u argument, set
-	 ;; checkboxes of each item to "[-]", whereas a single one
-	 ;; will toggle their presence according to the state of the
-	 ;; first item in the list.  Without an argument, repair the
-	 ;; list.
-	 (let* ((begin (org-element-property :contents-begin context))
-		(struct (org-element-property :structure context))
-		(old-struct (copy-tree struct))
-		(first-box (save-excursion
-			     (goto-char begin)
-			     (looking-at org-list-full-item-re)
-			     (match-string-no-properties 3)))
-		(new-box (cond ((equal arg '(16)) "[-]")
-			       ((equal arg '(4)) (unless first-box "[ ]"))
-			       ((equal first-box "[X]") "[ ]")
-			       (t "[X]"))))
-	   (cond
-	    (arg
-	     (dolist (pos
-		      (org-list-get-all-items
-		       begin struct (org-list-prevs-alist struct)))
-	       (org-list-set-checkbox pos struct new-box)))
-	    ((and first-box (eq (point) begin))
-	     ;; For convenience, when point is at bol on the first
-	     ;; item of the list and no argument is provided, simply
-	     ;; toggle checkbox of that item, if any.
-	     (org-list-set-checkbox begin struct new-box)))
-	   (when (equal
-		  (org-list-write-struct
-		   struct (org-list-parents-alist struct) old-struct)
-		  old-struct)
-	     (message "Cannot update this checkbox"))
-	   (org-update-checkbox-count-maybe)))
 	((or `property-drawer `node-property)
 	 (call-interactively #'org-property-action))
 	(`radio-target