From 561feb128d9d1067e613cce632c411ad6d9a4669 Mon Sep 17 00:00:00 2001 From: Bastien Date: Tue, 11 Feb 2020 23:55:53 +0100 Subject: [PATCH] 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. --- doc/org-manual.org | 15 +++++ etc/ORG-NEWS | 10 ++++ lisp/org-keys.el | 2 + lisp/org-list.el | 35 ++++++++++++ lisp/org.el | 140 +++++++++++++++++++++++---------------------- 5 files changed, 135 insertions(+), 67 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index f9f04060a..fa6fad1c5 100644 --- a/doc/org-manual.org +++ b/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 diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 5e50b96b5..06f0f743f 100644 --- a/etc/ORG-NEWS +++ b/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 == to +call ~org-toggle-radio-button~. + *** Looping agenda commands over headlines ~org-agenda-loop-over-headlines-in-active-region~ allows you to loop diff --git a/lisp/org-keys.el b/lisp/org-keys.el index 7f8683125..93e1bd7c7 100644 --- a/lisp/org-keys.el +++ b/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) diff --git a/lisp/org-list.el b/lisp/org-list.el index b61408965..edba22c8a 100644 --- a/lisp/org-list.el +++ b/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. diff --git a/lisp/org.el b/lisp/org.el index f7fa91557..f117401c0 100644 --- a/lisp/org.el +++ b/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