-
Notifications
You must be signed in to change notification settings - Fork 266
/
menu-definitions.lisp
474 lines (409 loc) · 19.2 KB
/
menu-definitions.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
;; Copyright (C) 2018 Stuart Dilts
;;
;; This file is part of stumpwm.
;;
;; stumpwm is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; stumpwm is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this software; see the file COPYING. If not, see
;; <http://www.gnu.org/licenses/>.
;; Commentary:
;;
;; Implementation of an interactive menu. This file contains the definitions of the menu
;; class as defined in menu-declarations.lisp.
;;
;; Code:
;;; interactive menu
(in-package #:stumpwm)
(export '(entries-from-nested-list
select-from-menu
select-from-batch-menu
command-menu))
(defun entries-from-nested-list (lst)
(mapcar (lambda (x)
(make-instance 'menu-entry
:label (car x)
:data (cadr x)))
lst))
(defmethod menu-entry-display ((entry menu-entry))
(concat (string (menu-entry-icon entry)) " " (menu-entry-label entry)))
(defmethod menu-entry-apply ((entry menu-entry) function)
(if (slot-boundp entry 'data)
(values (apply function (menu-entry-data entry)) t)
(values nil nil)))
(defmethod print-object ((obj menu-entry) out)
(print-unreadable-object (obj out :type t)
(format out ":LABEL ~s :ICON ~s :DATA ~a"
(menu-entry-label obj) (menu-entry-icon obj)
(when (slot-boundp obj 'data)
(menu-entry-data obj)))))
(defun menu-scrolling-required (menu)
(and *menu-maximum-height*
(> (length (menu-table menu))
*menu-maximum-height*)))
(defun menu-height (menu)
(let ((len (length (menu-table menu))))
(min (or *menu-maximum-height* len) len)))
(defun bound-check-menu (menu)
"Adjust the menu view and selected item based
on current view and new selection."
(let ((len (length (menu-table menu))))
;; Wrap around
(setf (menu-selected menu)
(cond ((< (menu-selected menu) 0) (1- len))
((>= (menu-selected menu) len) 0)
(t (menu-selected menu))))
(setf (values (menu-view-start menu)
(menu-view-end menu))
(if (zerop len)
(values 0 0)
(let* ((menu-height (menu-height menu))
(sel (menu-selected menu))
(start (- sel 1))
(end (+ sel menu-height -1)))
(multiple-value-bind
(start end) (cond ((< start 0) (values 0 menu-height))
((> end len) (values (- len menu-height) len))
(t (values start end)))
(assert (<= 0 start (- len menu-height)) (start))
(assert (<= menu-height end len) (end))
(values start end)))))))
(defmethod menu-up ((menu menu))
(decf (menu-selected menu))
(bound-check-menu menu))
;; before or after? probably doesn't matter
(defmethod menu-up :before ((menu single-menu))
"clear the search string if the cursor is moved"
(setf (fill-pointer (single-menu-current-input menu)) 0))
(defmethod menu-down ((menu menu))
(incf (menu-selected menu))
(bound-check-menu menu))
(defmethod menu-down :before ((menu single-menu))
"clear the search string if the cursor is moved"
(setf (fill-pointer (single-menu-current-input menu)) 0))
(defmethod menu-scroll-up ((menu menu))
(decf (menu-selected menu) *menu-scrolling-step*)
(bound-check-menu menu))
(defmethod menu-scroll-up :before ((menu single-menu))
"clear the search string if the cursor is moved"
(setf (fill-pointer (single-menu-current-input menu)) 0))
(defmethod menu-scroll-down ((menu menu))
(incf (menu-selected menu) *menu-scrolling-step*)
(bound-check-menu menu))
(defmethod menu-scroll-down :before ((menu single-menu))
"clear the search string if the cursor is moved"
(setf (fill-pointer (single-menu-current-input menu)) 0))
(defmethod menu-page-up ((menu menu))
(when *menu-maximum-height* ;;No scrolling = no page up/down
(decf (menu-selected menu) *menu-maximum-height*)
(let ((*menu-scrolling-step* *menu-maximum-height*))
(bound-check-menu menu))))
(defmethod menu-page-up :before ((menu single-menu))
(when *menu-maximum-height* ;;No scrolling = no page up/down
(setf (fill-pointer (single-menu-current-input menu)) 0)))
(defmethod menu-page-down ((menu menu))
(when *menu-maximum-height*
(incf (menu-selected menu) *menu-maximum-height*)
(let ((*menu-scrolling-step* *menu-maximum-height*))
(bound-check-menu menu))))
(defmethod menu-page-down :before ((menu single-menu))
(when *menu-maximum-height*
(setf (fill-pointer (single-menu-current-input menu)) 0)))
(defmethod menu-finish ((menu menu))
(throw :menu-quit (nth (menu-selected menu) (menu-table menu))))
(defmethod menu-finish ((menu batch-menu))
"Value returned with the signal is an alist, where the cdr of the value
returned by assoc is a list items that were marked with that character.
Example when entry1 and entry2 are marked with 'a', and entry3 is not marked:
((a entry1 entry2) (NIL entry3))"
(with-slots (allowed-markers table) menu
(let ((alist (list)))
(dolist (entry table)
(let ((mark (car entry))
(value (cdr entry)))
(if-let ((cell (assoc mark alist)))
(push value (cdr cell))
(setf alist (acons mark (list value) alist)))))
(throw :menu-quit alist))))
(defmethod menu-abort (menu)
(declare (ignore menu))
(throw :menu-quit nil))
(defun get-input-char (key)
"If @var{key} is a character suitable for menu completion (e.g. not
backspace or F9), return it otherwise return nil"
(let ((char (xlib:keysym->character *display* (key-keysym key))))
(if (or (key-mods-p key) (null char)
(not (characterp char)))
nil
char)))
;; menu-backspace might be in someone's custom *menu-map*:
;; leave this here just to be safe:
(defmethod menu-backspace ((menu menu))
(declare (ignore menu))
"By default, do nothing")
(defun batch-menu-unmark-selected (menu)
(with-accessors ((table menu-table)
(selected menu-selected))
menu
(setf (car (nth selected table)) nil)))
(defmethod menu-backspace ((menu batch-menu))
"If the cursor is not at the top, move cursor up. Regardless,
unmark the entry at the selected point."
(when (> (menu-selected menu) 0)
(menu-up menu))
(batch-menu-unmark-selected menu))
(defmethod menu-backspace ((menu single-menu))
(when (> (fill-pointer (single-menu-current-input menu)) 0)
(vector-pop (single-menu-current-input menu))
(typing-action menu nil)))
(defmethod menu-prompt-line ((menu menu))
"If there is a prompt, show it:"
(menu-prompt menu))
(defun menu-prompt-visible (menu)
(or (menu-prompt menu)
(> (length (single-menu-current-input menu)) 0)))
(defmethod menu-prompt-line ((menu single-menu))
"When a prompt is shown, also show the search string."
(when (menu-prompt-visible menu)
(format nil "~@[~A ~]~A"
(menu-prompt menu) (single-menu-current-input menu))))
(defmethod typing-action ((menu menu) key-seq)
"Default action is to do nothing"
(declare (ignore key-seq)))
(defmethod typing-action ((menu single-menu) key-seq)
"If the user entered a key not mapped in @var{*menu-map}, check it.
If he's trying to type an entry's name, either complete or not based
on COMPLETE-PARTIAL. Match is case insensitive. If @var{key-seq} is nil,
some other function has manipulated the current-input and is requesting
a re-computation of the match."
(let ((input-char (and key-seq (get-input-char key-seq))))
(when input-char
(vector-push-extend input-char (single-menu-current-input menu)))
(handler-case
(when (or input-char (not key-seq))
(labels ((match-p (table-item)
(funcall (single-menu-filter-pred menu)
(car table-item)
(second table-item)
(single-menu-current-input menu))))
(setf (menu-table menu) (remove-if-not #'match-p (single-menu-unfiltered-table menu))
(menu-selected menu) 0)
(bound-check-menu menu)))
(cl-ppcre:ppcre-syntax-error ()))))
(defmethod typing-action ((menu batch-menu) key-seq)
"Mark the selected item with the character that was typed. If the character
is not allowed, as specified by allowed-markers, item is not marked"
(let ((input-char (and key-seq (get-input-char key-seq))))
(with-slots (selected table allowed-markers) menu
(when (and input-char (or (not allowed-markers) (member input-char allowed-markers)))
(setf (car (nth selected table)) input-char)
(menu-down menu)))))
(defun menu-element-name (element)
"Used for the default menus: they don't use menu-entry."
(if (listp element)
(first element)
element))
(defmethod get-menu-items ((menu menu))
(mapcar #'menu-element-name
(subseq (menu-table menu)
(menu-view-start menu) (menu-view-end menu))))
(defmethod get-menu-items ((menu batch-menu))
(with-slots (table view-start view-end) menu
(mapcar (lambda (entry)
(if (car entry)
;; if there is a mark, show it, else show a space
(concat (string (car entry)) (menu-entry-display (cdr entry)))
(concat " " (menu-entry-display (cdr entry)))))
(subseq table
view-start view-end))))
(defun menu-item-matches-regexp (item-string item-object user-input)
"The default filter predicate for SELECT-FROM-MENU. When using this
predicate, an item is visible when it matches all of the regular
expressions in USER-INPUT (multiple regexps are separated by one or
more spaces; ARGUMENT-POP is used to split the string)."
(declare (ignore item-object))
(match-all-regexps user-input item-string))
(defun run-menu (screen menu)
"Runs the menu. Implement all of the methods in the menu, then pass an instance to this function"
(declare (type menu menu))
;; align the menu, make the pages
(bound-check-menu menu)
(catch :menu-quit
(unwind-protect
(with-focus (screen-key-window screen)
(let ((*suppress-echo-timeout* t)
(displaying-help-bindings nil))
(loop
(let* ((sel (menu-selected menu))
(start (menu-view-start menu))
(end (menu-view-end menu))
(len (length (menu-table menu)))
(prompt-line (menu-prompt-line menu))
(strings (get-menu-items menu))
(highlight (- sel start)))
(unless displaying-help-bindings
(unless (zerop start)
(setf strings (cons "..." strings))
(incf highlight))
(unless (= len end)
(setf strings (nconc strings '("..."))))
(when prompt-line
(push prompt-line strings)
(incf highlight))
(run-hook-with-args *menu-selection-hook* menu)
(echo-string-list screen strings highlight)))
(multiple-value-bind (action key-seq) (read-from-keymap (menu-keymap menu))
(cond ((and action
(not (or (fboundp action)
(functionp action)))
(help-key-p key-seq))
(setf displaying-help-bindings t))
((and displaying-help-bindings
(eql action 'menu-abort))
(setf displaying-help-bindings nil))
(t
(when displaying-help-bindings
(setf displaying-help-bindings nil))
(if (fboundp action)
(progn (funcall action menu)
(bound-check-menu menu))
(typing-action menu (first key-seq)))))))))
(unmap-all-message-windows))))
(defun select-from-menu (screen table &optional (prompt "Search:")
(initial-selection 0)
extra-keymap
(filter-pred #'menu-item-matches-regexp))
"Prompt the user to select from a menu on SCREEN. TABLE can be
a list of values or a nested list. If it's a nested list, the first
element in the sublist is displayed in the menu. What is displayed
as menu items must be strings.
EXTRA-KEYMAP can be a keymap whose bindings will take precedence
over the default bindings.
FILTER-PRED should be a a function returning T when a certain menu
item should be visible to the user. It should accept arguments
ITEM-STRING (the string shown to the user), ITEM-OBJECT (the object
corresponding to the menu item), and USER-INPUT (the current user
input). The default is MENU-ITEM-MATCHES-REGEXP.
Returns the selected element in TABLE or nil if aborted. "
(check-type screen screen)
(check-type table (or (cons string) (cons cons)))
(check-type prompt (or null string))
(check-type initial-selection integer)
(when table
(let ((menu (make-instance 'single-menu
:table (if (every #'listp table)
table
(mapcar #'list table))
:selected initial-selection
:prompt prompt
:view-start 0
:view-end 0
:additional-keymap extra-keymap
:FILTER-PRED filter-pred)))
(run-menu screen menu))))
(defun select-from-batch-menu (screen table &key (prompt "Select:")
allowed-markers
(initial-selection 0)
extra-keymap)
"Prompt the user with a menu that allows them to mark each item
with a character. They can exit the menu by pressing enter, or
whatever key is mapped to 'menu-finish' in *menu-map*. Value returned
is an alist, where the cdr of each entry is a list of
items that were marked with that character. Note that the lisp printer cannot
distinguish between '(a . (b c d)) and '(a b c d).
Example when \"foo\" and \"bar\" are marked with '#\d', and \"baz\" is not marked:
((#\d \"foo\" \"bar\") (NIL \"baz\"))
ALLOWED-MARKERS is a list of characters. If this parameter is specified, no
other markers are allowed.
EXTRA-KEYMAP can be a keymap whose bindings will take precedence
over the default bindings."
(check-type screen screen)
(check-type table list)
(check-type prompt (or null string))
(check-type allowed-markers list)
(when table
(let ((menu (make-instance 'batch-menu
:table table
:prompt prompt
:allowed-markers allowed-markers
:selected initial-selection
:additional-keymap extra-keymap)))
(run-menu screen menu))))
(defun command-menu (screen items command-list &key (prompt "Select:")
(initial-selection 0)
extra-keymap)
"Use batch-menu to make selections and run commands specified in command-list.
SCREEN: The screen to display the menu on.
ITEMS: The items to be shown in the list. This is expected to be a list of @code{menu-item}s.
COMMAND-LIST: A list of entries defining the commands associated with each mark.
Only marks that are defined are allowed in the menu. The format
for these entries is (mark-character function calling-options).
Available calling-options:
:single (Default) Each value is passed separately to the supplied function.
:all all values selected with this mark are passed to the function in a list.
Example:
'((#\d 'delete-window) (#\m 'move-multiple-windows :all))"
(let ((results
(select-from-batch-menu screen items
:prompt prompt
;; use the first value of every entry
;; except when it is nill:
:allowed-markers (mapcan (lambda (x)
(if (first x)
(list (first x))))
command-list)
:initial-selection initial-selection
:extra-keymap extra-keymap)))
(dolist (command-entry command-list)
(let ((selections (assoc (first command-entry) results))
(func (second command-entry))
;; change this to cddr if we ever have more than one option?
(options (caddr command-entry)))
(when selections
(cond
((eql :all options)
(funcall func (mapcar 'menu-entry-data (cdr selections))))
;; default option: check if it is nil:
((or (eql options :all) (eql options nil))
(dolist (data (cdr selections))
(funcall func (menu-entry-data data))))
(t (error (format nil "keyword ~s not a valid command option for selection-menu."
options)))))))))
(when (null *menu-map*)
(setf *menu-map*
(let ((m (make-sparse-keymap)))
(define-key m (kbd "C-p") 'menu-up)
(define-key m (kbd "Up") 'menu-up)
(define-key m (kbd "S-Up") 'menu-scroll-up)
(define-key m (kbd "SunPageUp") 'menu-page-up)
(define-key m (kbd "C-n") 'menu-down)
(define-key m (kbd "Down") 'menu-down)
(define-key m (kbd "S-Down") 'menu-scroll-down)
(define-key m (kbd "SunPageDown") 'menu-page-down)
(define-key m (kbd "C-g") 'menu-abort)
(define-key m (kbd "ESC") 'menu-abort)
(define-key m (kbd "RET") 'menu-finish)
m)))
(when (null *single-menu-map*)
(setf *single-menu-map*
(let ((m (make-sparse-keymap)))
(define-key m (kbd "DEL") 'menu-backspace)
m)))
(when (null *batch-menu-map*)
(setf *batch-menu-map*
(let ((m (make-sparse-keymap)))
(define-key m (kbd "DEL") 'menu-backspace)
(define-key m (kbd "n") 'menu-down)
(define-key m (kbd "p") 'menu-up)
(define-key m (kbd "space") 'menu-down)
(define-key m (kbd "u") (lambda (menu)
(batch-menu-unmark-selected menu)
(menu-down menu)))
(define-key m (kbd "x") 'menu-finish)
m)))