linux & mac support, refactoring on minimize

This commit is contained in:
2026-08-29 21:13:29 +02:00
parent c7137fc11f
commit 9f87eae8aa
9 changed files with 1760 additions and 535 deletions
+81 -43
View File
@@ -10,60 +10,79 @@
@defmodule[racket-tray]
Racket Tray provides a small API for adding a system tray icon to a Racket GUI
application. Version 0.1.1 supports Windows. The Windows implementation uses
the native @tt{HWND} of an existing Racket top-level window and requires no
additional native library.
Racket Tray provides a small cross-platform system tray API for Racket GUI
applications. Version 0.1.1 contains native backends for Windows, Linux and
macOS. The public API uses symbolic actions so application code does not depend
on a platform-specific tray menu implementation.
@section{Creating a Tray Icon}
@defproc[(mk-tray [frame (is-a?/c top-level-window<%>)]
[icon-file path-string?]
[on-click-cb (or/c #f (-> any))]
[action-spec list?]
[#:hide-on-minimize? hide-on-minimize? boolean? #f])
any/c]{
Creates a tray icon associated with @racket[frame]. The frame must already
have a native window handle. A @racket[frame%] or @racket[dialog%] is suitable.
Creates a tray icon associated with @racket[frame]. A @racket[frame%] or
@racket[dialog%] is suitable.
@racket[icon-file] can be a Windows @tt{.ico} file or a @tt{.png} file. PNG
alpha transparency is preserved when the image is converted to a native
Windows icon.
@racket[action-spec] is mandatory and must contain exactly two values:
@racket[(list callback default-action-id)]. @racket[callback] must accept one
argument and @racket[default-action-id] must be a symbol. Every action generated
by the tray is passed to @racket[callback] as a symbol.
When the tray icon is activated, @racket[on-click-cb] is queued in the
eventspace of @racket[frame]. Use @racket[#f] when no activation callback is
needed.
The default action must occur in the menu later installed with
@racket[tray-set-menu!]. Windows maps normal tray activation to this action.
Ayatana AppIndicator 0.6 or newer can do the same on Linux. Older AppIndicator
0.5.x implementations open the menu on primary activation instead. A native
macOS status item with an attached menu also opens its menu instead of invoking
the default action directly.
When @racket[hide-on-minimize?] is true, minimizing the associated window
hides it after Windows reports @tt{SIZE_MINIMIZED}. Hiding the window removes
it from the taskbar while leaving its native handle alive for the tray icon.
When showing such a window again, an application can restore it with
@racket[(send frame iconize #f)] when @racket[(send frame is-iconized?)] is
true.
When @racket[hide-on-minimize?] is true, Racket Tray periodically checks
@method[frame% is-iconized?]. When the frame changes to the iconized state it
is hidden with @racket[(send frame show #f)]. This implementation is entirely
platform independent and does not use a Win32, GTK or AppKit minimize hook.
The returned value represents the tray icon and is accepted by the other
procedures in this library.
}
@section{Tray Actions and Menus}
@defproc[(tray-set-menu! [tray any/c]
[menu-spec list?]) void?]{
Sets the menu for @racket[tray]. An actionable entry has the form
@racket[(list action-id label)], where @racket[action-id] is a symbol and
@racket[label] is a string. @racket['separator] or @racket[#f] creates a
separator. Action identifiers must be unique and the menu must contain the
default action supplied to @racket[mk-tray].
For example:
@racketblock[
(tray-set-menu!
tray
(list
(list 'open "Open")
'separator
(list 'exit "Exit")))
]
Choosing @racket["Open"] invokes the callback as @racket[(callback 'open)]. The
same callback receives every other menu action.
}
@section{Changing and Closing a Tray Icon}
@defproc[(tray-set-icon! [tray any/c]
[icon-file path-string?]) void?]{
Replaces the image of @racket[tray]. Both @tt{.ico} and @tt{.png} files are
accepted.
}
@defproc[(tray-set-menu! [tray any/c]
[menu any/c]) void?]{
Sets the context menu for @racket[tray]. @racket[menu] can be a
@racket[popup-menu%], @racket[#f], or a list. A list entry of the form
@racket[(list label callback)] creates a menu item. @racket['separator] and
@racket[#f] create a separator. Each callback must accept zero arguments.
Replaces the image of @racket[tray]. PNG files can be used on all supported
platforms. The Windows backend also accepts ICO files.
}
@defproc[(tray-close [tray any/c]) void?]{
Removes @racket[tray], detaches its native window hook, and releases the
Windows icon resources owned by the tray object. Calling this procedure does
not close the associated Racket window.
Removes @racket[tray], stops its optional portable minimize watcher and releases
the native resources owned by the active platform backend. Calling this
procedure does not close the associated Racket window.
}
@section{Closing a Window to the Tray}
@@ -81,17 +100,36 @@ of a frame. A tray application normally overrides it and hides the frame:
]
This is intentionally separate from @racket[#:hide-on-minimize?]. Closing a
window is already represented by a public Racket GUI callback, while Windows
does not expose minimizing through a corresponding public Racket callback.
window already has a portable public Racket callback; minimizing does not.
@section{Platform Behaviour}
On Windows, the backend uses the native HWND returned by Racket GUI,
@tt{Shell_NotifyIconW} and @tt{SetWindowSubclass}. A normal tray activation
invokes the configured default action. No additional native library is needed.
On Linux, the backend uses Ayatana AppIndicator and GTK3. AppIndicator 0.6 or
newer provides primary activation, which Racket Tray maps to the default action.
With AppIndicator 0.5.x, primary activation opens the menu instead. Menu choices
have the same symbolic callback behaviour on both library versions.
On macOS, the backend uses AppKit @tt{NSStatusItem}, @tt{NSStatusBarButton} and
@tt{NSMenu} through Racket's Objective-C FFI. A status item with an attached
menu opens that menu when activated. Menu choices invoke the common symbolic
callback. No additional native library is needed.
@section{Linux Runtime Dependency}
The Linux backend requires the Ayatana AppIndicator GTK3 runtime library. If it
cannot be loaded, @racket[mk-tray] reports the missing dependency and suggests
the native package to install.
Debian and Ubuntu use @tt{libayatana-appindicator3-1}. Fedora uses
@tt{libayatana-appindicator-gtk3}. Arch Linux uses
@tt{libayatana-appindicator}.
@section{Example}
The package contains @filepath{examples/simple.rkt}. It demonstrates hiding a
frame on both minimize and close, restoring it from the tray, and terminating
the application only through the tray menu.
@section{Platform Support}
Version 0.1.1 implements Windows. Requiring @racketmodname[racket-tray] is safe
on other operating systems, but calling its tray operations reports that the
platform is not yet supported.
The package contains @filepath{examples/simple.rkt}. It demonstrates symbolic
tray actions, hiding a frame on minimize and close, restoring it from the tray,
and terminating the application only through the @racket['exit] action.