diff --git a/doc/org-manual.org b/doc/org-manual.org index fa6fad1c5..148708939 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -4539,12 +4539,13 @@ The following commands work with checkboxes: 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 + the presence of the checkbox. With a 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. + {{{kdb(C-c C-c)}}} can be told to consider checkboxes as radio buttons by + setting =#+ATTR_ORG: :radio= right before the list or by calling + {{{kbd(M-x org-list-checkbox-radio-mode)}}} to activate this minor mode. - {{{kbd(M-S-RET)}}} (~org-insert-todo-heading~) :: diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 06f0f743f..8ed6a1adf 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -54,6 +54,9 @@ 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~. +You can also add =#+ATTR_ORG: :radio= right before the list to tell +Org to use radio buttons for this list only. + *** Looping agenda commands over headlines ~org-agenda-loop-over-headlines-in-active-region~ allows you to loop diff --git a/lisp/org-list.el b/lisp/org-list.el index edba22c8a..6d50ea0ed 100644 --- a/lisp/org-list.el +++ b/lisp/org-list.el @@ -2337,6 +2337,16 @@ is an integer, 0 means `-', 1 means `+' etc. If WHICH is (org-list-struct-apply-struct struct old-struct) (org-update-checkbox-count-maybe)))) +(defsubst org-at-radio-list-p () + "Is point in a list with radio buttons?" + (let (attr) + (save-excursion + (org-at-item-p) + (goto-char (caar (org-list-struct))) + (org-backward-element) + (setq attr (car (org-element-property :attr_org (org-element-at-point)))) + (when attr (string-match-p ":radio" attr))))) + (defun org-toggle-checkbox (&optional toggle-presence) "Toggle the checkbox in the current line. @@ -2351,92 +2361,94 @@ If point is on a headline, apply this to all checkbox items in the text below the heading, taking as reference the first item in subtree, ignoring planning line and any drawer following it." (interactive "P") - (save-excursion - (let* (singlep - block-item - lim-up - lim-down - (orderedp (org-entry-get nil "ORDERED")) - (_bounds - ;; In a region, start at first item in region. + (if (org-at-radio-list-p) + (org-toggle-radio-button toggle-presence) + (save-excursion + (let* (singlep + block-item + lim-up + lim-down + (orderedp (org-entry-get nil "ORDERED")) + (_bounds + ;; In a region, start at first item in region. + (cond + ((org-region-active-p) + (let ((limit (region-end))) + (goto-char (region-beginning)) + (if (org-list-search-forward (org-item-beginning-re) limit t) + (setq lim-up (point-at-bol)) + (error "No item in region")) + (setq lim-down (copy-marker limit)))) + ((org-at-heading-p) + ;; On a heading, start at first item after drawers and + ;; time-stamps (scheduled, etc.). + (let ((limit (save-excursion (outline-next-heading) (point)))) + (org-end-of-meta-data t) + (if (org-list-search-forward (org-item-beginning-re) limit t) + (setq lim-up (point-at-bol)) + (error "No item in subtree")) + (setq lim-down (copy-marker limit)))) + ;; Just one item: set SINGLEP flag. + ((org-at-item-p) + (setq singlep t) + (setq lim-up (point-at-bol) + lim-down (copy-marker (point-at-eol)))) + (t (error "Not at an item or heading, and no active region")))) + ;; Determine the checkbox going to be applied to all items + ;; within bounds. + (ref-checkbox + (progn + (goto-char lim-up) + (let ((cbox (and (org-at-item-checkbox-p) (match-string 1)))) + (cond + ((equal toggle-presence '(16)) "[-]") + ((equal toggle-presence '(4)) + (unless cbox "[ ]")) + ((equal "[X]" cbox) "[ ]") + (t "[X]")))))) + ;; When an item is found within bounds, grab the full list at + ;; point structure, then: (1) set check-box of all its items + ;; within bounds to REF-CHECKBOX, (2) fix check-boxes of the + ;; whole list, (3) move point after the list. + (goto-char lim-up) + (while (and (< (point) lim-down) + (org-list-search-forward (org-item-beginning-re) + lim-down 'move)) + (let* ((struct (org-list-struct)) + (struct-copy (copy-tree struct)) + (parents (org-list-parents-alist struct)) + (prevs (org-list-prevs-alist struct)) + (bottom (copy-marker (org-list-get-bottom-point struct))) + (items-to-toggle (cl-remove-if + (lambda (e) (or (< e lim-up) (> e lim-down))) + (mapcar #'car struct)))) + (mapc (lambda (e) (org-list-set-checkbox + e struct + ;; If there is no box at item, leave as-is + ;; unless function was called with C-u prefix. + (let ((cur-box (org-list-get-checkbox e struct))) + (if (or cur-box (equal toggle-presence '(4))) + ref-checkbox + cur-box)))) + items-to-toggle) + (setq block-item (org-list-struct-fix-box + struct parents prevs orderedp)) + ;; Report some problems due to ORDERED status of subtree. + ;; If only one box was being checked, throw an error, else, + ;; only signal problems. (cond - ((org-region-active-p) - (let ((limit (region-end))) - (goto-char (region-beginning)) - (if (org-list-search-forward (org-item-beginning-re) limit t) - (setq lim-up (point-at-bol)) - (error "No item in region")) - (setq lim-down (copy-marker limit)))) - ((org-at-heading-p) - ;; On a heading, start at first item after drawers and - ;; time-stamps (scheduled, etc.). - (let ((limit (save-excursion (outline-next-heading) (point)))) - (org-end-of-meta-data t) - (if (org-list-search-forward (org-item-beginning-re) limit t) - (setq lim-up (point-at-bol)) - (error "No item in subtree")) - (setq lim-down (copy-marker limit)))) - ;; Just one item: set SINGLEP flag. - ((org-at-item-p) - (setq singlep t) - (setq lim-up (point-at-bol) - lim-down (copy-marker (point-at-eol)))) - (t (error "Not at an item or heading, and no active region")))) - ;; Determine the checkbox going to be applied to all items - ;; within bounds. - (ref-checkbox - (progn - (goto-char lim-up) - (let ((cbox (and (org-at-item-checkbox-p) (match-string 1)))) - (cond - ((equal toggle-presence '(16)) "[-]") - ((equal toggle-presence '(4)) - (unless cbox "[ ]")) - ((equal "[X]" cbox) "[ ]") - (t "[X]")))))) - ;; When an item is found within bounds, grab the full list at - ;; point structure, then: (1) set check-box of all its items - ;; within bounds to REF-CHECKBOX, (2) fix check-boxes of the - ;; whole list, (3) move point after the list. - (goto-char lim-up) - (while (and (< (point) lim-down) - (org-list-search-forward (org-item-beginning-re) - lim-down 'move)) - (let* ((struct (org-list-struct)) - (struct-copy (copy-tree struct)) - (parents (org-list-parents-alist struct)) - (prevs (org-list-prevs-alist struct)) - (bottom (copy-marker (org-list-get-bottom-point struct))) - (items-to-toggle (cl-remove-if - (lambda (e) (or (< e lim-up) (> e lim-down))) - (mapcar #'car struct)))) - (mapc (lambda (e) (org-list-set-checkbox - e struct - ;; If there is no box at item, leave as-is - ;; unless function was called with C-u prefix. - (let ((cur-box (org-list-get-checkbox e struct))) - (if (or cur-box (equal toggle-presence '(4))) - ref-checkbox - cur-box)))) - items-to-toggle) - (setq block-item (org-list-struct-fix-box - struct parents prevs orderedp)) - ;; Report some problems due to ORDERED status of subtree. - ;; If only one box was being checked, throw an error, else, - ;; only signal problems. - (cond - ((and singlep block-item (> lim-up block-item)) - (error - "Checkbox blocked because of unchecked box at line %d" - (org-current-line block-item))) - (block-item - (message - "Checkboxes were removed due to unchecked box at line %d" - (org-current-line block-item)))) - (goto-char bottom) - (move-marker bottom nil) - (org-list-struct-apply-struct struct struct-copy))) - (move-marker lim-down nil))) + ((and singlep block-item (> lim-up block-item)) + (error + "Checkbox blocked because of unchecked box at line %d" + (org-current-line block-item))) + (block-item + (message + "Checkboxes were removed due to unchecked box at line %d" + (org-current-line block-item)))) + (goto-char bottom) + (move-marker bottom nil) + (org-list-struct-apply-struct struct struct-copy))) + (move-marker lim-down nil)))) (org-update-checkbox-count-maybe)) (defun org-reset-checkbox-state-subtree () diff --git a/lisp/org.el b/lisp/org.el index f117401c0..464387b13 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -17171,6 +17171,7 @@ This command does many different things, depending on context: src-block statistics-cookie table table-cell table-row timestamp) t)) + (radio-list-p (org-at-radio-list-p)) (type (org-element-type context))) ;; For convenience: at the first line of a paragraph on the same ;; line as an item, apply function on that item instead. @@ -17217,8 +17218,9 @@ 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. - (if (and (boundp org-list-checkbox-radio-mode) - org-list-checkbox-radio-mode) + (if (or radio-list-p + (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)) @@ -17259,8 +17261,9 @@ This command does many different things, depending on context: ;; 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) + (if (or radio-list-p + (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))