forked from mirrors/org-mode
Blocking: Make it possible that checkboxes block TODO state changes
See the documentation for details.
This commit is contained in:
parent
f99b00f61c
commit
dc6658d9ed
|
@ -1,3 +1,8 @@
|
|||
2009-01-30 Carsten Dominik <carsten.dominik@gmail.com>
|
||||
|
||||
* org.texi (TODO dependencies): Document TODO dependencies on
|
||||
checkboxes.
|
||||
|
||||
2009-01-27 Carsten Dominik <carsten.dominik@gmail.com>
|
||||
|
||||
* org.texi (TODO dependencies): New section.
|
||||
|
|
11
doc/org.texi
11
doc/org.texi
|
@ -3310,6 +3310,8 @@ necessary, define a special face and use that.
|
|||
|
||||
@node TODO dependencies, , Faces for TODO keywords, TODO extensions
|
||||
@subsection TODO dependencies
|
||||
@cindex TODO dependencies
|
||||
@cindex dependencies, of TODO states
|
||||
|
||||
The structure of Org files (hierarchy and lists) makes it easy to define TODO
|
||||
dependencies. Usually, a parent TODO task should not be marked DONE until
|
||||
|
@ -3339,12 +3341,21 @@ blocked until all earlier siblings are marked DONE. Here is an example:
|
|||
@kindex C-c C-x o
|
||||
@item C-c C-x o
|
||||
Toggle the @code{ORDERED} property of the current entry.
|
||||
@kindex C-u C-u C-u C-c C-t
|
||||
@item C-u C-u C-u C-c C-t
|
||||
Change TODO state, circumventin any state blocking.
|
||||
@end table
|
||||
|
||||
If you set the variable @code{org-agenda-dim-blocked-tasks}, TODO entries
|
||||
that cannot be closed because of such dependencies will be shown in a dimmed
|
||||
font or even made invisible in agenda views (@pxref{Agenda Views}).
|
||||
|
||||
@cindex checkboxes and TODO dependencies
|
||||
You can also block changes of TODO states by looking at checkboxes
|
||||
(@pxref{Checkboxes}). If you set the variable
|
||||
@code{org-enforce-todo-checkbox-dependencies}, an entry that has unchecked
|
||||
checkboxes will be blocked from switching to DONE.
|
||||
|
||||
If you need more complex dependency structures, for example dependencies
|
||||
between entries in different trees or files, check out the contributed
|
||||
module @file{org-depend.el}.
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
2009-01-30 Carsten Dominik <carsten.dominik@gmail.com>
|
||||
|
||||
* org.el (org-enforce-todo-checkbox-dependencies): New option.
|
||||
(org-block-todo-from-checkboxes): New function.
|
||||
(org-todo): Make tripple prefix arg circumvent blocking.
|
||||
|
||||
* org-timer.el (org-timer): Provide the timer feature.
|
||||
|
||||
* org.el (org-require-autoloaded-modules): Add a few more files to
|
||||
|
|
380
lisp/org.el
380
lisp/org.el
|
@ -1641,8 +1641,27 @@ restart emacs after changing the value."
|
|||
:set (lambda (var val)
|
||||
(set var val)
|
||||
(if val
|
||||
(add-hook 'org-blocker-hook 'org-block-todo-from-children-or-siblings)
|
||||
(remove-hook 'org-blocker-hook 'org-block-todo-from-children-or-siblings)))
|
||||
(add-hook 'org-blocker-hook
|
||||
'org-block-todo-from-children-or-siblings)
|
||||
(remove-hook 'org-blocker-hook
|
||||
'org-block-todo-from-children-or-siblings)))
|
||||
:group 'org-todo
|
||||
:type 'boolean)
|
||||
|
||||
(defcustom org-enforce-todo-checkbox-dependencies nil
|
||||
"Non-nil means, unchecked boxes will block switching the parent to DONE.
|
||||
When this is nil, checkboxes have no influence on switching TODO states.
|
||||
When non-nil, you first need to check off all check boxes before the TODO
|
||||
entry can be switched to DONE.
|
||||
You need to set this variable through the customize interface, or to
|
||||
restart emacs after changing the value."
|
||||
:set (lambda (var val)
|
||||
(set var val)
|
||||
(if val
|
||||
(add-hook 'org-blocker-hook
|
||||
'org-block-todo-from-checkboxes)
|
||||
(remove-hook 'org-blocker-hook
|
||||
'org-block-todo-from-checkboxes)))
|
||||
:group 'org-todo
|
||||
:type 'boolean)
|
||||
|
||||
|
@ -8332,6 +8351,7 @@ DONE are present, add TODO at the beginning of the heading.
|
|||
With C-u prefix arg, use completion to determine the new state.
|
||||
With numeric prefix arg, switch to that state.
|
||||
With a double C-u prefix, switch to the next set of TODO keywords (nextset).
|
||||
With a tripple C-u prefix, circumvent any state blocking.
|
||||
|
||||
For calling through lisp, arg is also interpreted in the following way:
|
||||
'none -> empty state
|
||||
|
@ -8343,169 +8363,176 @@ For calling through lisp, arg is also interpreted in the following way:
|
|||
really is a member of `org-todo-keywords'."
|
||||
(interactive "P")
|
||||
(if (equal arg '(16)) (setq arg 'nextset))
|
||||
(save-excursion
|
||||
(catch 'exit
|
||||
(org-back-to-heading)
|
||||
(if (looking-at outline-regexp) (goto-char (1- (match-end 0))))
|
||||
(or (looking-at (concat " +" org-todo-regexp " *"))
|
||||
(looking-at " *"))
|
||||
(let* ((match-data (match-data))
|
||||
(startpos (point-at-bol))
|
||||
(logging (save-match-data (org-entry-get nil "LOGGING" t)))
|
||||
(org-log-done org-log-done)
|
||||
(org-log-repeat org-log-repeat)
|
||||
(org-todo-log-states org-todo-log-states)
|
||||
(this (match-string 1))
|
||||
(hl-pos (match-beginning 0))
|
||||
(head (org-get-todo-sequence-head this))
|
||||
(ass (assoc head org-todo-kwd-alist))
|
||||
(interpret (nth 1 ass))
|
||||
(done-word (nth 3 ass))
|
||||
(final-done-word (nth 4 ass))
|
||||
(last-state (or this ""))
|
||||
(completion-ignore-case t)
|
||||
(member (member this org-todo-keywords-1))
|
||||
(tail (cdr member))
|
||||
(state (cond
|
||||
((and org-todo-key-trigger
|
||||
(or (and (equal arg '(4)) (eq org-use-fast-todo-selection 'prefix))
|
||||
(and (not arg) org-use-fast-todo-selection
|
||||
(not (eq org-use-fast-todo-selection 'prefix)))))
|
||||
;; Use fast selection
|
||||
(org-fast-todo-selection))
|
||||
((and (equal arg '(4))
|
||||
(or (not org-use-fast-todo-selection)
|
||||
(not org-todo-key-trigger)))
|
||||
;; Read a state with completion
|
||||
(org-ido-completing-read "State: " (mapcar (lambda(x) (list x))
|
||||
org-todo-keywords-1)
|
||||
nil t))
|
||||
((eq arg 'right)
|
||||
(if this
|
||||
(if tail (car tail) nil)
|
||||
(car org-todo-keywords-1)))
|
||||
((eq arg 'left)
|
||||
(if (equal member org-todo-keywords-1)
|
||||
nil
|
||||
(let ((org-blocker-hook org-blocker-hook))
|
||||
(when (equal arg '(64))
|
||||
(setq arg nil org-blocker-hook nil))
|
||||
(save-excursion
|
||||
(catch 'exit
|
||||
(org-back-to-heading)
|
||||
(if (looking-at outline-regexp) (goto-char (1- (match-end 0))))
|
||||
(or (looking-at (concat " +" org-todo-regexp " *"))
|
||||
(looking-at " *"))
|
||||
(let* ((match-data (match-data))
|
||||
(startpos (point-at-bol))
|
||||
(logging (save-match-data (org-entry-get nil "LOGGING" t)))
|
||||
(org-log-done org-log-done)
|
||||
(org-log-repeat org-log-repeat)
|
||||
(org-todo-log-states org-todo-log-states)
|
||||
(this (match-string 1))
|
||||
(hl-pos (match-beginning 0))
|
||||
(head (org-get-todo-sequence-head this))
|
||||
(ass (assoc head org-todo-kwd-alist))
|
||||
(interpret (nth 1 ass))
|
||||
(done-word (nth 3 ass))
|
||||
(final-done-word (nth 4 ass))
|
||||
(last-state (or this ""))
|
||||
(completion-ignore-case t)
|
||||
(member (member this org-todo-keywords-1))
|
||||
(tail (cdr member))
|
||||
(state (cond
|
||||
((and org-todo-key-trigger
|
||||
(or (and (equal arg '(4))
|
||||
(eq org-use-fast-todo-selection 'prefix))
|
||||
(and (not arg) org-use-fast-todo-selection
|
||||
(not (eq org-use-fast-todo-selection
|
||||
'prefix)))))
|
||||
;; Use fast selection
|
||||
(org-fast-todo-selection))
|
||||
((and (equal arg '(4))
|
||||
(or (not org-use-fast-todo-selection)
|
||||
(not org-todo-key-trigger)))
|
||||
;; Read a state with completion
|
||||
(org-ido-completing-read
|
||||
"State: " (mapcar (lambda(x) (list x))
|
||||
org-todo-keywords-1)
|
||||
nil t))
|
||||
((eq arg 'right)
|
||||
(if this
|
||||
(nth (- (length org-todo-keywords-1) (length tail) 2)
|
||||
org-todo-keywords-1)
|
||||
(org-last org-todo-keywords-1))))
|
||||
((and (eq org-use-fast-todo-selection t) (equal arg '(4))
|
||||
(setq arg nil))) ; hack to fall back to cycling
|
||||
(arg
|
||||
;; user or caller requests a specific state
|
||||
(cond
|
||||
((equal arg "") nil)
|
||||
((eq arg 'none) nil)
|
||||
((eq arg 'done) (or done-word (car org-done-keywords)))
|
||||
((eq arg 'nextset)
|
||||
(or (car (cdr (member head org-todo-heads)))
|
||||
(car org-todo-heads)))
|
||||
((eq arg 'previousset)
|
||||
(let ((org-todo-heads (reverse org-todo-heads)))
|
||||
(if tail (car tail) nil)
|
||||
(car org-todo-keywords-1)))
|
||||
((eq arg 'left)
|
||||
(if (equal member org-todo-keywords-1)
|
||||
nil
|
||||
(if this
|
||||
(nth (- (length org-todo-keywords-1)
|
||||
(length tail) 2)
|
||||
org-todo-keywords-1)
|
||||
(org-last org-todo-keywords-1))))
|
||||
((and (eq org-use-fast-todo-selection t) (equal arg '(4))
|
||||
(setq arg nil))) ; hack to fall back to cycling
|
||||
(arg
|
||||
;; user or caller requests a specific state
|
||||
(cond
|
||||
((equal arg "") nil)
|
||||
((eq arg 'none) nil)
|
||||
((eq arg 'done) (or done-word (car org-done-keywords)))
|
||||
((eq arg 'nextset)
|
||||
(or (car (cdr (member head org-todo-heads)))
|
||||
(car org-todo-heads))))
|
||||
((car (member arg org-todo-keywords-1)))
|
||||
((nth (1- (prefix-numeric-value arg))
|
||||
org-todo-keywords-1))))
|
||||
((null member) (or head (car org-todo-keywords-1)))
|
||||
((equal this final-done-word) nil) ;; -> make empty
|
||||
((null tail) nil) ;; -> first entry
|
||||
((eq interpret 'sequence)
|
||||
(car tail))
|
||||
((memq interpret '(type priority))
|
||||
(if (eq this-command last-command)
|
||||
(car tail)
|
||||
(if (> (length tail) 0)
|
||||
(or done-word (car org-done-keywords))
|
||||
nil)))
|
||||
(t nil)))
|
||||
(next (if state (concat " " state " ") " "))
|
||||
(change-plist (list :type 'todo-state-change :from this :to state
|
||||
:position startpos))
|
||||
dolog now-done-p)
|
||||
(when org-blocker-hook
|
||||
(car org-todo-heads)))
|
||||
((eq arg 'previousset)
|
||||
(let ((org-todo-heads (reverse org-todo-heads)))
|
||||
(or (car (cdr (member head org-todo-heads)))
|
||||
(car org-todo-heads))))
|
||||
((car (member arg org-todo-keywords-1)))
|
||||
((nth (1- (prefix-numeric-value arg))
|
||||
org-todo-keywords-1))))
|
||||
((null member) (or head (car org-todo-keywords-1)))
|
||||
((equal this final-done-word) nil) ;; -> make empty
|
||||
((null tail) nil) ;; -> first entry
|
||||
((eq interpret 'sequence)
|
||||
(car tail))
|
||||
((memq interpret '(type priority))
|
||||
(if (eq this-command last-command)
|
||||
(car tail)
|
||||
(if (> (length tail) 0)
|
||||
(or done-word (car org-done-keywords))
|
||||
nil)))
|
||||
(t nil)))
|
||||
(next (if state (concat " " state " ") " "))
|
||||
(change-plist (list :type 'todo-state-change :from this :to state
|
||||
:position startpos))
|
||||
dolog now-done-p)
|
||||
(when org-blocker-hook
|
||||
(setq org-last-todo-state-is-todo
|
||||
(not (member this org-done-keywords)))
|
||||
(unless (save-excursion
|
||||
(save-match-data
|
||||
(run-hook-with-args-until-failure
|
||||
'org-blocker-hook change-plist)))
|
||||
(if (interactive-p)
|
||||
(error "TODO state change from %s to %s blocked" this state)
|
||||
;; fail silently
|
||||
(message "TODO state change from %s to %s blocked" this state)
|
||||
(throw 'exit nil))))
|
||||
(store-match-data match-data)
|
||||
(replace-match next t t)
|
||||
(unless (pos-visible-in-window-p hl-pos)
|
||||
(message "TODO state changed to %s" (org-trim next)))
|
||||
(unless head
|
||||
(setq head (org-get-todo-sequence-head state)
|
||||
ass (assoc head org-todo-kwd-alist)
|
||||
interpret (nth 1 ass)
|
||||
done-word (nth 3 ass)
|
||||
final-done-word (nth 4 ass)))
|
||||
(when (memq arg '(nextset previousset))
|
||||
(message "Keyword-Set %d/%d: %s"
|
||||
(- (length org-todo-sets) -1
|
||||
(length (memq (assoc state org-todo-sets) org-todo-sets)))
|
||||
(length org-todo-sets)
|
||||
(mapconcat 'identity (assoc state org-todo-sets) " ")))
|
||||
(setq org-last-todo-state-is-todo
|
||||
(not (member this org-done-keywords)))
|
||||
(unless (save-excursion
|
||||
(save-match-data
|
||||
(run-hook-with-args-until-failure
|
||||
'org-blocker-hook change-plist)))
|
||||
(if (interactive-p)
|
||||
(error "TODO state change from %s to %s blocked" this state)
|
||||
;; fail silently
|
||||
(message "TODO state change from %s to %s blocked" this state)
|
||||
(throw 'exit nil))))
|
||||
(store-match-data match-data)
|
||||
(replace-match next t t)
|
||||
(unless (pos-visible-in-window-p hl-pos)
|
||||
(message "TODO state changed to %s" (org-trim next)))
|
||||
(unless head
|
||||
(setq head (org-get-todo-sequence-head state)
|
||||
ass (assoc head org-todo-kwd-alist)
|
||||
interpret (nth 1 ass)
|
||||
done-word (nth 3 ass)
|
||||
final-done-word (nth 4 ass)))
|
||||
(when (memq arg '(nextset previousset))
|
||||
(message "Keyword-Set %d/%d: %s"
|
||||
(- (length org-todo-sets) -1
|
||||
(length (memq (assoc state org-todo-sets) org-todo-sets)))
|
||||
(length org-todo-sets)
|
||||
(mapconcat 'identity (assoc state org-todo-sets) " ")))
|
||||
(setq org-last-todo-state-is-todo
|
||||
(not (member state org-done-keywords)))
|
||||
(setq now-done-p (and (member state org-done-keywords)
|
||||
(not (member this org-done-keywords))))
|
||||
(and logging (org-local-logging logging))
|
||||
(when (and (or org-todo-log-states org-log-done)
|
||||
(not (memq arg '(nextset previousset))))
|
||||
;; we need to look at recording a time and note
|
||||
(setq dolog (or (nth 1 (assoc state org-todo-log-states))
|
||||
(nth 2 (assoc this org-todo-log-states))))
|
||||
(when (and state
|
||||
(member state org-not-done-keywords)
|
||||
(not (member this org-not-done-keywords)))
|
||||
;; This is now a todo state and was not one before
|
||||
;; If there was a CLOSED time stamp, get rid of it.
|
||||
(org-add-planning-info nil nil 'closed))
|
||||
(when (and now-done-p org-log-done)
|
||||
;; It is now done, and it was not done before
|
||||
(org-add-planning-info 'closed (org-current-time))
|
||||
(if (and (not dolog) (eq 'note org-log-done))
|
||||
(org-add-log-setup 'done state 'findpos 'note)))
|
||||
(when (and state dolog)
|
||||
;; This is a non-nil state, and we need to log it
|
||||
(org-add-log-setup 'state state 'findpos dolog)))
|
||||
;; Fixup tag positioning
|
||||
(org-todo-trigger-tag-changes state)
|
||||
(and org-auto-align-tags (not org-setting-tags) (org-set-tags nil t))
|
||||
(when org-provide-todo-statistics
|
||||
(org-update-parent-todo-statistics))
|
||||
(run-hooks 'org-after-todo-state-change-hook)
|
||||
(if (and arg (not (member state org-done-keywords)))
|
||||
(setq head (org-get-todo-sequence-head state)))
|
||||
(put-text-property (point-at-bol) (point-at-eol) 'org-todo-head head)
|
||||
;; Do we need to trigger a repeat?
|
||||
(when now-done-p
|
||||
(when (boundp 'org-agenda-headline-snapshot-before-repeat)
|
||||
;; This is for the agenda, take a snapshot of the headline.
|
||||
(save-match-data
|
||||
(setq org-agenda-headline-snapshot-before-repeat
|
||||
(org-get-heading))))
|
||||
(org-auto-repeat-maybe state))
|
||||
;; Fixup cursor location if close to the keyword
|
||||
(if (and (outline-on-heading-p)
|
||||
(not (bolp))
|
||||
(save-excursion (beginning-of-line 1)
|
||||
(looking-at org-todo-line-regexp))
|
||||
(< (point) (+ 2 (or (match-end 2) (match-end 1)))))
|
||||
(progn
|
||||
(goto-char (or (match-end 2) (match-end 1)))
|
||||
(just-one-space)))
|
||||
(when org-trigger-hook
|
||||
(save-excursion
|
||||
(run-hook-with-args 'org-trigger-hook change-plist)))))))
|
||||
(not (member state org-done-keywords)))
|
||||
(setq now-done-p (and (member state org-done-keywords)
|
||||
(not (member this org-done-keywords))))
|
||||
(and logging (org-local-logging logging))
|
||||
(when (and (or org-todo-log-states org-log-done)
|
||||
(not (memq arg '(nextset previousset))))
|
||||
;; we need to look at recording a time and note
|
||||
(setq dolog (or (nth 1 (assoc state org-todo-log-states))
|
||||
(nth 2 (assoc this org-todo-log-states))))
|
||||
(when (and state
|
||||
(member state org-not-done-keywords)
|
||||
(not (member this org-not-done-keywords)))
|
||||
;; This is now a todo state and was not one before
|
||||
;; If there was a CLOSED time stamp, get rid of it.
|
||||
(org-add-planning-info nil nil 'closed))
|
||||
(when (and now-done-p org-log-done)
|
||||
;; It is now done, and it was not done before
|
||||
(org-add-planning-info 'closed (org-current-time))
|
||||
(if (and (not dolog) (eq 'note org-log-done))
|
||||
(org-add-log-setup 'done state 'findpos 'note)))
|
||||
(when (and state dolog)
|
||||
;; This is a non-nil state, and we need to log it
|
||||
(org-add-log-setup 'state state 'findpos dolog)))
|
||||
;; Fixup tag positioning
|
||||
(org-todo-trigger-tag-changes state)
|
||||
(and org-auto-align-tags (not org-setting-tags) (org-set-tags nil t))
|
||||
(when org-provide-todo-statistics
|
||||
(org-update-parent-todo-statistics))
|
||||
(run-hooks 'org-after-todo-state-change-hook)
|
||||
(if (and arg (not (member state org-done-keywords)))
|
||||
(setq head (org-get-todo-sequence-head state)))
|
||||
(put-text-property (point-at-bol) (point-at-eol) 'org-todo-head head)
|
||||
;; Do we need to trigger a repeat?
|
||||
(when now-done-p
|
||||
(when (boundp 'org-agenda-headline-snapshot-before-repeat)
|
||||
;; This is for the agenda, take a snapshot of the headline.
|
||||
(save-match-data
|
||||
(setq org-agenda-headline-snapshot-before-repeat
|
||||
(org-get-heading))))
|
||||
(org-auto-repeat-maybe state))
|
||||
;; Fixup cursor location if close to the keyword
|
||||
(if (and (outline-on-heading-p)
|
||||
(not (bolp))
|
||||
(save-excursion (beginning-of-line 1)
|
||||
(looking-at org-todo-line-regexp))
|
||||
(< (point) (+ 2 (or (match-end 2) (match-end 1)))))
|
||||
(progn
|
||||
(goto-char (or (match-end 2) (match-end 1)))
|
||||
(just-one-space)))
|
||||
(when org-trigger-hook
|
||||
(save-excursion
|
||||
(run-hook-with-args 'org-trigger-hook change-plist))))))))
|
||||
|
||||
(defun org-block-todo-from-children-or-siblings (change-plist)
|
||||
"Block turning an entry into a TODO, using the hierarchy.
|
||||
|
@ -8522,7 +8549,9 @@ changes. Such blocking occurs when:
|
|||
;; do not block
|
||||
(when (or (not (eq (plist-get change-plist :type) 'todo-state-change))
|
||||
(member (plist-get change-plist :from)
|
||||
(cons 'done org-done-keywords)))
|
||||
(cons 'done org-done-keywords))
|
||||
(member (plist-get change-plist :to)
|
||||
(cons 'todo org-not-done-keywords)))
|
||||
(throw 'dont-block t))
|
||||
;; If this task has children, and any are undone, it's blocked
|
||||
(save-excursion
|
||||
|
@ -8573,6 +8602,31 @@ changes. Such blocking occurs when:
|
|||
(org-entry-put nil "ORDERED" "t")
|
||||
(message "Subtasks must be completed in sequence"))))
|
||||
|
||||
(defun org-block-todo-from-checkboxes (change-plist)
|
||||
"Block turning an entry into a TODO, using checkboxes.
|
||||
This checks whether the current task should be blocked from state
|
||||
changes because there are uncheckd boxes in this entry."
|
||||
(catch 'dont-block
|
||||
;; If this is not a todo state change, or if this entry is already DONE,
|
||||
;; do not block
|
||||
(when (or (not (eq (plist-get change-plist :type) 'todo-state-change))
|
||||
(member (plist-get change-plist :from)
|
||||
(cons 'done org-done-keywords))
|
||||
(member (plist-get change-plist :to)
|
||||
(cons 'todo org-not-done-keywords)))
|
||||
(throw 'dont-block t))
|
||||
;; If this task has checkboxes that are not checked, it's blocked
|
||||
(save-excursion
|
||||
(org-back-to-heading t)
|
||||
(let ((beg (point)) end)
|
||||
(outline-next-heading)
|
||||
(setq end (point))
|
||||
(goto-char beg)
|
||||
(if (re-search-forward "^[ \t]*\\([-+*]\\|[0-9]+[.)]\\)[ \t]+\\[[- ]\\]"
|
||||
end t)
|
||||
(throw 'dont-block nil))))
|
||||
t)) ; do not block
|
||||
|
||||
(defun org-update-parent-todo-statistics ()
|
||||
"Update any statistics cookie in the parent of the current headline."
|
||||
(interactive)
|
||||
|
|
Loading…
Reference in New Issue