Use "#+attr_org: :radio" before lists to enable radio buttons

* doc/org-manual.org (Checkboxes): Document the use of
"#+attr_org".

* lisp/org.el (org-ctrl-c-ctrl-c): When the list at point is
preceded by "#+attr_org: :radio" use `org-toggle-radio-button'
instead of `org-toggle-checkbox'.

* lisp/org-list.el (org-at-radio-list-p): New defsubst.
(org-toggle-checkbox): Use it.

* etc/ORG-NEWS: Document the use of "#+attr_org".
This commit is contained in:
Bastien 2020-02-12 01:51:46 +01:00
parent 561feb128d
commit 4028cc731b
4 changed files with 111 additions and 92 deletions

View File

@ -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~) ::

View File

@ -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 =<C-c C-x C-r>= 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

View File

@ -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 ()

View File

@ -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))