Module: win32-duim Synopsis: Win32 basic gadget implementation Author: Andy Armstrong, David Gray, Scott McKay 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 /// Some magic Win32 constants //---*** All of the following should be computed define constant $default-label :: = ""; // The thickness of the nice 3d border we use define constant $gadget-border-thickness :: = 2; // in pixels define constant $default-vertical-spacing :: = 1; define constant $minimum-visible-characters :: = 25; define constant $minimum-visible-lines :: = 3; define constant $push-button-extra-text-width :: = 12; define constant $push-button-extra-text-height :: = 4; define constant $push-button-extra-icon-width :: = 2; define constant $push-button-extra-icon-height :: = 2; define constant $button-icon-width :: = 16; define constant $list-box-minimum-visible-lines :: = 3; define constant $list-box-default-visible-lines :: = 5; define constant $list-box-extra-height :: = 2; define constant $option-box-maximum-popup-height :: = 200; define constant $option-box-extra-height :: = 8; define constant $minimum-scroll-shaft-length :: = 50; define constant $text-field-extra-width :: = 0; // in pixels define constant $text-field-extra-height :: = 8; // in pixels define constant $text-editor-extra-width :: = 0; // in pixels define constant $text-editor-extra-height :: = 12; // in pixels /// The Win32 backend gadgets protocol define protocol <> () function make-gadget-control (gadget :: , parent :: , options :: , #key x, y, width, height) => (handle :: ); function update-mirror-label (gadget :: , mirror :: ) => (); function update-gadget-image (gadget :: , handle :: , image :: ) => (); end protocol <>; /// Win32 gadgets define open abstract class (, ) end class ; define sealed domain defaulted-gadget-accelerator (, ); define sealed domain defaulted-gadget-mnemonic (, ); define sealed domain compute-mnemonic-from-label (, ); // We arrange to map this to DEFAULT_GUI_FONT define constant $win32-default-gadget-text-style = make(, family: #"system", weight: #"normal", slant: #"roman", size: #"normal"); define sealed method port-default-text-style (_port :: , gadget :: ) => (text-style :: false-or()) $win32-default-gadget-text-style end method port-default-text-style; define sealed method make-sheet-mirror (parent :: , gadget :: ) => (mirror :: ) let (left, top, right, bottom) = sheet-native-edges(gadget); let parent-handle = window-handle(parent); let handle = make-gadget-control (gadget, parent-handle, gadget-default-options(gadget), x: left, y: top, width: right - left, height: bottom - top); make(, sheet: gadget, handle: handle, region: make-bounding-box(left, top, right, bottom)) end method make-sheet-mirror; define sealed method gadget-default-options (gadget :: ) => (options :: ) %logior($WS-CHILD, $WS-CLIPSIBLINGS, if (gadget-enabled?(gadget)) 0 else $WS-DISABLED end) end method gadget-default-options; define sealed method gadget-default-options (gadget :: ) => (options :: ) %logior(next-method(), if (gadget-scrolling-vertically?(gadget)) $WS-VSCROLL else 0 end, if (gadget-scrolling-horizontally?(gadget)) $WS-HSCROLL else 0 end) end method gadget-default-options; define sealed method gadget-extended-options (gadget :: , #key) => (options :: ) 0 end method gadget-extended-options; define sealed method gadget-extended-options (gadget :: , #key default-border? = #t) => (options :: ) let has-border? = select (border-type(gadget)) #f => default-border?; #"none" => #f; otherwise => #t; end; %logior(next-method(), if (has-border?) $WS-EX-CLIENTEDGE else 0 end) end method gadget-extended-options; define sealed method make-sheet-mirror-from-resource (parent :: , gadget :: , resource-id :: ) => (mirror :: ) let _port = port(gadget); // If for some reason the parent is not a dialog resource, // we have to make sure that lookup can handle it //--- Update Win32-Resources library let resource :: = lookup-control(sheet-mirror(parent).%mirror-resource, resource-id); let handle :: = GetDlgItem(window-handle(parent), resource-id); check-result("GetDlgItem", handle); let (x, y) = window-position(resource); let (width, height) = window-size(resource); let (x, y) = win32-dialog-units->pixels(_port, x, y); let (width, height) = win32-dialog-units->pixels(_port, width, height); duim-debug-message("Gadget geometry %= from resource: %d x %d at %d, %d", gadget, width, height, x, y); initialize-sheet-geometry(gadget, x, y, width, height); initialize-sheet-from-resource(gadget, handle); let (left, top, right, bottom) = sheet-native-edges(gadget); make(, sheet: gadget, handle: handle, resource: resource, region: make-bounding-box(left, top, right, bottom)) end method make-sheet-mirror-from-resource; define sealed method update-gadget-font (gadget :: , mirror :: ) => () let handle = window-handle(mirror); let _port = port(gadget); let text-style = get-default-text-style(_port, gadget); let font = text-style-mapping(_port, text-style); SendMessage(handle, $WM-SETFONT, pointer-address(font.%font-handle), 0) end method update-gadget-font; define sealed method note-mirror-created (gadget :: , mirror :: ) => () next-method(); let documentation = gadget-documentation(gadget); documentation & register-tooltip-for-sheet(gadget, documentation); update-gadget-font(gadget, mirror) end method note-mirror-created; // Update the tool tip when the gadget documentation changes define method gadget-documentation-setter (documentation, gadget :: ) => (documentation :: false-or()) unregister-tooltip-for-sheet(gadget); next-method(); documentation & register-tooltip-for-sheet(gadget, documentation); documentation end method gadget-documentation-setter; define sealed method defaulted-gadget-label (gadget :: ) => (label) gadget-label(gadget) | $default-label end method defaulted-gadget-label; define sealed method note-gadget-label-changed (gadget :: ) => () next-method(); let mirror = sheet-direct-mirror(gadget); mirror & update-mirror-label(gadget, mirror) end method note-gadget-label-changed; define sealed method get-window-text (handle :: ) => (text :: ) let length = SendMessage(handle, $WM-GETTEXTLENGTH, 0, 0); if (length = 0) "" else let buffer-size = length + 1; with-stack-structure (buffer :: , size: buffer-size) let actual-length = GetWindowText(handle, buffer, buffer-size); when (actual-length = 0) ensure-no-error("GetWindowText") end; as(, buffer) end end end method get-window-text; define sealed method update-mirror-label (gadget :: , mirror :: ) => () let handle = window-handle(mirror); let (text, image, mnemonic, index) = text-or-image-from-gadget-label(gadget); when (text) check-result("SetWindowText", SetWindowText(handle, text)) end; when (image) update-gadget-image(gadget, handle, image) end end method update-mirror-label; define sealed method note-gadget-enabled (client, gadget :: ) => () ignore(client); next-method(); let handle = window-handle(gadget); handle & EnableWindow(handle, #t) end method note-gadget-enabled; define sealed method note-gadget-disabled (client, gadget :: ) => () ignore(client); next-method(); let handle = window-handle(gadget); handle & EnableWindow(handle, #f) end method note-gadget-disabled; define sealed method activate-win32-gadget (gadget :: ) => (activated? :: ) when (gadget-activate-callback(gadget)) distribute-activate-callback(gadget); #t end end method activate-win32-gadget; define sealed method activate-win32-gadget (gadget :: ) => (activated? :: ) handle-text-gadget-changed(gadget); next-method() end method activate-win32-gadget; /// Gadget id handling define sealed method gadget->id (gadget :: ) => (id :: ) let top-mirror = top-level-mirror(gadget, error?: #t); block (return) when (top-mirror) for (g keyed-by id in top-mirror.%resource-id-table) when (gadget == g) return(id) end end end; error("Failed to find id for gadget %=", gadget) end end method gadget->id; define sealed method gadget->id-setter (id :: , gadget :: ) => (id :: ) let top-mirror = top-level-mirror(gadget, error?: #t); top-mirror.%resource-id-table[id] := gadget; id end method gadget->id-setter; define sealed method id->gadget (sheet :: , id :: ) => (gadget :: false-or()) let top-mirror = top-level-mirror(sheet); top-mirror & element(top-mirror.%resource-id-table, id, default: #f) end method id->gadget; //---*** How do we avoid clashing with all resource ids currently //---*** chosen by the user? Maybe we just advertise which ids we //---*** reserve for DUIM's use. define sealed method ensure-gadget-id (gadget :: ) => (id :: ) let resource-id = sheet-resource-id(gadget); let id = select (resource-id by instance?) => resource-id; otherwise => generate-next-gadget-id(gadget); end; register-gadget-id(gadget, id) end method ensure-gadget-id; define sealed method generate-next-gadget-id (gadget :: ) => (id :: ) let top-mirror = top-level-mirror(gadget, error?: #t); let id = top-mirror.%next-resource-id; register-gadget-id(gadget, id) end method generate-next-gadget-id; define sealed method register-gadget-id (gadget :: , id :: ) => (id :: ) let top-mirror = top-level-mirror(gadget, error?: #t); top-mirror.%resource-id-table[id] := gadget; top-mirror.%next-resource-id := id + 1; id end method register-gadget-id; /// Exit, cancel, default button, etc. define method handle-command-for-id (sheet :: , id :: ) => (handled? :: ) let frame = sheet-frame(sheet); select (id) $IDOK => duim-debug-message("Handling command IDOK for %=", sheet); activate-default-button(frame); $IDCANCEL => duim-debug-message("Handling command IDCANCEL for %=", sheet); handle-cancel(frame); otherwise => let gadget = id->gadget(sheet, id); if (gadget) when (sheet-mapped?(gadget) & gadget-enabled?(gadget)) handle-gadget-activation(gadget) end else handle-id-activation(frame, id) end; end end method handle-command-for-id; define sealed method handle-gadget-activation (gadget :: ) => (handled? :: ) duim-debug-message("Ignoring activation command for gadget %=", gadget); #f end method handle-gadget-activation; // This handles IDOK commands for more than just buttons... define method activate-default-button (frame :: ) => (activated? :: ) let gadget = win32-sheet-with-focus(); duim-debug-message(" Handling IDOK: focus currently %=", gadget); let activated? = instance?(gadget, ) & gadget-enabled?(gadget) & activate-win32-gadget(gadget); // If we didn't activate the gadget, try to activate the default button unless (activated?) let button = frame-default-button(frame); // Don't activate an upmapped or disabled default button... when (button & sheet-mapped?(button) & gadget-enabled?(button)) handle-gadget-activation(button) end end end method activate-default-button; define function win32-sheet-with-focus () => (sheet :: false-or()) let handle = GetFocus(); let sheet = handle-sheet(handle); if (instance?(sheet, )) subgadget-owner(sheet) else sheet end end function win32-sheet-with-focus; define function handle-cancel (frame :: ) => (handled? :: ) let gadget = win32-sheet-with-focus(); duim-debug-message(" Handling IDCANCEL: focus currently %=", gadget); if (instance?(gadget, ) & cancel-gadget(gadget)) #t else cancel-frame(frame) end end function handle-cancel; define sealed method cancel-frame (frame :: ) => (handled? :: ) //---*** We should handle ESCAPE as canceling popups by default, //---*** for example in combo boxes. #f end method cancel-frame; define sealed method cancel-gadget (gadget :: ) => (handled? :: ) #f end method cancel-gadget; //---*** What do we do about setting the color and font of a gadget? /// "Subgadgets" // This is the class of gadgets that are created by Windows as a // child of a gadget. For example, a combo box has a child text field // that we need to model, but that we don't create ourselves. define abstract class () sealed constant slot subgadget-owner :: , required-init-keyword: owner:; end class ; define sealed method initialize (gadget :: , #key handle) => () next-method(); let mirror = make(, sheet: gadget, handle: handle, region: make-bounding-box(0, 0, 100, 100)); note-mirror-created(gadget, mirror) end method initialize; //--- Slight hack to allow people to find the frame etc. of this gadget define sealed method top-level-sheet (gadget :: ) => (sheet :: false-or()) top-level-sheet(subgadget-owner(gadget)) end method top-level-sheet; define sealed method update-gadget-font (gadget :: , mirror :: ) => () //--- Use the font for the owner update-gadget-font(subgadget-owner(gadget), mirror) end method update-gadget-font; define sealed method gadget-documentation (gadget :: ) => (documentation) gadget-documentation(subgadget-owner(gadget)) end method gadget-documentation; /// Subclassed windows gadgets /// This is the class of gadgets that need to see the control's events /// in order to grab some of them. For example, our text-field code /// needs to see the return key press to do the value changed callback. define abstract class () sealed slot %old-WndProc :: ; end class ; // This function gets the first crack at WM_xxx messages... define open generic handle-control-message (gadget :: , message :: , wParam :: , lParam :: ) => (handled? :: ); define sealed method subclassed-window-callback-function (handle :: , // window handle message :: , // type of message wParam :: , // additional information lParam :: ) // additional information => (result :: ) let gadget = handle-sheet(handle); assert(instance?(gadget, ), "Can't find sheet for subclassed mirror -- this can't happen!"); if (handle-control-message(gadget, message, wParam, lParam)) 0 else default-subclassed-window-callback(gadget, handle, message, wParam, lParam) end end method subclassed-window-callback-function; define sealed inline method default-subclassed-window-callback (gadget :: , handle :: , // window handle message :: , // type of message wParam :: , // additional information lParam :: ) // additional information => (result :: ) CallWindowProc(gadget.%old-WndProc, handle, message, wParam, lParam) end method default-subclassed-window-callback; define callback SubclassedWndProc :: = subclassed-window-callback-function; define sealed method note-mirror-created (gadget :: , mirror :: ) => () next-method(); let handle = window-handle(mirror); let old-wndproc = SetWindowLong(handle, $GWL-WNDPROC, pointer-address(SubclassedWndProc)); gadget.%old-WndProc := make(, address: old-wndproc) end method note-mirror-created; /// Layout gadget mixins // This is the class of gadgets that also behave as a layout of any // children that they have. In order to make tabbing between gadgets // work, we make all of the mirrors as siblings parented into the main // window, by specifying 'mirror-accepts-children?: #f'. define class () end class ; define method initialize (gadget :: , #key) => () next-method(); sheet-mirror-accepts-children?(gadget) := #f end method initialize; /// Labels define sealed class (,