Module: win32-duim Synopsis: Win32 top level window handling Author: David Gray, 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 /// Some magic Win32 constants define constant $first-gadget-id :: = 1000; //---*** This should really be computed define constant $top-level-y-spacing = 3; // in pixels //---*** These should be defined in Win32-User define constant $ACCEL-FVIRTKEY :: = #x01; // define constant $ACCEL-FNOINVERT :: = #x02; define constant $ACCEL-FSHIFT :: = #x04; define constant $ACCEL-FCONTROL :: = #x08; define constant $ACCEL-FALT :: = #x10; /// Frame protocols define protocol <> () function note-win32-frame-destroyed (frame :: ) => (); end protocol <>; /// Top level mirrors define sealed class () // State for allocating resource ids and mapping ids<->gadgets sealed slot %next-resource-id = $first-gadget-id; sealed slot %resource-id-table :: = make(); // Accelerator table for the frame sealed slot %accelerator-table :: false-or() = #f; // A shared ToolTip for all of the sheets in the frame sealed slot %tool-tip = #f, init-keyword: tool-tip:; sealed slot %dialog-mirrors :: = make(); end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method top-level-mirror (sheet :: , #key error? = #f) => (mirror :: false-or()) let sheet = top-level-sheet(sheet); let mirror = sheet & sheet-direct-mirror(sheet); mirror | (error? & error("Failed to find top-level mirror for %=", sheet)) end method top-level-mirror; define sealed method top-level-mirror (frame :: , #key error? = #f) => (mirror :: false-or()) let sheet = top-level-sheet(frame); let mirror = sheet & sheet-direct-mirror(sheet); mirror | (error? & error("Failed to find top-level mirror for %=", sheet)) end method top-level-mirror; /// Dialog handling define method mirror-registered-dialogs (mirror :: ) => (dialogs :: ) mirror.%dialog-mirrors end method mirror-registered-dialogs; define method register-dialog-mirror (frame :: , dialog-mirror :: ) => () let top-sheet = top-level-sheet(frame); when (top-sheet) let top-mirror = sheet-direct-mirror(top-sheet); add!(top-mirror.%dialog-mirrors, dialog-mirror) end end method register-dialog-mirror; define method unregister-dialog-mirror (frame :: , dialog-mirror :: ) => () let top-sheet = top-level-sheet(frame); when (top-sheet) let top-mirror = sheet-direct-mirror(top-sheet); remove!(top-mirror.%dialog-mirrors, dialog-mirror) end end method unregister-dialog-mirror; /// Top level sheets define abstract class (, , ) slot %needs-activation? :: = #f; end class ; define sealed class (, ) end class ; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method initialize (sheet :: , #key) => () next-method(); sheet-accepts-focus?(sheet) := #f end method initialize; define method class-for-make-pane (framem :: , class == , #key) => (class :: , options :: false-or()) values(, #f) end method class-for-make-pane; define sealed domain make (singleton()); define sealed domain initialize (); // Like a top-level sheet, but for embedded apps such as OLE parts define sealed class (, ) end class ; define method class-for-make-pane (framem :: , class == , #key) => (class :: , options :: false-or()) values(, #f) end method class-for-make-pane; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method do-make-mirror (_port :: , sheet :: ) => (mirror :: ) ignore(_port); let frame = sheet-frame(sheet); let resource-id = frame-resource-id(frame); let (handle, resource, mirror-class, mirror-initargs) = make-top-level-window(frame, sheet, resource-id); let (left, top, right, bottom) = get-window-edges(handle); let mirror = apply(make, mirror-class, sheet: sheet, handle: handle, resource: resource, region: make-bounding-box(left, top, right, bottom), mirror-initargs); let container = sheet-parent-window(sheet); if (container == #f) // Create a shared ToolTip so that the entire application can use it make-frame-tooltip(frame, mirror); // Update the icon for the frame update-mirror-icon(mirror, frame-icon(frame)); end; when (frame-keyboard-interrupt?(frame)) register-keyboard-interrupt-handler(_port, sheet) end; mirror end method do-make-mirror; // Returns the handle of a container, or #f define sealed method sheet-parent-window (sheet :: ) => (window :: false-or()) let container = sheet-container(sheet); when (container) select (container by instance?) => let top-sheet = top-level-sheet(container); top-sheet & window-handle(top-sheet); => window-handle(container); => window-handle(container); otherwise => // presumably an ... container; end end end method sheet-parent-window; // Returns the handle of the (nearest) parent window, or #f define sealed method sheet-parent-window (sheet :: ) => (window :: false-or()) let mirror = sheet-mirror(sheet); mirror & window-handle(mirror) end method sheet-parent-window; define method frame-window-styles (frame :: ) => (style :: , extended-style :: ) let style = %logior($WS-OVERLAPPEDWINDOW, if (frame-iconified?(frame)) $WS-ICONIC else 0 end, if (frame-maximized?(frame)) $WS-MAXIMIZE else 0 end); let extended-style = %logior($WS-EX-CONTROLPARENT, if (frame-always-on-top?(frame)) $WS-EX-TOPMOST else 0 end); values(style, extended-style) end method frame-window-styles; define sealed method make-top-level-window (frame :: , sheet :: , resource-id :: ) => (handle :: , resource :: , mirror-class :: , mirror-initargs) let _port = port(sheet); let resource :: = lookup-resource($RT-DIALOG, resource-id); //---*** Need to do some error handling here let handle :: = CreateDialog(application-instance-handle(), encode-resource(resource-id), $NULL-HWND, Null-Proc); check-result("CreateDialog (top-level)", handle); // Set the default font let font-name = as(, dialog-font-name(resource)); let font-size = dialog-font-size(resource); let text-style = make-text-style-from-name-and-size(_port, font-name, font-size); when (text-style) default-text-style(frame) := text-style end; // Set up the geometry 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("Frame geometry %= from resource: %d x %d at %d, %d", frame, width, height, x, y); set-frame-position(frame, x, y); set-frame-size(frame, width, height); initialize-sheet-geometry(sheet, x, y, width, height); values(handle, resource, , #[]) end method make-top-level-window; define sealed method make-top-level-window (frame :: , sheet :: , resource-id == #f) => (handle :: , resource :: singleton(#f), mirror-class :: , mirror-initargs) let (left, top, right, bottom) = sheet-native-edges(sheet); let title = frame-title(frame) | "DUIM Window"; let x = frame-geometry(frame)[0]; let y = frame-geometry(frame)[1]; let width = frame-geometry(frame)[2]; let height = frame-geometry(frame)[3]; let (style, extended-style) = frame-window-styles(frame); //--- Call compute-default-foreground/background/text-style to //--- figure out what characteristics the mirror should have let handle :: = CreateWindowEx (extended-style, $window-class-name, // See RegisterClass call title, // Text for window title bar style, // Style for a normal top-level window x | $CW-USEDEFAULT, // x position y | $CW-USEDEFAULT, // y position width | right - left, // width height | bottom - top, // height $NULL-HWND, // No parent $null-hMenu, // Use the window class menu application-instance-handle(), $NULL-VOID); // No data in our WM_CREATE check-result("CreateWindow (top-level)", handle); values(handle, #f, , #[]) end method make-top-level-window; define sealed method make-top-level-window (frame :: , //--- should be , sheet :: , resource-id :: ) => (handle :: , resource :: singleton(#f), mirror-class :: , mirror-initargs) ignore(resource-id); make-top-level-embedded-window(frame, sheet) end method make-top-level-window; define sealed method make-top-level-window (frame :: , //--- should be , sheet :: , resource-id == #f) => (handle :: , resource :: singleton(#f), mirror-class :: , mirror-initargs) make-top-level-embedded-window(frame, sheet) end method make-top-level-window; define method make-top-level-embedded-window (frame :: , // , sheet :: ) // Like 'make-top-level-window', except: // (1) Don't call 'frame-window-styles(frame)', just assume: // style = $WS-CHILD + $WS-TABSTOP, no border or title bar // & extended-style = $WS-EX-PARENTNOTIFY // & hence just use $null-string rather than frame-title for title. // (2) Don't use 'frame-geometry(frame)[3,4]' for width/height, act as if // both returned #f. // (3) Use 'sheet-parent-window' for parent if present, instead of $NULL-HWND. let (left, top, right, bottom) = sheet-native-edges(sheet); let x = frame-geometry(frame)[0]; let y = frame-geometry(frame)[1]; //--- Call compute-default-foreground/background/text-style to //--- figure out what characteristics the mirror should have let handle :: = CreateWindowEx ($WS-EX-NOPARENTNOTIFY, $window-class-name, // See RegisterClass call $null-string, // no title %logior($WS-CHILD, $WS-TABSTOP),// no border or title bar x | $CW-USEDEFAULT, // x position y | $CW-USEDEFAULT, // y position right - left, // width bottom - top, // height sheet-parent-window(sheet) | $NULL-HWND, // parent, if known yet $null-hMenu, // no menu application-instance-handle(), $NULL-VOID); // No data in our WM_CREATE check-result("CreateWindow (embedded top-level)", handle); values(handle, #f, , #[]) end make-top-level-embedded-window; define locked variable *first-show-window?* :: = #t; define sealed method map-mirror (_port :: , sheet :: , mirror :: ) => () local method show-window (handle :: ) => (was-visible? :: ) //--- This should maybe be done with 'conditional-update' if (*first-show-window?*) *first-show-window?* := #f; ShowWindow(handle, application-show-window()) else ShowWindow(handle, $SW-SHOW) end end method; let frame = sheet-frame(sheet); let handle :: = window-handle(mirror); show-window(handle); unless (frame-input-focus(frame)) let layout = frame-layout(frame); let focus = layout & find-child-for-focus(layout); when (focus) duim-debug-message("'map-mirror' setting frame focus to %=", focus); frame-input-focus(frame) := focus end end; UpdateWindow(handle) // Sends WM_PAINT message and returns status end method map-mirror; define sealed method raise-mirror (_port :: , sheet :: , mirror :: , #key activate? = #t) => () let handle :: = window-handle(mirror); if (activate?) unless (handle = GetForegroundWindow()) duim-debug-message("Raising top level window %=", sheet); check-result("SetForegroundWindow", SetForegroundWindow(handle)) end else check-result("SetWindowPos ($HWND-TOP)", SetWindowPos(handle, $HWND-TOP, 0, 0, 0, 0, %logior($SWP-NOMOVE, $SWP-NOSIZE, $SWP-NOACTIVATE))) end end method raise-mirror; define sealed method set-mirror-edges (_port :: , sheet :: , mirror :: , left :: , top :: , right :: , bottom :: ) => () let handle :: = window-handle(mirror); let frame = sheet-frame(sheet); let (old-left, old-top, old-right, old-bottom) = box-edges(mirror.%region); let old-width = old-right - old-left; let old-height = old-bottom - old-top; let width = right - left; let height = bottom - top; mirror.%region := set-box-edges(mirror.%region, left, top, right, bottom); duim-debug-message("Setting top level geometry for %= to %d, %d, %d x %d", sheet, left, top, width, height); duim-debug-message(" [old geometry was %d, %d, %d x %d]", old-left, old-top, old-width, old-height); let same-position? = (left = old-left) & (right = old-right); let same-size? = (width = old-width) & (height = old-height); let flags = %logior($SWP-NOACTIVATE, $SWP-NOZORDER, if (same-position?) $SWP-NOMOVE else 0 end, if (same-size?) $SWP-NOSIZE else 0 end); check-result("SetWindowPos", SetWindowPos(handle, $NULL-HWND, left, top, width, height, flags)) end method set-mirror-edges; define sealed method destroy-mirror (_port :: , sheet :: , mirror :: ) => () destroy-accelerator-table(sheet); destroy-tooltip(mirror); next-method() end method destroy-mirror; define method note-win32-frame-destroyed (frame :: ) => () //--- This test needs to be stronger! We shouldn't post a quit message //--- for any frame that gets started from within a thread, if that thread //--- can ever be used to start another frame unless (frame-owner(frame)) duim-debug-message("Quitting frame %=", frame); PostQuitMessage(0) end end method note-win32-frame-destroyed; define method note-win32-frame-destroyed (frame :: ) => () when (container-uses-own-thread?(frame)) duim-debug-message("Quitting container frame %=", frame); PostQuitMessage(0) end end method note-win32-frame-destroyed; /// Top level layout define class () sealed slot top-level-client-layout, init-keyword: client-layout:; end class ; define sealed method do-compose-space (layout :: , #key width, height) => (space-req :: ) let frame = sheet-frame(layout); let menu-width = frame-menu-bar-size(frame); let client-layout = top-level-client-layout(layout); let (extra-width, extra-height) = window-frame-extra-size(frame); if (client-layout) let client-width = width & (width - extra-width); let client-height = height & (height - extra-height); let child-space = compose-space(client-layout, width: client-width, height: client-height); let (w, w-, w+, h, h-, h+) = space-requirement-components(client-layout, child-space); let best-width = max(w, menu-width) + extra-width; let min-width = max(w-, menu-width) + extra-width; let max-width = max(w+, menu-width) + extra-width; let best-height = h + extra-height; let min-height = h- + extra-height; let max-height = h+ + extra-height; make(, width: best-width, min-width: min-width, max-width: max-width, height: best-height, min-height: min-height, max-height: max-height) else let min-width = extra-width; let min-height = extra-height; let best-width = max(width | 0, min-width); let best-height = max(height | 0, min-height); make(, width: best-width, min-width: min-width, max-width: $fill, height: best-height, min-height: min-height, max-height: $fill) end end method do-compose-space; define sealed method do-allocate-space (layout :: , width :: , height :: ) => () let frame = sheet-frame(layout); let (extra-width, extra-height) = window-frame-extra-size(frame); let client-layout = top-level-client-layout(layout); set-sheet-edges(client-layout, 0, 0, width - extra-width, height - extra-height) end method do-allocate-space; define sealed method frame-menu-bar-size (frame :: ) => (width :: , height :: ) let menu-bar = frame-menu-bar(frame); // Menu bars aren't part of the DUIM sheet hierarchy in Windows, // so we have to call 'compose-space' ourselves to measure it if (menu-bar) let space-req = compose-space(menu-bar); let (w, w-, w+, h, h-, h+) = space-requirement-components(menu-bar, space-req); ignore(w-, w+, h-, h+); values(w, h) else values(0, 0) end end method frame-menu-bar-size; define method frame-client-area-offset (frame :: ) => (x :: , y :: ) let (x, y) = values(100, 100); let (width, height) = frame-size(frame); let width = width | $default-sheet-size; let height = height | $default-sheet-size; frame-non-client-geometry(frame, x, y, x + width, y + height) end method frame-client-area-offset; define method window-frame-extra-size (frame :: ) => (width :: , height :: ) let (x, y) = values(100, 100); let (width, height) = frame-size(frame); let width = width | $default-sheet-size; let height = height | $default-sheet-size; let (left, top, right, bottom) = client->frame-edges(frame, x, y, x + width, y + height); values(right - left - width, bottom - top - height) end method window-frame-extra-size; define method frame-non-client-geometry (frame :: , left :: , top :: , right :: , bottom :: ) => (x :: , y :: , width :: , height :: ) let (width, height) = values(right - left, bottom - top); let (l, t, r, b) = client->frame-edges(frame, left, top, right, bottom); let (w, h) = values(r - l, b - t); let x-offset = left - l; let y-offset = top - t; let extra-width = width - w; let extra-height = height - h; values(x-offset, y-offset, extra-width, extra-height) end method frame-non-client-geometry; define method client->frame-edges (frame :: , left :: , top :: , right :: , bottom :: ) => (l :: , t :: , r :: , b :: ) with-stack-structure (rect :: ) rect.left-value := left; rect.top-value := top; rect.right-value := right; rect.bottom-value := bottom; let menu-bar? = frame-menu-bar(frame) & #t; let (style, extended-style) = frame-window-styles(frame); check-result("AdjustWindowRectEx", AdjustWindowRectEx(rect, style, menu-bar?, extended-style)); let frame-left = rect.left-value; let frame-top = rect.top-value; let frame-right = rect.right-value; let frame-bottom = rect.bottom-value; duim-debug-message ("Adjusted [%=,%=,%=,%=] => [%=,%=,%=,%=]: diff [%=,%=,%=,%=]", left, top, right, bottom, frame-left, frame-top, frame-right, frame-bottom, frame-left - left, frame-top - top, frame-right - right, frame-bottom - bottom); values(frame-left, frame-top, frame-right, frame-bottom) end end method client->frame-edges; /// Frame wrapper define method frame-wrapper (framem :: , frame :: , layout :: false-or()) => (wrapper :: false-or()) let menu-bar = frame-menu-bar(frame); let client-layout = make-client-layout(framem, frame, layout); make(, client-layout: client-layout, children: make-children(menu-bar, client-layout)) end method frame-wrapper; define sealed method update-frame-wrapper (framem :: , frame :: ) => () let top-sheet = top-level-sheet(frame); when (top-sheet) let top-layout :: false-or() = sheet-child(top-sheet); if (top-layout) let menu-bar = frame-menu-bar(frame); let children = sheet-children(top-layout); let client-layout = ~empty?(children) & children[size(children) - 1]; let client-layout = if (client-layout) update-client-layout(framem, frame, client-layout) else make-client-layout(framem, frame, frame-layout(frame)) end; let new-children = make-children(menu-bar, client-layout); unless (new-children = sheet-children(top-layout)) sheet-children(top-layout) := new-children; if (sheet-mapped?(top-layout)) if (client-layout) sheet-mapped?(client-layout) := #t; relayout-parent(client-layout) end end end else let wrapper = frame-wrapper(framem, frame, frame-layout(frame)); sheet-child(top-sheet) := wrapper; relayout-parent(wrapper); if (sheet-mapped?(top-sheet)) sheet-mapped?(wrapper, clear?: #t, repaint?: #t) := #t end end end end method update-frame-wrapper; define sealed method make-client-layout (framem :: , frame :: , layout :: false-or()) => (client-layout :: ) let tool-bar = frame-tool-bar(frame); let status-bar = frame-status-bar(frame); with-frame-manager (framem) let indented-children = make-children(tool-bar & tool-bar-decoration(tool-bar), layout); let indented-children-layout = unless (empty?(indented-children)) with-spacing (spacing: win32-dialog-x-pixels(framem, 1)) make(, children: indented-children, y-spacing: $top-level-y-spacing) end end; make(, children: make-children(indented-children-layout, status-bar), y-spacing: $top-level-y-spacing) end end method make-client-layout; define sealed method update-client-layout (framem :: , frame :: , client-layout :: ) => () let tool-bar = frame-tool-bar(frame); let layout = frame-layout(frame); let status-bar = frame-status-bar(frame); with-frame-manager (framem) let old-children = sheet-children(client-layout); let old-first-child = ~empty?(old-children) & old-children[0]; let old-layout = instance?(old-first-child, ) & old-first-child; let indented-children = make-children(tool-bar & tool-bar-decoration(tool-bar), layout); let new-layout = if (~empty?(indented-children)) if (old-layout) let column-layout = sheet-child(old-layout); update-sheet-children(column-layout, indented-children) else with-spacing (spacing: win32-dialog-x-pixels(framem, 1)) make(, children: indented-children, y-spacing: $top-level-y-spacing) end end end; let new-children = make-children(new-layout, status-bar); update-sheet-children(client-layout, new-children) end end method update-client-layout; define function make-children (#rest maybe-children) => (children :: ) let children :: = make(); for (child in maybe-children) child & add!(children, child) end; children end function make-children; define function update-sheet-children (sheet :: , children :: ) => () unless (children = sheet-children(sheet)) sheet-children(sheet) := children; if (sheet-mapped?(sheet)) relayout-parent(sheet); for (child in sheet-children(sheet)) sheet-mapped?(child) := #t end end end end function update-sheet-children; define method update-frame-layout (framem :: , frame :: ) => () update-frame-wrapper(framem, frame) end method update-frame-layout; /// Focus handling // We have yet to find any way in Windows to be informed as children take // and lose the focus, so we just poll the focus on a timer. define constant $use-focus-timer :: = #t; define constant $focus-timer-id :: = 100; define constant $focus-updates-per-second :: = 20; define method note-sheet-activated (sheet :: ) => () let frame = sheet-frame(sheet); let focus = frame & frame-input-focus(frame); let _port = frame & port(frame); when (_port) port-input-focus(_port) := focus; $use-focus-timer & install-focus-timer(sheet); distribute-event(_port, make(, frame: frame)) end end method note-sheet-activated; define method note-sheet-deactivated (sheet :: ) => () let frame = sheet-frame(sheet); let _port = frame & port(frame); $use-focus-timer & uninstall-focus-timer(sheet); when (_port) distribute-event(_port, make(, frame: frame)) end end method note-sheet-deactivated; define sealed method install-focus-timer (sheet :: ) => () let handle = window-handle(sheet); let timeout = floor/(1000, $focus-updates-per-second); if (zero?(SetTimer(handle, $focus-timer-id, timeout, Timer-Proc))) report-error("SetTimer") end end method install-focus-timer; define sealed method uninstall-focus-timer (sheet :: ) => () let handle = window-handle(sheet); check-result("KillTimer", KillTimer(handle, $focus-timer-id)) end method uninstall-focus-timer; define sealed method handle-timer (handle :: , message, event, time) => () #f end method handle-timer; define sealed method handle-timer (handle :: , message, event == $focus-timer-id, time) => () let top-sheet :: = handle-sheet(handle); maybe-update-focus(port(top-sheet)) end method handle-timer; define callback Timer-Proc :: = handle-timer; /// Geometry updating define sealed method handle-move (sheet :: , mirror :: , x :: , y :: ) => (handled? :: ) let (old-x, old-y) = sheet-position(sheet); unless (x = old-x & y = old-y) let frame = sheet-frame(sheet); let (x-offset, y-offset) = frame-client-area-offset(frame); let x :: = x - x-offset; let y :: = y - y-offset; duim-debug-message("Sheet %= moved to %=, %= (from %=, %=)", sheet, x, y, old-x, old-y); set-sheet-position(sheet, x, y) end; #t end method handle-move; define sealed method handle-resize (sheet :: , mirror :: , client-width :: , client-height :: ) => (handled? :: ) let frame = sheet-frame(sheet); let (left, top, right, bottom) = client->frame-edges(frame, 0, 0, client-width, client-height); let (width, height) = values(right - left, bottom - top); let (left, top) = box-position(mirror.%region); let region = make-bounding-box(left, top, left + width, top + height); //--- This hack is to avoid doing anything on the first WM_SIZE //--- which comes in before the children are attached. Can we //--- actually do something with the event? if (sheet-mapped?(sheet)) let (old-width, old-height) = box-size(sheet-region(sheet)); duim-debug-message("Resizing %= to %dx%d -- was %dx%d", sheet, width, height, old-width, old-height); distribute-event(port(sheet), make(, sheet: sheet, region: region)) else duim-debug-message("Ignoring WM_SIZE event for %= to size %dx%d", sheet, width, height) end; #t end method handle-resize; /// Accelerator handling define function make-keyboard-gesture (keysym :: , #rest modifiers) => (gesture :: ) make(, keysym: keysym, modifiers: modifiers) end function make-keyboard-gesture; define function gesture-modifiers (gesture :: ) => (shift? :: , control? :: , alt? :: ) let modifier-state = gesture-modifier-state(gesture); values(~zero?(logand(modifier-state, $shift-key)), ~zero?(logand(modifier-state, $control-key)), ~zero?(logand(modifier-state, $alt-key))) end function gesture-modifiers; define table $accelerator-table :: = { // This is the set defined by WIG, Appendix B, Table B.2, page 438 #"Copy" => make-keyboard-gesture(#"c", #"control"), #"Cut" => make-keyboard-gesture(#"x", #"control"), #"Help" => make-keyboard-gesture(#"f1"), #"Open" => make-keyboard-gesture(#"o", #"control"), #"Open..." => make-keyboard-gesture(#"o", #"control"), #"Paste" => make-keyboard-gesture(#"v", #"control"), #"Print" => make-keyboard-gesture(#"p", #"control"), #"Print..." => make-keyboard-gesture(#"p", #"control"), #"Save" => make-keyboard-gesture(#"s", #"control"), #"Undo" => make-keyboard-gesture(#"z", #"control"), // The same set with the mnemonics already in (a bit of a hack!) #"&Copy" => make-keyboard-gesture(#"c", #"control"), #"Cu&t" => make-keyboard-gesture(#"x", #"control"), #"&Help" => make-keyboard-gesture(#"f1"), #"&Open" => make-keyboard-gesture(#"o", #"control"), #"&Open..." => make-keyboard-gesture(#"o", #"control"), #"&Paste" => make-keyboard-gesture(#"v", #"control"), #"&Print" => make-keyboard-gesture(#"p", #"control"), #"&Print..." => make-keyboard-gesture(#"p", #"control"), #"&Save" => make-keyboard-gesture(#"s", #"control"), #"&Undo" => make-keyboard-gesture(#"z", #"control"), // Some extras that seemed to be missing #"Delete" => make-keyboard-gesture(#"delete"), #"Find" => make-keyboard-gesture(#"f", #"control"), #"Find..." => make-keyboard-gesture(#"f", #"control"), #"New" => make-keyboard-gesture(#"n", #"control"), #"New..." => make-keyboard-gesture(#"n", #"control"), #"Redo" => make-keyboard-gesture(#"y", #"control"), #"Select All" => make-keyboard-gesture(#"a", #"control"), // The same set with the mnemonics already in (a bit of a hack!) #"&Delete" => make-keyboard-gesture(#"delete"), #"&Find" => make-keyboard-gesture(#"f", #"control"), #"&Find..." => make-keyboard-gesture(#"f", #"control"), #"&New" => make-keyboard-gesture(#"n", #"control"), #"&New..." => make-keyboard-gesture(#"n", #"control"), #"&Redo" => make-keyboard-gesture(#"y", #"control"), #"&Select All" => make-keyboard-gesture(#"a", #"control") }; define sealed method defaulted-gadget-accelerator (framem :: , gadget :: ) => (accelerator :: false-or()) let accelerator = gadget-accelerator(gadget); if (unsupplied?(accelerator)) let label = gadget-label(gadget); let key = instance?(label, ) & as(, label); element($accelerator-table, key, default: #f) else accelerator end end method defaulted-gadget-accelerator; define sealed method gadget-label-postfix (gadget :: ) => (label :: ) "" end method gadget-label-postfix; define sealed method gadget-label-postfix (gadget :: ) => (label :: ) let framem = frame-manager(gadget); let gesture = defaulted-gadget-accelerator(framem, gadget); if (gesture) let keysym = gesture-keysym(gesture); let (shift?, control?, alt?) = gesture-modifiers(gesture); concatenate-as(, "\t", if (control?) "Ctrl+" else "" end, if (alt?) "Alt+" else "" end, if (shift?) "Shift+" else "" end, keysym->key-name(keysym)) else "" end end method gadget-label-postfix; // Map keysyms to their labels on a typical keyboard define table $keysym->key-name :: = { #"return" => "Enter", #"newline" => "Shift+Enter", #"linefeed" => "Line Feed", #"up" => "Up Arrow", #"down" => "Down Arrow", #"left" => "Left Arrow", #"right" => "Right Arrow", #"prior" => "Page Up", #"next" => "Page Down", #"lwin" => "Left Windows", #"rwin" => "Right Windows", #"numpad0" => "Num 0", #"numpad1" => "Num 1", #"numpad2" => "Num 2", #"numpad3" => "Num 3", #"numpad4" => "Num 4", #"numpad5" => "Num 5", #"numpad6" => "Num 6", #"numpad7" => "Num 7", #"numpad8" => "Num 8", #"numpad9" => "Num 9", #"num-lock" => "Num Lock", #"caps-lock" => "Caps Lock" }; define function keysym->key-name (keysym) => (name :: ) element($keysym->key-name, keysym, default: #f) | string-capitalize(as(, keysym | "")) end function keysym->key-name; define sealed method accelerator-table (sheet :: ) => (accelerators :: false-or()) let mirror = sheet-direct-mirror(sheet); // Ensure that we don't build the accelerator table too early (i.e., // before all of the resource ids have been created). This isn't as bad // as it seems, since users won't have been able to use an accelerator // before the top-level sheet is mapped anyway... when (sheet-mapped?(sheet)) mirror.%accelerator-table | (mirror.%accelerator-table := make-accelerator-table(sheet)) end end method accelerator-table; // Useful trampoline... define sealed method accelerator-table (sheet :: ) => (accelerators :: false-or()) let top-sheet = top-level-sheet(sheet); top-sheet & accelerator-table(top-sheet) end method accelerator-table; define method make-accelerator-table (sheet :: ) => (accelerators :: ) local method fill-accelerator-entry (gadget :: , accelerator :: , entry :: ) => () let keysym = gesture-keysym(accelerator); let modifiers = gesture-modifier-state(accelerator); let char = gesture-character(accelerator); let (vkey :: , fVirt :: ) = if (char & zero?(logand(modifiers, logior($control-key, $meta-key))) & character->virtual-key(char)) values(character->virtual-key(char), 0) else let vkey = keysym->virtual-key(keysym); if (vkey) values(vkey, logior($ACCEL-FVIRTKEY, if (zero?(logand(modifiers, $shift-key))) 0 else $ACCEL-FSHIFT end, if (zero?(logand(modifiers, $control-key))) 0 else $ACCEL-FCONTROL end, if (zero?(logand(modifiers, $alt-key))) 0 else $ACCEL-FALT end)) else error("Can't decode the gesture with keysym %=, modifiers #o%o", keysym, modifiers) end end; let cmd :: = sheet-resource-id(gadget) | gadget->id(gadget); entry.fVirt-value := fVirt; entry.key-value := vkey; entry.cmd-value := cmd; end method; let accelerators = frame-accelerators(sheet-frame(sheet)); let n :: = size(accelerators); if (n > 0) with-stack-structure (entries :: , element-count: n) for (i :: from 0 below n) let entry = accelerators[i]; let gadget = entry[0]; let accel = entry[1]; let entry = pointer-value-address(entries, index: i); fill-accelerator-entry(gadget, accel, entry) end; check-result("CreateAcceleratorTable", CreateAcceleratorTable(entries, n)) end else $null-HACCEL end end method make-accelerator-table; define sealed method destroy-accelerator-table (sheet :: ) => () let accelerator-table = accelerator-table(sheet); when (accelerator-table & ~null-handle?(accelerator-table)) DestroyAcceleratorTable(accelerator-table) end; let mirror = sheet-direct-mirror(sheet); mirror.%accelerator-table := #f end method destroy-accelerator-table; define method note-accelerators-changed (framem :: , frame :: ) => () // Force the accelerators to be recomputed let top-sheet = top-level-sheet(frame); when (top-sheet) destroy-accelerator-table(top-sheet) end end method note-accelerators-changed; /// Tooltip handling define sealed method make-frame-tooltip (frame :: , mirror :: ) => (tooltip :: ) let handle = window-handle(mirror); initialize-common-controls(port(frame)); let tooltip = CreateWindowEx (0, $TOOLTIPS-CLASS, $NULL-string, %logior($TTS-NOPREFIX, $TTS-ALWAYSTIP), $CW-USEDEFAULT, // x position $CW-USEDEFAULT, // y position $CW-USEDEFAULT, // width $CW-USEDEFAULT, // height handle, // top-level sheet is the parent $null-hMenu, // no menu application-instance-handle(), $NULL-VOID); // No data in our WM_CREATE check-result("CreateWindow (TOOLTIP)", tooltip); mirror.%tool-tip := tooltip end method make-frame-tooltip; define sealed method destroy-tooltip (mirror :: ) => () let tooltip = mirror.%tool-tip; when (tooltip) unless (null-handle?(tooltip)) DestroyWindow(tooltip) // error-checking probably won't buy anything... end end end method destroy-tooltip; //--- We could support tooltips for unmirrored sheets by using $LPSTR-TEXTCALLBACK //--- and handling the $TTN-NEEDTEXT notification messages define sealed method register-tooltip-for-sheet (sheet :: , documentation :: ) => () let top-mirror = top-level-mirror(sheet); let tooltip = top-mirror & top-mirror.%tool-tip; let handle = window-handle(sheet); when (tooltip) // Register a tool tip for this gadget with-c-string (c-string = documentation) with-stack-structure (ti :: ) ti.cbSize-value := safe-size-of(); ti.uFlags-value := %logior($TTF-IDISHWND, $TTF-SUBCLASS); ti.hWnd-value := handle; ti.uId-value := pointer-address(handle); ti.lpszText-value := c-string; SendMessage(tooltip, $TTM-ADDTOOL, 0, pointer-address(ti)) end end end end method register-tooltip-for-sheet; define sealed method unregister-tooltip-for-sheet (sheet :: ) => () let top-mirror = top-level-mirror(sheet); let tooltip = top-mirror & top-mirror.%tool-tip; let handle = window-handle(sheet); when (tooltip) with-stack-structure (ti :: ) ti.cbSize-value := safe-size-of(); ti.uFlags-value := %logior($TTF-IDISHWND, $TTF-SUBCLASS); ti.hWnd-value := handle; ti.uId-value := pointer-address(handle); SendMessage(tooltip, $TTM-DELTOOL, 0, pointer-address(ti)) end end end method unregister-tooltip-for-sheet; /// Keyboard interrupt (i.e., hot key) handling define sealed method register-keyboard-interrupt-handler (_port :: , sheet :: ) => () let id = gethash(_port.%hot-keys, sheet); unless (id) with-port-locked (_port) let handle = window-handle(sheet); when (handle) // Compute a hot key id that isn't currently in use let id = block (return) for (id :: from 0) unless (member?(id, _port.%hot-keys)) return(id) end end end; gethash(_port.%hot-keys, sheet) := id; // By fiat, the "break" key on Windows is Crtl+Cancel (== Ctrl+Pause) //---*** Too bad this registers a system-wide key! // RegisterHotKey(handle, id, $MOD-CONTROL, $VK-CANCEL) end end end end method register-keyboard-interrupt-handler; define sealed method unregister-keyboard-interrupt-handler (_port :: , sheet :: ) => () let id = gethash(_port.%hot-keys, sheet); when (id) with-port-locked (_port) let handle = window-handle(sheet); when (handle) //---*** Fix this when we actually call 'RegisterHotKey' // UnregisterHotKey(handle, id) end end end end method unregister-keyboard-interrupt-handler;