forked from FG42/FG42
362 lines
13 KiB
EmacsLisp
362 lines
13 KiB
EmacsLisp
|
;;; pulldown.el --- Visual pulldown menu interface
|
||
|
|
||
|
;; Copyright (C) 2009 Tomohiro Matsuyama
|
||
|
|
||
|
;; Author: Tomohiro Matsuyama <m2ym.pub@gmail.com>
|
||
|
;; Keywords: lisp
|
||
|
;; Version: 0.1
|
||
|
|
||
|
;; This program 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 3 of the License, or
|
||
|
;; (at your option) any later version.
|
||
|
|
||
|
;; This program 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 program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;;
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(eval-when-compile
|
||
|
(require 'cl))
|
||
|
|
||
|
(defgroup pulldown nil
|
||
|
"Visual pulldown menu interface"
|
||
|
:group 'lisp
|
||
|
:prefix "pulldown-")
|
||
|
|
||
|
(defface pulldown-default-face
|
||
|
'((t (:background "lightgray" :foreground "black" :underline "darkgray")))
|
||
|
"Face for pulldown menu."
|
||
|
:group 'pulldown)
|
||
|
|
||
|
(defface pulldown-default-selection-face
|
||
|
'((t (:background "steelblue" :foreground "white")))
|
||
|
"Face for selection."
|
||
|
:group 'pulldown)
|
||
|
|
||
|
(defstruct pulldown
|
||
|
point row column width height direction overlays
|
||
|
newline-added
|
||
|
face selection-face
|
||
|
cursor offset scroll-top list)
|
||
|
|
||
|
(defun pulldown-x-to-string (x)
|
||
|
"Convert anything to string effeciently."
|
||
|
(typecase x
|
||
|
(string x)
|
||
|
(symbol (symbol-name x))
|
||
|
(integer (number-to-string x))
|
||
|
(float (number-to-string x))
|
||
|
(t (format "%s" x))))
|
||
|
|
||
|
(defun pulldown-goto-line (line)
|
||
|
"Goto `LINE' regarding of narrowing."
|
||
|
(goto-char (point-min))
|
||
|
(forward-line (1- line)))
|
||
|
|
||
|
(defun pulldown-current-physical-column ()
|
||
|
"Current physical column. (not logical column)"
|
||
|
(car (posn-col-row (posn-at-point))))
|
||
|
|
||
|
(defun pulldown-last-line-of-buffer ()
|
||
|
"Return non-nil if at last line of buffer."
|
||
|
(save-excursion (/= (forward-line) 0)))
|
||
|
|
||
|
(defun pulldown-item-propertize (item &rest properties)
|
||
|
(if (stringp item)
|
||
|
(apply 'propertize item properties)
|
||
|
item))
|
||
|
|
||
|
(defun pulldown-item-property (item property)
|
||
|
(if (stringp item)
|
||
|
(get-text-property 0 property item)))
|
||
|
|
||
|
(defun pulldown-set-list (menu list)
|
||
|
"Set menu list."
|
||
|
(setf (pulldown-list menu) list
|
||
|
(pulldown-offset menu) (if (> (pulldown-direction menu) 0)
|
||
|
0
|
||
|
(max (- (pulldown-height menu) (length list)) 0))))
|
||
|
|
||
|
(defun pulldown-line-overlay (menu line)
|
||
|
"Return a overlay of `MENU' at `LINE'."
|
||
|
(aref (pulldown-overlays menu) line))
|
||
|
|
||
|
(defun pulldown-hide-line (menu line)
|
||
|
"Hide `LINE' in `MENU'."
|
||
|
(let ((overlay (pulldown-line-overlay menu line)))
|
||
|
(overlay-put overlay 'invisible nil)
|
||
|
(overlay-put overlay 'after-string nil)))
|
||
|
|
||
|
(defun pulldown-show-line (menu line)
|
||
|
"Show `LINE' in `MENU'."
|
||
|
(overlay-put (pulldown-line-overlay menu line) 'invisible t))
|
||
|
|
||
|
(defun pulldown-set-line-item (menu line item &optional face)
|
||
|
"Set list of `LINE' in `MENU'."
|
||
|
(let ((overlay (pulldown-line-overlay menu line)))
|
||
|
(overlay-put overlay 'real-item item)
|
||
|
(overlay-put overlay
|
||
|
'after-string
|
||
|
(concat (overlay-get overlay 'prefix)
|
||
|
(propertize (pulldown-create-line-string menu item) 'face face)
|
||
|
(overlay-get overlay 'postfix)))))
|
||
|
|
||
|
(defun pulldown-create-line-string (menu item)
|
||
|
"Create string for showing `ITEM' in `MENU'."
|
||
|
(loop with string = (pulldown-x-to-string item)
|
||
|
with width = 0
|
||
|
with menu-width = (pulldown-width menu)
|
||
|
for length from 0
|
||
|
for c in (append string nil)
|
||
|
while (<= (incf width (char-width c)) menu-width)
|
||
|
finally return
|
||
|
(let ((string-width (string-width (if (< length (length string))
|
||
|
(substring string 0 length)
|
||
|
string))))
|
||
|
(if (< string-width menu-width)
|
||
|
;; Padding
|
||
|
(concat string (make-string (- menu-width string-width) ? ))
|
||
|
string))))
|
||
|
|
||
|
(defun pulldown-hide (menu)
|
||
|
"Hide `MENU'."
|
||
|
(dotimes (i (pulldown-height menu))
|
||
|
(pulldown-hide-line menu i)))
|
||
|
|
||
|
(defun pulldown-draw (menu)
|
||
|
"Draw `MENU'."
|
||
|
(loop with height = (pulldown-height menu)
|
||
|
with list = (pulldown-list menu)
|
||
|
with cursor = (pulldown-cursor menu)
|
||
|
with scroll-top = (pulldown-scroll-top menu)
|
||
|
with offset = (pulldown-offset menu)
|
||
|
for o from offset
|
||
|
for i from scroll-top
|
||
|
for item in (nthcdr scroll-top list)
|
||
|
while (< o height)
|
||
|
|
||
|
do
|
||
|
;; Show line and set item to the line
|
||
|
(pulldown-show-line menu o)
|
||
|
(pulldown-set-line-item
|
||
|
menu o item
|
||
|
(if (= i cursor)
|
||
|
(or (pulldown-item-property item 'selection-face) (pulldown-selection-face menu))
|
||
|
(or (pulldown-item-property item 'menu-face) (pulldown-face menu))))
|
||
|
|
||
|
finally
|
||
|
;; Hide remaining lines
|
||
|
(if (> (pulldown-direction menu) 0)
|
||
|
(while (< o height)
|
||
|
(pulldown-hide-line menu o)
|
||
|
(incf o))
|
||
|
(dotimes (o offset)
|
||
|
(pulldown-hide-line menu o)))))
|
||
|
|
||
|
(defun pulldown-next (menu)
|
||
|
"Select next item."
|
||
|
(let ((height (pulldown-height menu))
|
||
|
(cursor (1+ (pulldown-cursor menu)))
|
||
|
(scroll-top (pulldown-scroll-top menu))
|
||
|
(length (length (pulldown-list menu))))
|
||
|
(cond
|
||
|
((>= cursor length)
|
||
|
;; Back to first page
|
||
|
(setq cursor 0
|
||
|
scroll-top 0))
|
||
|
((= cursor (+ scroll-top height))
|
||
|
;; Go to next page
|
||
|
(setq scroll-top (min (1+ scroll-top) (max (- length height) 0)))))
|
||
|
(setf (pulldown-cursor menu) cursor
|
||
|
(pulldown-scroll-top menu) scroll-top)
|
||
|
(pulldown-draw menu)))
|
||
|
|
||
|
(defun pulldown-previous (menu)
|
||
|
"Select previous item."
|
||
|
(let ((height (pulldown-height menu))
|
||
|
(cursor (1- (pulldown-cursor menu)))
|
||
|
(scroll-top (pulldown-scroll-top menu))
|
||
|
(length (length (pulldown-list menu))))
|
||
|
(cond
|
||
|
((< cursor 0)
|
||
|
;; Go to last page
|
||
|
(setq cursor (1- length)
|
||
|
scroll-top (max (- length height) 0)))
|
||
|
((= cursor (1- scroll-top))
|
||
|
;; Go to previous page
|
||
|
(decf scroll-top)))
|
||
|
(setf (pulldown-cursor menu) cursor
|
||
|
(pulldown-scroll-top menu) scroll-top)
|
||
|
(pulldown-draw menu)))
|
||
|
|
||
|
(defun* pulldown-create (point width height
|
||
|
&key
|
||
|
(face 'pulldown-default-face)
|
||
|
(selection-face 'pulldown-default-selection-face))
|
||
|
"Create pulldown menu."
|
||
|
(save-excursion
|
||
|
(goto-char point)
|
||
|
(let* ((row (line-number-at-pos))
|
||
|
(column (pulldown-current-physical-column))
|
||
|
(overlays (make-vector height nil))
|
||
|
(window (selected-window))
|
||
|
(window-start (window-start))
|
||
|
(window-hscroll (window-hscroll))
|
||
|
(window-width (window-width))
|
||
|
(right (+ column width))
|
||
|
(direction (if (and (> row height)
|
||
|
(> height (- (max 1 (- (window-height)
|
||
|
(if mode-line-format 1 0)
|
||
|
(if header-line-format 1 0)))
|
||
|
(count-lines window-start (point)))))
|
||
|
-1
|
||
|
1))
|
||
|
(newline-added (save-excursion
|
||
|
(goto-char (point-max))
|
||
|
(unless (bolp)
|
||
|
(newline)
|
||
|
t)))
|
||
|
current-column)
|
||
|
(if (and (> right window-width)
|
||
|
(>= right width)
|
||
|
(>= column width))
|
||
|
(decf column width))
|
||
|
(dotimes (i height)
|
||
|
(let (overlay begin w (prefix "") (postfix ""))
|
||
|
(if (>= emacs-major-version 23)
|
||
|
(vertical-motion (cons column direction))
|
||
|
(vertical-motion direction)
|
||
|
(move-to-column (+ (current-column) column)))
|
||
|
(setq current-column (pulldown-current-physical-column))
|
||
|
|
||
|
(when (> current-column column)
|
||
|
(backward-char)
|
||
|
(setq current-column (pulldown-current-physical-column)))
|
||
|
(when (< current-column column)
|
||
|
;; Extend short buffer lines by menu prefix (line of spaces)
|
||
|
(setq prefix (make-string (+ (if (= current-column 0)
|
||
|
(- window-hscroll (current-column))
|
||
|
0)
|
||
|
(- column current-column))
|
||
|
? )))
|
||
|
|
||
|
(setq begin (point))
|
||
|
(setq w (+ width (length prefix)))
|
||
|
(while (and (not (eolp)) (> w 0))
|
||
|
(decf w (char-width (char-after)))
|
||
|
(forward-char))
|
||
|
(if (< w 0)
|
||
|
(setq postfix (make-string (- w) ? )))
|
||
|
(if (pulldown-last-line-of-buffer)
|
||
|
(setq postfix (concat postfix "\n")))
|
||
|
|
||
|
(setq overlay (make-overlay begin (point)))
|
||
|
(overlay-put overlay 'window window)
|
||
|
(overlay-put overlay 'prefix prefix)
|
||
|
(overlay-put overlay 'postfix postfix)
|
||
|
(overlay-put overlay 'width width)
|
||
|
(aset overlays
|
||
|
(if (> direction 0) i (- height i 1))
|
||
|
overlay)))
|
||
|
(loop for p from 100
|
||
|
for overlay in (nreverse (append overlays nil))
|
||
|
do (overlay-put overlay 'priority p))
|
||
|
(make-pulldown :point point
|
||
|
:row row
|
||
|
:column column
|
||
|
:width width
|
||
|
:height height
|
||
|
:direction direction
|
||
|
:newline-added newline-added
|
||
|
:face face
|
||
|
:selection-face selection-face
|
||
|
:cursor 0
|
||
|
:scroll-top 0
|
||
|
:list '()
|
||
|
:overlays overlays))))
|
||
|
|
||
|
(defun pulldown-delete (menu)
|
||
|
(mapcar 'delete-overlay (pulldown-overlays menu))
|
||
|
(setf (pulldown-overlays menu) nil)
|
||
|
(if (pulldown-newline-added menu)
|
||
|
(save-excursion
|
||
|
(goto-char (point-max))
|
||
|
(if (and (bolp) (eolp))
|
||
|
(delete-char -1)))))
|
||
|
|
||
|
(defun pulldown-live-p (menu)
|
||
|
(and menu (pulldown-overlays menu) t))
|
||
|
|
||
|
(defun pulldown-preferred-width (list)
|
||
|
"Return preferred width of pulldown menu to show `LIST' beautifully."
|
||
|
(loop for item in list
|
||
|
maximize (string-width (pulldown-x-to-string item)) into width
|
||
|
finally return (* (ceiling (/ (or width 0) 10.0)) 10)))
|
||
|
|
||
|
(defun pulldown-lookup-key-by-event (function event)
|
||
|
(or (funcall function (vector event))
|
||
|
(if (symbolp event)
|
||
|
(let ((mask (get event 'event-symbol-element-mask)))
|
||
|
(if mask
|
||
|
(funcall function (vector (logior (or (get (car mask) 'ascii-character) 0)
|
||
|
(cadr mask)))))))))
|
||
|
|
||
|
(defun* pulldown-event-loop (menu keymap fallback &optional message &aux event binding)
|
||
|
(unwind-protect
|
||
|
(block nil
|
||
|
(while (and (pulldown-live-p menu)
|
||
|
(setq event (progn (clear-this-command-keys) (read-event message))))
|
||
|
(if (eq event 'Quit)
|
||
|
(return nil))
|
||
|
(setq binding (pulldown-lookup-key-by-event (lambda (key) (lookup-key keymap key)) event))
|
||
|
(cond
|
||
|
((eq binding 'pulldown-select)
|
||
|
(return (nth (pulldown-cursor menu) (pulldown-list menu))))
|
||
|
((eq binding 'pulldown-next)
|
||
|
(pulldown-next menu))
|
||
|
((eq binding 'pulldown-previous)
|
||
|
(pulldown-previous menu))
|
||
|
(binding
|
||
|
(call-interactively binding))
|
||
|
(t
|
||
|
(funcall fallback event (pulldown-lookup-key-by-event (lambda (key) (key-binding key)) event))))))
|
||
|
(pulldown-delete menu)))
|
||
|
|
||
|
(defun pulldown-default-fallback (event default))
|
||
|
|
||
|
(defun* pulldown-menu (list
|
||
|
&key
|
||
|
(width (pulldown-preferred-width list))
|
||
|
(height 10)
|
||
|
(keymap pulldown-keymap)
|
||
|
(fallback 'pulldown-default-fallback)
|
||
|
message
|
||
|
&aux menu event)
|
||
|
(setq menu (pulldown-create (point) width height))
|
||
|
(pulldown-set-list menu list)
|
||
|
(pulldown-draw menu)
|
||
|
(pulldown-event-loop menu keymap fallback message))
|
||
|
|
||
|
(defvar pulldown-keymap
|
||
|
(let ((map (make-sparse-keymap)))
|
||
|
(define-key map "\r" 'pulldown-select)
|
||
|
|
||
|
(define-key map "\C-n" 'pulldown-next)
|
||
|
(define-key map "\C-p" 'pulldown-previous)
|
||
|
(define-key map [down] 'pulldown-next)
|
||
|
(define-key map [up] 'pulldown-previous)
|
||
|
map))
|
||
|
|
||
|
(provide 'pulldown)
|
||
|
;;; pulldown.el ends here
|