org-agenda.el: Implement new effort filter

* org-agenda.el (org-agenda-custom-commands-local-options):
Add `org-agenda-effort-filter-preset'.
(org-agenda-filter-effort-default-operator): Delete.
(org-agenda-local-vars): Add `org-agenda-effort-filter'.
(org-agenda-mode-map): Use "_" to filter by effort.
(org-agenda-effort-filter, org-agenda-effort-filter-preset):
New variables.
(org-agenda-prepare-window, org-agenda-prepare)
(org-agenda-finalize, org-agenda-redo)
(org-agenda-filter-remove-all, org-agenda-filter-apply)
(org-agenda-set-mode-name, org-agenda-reapply-filters): Handle
effort filter.
(org-agenda-finalize-entries): Use
`org-sort-agenda-noeffort-is-high'.
(org-agenda-limit-entries): Get the property from the correct
location.
(org-agenda-limit-interactively): Throw a user error on wrong
input.
(org-agenda-filter-by-effort): New command.
(org-agenda-filter-by-tag): Don't filter by effort.
(org-agenda-filter-make-matcher): Handle effort filter.
(org-agenda-compare-effort): Don't handle the "?" operator.
(org-agenda-filter-show-all-effort): New command.

Note: This calls for some refactoring in the filter area.
This commit is contained in:
Bastien Guerry 2014-05-28 12:26:54 +02:00
parent 7dceecbb30
commit aa86e4bc9f
1 changed files with 113 additions and 54 deletions

View File

@ -361,6 +361,12 @@ the daily/weekly agenda, see `org-agenda-skip-function'.")
(const :format "" quote)
(repeat
(string :tag "+tag or -tag"))))
(list :tag "Effort filter preset"
(const org-agenda-effort-filter-preset)
(list
(const :format "" quote)
(repeat
(string :tag "+=10 or -=10 or +<10 or ->10"))))
(list :tag "Regexp filter preset"
(const org-agenda-regexp-filter-preset)
(list
@ -607,15 +613,6 @@ or `C-c a #' to produce the list."
(repeat :tag "Projects are *not* stuck if they have an entry with TAG being any of" (string))
(regexp :tag "Projects are *not* stuck if this regexp matches inside the subtree")))
(defcustom org-agenda-filter-effort-default-operator "<"
"The default operator for effort estimate filtering.
If you select an effort estimate limit without first pressing an operator,
this one will be used."
:group 'org-agenda-custom-commands
:type '(choice (const :tag "less or equal" "<")
(const :tag "greater or equal"">")
(const :tag "equal" "=")))
(defgroup org-agenda-skip nil
"Options concerning skipping parts of agenda files."
:tag "Org Agenda Skip"
@ -2097,6 +2094,7 @@ When nil, `q' will kill the single agenda buffer."
org-agenda-category-filter
org-agenda-top-headline-filter
org-agenda-regexp-filter
org-agenda-effort-filter
org-agenda-markers
org-agenda-last-search-view-search-was-boolean
org-agenda-filtered-by-category
@ -2305,6 +2303,7 @@ The following commands are available:
(org-defkey org-agenda-mode-map "{" 'org-agenda-manipulate-query-add-re)
(org-defkey org-agenda-mode-map "}" 'org-agenda-manipulate-query-subtract-re)
(org-defkey org-agenda-mode-map "/" 'org-agenda-filter-by-tag)
(org-defkey org-agenda-mode-map "_" 'org-agenda-filter-by-effort)
(org-defkey org-agenda-mode-map "=" 'org-agenda-filter-by-regexp)
(org-defkey org-agenda-mode-map "|" 'org-agenda-filter-remove-all)
(org-defkey org-agenda-mode-map "\\" 'org-agenda-filter-by-tag-refine)
@ -3534,6 +3533,7 @@ removed from the entry content. Currently only `planning' is allowed here."
(defvar org-agenda-tag-filter nil)
(defvar org-agenda-category-filter nil)
(defvar org-agenda-regexp-filter nil)
(defvar org-agenda-effort-filter nil)
(defvar org-agenda-top-headline-filter nil)
(defvar org-agenda-tag-filter-while-redo nil)
(defvar org-agenda-tag-filter-preset nil
@ -3566,6 +3566,16 @@ the entire agenda view. In a block agenda, it will not work reliably to
define a filter for one of the individual blocks. You need to set it in
the global options and expect it to be applied to the entire view.")
(defvar org-agenda-effort-filter-preset nil
"A preset of the effort condition used for secondary agenda filtering.
This must be a list of strings, each string must be a single regexp
preceded by \"+\" or \"-\".
This variable should not be set directly, but agenda custom commands can
bind it in the options section. The preset filter is a global property of
the entire agenda view. In a block agenda, it will not work reliably to
define a filter for one of the individual blocks. You need to set it in
the global options and expect it to be applied to the entire view.")
(defun org-agenda-use-sticky-p ()
"Return non-nil if an agenda buffer named
`org-agenda-buffer-name' exists and should be shown instead of
@ -3608,6 +3618,7 @@ FILTER-ALIST is an alist of filters we need to apply when
(org-switch-to-buffer-other-window abuf)))
(setq org-agenda-tag-filter (cdr (assoc 'tag filter-alist)))
(setq org-agenda-category-filter (cdr (assoc 'cat filter-alist)))
(setq org-agenda-effort-filter (cdr (assoc 'effort filter-alist)))
(setq org-agenda-regexp-filter (cdr (assoc 're filter-alist)))
;; Additional test in case agenda is invoked from within agenda
;; buffer via elisp link.
@ -3620,6 +3631,7 @@ FILTER-ALIST is an alist of filters we need to apply when
(let ((filter-alist (if org-agenda-persistent-filter
(list `(tag . ,org-agenda-tag-filter)
`(re . ,org-agenda-regexp-filter)
`(effort . ,org-agenda-effort-filter)
`(car . ,org-agenda-category-filter)))))
(if (org-agenda-use-sticky-p)
(progn
@ -3636,6 +3648,8 @@ FILTER-ALIST is an alist of filters we need to apply when
org-agenda-category-filter-preset)
(put 'org-agenda-regexp-filter :preset-filter
org-agenda-regexp-filter-preset)
(put 'org-agenda-effort-filter :preset-filter
org-agenda-effort-filter-preset)
(if org-agenda-multi
(progn
(setq buffer-read-only nil)
@ -3746,6 +3760,11 @@ FILTER-ALIST is an alist of filters we need to apply when
(when (get 'org-agenda-regexp-filter :preset-filter)
(org-agenda-filter-apply
(get 'org-agenda-regexp-filter :preset-filter) 'regexp))
(when org-agenda-effort-filter
(org-agenda-filter-apply org-agenda-effort-filter 'effort))
(when (get 'org-agenda-effort-filter :preset-filter)
(org-agenda-filter-apply
(get 'org-agenda-effort-filter :preset-filter) 'effort))
(org-add-hook 'kill-buffer-hook 'org-agenda-reset-markers 'append 'local)))))
(defun org-agenda-mark-clocking-task ()
@ -6801,7 +6820,9 @@ The optional argument TYPE tells the agenda type."
list (mapcar 'identity (sort list 'org-entries-lessp)))
(when max-effort
(setq list (org-agenda-limit-entries
list 'effort-minutes max-effort 'identity)))
list 'effort-minutes max-effort
(lambda (e) (or e (if org-sort-agenda-noeffort-is-high
32767 -1))))))
(when max-todo
(setq list (org-agenda-limit-entries list 'todo-state max-todo)))
(when max-tags
@ -6819,7 +6840,9 @@ The optional argument TYPE tells the agenda type."
(delq nil
(mapcar
(lambda (e)
(let ((pval (funcall fun (get-text-property 1 prop e))))
(let ((pval (funcall
fun (get-text-property (1- (length e))
prop e))))
(if pval (setq lim (+ lim pval)))
(cond ((and pval (<= lim (abs limit))) e)
((and include (not pval)) e))))
@ -6839,7 +6862,8 @@ The optional argument TYPE tells the agenda type."
(msg (cond ((= max ?E) "How many minutes? ")
((= max ?e) "How many entries? ")
((= max ?t) "How many TODO entries? ")
((= max ?T) "How many tagged entries? ")))
((= max ?T) "How many tagged entries? ")
(t (user-error "Wrong input"))))
(num (string-to-number (read-from-minibuffer msg))))
(cond ((equal max ?e)
(let ((org-agenda-max-entries num)) (org-agenda-redo)))
@ -7253,6 +7277,8 @@ in the agenda."
(cat-preset (get 'org-agenda-category-filter :preset-filter))
(re-filter org-agenda-regexp-filter)
(re-preset (get 'org-agenda-regexp-filter :preset-filter))
(effort-filter org-agenda-effort-filter)
(effort-preset (get 'org-agenda-effort-filter :preset-filter))
(org-agenda-tag-filter-while-redo (or tag-filter tag-preset))
(cols org-agenda-columns-active)
(line (org-current-line))
@ -7271,6 +7297,7 @@ in the agenda."
(put 'org-agenda-tag-filter :preset-filter nil)
(put 'org-agenda-category-filter :preset-filter nil)
(put 'org-agenda-regexp-filter :preset-filter nil)
(put 'org-agenda-effort-filter :preset-filter nil)
(and cols (org-columns-quit))
(message "Rebuilding agenda buffer...")
(if series-redo-cmd
@ -7281,16 +7308,20 @@ in the agenda."
org-agenda-tag-filter tag-filter
org-agenda-category-filter cat-filter
org-agenda-regexp-filter re-filter
org-agenda-effort-filter effort-filter
org-agenda-top-headline-filter top-hl-filter)
(message "Rebuilding agenda buffer...done")
(put 'org-agenda-tag-filter :preset-filter tag-preset)
(put 'org-agenda-category-filter :preset-filter cat-preset)
(put 'org-agenda-regexp-filter :preset-filter re-preset)
(put 'org-agenda-effort-filter :preset-filter effort-preset)
(let ((tag (or tag-filter tag-preset))
(cat (or cat-filter cat-preset))
(re (or re-filter re-preset)))
(effort (or effort-filter effort-preset))
(re (or re-filter re-preset)))
(when tag (org-agenda-filter-apply tag 'tag))
(when cat (org-agenda-filter-apply cat 'category))
(when effort (org-agenda-filter-apply effort 'effort))
(when re (org-agenda-filter-apply re 'regexp)))
(and top-hl-filter (org-agenda-filter-top-headline-apply top-hl-filter))
(and cols (org-called-interactively-p 'any) (org-agenda-columns))
@ -7362,6 +7393,39 @@ With two prefix arguments, remove the regexp filters."
(org-agenda-filter-show-all-re)
(message "Regexp filter removed")))
(defvar org-agenda-effort-filter nil)
(defun org-agenda-filter-by-effort (strip)
"Filter agenda entries by effort.
With no prefix argument, keep entries matching the effort condition.
With one prefix argument, filter out entries matching the condition.
With two prefix arguments, remove the effort filters."
(interactive "P")
(cond ((member strip '(nil 4))
(let ((efforts (org-split-string
(or (cdr (assoc (concat org-effort-property "_ALL")
org-global-properties))
"0 0:10 0:30 1:00 2:00 3:00 4:00 5:00 6:00 7:00 8:00"
"")))
(eff -1)
effort-prompt op)
(while (not (member op '(?< ?> ?=)))
(setq op (read-char-exclusive "Effort operator? (> = or <)")))
(loop for i from 0 to 9 do
(setq effort-prompt
(concat
effort-prompt " ["
(if (= i 9) "0" (int-to-string (1+ i)))
"]" (nth i efforts))))
(message "Effort %s%s" (char-to-string op) effort-prompt)
(while (or (< eff 0) (> eff 9))
(setq eff (string-to-number (char-to-string (read-char-exclusive)))))
(setq org-agenda-effort-filter
(list (concat (if strip "-" "+")
(char-to-string op) (nth (1- eff) efforts))))
(org-agenda-filter-apply org-agenda-effort-filter 'effort)))
(t (org-agenda-filter-show-all-effort)
(message "Effort filter removed"))))
(defun org-agenda-filter-remove-all ()
"Remove all filters from the current agenda buffer."
(interactive)
@ -7373,6 +7437,8 @@ With two prefix arguments, remove the regexp filters."
(org-agenda-filter-show-all-re))
(when org-agenda-top-headline-filter
(org-agenda-filter-show-all-top-filter))
(when org-agenda-effort-filter
(org-agenda-filter-show-all-effort))
(org-agenda-finalize))
(defun org-agenda-filter-by-tag (strip &optional char narrow)
@ -7390,19 +7456,12 @@ to switch to narrowing."
(char-to-string (cdr x))
""))
alist ""))
(efforts (org-split-string
(or (cdr (assoc (concat org-effort-property "_ALL")
org-global-properties))
"0 0:10 0:30 1:00 2:00 3:00 4:00 5:00 6:00 7:00 8:00"
"")))
(effort-op org-agenda-filter-effort-default-operator)
(effort-prompt "")
(inhibit-read-only t)
(current org-agenda-tag-filter)
maybe-refresh a n tag)
(unless char
(message
"%s by tag [%s ], [TAB], %s[/]:off, [+-]:narrow, [>=<?]:effort: "
"%s by tag [%s ], [TAB], %s[/]:off, [+-]:narrow"
(if narrow "Narrow" "Filter") tag-chars
(if org-agenda-auto-exclude-function "[RET], " ""))
(setq char (read-char-exclusive)))
@ -7411,23 +7470,8 @@ to switch to narrowing."
(cond ((equal char ?-) (setq strip t narrow t))
((equal char ?+) (setq strip nil narrow t)))
(message
"Narrow by tag [%s ], [TAB], [/]:off, [>=<]:effort: " tag-chars)
"Narrow by tag [%s ], [TAB], [/]:off" tag-chars)
(setq char (read-char-exclusive)))
(when (member char '(?< ?> ?= ??))
;; An effort operator
(setq effort-op (char-to-string char))
(setq alist nil) ; to make sure it will be interpreted as effort.
(unless (equal char ??)
(loop for i from 0 to 9 do
(setq effort-prompt
(concat
effort-prompt " ["
(if (= i 9) "0" (int-to-string (1+ i)))
"]" (nth i efforts))))
(message "Effort%s: %s " effort-op effort-prompt)
(setq char (read-char-exclusive))
(when (or (< char ?0) (> char ?9))
(error "Need 1-9,0 to select effort"))))
(when (equal char ?\t)
(unless (local-variable-p 'org-global-tags-completion-table (current-buffer))
(org-set-local 'org-global-tags-completion-table
@ -7460,13 +7504,6 @@ to switch to narrowing."
(setq maybe-refresh t))
((or (equal char ?\ )
(setq a (rassoc char alist))
(and (>= char ?0) (<= char ?9)
(setq n (if (= char ?0) 9 (- char ?0 1))
tag (concat effort-op (nth n efforts))
a (cons tag nil)))
(and (= char ??)
(setq tag "?eff")
a (cons tag nil))
(and tag (setq a (cons tag nil))))
(org-agenda-filter-show-all-tag)
(setq tag (car a))
@ -7513,10 +7550,8 @@ to switch to narrowing."
(dolist (x fltr)
(if (member x '("-" "+"))
(setq nf01 (if (equal x "-") 'tags '(not tags)))
(if (string-match "[<=>?]" x)
(setq nf01 (org-agenda-filter-effort-form x))
(setq nf01 (list 'member (downcase (substring x 1))
'tags)))
(setq nf01 (list 'member (downcase (substring x 1))
'tags))
(when (equal (string-to-char x) ?-)
(setq nf01 (list 'not nf01))
(when (not notgroup) (setq op 'and))))
@ -7550,7 +7585,15 @@ to switch to narrowing."
(if (equal "-" (substring x 0 1))
(setq f1 (list 'not (list 'string-match (substring x 1) 'txt)))
(setq f1 (list 'string-match (substring x 1) 'txt)))
(push f1 f))))
(push f1 f)))
;; Effort filter
((eq type 'effort)
(setq filter
(delete-dups
(append (get 'org-agenda-effort-filter :preset-filter)
filter)))
(dolist (x filter)
(push (org-agenda-filter-effort-form x) f))))
(cons 'and (nreverse f))))
(defun org-agenda-filter-effort-form (e)
@ -7570,10 +7613,8 @@ E looks like \"+<2:25\"."
"Compare the effort of the current line with VALUE, using OP.
If the line does not have an effort defined, return nil."
(let ((eff (org-get-at-eol 'effort-minutes 1)))
(if (equal op ??)
(not eff)
(funcall op (or eff (if org-sort-agenda-noeffort-is-high 32767 0))
value))))
(funcall op (or eff (if org-sort-agenda-noeffort-is-high 32767 -1))
value)))
(defun org-agenda-filter-expand-tags (filter &optional no-operator)
"Expand group tags in FILTER for the agenda.
@ -7617,7 +7658,8 @@ When NO-OPERATOR is non-nil, do not add the + operator to returned tags."
(org-agenda-filter-expand-tags (list f) t))
(org-get-at-bol 'tags)))
cat (org-get-at-eol 'org-category 1)
txt (get-text-property (point) 'txt))
txt (org-get-at-eol 'txt 1)
effort-minutes (org-get-at-eol 'effort-minutes 1))
(if (not (eval org-agenda-filter-form))
(org-agenda-filter-hide-line type))
(beginning-of-line 2))
@ -7670,6 +7712,8 @@ When NO-OPERATOR is non-nil, do not add the + operator to returned tags."
(org-agenda-remove-filter 'tag))
(defun org-agenda-filter-show-all-re nil
(org-agenda-remove-filter 'regexp))
(defun org-agenda-filter-show-all-effort nil
(org-agenda-remove-filter 'effort))
(defun org-agenda-filter-show-all-cat nil
(org-agenda-remove-filter 'category))
(defun org-agenda-filter-show-all-top-filter nil
@ -8208,6 +8252,19 @@ When called with a prefix argument, include all archive files as well."
"}")
'face 'org-agenda-filter-tags
'help-echo "Tags used in filtering")) "")
(if (or org-agenda-effort-filter
(get 'org-agenda-effort-filter :preset-filter))
'(:eval (org-propertize
(concat " {"
(mapconcat
'identity
(append
(get 'org-agenda-effort-filter :preset-filter)
org-agenda-effort-filter)
"")
"}")
'face 'org-agenda-filter-effort
'help-echo "Effort conditions used in filtering")) "")
(if (or org-agenda-regexp-filter
(get 'org-agenda-regexp-filter :preset-filter))
'(:eval (org-propertize
@ -9915,8 +9972,10 @@ current HH:MM time."
`((,org-agenda-tag-filter tag)
(,org-agenda-category-filter category)
(,org-agenda-regexp-filter regexp)
(,org-agenda-effort-filter effort)
(,(get 'org-agenda-tag-filter :preset-filter) tag)
(,(get 'org-agenda-category-filter :preset-filter) category)
(,(get 'org-agenda-effort-filter :preset-filter) effort)
(,(get 'org-agenda-regexp-filter :preset-filter) regexp))))
(defun org-agenda-drag-line-forward (arg &optional backward)