Module: duim-frames-internals Synopsis: DUIM frames Author: Scott McKay, Andy Armstrong Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Command tables // Command table registry, maps command table names to command tables define variable *command-tables* :: = make(); // Command tables are a way of grouping a set of related command objects // or "command functions" and providing user interfaces to the commands. // Interaction styles include via pull-down menus, keystroke accelerators, // direct manipulations (such as presentation translations and drag&drop), // and command lines. define sealed class () // A symbol that names the command table sealed constant slot command-table-name, required-init-keyword: name:; // A sequence of command tables from which this one inherits sealed slot command-table-inherit-from, required-init-keyword: inherit-from:, setter: %inherit-from-setter; // A table of all the commands in this command table; the key is a // object, the value is the command line name string // or a boolean sealed slot command-table-commands :: = make(
); // This is a sequence of decorators used to build menu and tool bars, etc. sealed slot command-table-menu :: = make(); // A cache of all the keystroke accelerators for this command table sealed slot %accelerators = #f; /* //--- This belongs in a command-line layer // A sorted sequence that maps from command-line names to // objects for this command table; entries are a pair consisting of the // command line name string and the sealed slot %command-line-names :: false-or() = #f; // A completion alist of command line names for this command table and // all the command tables it inherits from sealed slot %completion-alist = #f; sealed slot %completion-alist-tick = 0; */ /* //--- This belongs in a presentation layer // The set of presentation translators for this command table sealed slot command-table-translators :: false-or() = #f; sealed slot %translators-cache :: false-or() = #f; */ // A resource id for creating menus from this command table sealed constant slot command-table-resource-id = #f, init-keyword: resource-id:; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define method command-table-inherit-from-setter (inherit-from, command-table :: ) => (command-tables :: ) unless (inherit-from == command-table-inherit-from(command-table)) command-table.%inherit-from := inherit-from; /* command-table.%completion-alist := #f; */ /* when (command-table.%translators-cache) remove-all-keys!(command-table.%translators-cache) end */ end; inherit-from end method command-table-inherit-from-setter; /// Making command tables // define command-table *compiler* (*file*, *edit*) // command-table {a command table decorator} // menu {a menu decorator} // command {a command or function decorator} // menu-item "Compile" [of ] = compile-file // menu-item "Load" [of ] = make(] = ; // menu-item "About" [of ] = (, about?: #t); // end define macro command-table-definer { define command-table ?:name (?supers:*) ?options:* end } => { define command-table-variable ?name (?supers) ?options end; define command-table-menu ?name (?supers) ?options end } end macro command-table-definer; define macro command-table-variable-definer { define command-table-variable ?:name (?supers:*) ?options:* end } => { define variable ?name :: = apply(make, , name: ?#"name", inherit-from: vector(?supers), ?options) } supers: { } => { } { ?super:*, ... } => { ?super, ... } options: { } => { #[] } { ?option:*; ... } => { concatenate(?option, ...) } option: { inherit-menu ?:expression } => { vector(inherit-menu:, ?expression) } { resource-id ?:expression } => { vector(resource-id:, ?expression) } { command-table ?:expression } => { #[] } { command ?:expression } => { #[] } { menu ?:expression } => { #[] } { menu-item ?item:* } => { #[] } { include ?item:* } => { #[] } { separator ?item:* } => { #[] } { separator } => { #[] } end macro command-table-variable-definer; /* //--- Used to be this... define macro command-table-variable-definer { define command-table-variable ?:name (?supers:*) ?options:* end } => { define variable ?name :: = make(, name: ?#"name", inherit-from: vector(?supers), ?options) } supers: { } => { } { ?super:*, ... } => { ?super, ... } options: { } => { } { ?option:*; ... } => { ?option, ... } option: { inherit-menu ?:expression } => { inherit-menu: ?expression } { resource-id ?:expression } => { resource-id: ?expression } { command-table ?:expression } => { } { command ?:expression } => { } { menu ?:expression } => { } { menu-item ?item:* } => { } { include ?item:* } => { } { separator ?item:* } => { } { separator } => { } end macro command-table-variable-definer; */ define macro command-table-menu-definer { define command-table-menu ?:name (?supers:*) end } => { } // The decorator cases { define command-table-menu ?:name (?supers:*) command-table ?decorator:expression; ?more-items:* end } => { add-command(?name, ?decorator); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) menu ?decorator:expression; ?more-items:* end } => { add-command(?name, ?decorator); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) command ?decorator:expression; ?more-items:* end } => { add-command(?name, ?decorator); define command-table-menu ?name (?supers) ?more-items end } // The 'menu-item' cases { define command-table-menu ?:name (?supers:*) menu-item ?label of ?type = ?command, #rest ?options:expression; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, ?type, ?command, ?options); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) menu-item ?label of ?type = ?command; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, ?type, ?command); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) menu-item ?label = ?command, #rest ?options:expression; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, #f, ?command, ?options); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) menu-item ?label = ?command; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, #f, ?command); define command-table-menu ?name (?supers) ?more-items end } // The 'separator' cases { define command-table-menu ?:name (?supers:*) separator; ?more-items:* end } => { add-command-table-menu-item (?name, #f, , #f); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) separator ?label; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, , #f); define command-table-menu ?name (?supers) ?more-items end } // The 'include' cases { define command-table-menu ?:name (?supers:*) include ?command-table; ?more-items:* end } => { add-command-table-menu-item (?name, #f, , ?command-table); define command-table-menu ?name (?supers) ?more-items end } { define command-table-menu ?:name (?supers:*) include ?label = ?command-table; ?more-items:* end } => { add-command-table-menu-item (?name, ?label, , ?command-table); define command-table-menu ?name (?supers) ?more-items end } // Fall-through... { define command-table-menu ?:name (?supers:*) ?non-menu-item:*; ?more-items:* end } => { define command-table-menu ?name (?supers) ?more-items end } // We use these secondary patterns instead of '?label:expression' in order // to prevent the whole '"label" of = command' input from matching label: { ?:expression } => { ?expression } type: { ?:expression } => { ?expression } command: { (?class:expression, #rest ?options:expression) } => { list(?class, ?options) } { ?:expression } => { ?expression } command-table: { ?:expression } => { ?expression } end macro command-table-menu-definer; define sealed method make (class == , #key name, inherit-from = #[], inherit-menu = #f, resource-id = #f) => (command-table :: ) let inherit-from = as(, inherit-from); let command-table = make(, name: name, inherit-from: inherit-from, resource-id: resource-id); when (inherit-menu) install-inherited-menus(command-table, inherit-menu) end; gethash(*command-tables*, name) := command-table; command-table end method make; define method install-inherited-menus (command-table :: , inherit-menu) => () check-type(inherit-menu, type-union(one-of(#f, #t, #"menu", #"accelerators"), )); for (comtab in command-table-inherit-from(command-table)) when (~instance?(inherit-menu, ) | member?(comtab, inherit-menu)) let menu = command-table-menu(comtab); when (menu) for (decorator :: in menu) let object = decorator-object(decorator); let type = decorator-type(decorator); let label = decorator-label(decorator); let image = decorator-image(decorator); let options = decorator-options(decorator); let accelerator = decorator-accelerator(decorator); let mnemonic = decorator-mnemonic(decorator); let documentation = decorator-documentation(decorator); let resource-id = decorator-resource-id(decorator); select (inherit-menu) #"menu" => accelerator := $unsupplied; mnemonic := $unsupplied; #"accelerators" => label := #f; otherwise => #f end; apply(add-command-table-menu-item, command-table, label, type, object, image: image, accelerator: accelerator, mnemonic: mnemonic, documentation: documentation, resource-id: resource-id, error?: #f, options) end end end end end method install-inherited-menus; define method remove-command-table (command-table :: ) => () // This could hack inheritance, too... remhash(*command-tables*, command-table) end method remove-command-table; // The global command table // Things like the Mouse-Right (menu) translator live here. define variable *global-command-table* :: = make(, name: #"global-command-table", inherit-from: #[]); // CLIM's general "user" command table define variable *user-command-table* :: = make(, name: #"user-command-table", inherit-from: vector(*global-command-table*)); /// Inheritance //--- This should be more careful to hit each command table only once //--- (see the new version in CLIM for how to do this) define method do-command-table-inheritance (function :: , command-table :: , #key do-inherited? = #t) => () function(command-table); when (do-inherited?) for (comtab in command-table-inherit-from(command-table)) do-command-table-inheritance(function, comtab, do-inherited?: #t) end end end method do-command-table-inheritance; define method command-present? (command-table :: , command :: ) => (present?) gethash(command-table-commands(command-table), command) end method command-present?; define method command-accessible? (command-table :: , command :: ) => (command-table :: false-or()) block (return) do-command-table-inheritance (method (comtab) when (command-present?(comtab, command)) return(comtab) end end method, command-table); #f end end method command-accessible?; /// Adding and removing commands define method add-command (command-table :: , command :: , #key label, name, menu, image, accelerator = $unsupplied, mnemonic = $unsupplied, resource-id, error? = #t) => () let label = label | name; // for compatibility check-type(label, false-or()); when (command-present?(command-table, command)) when (error?) cerror("Remove the command and proceed", "The command %= is already present in %=", command, command-table) end; remove-command(command-table, command) end; let menu-label = #f; let menu-options = #(); when (menu) menu-label := if (instance?(menu, )) head(menu) else menu end; menu-options := if (instance?(menu, )) tail(menu) else #() end end; check-type(menu-label, false-or()); let commands = command-table-commands(command-table); if (label) add-command-line-name(command-table, command, label) else gethash(commands, command) := #t end; case menu => let type = if (instance?(command, )) else end; apply(add-command-table-menu-item, command-table, menu-label, type, command, image: image, accelerator: accelerator, mnemonic: mnemonic, resource-id: resource-id, error?: error?, menu-options); (supplied?(accelerator) & accelerator) | (supplied?(mnemonic) & mnemonic) => let type = if (instance?(command, )) else end; add-command-table-menu-item (command-table, #f, type, command, accelerator: accelerator, mnemonic: mnemonic, resource-id: resource-id, error?: error?); end end method add-command; define method add-command (command-table :: , decorator :: , #key label, name, menu, image, accelerator = $unsupplied, mnemonic = $unsupplied, resource-id, error? = #t) => () ignore(menu); let object = decorator-object(decorator); let type = decorator-type(decorator); let label = label | name | decorator-label(decorator); let image = image | decorator-image(decorator); let accelerator = if (supplied?(accelerator)) accelerator else decorator-accelerator(decorator) end; let mnemonic = if (supplied?(mnemonic)) mnemonic else decorator-mnemonic(decorator) end; let resource-id = resource-id | decorator-resource-id(decorator); add-command-table-menu-item (command-table, label, type, object, image: image, accelerator: accelerator, mnemonic: mnemonic, resource-id: resource-id, error?: error?) end method add-command; define method remove-command (command-table :: , command :: ) => () // Remove old command names /* let names = command-table.%command-line-names; block (break) while (names) let index = find-key(names, method (entry) second(entry) = command end); case index => names := remove-at!(names, index); inc!(*completion-cache-tick*); otherwise => break() end end end; */ // Remove old menu items let menu = command-table-menu(command-table); let decorators = #(); when (menu) for (decorator :: in menu) when (( decorator-type(decorator) == | decorator-type(decorator) == ) & decorator-object(decorator) = command) push!(decorators, decorator) end end; for (decorator in decorators) remove!(menu, decorator) end end; remhash(command-table-commands(command-table), command) end method remove-command; define method remove-command (command-table :: , decorator :: ) => () remove-command(command-table, decorator-object(decorator)) end method remove-command; /* define method remove-command-entirely (command :: ) => () for (command-table in *command-tables*) remove-command(command-table, command) end end method remove-command-entirely; */ define method do-command-table-commands (function :: , command-table :: , #key do-inherited? = #f) => () dynamic-extent(function); do-command-table-inheritance (method (comtab) for (command in key-sequence(command-table-commands(comtab))) function(command, comtab) end end method, command-table, do-inherited?: do-inherited?) end method do-command-table-commands; /// Command table menus // The radio box and check box types are a bit odd: // add-command-table-menu-item // (command-table, symbol, , #t, ;or // items: #[#["True", #t], #["False", #f]], // label-key: first, value-key: second, // callback: method (g) format-out("Value changed to %=", gadget-value(g)) end) define method add-command-table-menu-item (command-table :: , label, type, object, #rest keys, #key documentation, after = #"end", accelerator = $unsupplied, mnemonic = $unsupplied, resource-id, image, text-style, error? = #t, items, label-key, value-key, test, callback, update-callback) => (decorator :: ) dynamic-extent(keys); ignore(items, text-style, label-key, value-key, test, callback, update-callback); let type = type | select (object by instance?) => // menu-item "New" = new-file ; => // menu-item "New" = make(, function: new-file) ; => // menu-item "File" = *file-command-table* ; subclass() => // menu-item "New" = ; => // menu-item "New" = (, new?: #t) ; end; select (type) , , => check-type(label, false-or()); , , , , => check-type(label, false-or(type-union(, ))); end; when (supplied?(accelerator) & accelerator) assert(instance?(accelerator, ), "%= is not a character or keyboard gesture", accelerator) end; when (supplied?(mnemonic) & mnemonic) assert(instance?(mnemonic, ), "%= is not a character or keyboard gesture", mnemonic) end; check-type(documentation, false-or()); let menu = command-table-menu(command-table); let old-item = label & find-value(menu, method (decorator :: ) label-equal?(decorator-label(decorator), label) end method); when (old-item) when (error?) cerror("Remove the menu item and proceed", "The menu item %= is already present in %=", label, command-table) end; remove-command-table-menu-item(command-table, label) end; command-table.%accelerators := #f; // decache with-keywords-removed (options = keys, #[after:, accelerator:, mnemonic:, error?:]) let menu = command-table-menu(command-table); let decorator = make(, object: object, type: type, label: label, image: image, options: options, accelerator: accelerator, mnemonic: mnemonic, documentation: documentation, resource-id: resource-id); select (after) #"start" => menu := insert-at!(menu, decorator, #"start"); #"end", #f => menu := insert-at!(menu, decorator, #"end"); #"sort" => add!(menu, decorator); local method label-less? (x, y) => (less? :: ) case ~x => #t; ~y => #f; otherwise => string-less?(x, y); end end method; command-table-menu(command-table) := sort!(menu, test: method (x :: , y :: ) label-less?(decorator-label(x), decorator-label(y)) end); otherwise => if (instance?(after, )) let index = find-key(menu, method (decorator :: ) label-equal?(decorator-label(decorator), after) end method); if (index) menu := insert-at!(menu, decorator, index) else error("There is no menu item named %= for 'after:'", after) end else error("The value for 'after:' is not a string, #\"start\", #\"end\", or #\"sort\"") end end; // Might have done something destructive above, so be careful command-table-menu(command-table) := menu; // Now that the command is accessible via a menu (or accelerator), // make sure that we've really imported it when (type == | type == ) let commands = command-table-commands(command-table); let old-name = gethash(commands, object); gethash(commands, object) := old-name | #t end; decorator end end method add-command-table-menu-item; define method remove-command-table-menu-item (command-table :: , label) => () check-type(label, false-or(type-union(, ))); let menu = command-table-menu(command-table); let index = find-key(menu, method (decorator :: ) label-equal?(decorator-label(decorator), label) end method); when (index) //--- Is it right for this to remove the whole item even //--- if there is still a keystroke accelerator? menu := remove-at!(menu, index); command-table.%accelerators := #f // decache end end method remove-command-table-menu-item; define method do-command-table-menu-items (function :: , command-table :: , #key label, name, do-inherited? = #f) => () dynamic-extent(function); let label = label | name; // for compatibility do-command-table-inheritance (method (comtab) for (decorator :: in command-table-menu(comtab)) when (~label | label-equal?(decorator-label(decorator), label)) function(decorator, comtab) end end end method, command-table, do-inherited?: do-inherited?) end method do-command-table-menu-items; // Starting from the given command table, calls 'function' on all // of the commands in the command table's menu and all its sub-menus define method do-command-table-menu-commands (function :: , command-table :: ) dynamic-extent(function); local method do-one (decorator :: , comtab) let object = decorator-object(decorator); let type = decorator-type(decorator); select (type) => function(object, comtab); => function(object, comtab); => do-command-table-menu-commands(function, object); otherwise => #f; end end method; do-command-table-menu-items(do-one, command-table) end method do-command-table-menu-commands; // Menu labels can be #f, but we don't want that to match the string "#f" define function label-equal? (s1, s2) => (true? :: ) s1 & s2 & if (instance?(s1, ) | instance?(s2, )) s1 == s2 else string-equal?(s1, s2) end end function label-equal?; /// Command menu bars and tool bars define method make-command-menu-bar (framem :: , frame :: , #key command-table = frame-command-table(frame)) => (menu-bar :: false-or()) when (command-table) with-frame-manager (framem) let menus = make-menus-from-command-table(command-table, frame, framem); when (menus) make(, children: menus) end end end end method make-command-menu-bar; //---*** If the command table has a resource ID, do we skip all this? define method make-menus-from-command-table (command-table :: , frame :: false-or(), framem :: , #key label = "Misc") => (menus :: ) let misc-items :: = make(); let menus :: = make(); for (decorator :: in command-table-menu(command-table)) let type = decorator-type(decorator); select (type) => let comtab = decorator-object(decorator); let menu = make-menu-from-command-table-menu (command-table-menu(comtab), frame, framem, label: as(, decorator-label(decorator)), mnemonic: decorator-mnemonic(decorator), documentation: decorator-documentation(decorator), resource-id: decorator-resource-id(decorator), command-table: comtab, use-accelerators?: #t); add!(menus, menu); => // Call the menu creation function to create the menu contents let menu = make(, children: decorator-object(decorator)(frame), label: as(, decorator-label(decorator)), mnemonic: decorator-mnemonic(decorator), documentation: decorator-documentation(decorator), resource-id: decorator-resource-id(decorator)); add!(menus, menu); otherwise => add!(misc-items, decorator); end end; unless (empty?(misc-items)) let misc-menu = make-menu-from-command-table-menu(misc-items, frame, framem, label: label, use-accelerators?: #t); add!(menus, misc-menu) end; menus end method make-menus-from-command-table; define method make-menu-from-command-table-menu (decorators :: , frame :: false-or(), framem :: , #key owner, command-table, label, documentation, resource-id, mnemonic = $unsupplied, item-callback = $unsupplied, use-accelerators? = #f) => (menu :: ) let menu-items :: = make(); with-frame-manager (framem) local method button-for-command (decorator :: ) => (button :: ) let command = decorator-object(decorator); let (callback, command) = callback-for-command(command); make(, command: command, label: as(, decorator-label(decorator)), value: command, accelerator: use-accelerators? & decorator-accelerator(decorator), mnemonic: decorator-mnemonic(decorator), documentation: decorator-documentation(decorator), resource-id: decorator-resource-id(decorator), enabled?: ~frame | command-enabled?(command, frame), activate-callback: if (supplied?(item-callback)) item-callback else callback end) end method, method menu-for-command-table (decorator :: ) => (menu :: ) let comtab = decorator-object(decorator); make-menu-from-command-table-menu (command-table-menu(comtab), frame, framem, label: as(, decorator-label(decorator)), mnemonic: decorator-mnemonic(decorator), documentation: decorator-documentation(decorator), resource-id: decorator-resource-id(decorator), command-table: comtab, use-accelerators?: use-accelerators?) end method, // Example: // add-command-table-menu-item // (command-table, symbol, , #f, // items: #[#["Direct methods", #f], #["All methods", #t]], // label-key: first, value-key: second, // callback: method (g) sheet-frame(g).all-methods? := gadget-value(g) end) method component-for-box (decorator :: , selection-mode) => (menu-box :: ) let options = decorator-options(decorator); let items = get-property(options, items:, default: #()); let callback = get-property(options, callback:); let label-key = get-property(options, label-key:, default: collection-gadget-default-label-key); let value-key = get-property(options, value-key:, default: collection-gadget-default-value-key); let test = get-property(options, test:, default: \==); let update-callback = get-property(options, update-callback:); let callback-args = if (selection-mode == #"none") vector(activate-callback: callback) else vector(value-changed-callback: callback) end; apply(make, , command: callback, items: items, selection-mode: selection-mode, resource-id: decorator-resource-id(decorator), value: decorator-object(decorator), label-key: label-key, value-key: value-key, test: test, update-callback: update-callback, callback-args) end method, method collect-buttons (buttons :: ) => () unless (empty?(buttons)) add!(menu-items, make(, children: as(, buttons))); buttons.size := 0 end end method; let buttons :: = make(); local method do-decorators (decorators :: ) => () for (decorator :: in decorators) let type = decorator-type(decorator); select (type) , => add!(buttons, button-for-command(decorator)); => add!(buttons, menu-for-command-table(decorator)); => // Call the menu creation function to create the menu contents let menu = make(, children: decorator-object(decorator)(frame), label: as(, decorator-label(decorator)), mnemonic: decorator-mnemonic(decorator), documentation: decorator-documentation(decorator), resource-id: decorator-resource-id(decorator)); add!(buttons, menu); => collect-buttons(buttons); // If the separator specifies a command table, that means we // should "inline" it in the current menu right after the separator when (instance?(decorator-object(decorator), )) do-decorators(command-table-menu(decorator-object(decorator))) end; => collect-buttons(buttons); add!(menu-items, component-for-box(decorator, #"single")); => collect-buttons(buttons); add!(menu-items, component-for-box(decorator, #"multiple")); => collect-buttons(buttons); add!(menu-items, component-for-box(decorator, #"none")); end end end method; do-decorators(decorators); make(, owner: owner, label: label, command: command-table, mnemonic: mnemonic, documentation: documentation, resource-id: resource-id, // No need to group the last buttons... children: concatenate(menu-items, buttons)) end end method make-menu-from-command-table-menu; define method make-command-tool-bar (framem :: , frame :: , #key command-table = frame-command-table(frame)) => (tool-bar :: false-or()) when (command-table) // An alist of #[command-table buttons] // We do this in order to flatten out any command table inheritance, // since tool bars are not hierarchical let comtab-buttons :: = make(); with-frame-manager (framem) local method button-for-command (decorator :: ) => (button ::