linux & mac support, refactoring on minimize
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
#lang racket/base
|
||||
|
||||
(require ffi/unsafe
|
||||
ffi/unsafe/objc
|
||||
ffi/unsafe/nsstring
|
||||
racket/class
|
||||
racket/gui/base
|
||||
racket/match)
|
||||
|
||||
(provide mk-tray
|
||||
tray-close
|
||||
tray-set-icon!
|
||||
tray-set-menu!)
|
||||
|
||||
;; AppKit and Foundation are standard macOS frameworks. Load them explicitly
|
||||
;; before importing Objective-C classes so this backend does not depend on
|
||||
;; another Racket library having loaded them first.
|
||||
(ffi-lib "/System/Library/Frameworks/Foundation.framework/Foundation")
|
||||
(ffi-lib "/System/Library/Frameworks/AppKit.framework/AppKit")
|
||||
|
||||
(import-class NSObject
|
||||
NSImage
|
||||
NSMenu
|
||||
NSMenuItem
|
||||
NSStatusBar)
|
||||
|
||||
;; NSVariableStatusItemLength is the native sentinel for a status item whose
|
||||
;; width follows the image or title supplied to its button.
|
||||
(define NSVariableStatusItemLength -1.0)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Internal state / functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; NSMenuItem does not own its target. RacketTrayMenuTarget instances are
|
||||
;; therefore kept in tray-menu-targets for as long as the corresponding menu
|
||||
;; exists. Each target owns only Racket values in Objective-C ivars.
|
||||
(struct tray (frame
|
||||
status-bar
|
||||
status-item
|
||||
button
|
||||
callback
|
||||
default-action
|
||||
eventspace
|
||||
menu
|
||||
menu-targets
|
||||
closed?)
|
||||
#:mutable)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Queue one application tray action in a Racket GUI eventspace.
|
||||
; pre : eventspace is live, callback accepts one argument and action-id is
|
||||
; the symbol associated with a menu item.
|
||||
; post : callback is queued in eventspace unless that eventspace has shut
|
||||
; down before the queue operation.
|
||||
; result : Unspecified.
|
||||
; internals:
|
||||
; AppKit invokes Objective-C target/action methods from its native
|
||||
; event processing. User code is kept outside that native callback by
|
||||
; forwarding the action to Racket's normal GUI callback queue.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (queue-action eventspace callback action-id)
|
||||
(unless (eventspace-shutdown? eventspace)
|
||||
(with-handlers ([exn:fail? (λ (_exn) (void))])
|
||||
(parameterize ([current-eventspace eventspace])
|
||||
(queue-callback
|
||||
(λ ()
|
||||
(callback action-id)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Receive native NSMenuItem target/action messages for tray actions.
|
||||
; pre : callback, action-id and eventspace ivars are set before the target
|
||||
; is assigned to an NSMenuItem.
|
||||
; post : trayAction: forwards the selected symbolic action to queue-action.
|
||||
; result : Objective-C class RacketTrayMenuTarget.
|
||||
; internals:
|
||||
; define-objc-class stores the three fields as Racket-managed ivars.
|
||||
; objc_lookUpClass reuses a class left by an earlier run in the same
|
||||
; process, which avoids redefining an Objective-C runtime class in
|
||||
; DrRacket. Instances are retained by the tray backend because
|
||||
; NSMenuItem does not provide the ownership needed to keep a Racket
|
||||
; callback target alive by itself.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define RacketTrayMenuTarget
|
||||
(or (objc_lookUpClass "RacketTrayMenuTarget")
|
||||
(let ()
|
||||
(define-objc-class RacketTrayMenuTarget NSObject
|
||||
[callback action-id eventspace]
|
||||
(- _void (trayAction: [_id _sender])
|
||||
(when (and callback action-id eventspace)
|
||||
(queue-action eventspace callback action-id))))
|
||||
RacketTrayMenuTarget)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Run thunk synchronously in the handler thread of eventspace.
|
||||
; pre : eventspace is live and thunk accepts no arguments.
|
||||
; post : thunk has completed, or its exception has been re-raised in the
|
||||
; calling thread.
|
||||
; result : The value returned by thunk.
|
||||
; internals:
|
||||
; AppKit objects that belong to a Racket GUI application are created
|
||||
; and modified on the GUI eventspace thread. When the caller is
|
||||
; already that thread, no callback round trip is needed.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (call-in-eventspace eventspace thunk)
|
||||
(let ((handler-thread (eventspace-handler-thread eventspace)))
|
||||
(unless handler-thread
|
||||
(error 'racket-tray "the frame's eventspace has been shut down"))
|
||||
(if (eq? (current-thread) handler-thread)
|
||||
(parameterize ([current-eventspace eventspace])
|
||||
(thunk))
|
||||
(let ((result-channel (make-channel)))
|
||||
(parameterize ([current-eventspace eventspace])
|
||||
(queue-callback
|
||||
(λ ()
|
||||
(with-handlers ([exn?
|
||||
(λ (exn)
|
||||
(channel-put result-channel
|
||||
(cons 'error exn)))])
|
||||
(channel-put result-channel (cons 'ok (thunk)))))))
|
||||
(match (channel-get result-channel)
|
||||
[(cons 'ok value) value]
|
||||
[(cons 'error exn) (raise exn)])))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Validate an icon filename and return its absolute native path.
|
||||
; pre : icon-file is any Racket value.
|
||||
; post : No state is changed.
|
||||
; result : An absolute path string when icon-file names an existing file;
|
||||
; otherwise raises a precise argument or file error.
|
||||
; internals:
|
||||
; NSImage loads normal macOS image formats such as PNG directly from
|
||||
; a filename, so the backend does not need an image converter.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (icon-path icon-file)
|
||||
(unless (path-string? icon-file)
|
||||
(raise-argument-error 'racket-tray "path-string?" icon-file))
|
||||
(unless (file-exists? icon-file)
|
||||
(error 'racket-tray "icon file does not exist: ~a" icon-file))
|
||||
(path->string (path->complete-path icon-file)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Load icon-file as an NSImage owned by the caller.
|
||||
; pre : icon-file names an existing image that AppKit can decode.
|
||||
; post : One retained NSImage exists when loading succeeds.
|
||||
; result : The retained NSImage; raises an exception if AppKit cannot load it.
|
||||
; internals:
|
||||
; alloc/initWithContentsOfFile: gives this procedure ownership. The
|
||||
; caller releases that ownership after setImage:, because the status
|
||||
; bar button retains the image it displays.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (load-image icon-file)
|
||||
(let* ((path (icon-path icon-file))
|
||||
(image
|
||||
(tell (tell NSImage alloc)
|
||||
initWithContentsOfFile: #:type _NSString path)))
|
||||
(unless image
|
||||
(error 'racket-tray "could not load tray icon: ~a" icon-file))
|
||||
image))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Release Objective-C menu target objects owned by racket-tray.
|
||||
; pre : targets is a list of retained RacketTrayMenuTarget instances.
|
||||
; post : Each target has received release exactly once.
|
||||
; result : void.
|
||||
; internals:
|
||||
; NSMenuItem's target reference is not used as an ownership boundary;
|
||||
; racket-tray explicitly retains targets by creating them with new
|
||||
; and explicitly releases them after detaching the menu.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (release-menu-targets targets)
|
||||
(for ((target (in-list targets)))
|
||||
(tellv target release))
|
||||
(void))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Build an NSMenu from the common racket-tray menu specification.
|
||||
; pre : t is open and menu-spec has been validated by main.rkt.
|
||||
; post : A retained NSMenu and retained target object for every actionable
|
||||
; item have been created.
|
||||
; result : Two values: the retained NSMenu and its retained target list.
|
||||
; internals:
|
||||
; Each item uses the same trayAction: selector. Its target stores the
|
||||
; corresponding action symbol, so all choices reach the one callback
|
||||
; supplied to mk-tray. Separators need no target object.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (make-menu t menu-spec)
|
||||
(let ((menu (tell NSMenu new))
|
||||
(targets '()))
|
||||
(tellv menu setAutoenablesItems: #:type _BOOL #f)
|
||||
(for ((entry (in-list menu-spec)))
|
||||
(cond
|
||||
[(or (eq? entry 'separator)
|
||||
(eq? entry #f))
|
||||
(tellv menu addItem: (tell NSMenuItem separatorItem))]
|
||||
[else
|
||||
(let* ((action-id (car entry))
|
||||
(label (cadr entry))
|
||||
(target (tell RacketTrayMenuTarget new))
|
||||
(item
|
||||
(tell (tell NSMenuItem alloc)
|
||||
initWithTitle: #:type _NSString label
|
||||
action: #:type _SEL (selector trayAction:)
|
||||
keyEquivalent: #:type _NSString "")))
|
||||
(set-ivar! target callback (tray-callback t))
|
||||
(set-ivar! target action-id action-id)
|
||||
(set-ivar! target eventspace (tray-eventspace t))
|
||||
(tellv item setTarget: target)
|
||||
(tellv menu addItem: item)
|
||||
(tellv item release)
|
||||
(set! targets (cons target targets)))]))
|
||||
(values menu (reverse targets))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Validate that t is an open macOS tray object.
|
||||
; pre : who names the calling procedure and t is any value.
|
||||
; post : No state is changed.
|
||||
; result : void when valid; otherwise raises an argument/state exception.
|
||||
; internals:
|
||||
; Native Objective-C objects must not receive messages after
|
||||
; tray-close has released the status item and menu resources.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (check-open-tray who t)
|
||||
(unless (tray? t)
|
||||
(raise-argument-error who "tray?" t))
|
||||
(when (tray-closed? t)
|
||||
(error who "tray icon is already closed")))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Provided functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Create a native macOS menu-bar status item for a Racket frame.
|
||||
; pre : frame implements top-level-window<%>, icon-file is readable,
|
||||
; callback accepts one symbol and default-action is a symbol.
|
||||
; post : A retained NSStatusItem exists in the system status bar and shows
|
||||
; icon-file. Its menu is initially empty.
|
||||
; result : A mutable tray object for the remaining backend procedures.
|
||||
; internals:
|
||||
; macOS status items conventionally open their NSMenu when clicked.
|
||||
; Menu selections invoke callback with their action symbol. The
|
||||
; default-action is retained for the common cross-platform API, but
|
||||
; AppKit does not use it for a status item that has an attached menu.
|
||||
; statusItemWithLength: does not transfer ownership to the status bar,
|
||||
; so racket-tray retains the returned status item until tray-close.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (mk-tray frame icon-file callback default-action)
|
||||
(unless (is-a? frame top-level-window<%>)
|
||||
(raise-argument-error 'mk-tray "(is-a?/c top-level-window<%>)" frame))
|
||||
(unless (and (procedure? callback)
|
||||
(procedure-arity-includes? callback 1))
|
||||
(raise-argument-error 'mk-tray "(procedure-arity-includes/c 1)" callback))
|
||||
(unless (symbol? default-action)
|
||||
(raise-argument-error 'mk-tray "symbol?" default-action))
|
||||
(let ((eventspace (send frame get-eventspace)))
|
||||
(call-in-eventspace
|
||||
eventspace
|
||||
(λ ()
|
||||
(let* ((status-bar (tell NSStatusBar systemStatusBar))
|
||||
(status-item
|
||||
(tell status-bar
|
||||
statusItemWithLength: #:type _double
|
||||
NSVariableStatusItemLength))
|
||||
(button (tell status-item button)))
|
||||
(unless (and status-item button)
|
||||
(when status-item
|
||||
(tellv status-bar removeStatusItem: status-item))
|
||||
(error 'mk-tray "could not create a macOS status item"))
|
||||
(tellv status-item retain)
|
||||
(let ((menu #f)
|
||||
(image #f))
|
||||
(with-handlers
|
||||
([exn?
|
||||
(λ (exn)
|
||||
(when image
|
||||
(tellv image release))
|
||||
(when menu
|
||||
(tellv menu release))
|
||||
(tellv status-bar removeStatusItem: status-item)
|
||||
(tellv status-item release)
|
||||
(raise exn))])
|
||||
(set! menu (tell NSMenu new))
|
||||
(set! image (load-image icon-file))
|
||||
(tellv button setImage: image)
|
||||
(tellv image release)
|
||||
(set! image #f)
|
||||
(let ((frame-label (send frame get-label)))
|
||||
(when (string? frame-label)
|
||||
(tellv button setToolTip: #:type _NSString frame-label)))
|
||||
(tellv status-item setMenu: menu)
|
||||
(tray frame
|
||||
status-bar
|
||||
status-item
|
||||
button
|
||||
callback
|
||||
default-action
|
||||
eventspace
|
||||
menu
|
||||
'()
|
||||
#f))))))))
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Remove a macOS status item and release resources owned by it.
|
||||
; pre : t is a tray object; repeated calls are allowed.
|
||||
; post : The status item is removed, its menu and item targets are released,
|
||||
; and t is marked closed.
|
||||
; result : void.
|
||||
; internals:
|
||||
; The menu is first detached from NSStatusItem so AppKit no longer
|
||||
; references it. Targets are released after the menu is detached, and
|
||||
; the explicit retain from mk-tray is balanced last.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (tray-close t)
|
||||
(unless (tray? t)
|
||||
(raise-argument-error 'tray-close "tray?" t))
|
||||
(unless (tray-closed? t)
|
||||
(call-in-eventspace
|
||||
(tray-eventspace t)
|
||||
(λ ()
|
||||
(unless (tray-closed? t)
|
||||
(tellv (tray-status-item t) setMenu: #f)
|
||||
(tellv (tray-status-bar t) removeStatusItem: (tray-status-item t))
|
||||
(when (tray-menu t)
|
||||
(tellv (tray-menu t) release)
|
||||
(set-tray-menu! t #f))
|
||||
(release-menu-targets (tray-menu-targets t))
|
||||
(set-tray-menu-targets! t '())
|
||||
(tellv (tray-status-item t) release)
|
||||
(set-tray-closed?! t #t)))))
|
||||
(void))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Replace the image shown by an open macOS status item.
|
||||
; pre : t is open and icon-file names an image AppKit can decode.
|
||||
; post : The status bar button displays the new image.
|
||||
; result : void.
|
||||
; internals:
|
||||
; The temporary retained NSImage is released immediately after
|
||||
; setImage:, leaving normal AppKit ownership with the button.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (tray-set-icon! t icon-file)
|
||||
(check-open-tray 'tray-set-icon! t)
|
||||
(call-in-eventspace
|
||||
(tray-eventspace t)
|
||||
(λ ()
|
||||
(let ((image (load-image icon-file)))
|
||||
(tellv (tray-button t) setImage: image)
|
||||
(tellv image release))))
|
||||
(void))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; goal : Replace the menu attached to an open macOS status item.
|
||||
; pre : t is open and menu-spec is the validated common menu format.
|
||||
; post : The status item opens the new NSMenu; resources belonging to the
|
||||
; previous menu have been released.
|
||||
; result : void.
|
||||
; internals:
|
||||
; The new menu is attached before the old menu and its retained target
|
||||
; objects are released. This prevents AppKit from observing a target
|
||||
; object whose Racket-owned retain has already been balanced.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(define (tray-set-menu! t menu-spec)
|
||||
(check-open-tray 'tray-set-menu! t)
|
||||
(call-in-eventspace
|
||||
(tray-eventspace t)
|
||||
(λ ()
|
||||
(let-values (((new-menu new-targets)
|
||||
(make-menu t menu-spec)))
|
||||
(let ((old-menu (tray-menu t))
|
||||
(old-targets (tray-menu-targets t)))
|
||||
(tellv (tray-status-item t) setMenu: new-menu)
|
||||
(set-tray-menu! t new-menu)
|
||||
(set-tray-menu-targets! t new-targets)
|
||||
(when old-menu
|
||||
(tellv old-menu release))
|
||||
(release-menu-targets old-targets)))))
|
||||
(void))
|
||||
Reference in New Issue
Block a user