\section{Menu System} This section presents a menu system created in LISP, some menus of Max objects and operations on them created in the menu system. The menu system can stand alone and is useful for any LISP program. <>= <> <> <> @ \subsection{A Menu System in LISP} <>= <> <> <> @ <>= (in-package max) @ A menu consists of a name and a list of menu items. To create a menu, call MAKE-MENU with the name and list of items. Name is a string. The list of items is a regular LISP list of menu item structures. The menu item structure is described below. MAKE-MENU will return a menu structure, which can then be passed to MENU-NAMES or MENU-ITEMS to retrieve the fields from the structure. The menu structure is implemented as the cons of its name (a string) with the list of menu items. <>= (defun make-menu (name items) (cons name items)) (defun menu-name (menu) (car menu)) (defun menu-items (menu) (cdr menu)) @ A menu item is a list consisting of fields named key, name, help, and thunk. The key is a string (usually one character, hence the name ``key'') typed by the user to choose this item. The name is shown to the user and describes what the item is or does. The help field is reserved for future use and is not implemented, but may eventually provide additional detail about the menu item. The thunk is a LISP function which will be called when the user invokes the menu item. Menu item structures should be treated as abstract data types by calling the following functions on them. Namely, MAKE-MENU-ITEM should be used to create each menu item. Put all the menu items in a list and pass the list to MAKE-MENU, as described above. The other functions are accessor functions and you probably don't need them (they are used by the menu implementation). <>= (defun make-menu-item (key name help thunk) (list key name help thunk)) (defun menu-item-key (menu-item) (car menu-item)) (defun menu-item-name (menu-item) (cadr menu-item)) (defun menu-item-help (menu-item) (caddr menu-item)) (defun menu-item-thunk (menu-item) (cadddr menu-item)) @ CALL-MAIN-MENU is the public interface to call a main menu of your creation. This is the standard entry point into the menu system. You'll need to pass it a menu structure that was previously created by MAKE-MENU and containing the specification of your main menu. <>= (defun call-main-menu (main-menu) (catch 'end-menu (call-menu main-menu :submenup nil) ) ) @ The public interface to be used to create submenus is CALL-MENU. CALL-MENU should be called from inside the thunks implementing menu items you want to be submenus. To call (invoke) a menu you require one argument, a menu structure which specifies the menu to invoke. The menu items are displayed, and a string is requested from the user. If the string matches one of the menu items' key fields, that item is executed. When the item is done executing, control returns to the menu. If the string does not match, the menu is redisplayed. Several optional keyword arguments are available to customize the menu display: \begin{itemize} \item[:submenup], boolean: is this the main menu or a submenu. Submenus have the additional options ``back to the previous menu'' and ``back to the main menu.'' The main menu is the most recently-invoked menu that was called with :submenup nil. Normally there is only one main menu, and the rest use the default :submenup value of t. This option does not need to be manually specified since it is set appropriately by CALL-MAIN-MENU. \item[:showheader], boolean: if NIL, do not display the menu name or decorative box around the name \item[:extratext], string: text to be displayed after the menu header and before the menu items. \end{itemize} <>= (defun call-menu (menu &key (submenup t) (showheader t) (extratext nil)) (catch 'previous-menu (let* ((extraitems (if submenup (list nil *back-item* *mainmenu-item* *quit-item*) (list nil *quit-item*)) ) (fullmenu (append menu extraitems)) (again t)) (tagbody repeat (catch (if submenup 'ignore 'main-menu) (display-menu fullmenu showheader extratext) (display-prompt fullmenu) (setq again (execute-choice (menu-items fullmenu) (read-menu-choice fullmenu))) ) (if again (go repeat)) ) ) ) ) @ The implementation of CALL-MENU requires some global variables (defining some items that may occur on any menu, such as Quit), and some helper functions it calls to perform the parts of its job. <>= <> <> <> <> @ The menu system uses LISP's catch/throw semantics for non-local branching to implement ``back to the previous menu'' and ``back to the main menu.'' <>= (defun end-menu () (throw 'end-menu t)) (defun previous-menu () (throw 'previous-menu t)) (defun main-menu () (throw 'main-menu t)) (defparameter *quit-item* (make-menu-item "Q" "Quit" "Exit from the menu system" #'end-menu) ) (defparameter *back-item* (make-menu-item "B" "Back" "Return to the previous menu" #'previous-menu) ) (defparameter *mainmenu-item* (make-menu-item "M" "Main Menu" "Go to the main menu" #'main-menu) ) @ Displaying a menu is performed by displaying the header, extra text, followed by each menu item in order, with appropriate blank lines in between. <>= (defun display-menu (&optional (menu *main-menu*) (showheader t) (extratext t)) (if showheader (progn (terpri) (display-menu-header (menu-name menu)) ) ) (if extratext (format t "~%~A" extratext)) (terpri) (display-menu-items (menu-items menu)) (terpri) ) @ The menu header consists of a line of equal signs, the menu name surrounded by vertical bars, and another line of equal signs. <>= (defun display-menu-header (title) (let* ((titlerow (format nil "| ~A |" title)) (bar (make-string (length titlerow) :initial-element #\=))) (write-line bar) (write-line titlerow) (write-line bar) ) ) @ Displaying a menu item consists of displaying its key followed by a period and a space, then its name, and a newline. The menu system internally recognizes NIL in place of a menu item to display a blank line in the menu at that point. <>= (defun display-menu-items (items) (dolist (item items) (if (not (null item)) (progn (write-string (menu-item-key item)) (write-string ". ") (write-string (menu-item-name item)) )) (terpri) ) ) @ EXECUTE-CHOICE will try to find and execute a menu item corresponding to a provided string. If the thunk associated with a menu item returns NIL, the menu is exited as if the user chose QUIT. This is usually not expected behavior, so ensure that all menu item handlers return non-NIL. <>= (defun execute-choice (menu-items string) (cond ((null menu-items) (write-line (format nil "Unknown option: ~A" string))) ((null (car menu-items)) (execute-choice (cdr menu-items) string)) ((equal (string-downcase string) (string-downcase (menu-item-key (car menu-items)))) (funcall (menu-item-thunk (car menu-items))) ) (t (execute-choice (cdr menu-items) string)) ) ) @ We'll use a simple arrow-like prompt and grab a string from the user terminated by a return. <>= (defun display-prompt (menu) (write-string "--> ")) (defun read-menu-choice (menu) (clear-input) (read-line)) @ \subsection{Max-specific menus} This section will describe the following items in backwards order from which they appear in the program. In the program, each item depends on the one before it, since the menus are built from the bottom up. However, it seems more intuitive from a reader's view for the description to be organized top-down. <>= <> <> <> <> <> <> <> <
> <
> @ The main Max menu is invoked by the LISP function (MENU) after Max is loaded. <
>= (defun menu () (call-main-menu *main-menu*)) @ The main menu consists of two items (Types and Functions) which go to submenus. The Tests item runs all the unit tests and prints the result (this is the same as typing (TEST) at the LISP prompt). <
>= (defparameter *main-menu* (make-menu "Main Menu" `( ,(make-menu-item "1" "Types" "Types are abstract collections of values" (lambda () (call-menu types-menu))) ,(make-menu-item "2" "Functions" "Pure mathematical functions, with one type for the domain and one for the codomain" (lambda () (call-menu functions-menu))) ,(make-menu-item "3" "Tests" "Run the automated unit tests" (lambda () (test))) ,(make-menu-item "4" "Workspace" "Show the workspace of sources" (lambda () (call-menu workspace-menu))) ) )) @ The types menu shows all the public types registered in the catalog. The LISP representation of the Max address of each type is shown as a numbered menu item. <>= (defparameter types-menu nil) (add-init-hook #'(lambda () (setf types-menu (make-menu "Types" (menu-items-from-set (get-public-types) #'type-menu-item-handler))) )) @ The functions menu shows all the public functions registered in the catalog. Each menu item is the LISP representation of a Max function address. <>= (defparameter functions-menu nil) (add-init-hook #'(lambda () (setf functions-menu (make-menu "Functions" (menu-items-from-set (get-public-functions) #'function-menu-item-handler)) ) )) @ The workspace menu shows all the sources you have created so far. To create a source, you need to first find a thunk in a function menu and choose "Make source from thunk." The new source will be added to the workspace. After you have one source, you can find a function taking the same type as that source, and choose "Make source from function." Then the workspace menu will come up; select the source for the function argument. The function and argument pair will be used to form a new source which will be added to your workspace. Choose a data source from the workspace to perform a data request (PULL) on it. While the functions and types menus are created statically at Max initialization time, the workspace menu has to be dynamically updated. It starts empty and is recreated every time something is added. There are two global variables: the *workspace* is a max set, and workspace-menu is a menu structure. <>= (defparameter workspace-menu (make-menu "Workspace" NIL)) @ When you choose a menu item corresponding to a type, it brings up a submenu showing all the functions in the public interface for that type. There is currently no way to distinguish between which functions have the current type as the domain or the codomain except by choosing that function's number and comparing what it says for its domain and codomain to the current type. <>= (defun type-menu-item-handler (typeaddr) (call-menu (make-menu (format nil "Public interface for ~A" typeaddr) (menu-items-from-set (public-interface typeaddr) #'function-menu-item-handler))) ) @ When you choose a menu item corresponding to a function, it brings up the domain and codomain of the function, along with its implementation. Enumerated functions show their raw hash tables, while builtin functions show their lambda expressions. There is no way to name functions yet, so you have to guess what they are from their implementations. <>= (defun function-menu-item-handler (funcaddr) (catch 'previous-menu (let* ((domain (function-apply *domain* funcaddr)) (codomain (function-apply *codomain* funcaddr)) (str (concatenate 'string (format nil "Domain : ~A~%" domain) (format nil "Codomain : ~A~%" codomain) (format nil "Implementation:~%~A~%" (mapping (function-implementation funcaddr))) )) (menu-items `(,(make-menu-item "1" "Go to domain" "" (lambda () (type-menu-item-handler domain))) ,(make-menu-item "2" "Go to codomain" "" (lambda () (type-menu-item-handler codomain))))) ) (call-menu (make-menu (format nil "Function ~A" funcaddr) (append menu-items (list (if (addr-equal domain *null-type*) (make-menu-item "3" "Make source from thunk" "" (lambda () (thunk-to-source funcaddr))) (make-menu-item "3" "Make source from function" "" (lambda () (function-to-source funcaddr))) ) ) )) :extratext str) t ) ) ) @ You can turn a function into a data source from the menu. If the function takes an argument, you'll need to choose an already-existing data source of the function's argument type before the new source will be created. When successful, a new source will appear in the workspace with the type of the function's return value. <>= (defparameter *workspace* (get-empty-set *sources*)) (defun source-menu-item-handler (sourceaddr) (format t "Performing data request on ~A...~%Result: ~A~%" sourceaddr (source-pull sourceaddr)) t ) (defun thunk-to-source (funcaddr) (update-workspace (make-function-source funcaddr)) ) (defun update-workspace (sourceaddr) (setf *workspace* (set-add *workspace* sourceaddr )) (setf workspace-menu (make-menu "Workspace" (menu-items-from-set *workspace* #'source-menu-item-handler))) ) (defun function-to-source (funcaddr) (if (not (catch 'type-mismatch (call-menu (make-menu "Select source for argument" (menu-items-from-set *workspace* #'(lambda (item) (update-workspace (make-function-source funcaddr item)) (throw 'previous-menu t) ) ) ) ) )) (format t "Type mismatch~%")) ) @ In order to create menus from max sets, such as the set of public types or functions, we iterate through the set with MAPSET, building a list of menu items that call the provided function on the set item. <>= (defun menu-items-from-set (set op) (let ((items nil) (n 1)) (mapset #'(lambda (item) (setq items (cons (make-menu-item (format nil "~A" n) (format nil "~A" item) "" ; No online help #'(lambda () (funcall op item)) ) items) ) (incf n) ) set ) (reverse items) ) ) @