diff options
Diffstat (limited to 'library/systray.tcl')
-rw-r--r-- | library/systray.tcl | 483 |
1 files changed, 483 insertions, 0 deletions
diff --git a/library/systray.tcl b/library/systray.tcl new file mode 100644 index 0000000..56cfbf9 --- /dev/null +++ b/library/systray.tcl @@ -0,0 +1,483 @@ +# systray.tcl -- + +# This file defines the 'tk systray' command for icon display and manipulation +# in the system tray on X11, Windows, and macOS, and the 'tk sysnotify' command +# for system alerts on each platform. It implements an abstraction layer that +# presents a consistent API across the three platforms. + +# Copyright © 2020 Kevin Walzer/WordTech Communications LLC. +# Copyright © 2020 Eric Boudaillier. +# Copyright © 2020 Francois Vogel. +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. + +# Pure-Tcl system tooltip window for use with system tray icon if native +# implementation not available. + +namespace eval ::tk::systray { + variable _created 0 + variable _options {-image "" -text "" -button1 "" -button3 ""} + variable _current {} + variable _ico + + proc _balloon {w help} { + bind $w <Any-Enter> "after 100 [list [namespace current]::_balloon_show %W [list $help] cursor]" + bind $w <Any-Leave> "destroy %W._balloon" + } + + proc _balloon_show {w msg i} { + if {![winfo exists $w]} { return } + + # Use string match to allow that the help will be shown when + # the pointer is in any child of the desired widget + if {([winfo class $w] ne "Menu") && ![string match $w* [eval [list winfo containing] \ + [winfo pointerxy $w]]]} { + return + } + + set top $w._balloon + ::destroy $top + toplevel $top -bg black -bd 1 + wm overrideredirect $top 1 + wm state $top withdrawn + if {[tk windowingsystem] eq "aqua"} { + ::tk::unsupported::MacWindowStyle style $top help none + } + pack [message $top._txt -aspect 10000 -text $msg] + + update idletasks + set screenw [winfo screenwidth $w] + set screenh [winfo screenheight $w] + set reqw [winfo reqwidth $top] + set reqh [winfo reqheight $top] + # When adjusting for being on the screen boundary, check that we are + # near the "edge" already, as Tk handles multiple monitors oddly + if {$i eq "cursor"} { + set y [expr {[winfo pointery $w]+20}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + set y [expr {[winfo pointery $w]-$reqh-5}] + } + } elseif {$i ne ""} { + set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[$w yposition $i]+25}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + # show above if we would be offscreen + set y [expr {[winfo rooty $w]+[$w yposition $i]-$reqh-5}] + } + } else { + set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[winfo height $w]+5}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + # show above if we would be offscreen + set y [expr {[winfo rooty $w]-$reqh-5}] + } + } + if {$i eq "cursor"} { + set x [winfo pointerx $w] + } else { + set x [expr {[winfo rootx $w]+[winfo vrootx $w]+ ([winfo width $w]-$reqw)/2}] + } + # only readjust when we would appear right on the screen edge + if {$x<0 && ($x+$reqw)>0} { + set x 0 + } elseif {($x < $screenw) && ($x+$reqw) > $screenw} { + set x [expr {$screenw-$reqw}] + } + if {[tk windowingsystem] eq "aqua"} { + set focus [focus] + } + + wm geometry $top +$x+$y + wm deiconify $top + raise $top + + if {[tk windowingsystem] eq "aqua" && $focus ne ""} { + # Aqua's help window steals focus on display + after idle [list focus -force $focus] + } + } + + proc _win_callback {msg} { + variable _current + # The API at the Tk level does not feature bindings to double clicks. Whatever + # the speed the user clicks with, he expects the single click binding to fire. + # Therefore we need to bind to both WM_*BUTTONDOWN and to WM_*BUTTONDBLCLK. + switch -exact -- $msg { + WM_LBUTTONDOWN - WM_LBUTTONDBLCLK { + uplevel #0 [dict get $_current -button1] + } + WM_RBUTTONDOWN - WM_RBUTTONDBLCLK { + uplevel #0 [dict get $_current -button3] + } + } + } + + namespace export create configure destroy exists + namespace ensemble create +} + + +# Pure-Tcl system notification window for use if native implementation not available. +# This is supposed to happen only on X11 when libnotify is not present. +namespace eval ::tk::sysnotify:: { + # These defaults mimic the default behaviour of gnome and xfce notifications. + # These are hardcoded defaults. + variable defaults { + padX 3 + padY 3 + background gray15 + foreground white + delay 10000 + alpha 0.85 + } + # These options are meant to be "public". The user could tinker with + # these values to adjust the system notification appearance/behaviour. + option add *Sysnotify.padX [dict get $defaults padX] + option add *Sysnotify.padY [dict get $defaults padY] + option add *Sysnotify.background [dict get $defaults background] + option add *Sysnotify.foreground [dict get $defaults foreground] + option add *Sysnotify.delay [dict get $defaults delay] + option add *Sysnotify.alpha [dict get $defaults alpha] + + proc _notifywindow {title msg} { + variable defaults + + # cleanup any previous notify window and create a new one + set w ._notify + _notifyDestroy $w + toplevel $w -class Sysnotify + + # read the option database to check out whether the user has set + # some options; fall back to our hardcoded defaults otherwise + dict for {option value} [dict remove $defaults alpha] { + set $option [option get $w $option ""] + if {[set $option] eq ""} { + set $option $value + } + } + + set xpos [tk::ScaleNum 16] + set ypos [tk::ScaleNum 48] + # position from the "ne" corner + wm geometry $w -$xpos+$ypos + wm overrideredirect $w true + + # internal options + option add *Sysnotify.Label.anchor w + option add *Sysnotify.Label.justify left + option add *Sysnotify.Label.wrapLength [expr {[winfo screenwidth .] / 4}] + foreach option {padX padY foreground background} { + option add *Sysnotify.Label.$option [set $option] + } + set icon ::tk::icons::information + set width [expr {[image width $icon] + 2 * $padX}] + set height [expr {[image height $icon] + 2 * $padY}] + label $w.icon -image $icon -width $width -height $height -anchor c + label $w.title -text $title -font TkHeadingFont + label $w.message -text [_filterMarkup $msg] -font TkTooltipFont + + grid $w.icon $w.title -sticky news + grid ^ $w.message -sticky news + + bind Sysnotify <Map> [namespace code { + # set the wm attribute here; it is ignored if set + # before the window is mapped + wm attributes %W -alpha 0.0 + if {[wm attributes %W -alpha] == 0.0} { + _fadeIn %W + } + }] + bind Sysnotify <Enter> [namespace code {_onEnter %W}] + bind Sysnotify <Leave> [namespace code {_onLeave %W}] + bind $w <Button-1> [namespace code [list _notifyDestroy $w]] + after $delay [namespace code [list _fadeOut $w]] + return + } + + # Fade the window into view. + proc _fadeIn {w} { + variable defaults + if {![winfo exists $w]} {return} + if {[set alpha [option get $w alpha ""]] eq ""} { + set alpha [dict get $defaults alpha] + } + raise $w + set before [wm attributes $w -alpha] + set new [expr { min($alpha, $before + 0.10) }] + wm attributes $w -alpha $new + set after [wm attributes $w -alpha] + if {($before == 1.0) || ($before == $after)} { + # not supported or we're done + return + } + after 40 [namespace code [list _fadeIn $w]] + } + + # Fade out and destroy window. + proc _fadeOut {w} { + if {![winfo exists $w]} {return} + set before [wm attributes $w -alpha] + set new [expr { $before - 0.02 }] + wm attributes $w -alpha $new + set after [wm attributes $w -alpha] + if {($after == 1.0) || ($before == $after)} { + _notifyDestroy $w + return + } + after 40 [namespace code [list _fadeOut $w]] + } + + proc _notifyDestroy {w} { + # cancel any pending fade in or fade out + _cancelFading $w + destroy $w + } + + proc _onEnter {w} { + wm attributes $w -alpha 1.0 + _cancelFading $w + } + + proc _onLeave {w} { + variable defaults + if {[set alpha [option get $w alpha ""]] eq ""} { + set alpha [dict get $defaults alpha] + } + if {[set delay [option get $w delay ""]] eq ""} { + set delay [dict get $defaults delay] + } + wm attributes $w -alpha $alpha + after $delay [namespace code [list _fadeOut $w]] + } + + proc _cancelFading {w} { + after cancel [namespace code [list _fadeOut $w]] + after cancel [namespace code [list _fadeIn $w]] + } + + # The Desktop Notifications Specification allow for some markup + # in the message to display. It also specifies + # "Notification servers that do not support these tags should + # filter them out" + # See https://specifications.freedesktop.org/notification-spec/latest/ar01s04.html + # We don't event try to render those properly + proc _filterMarkup {txt} { + # remove fixed tags + set maplist {<b> "" </b> "" <i> "" </i> "" <u> "" </u> "" </a> ""} + set txt [string map $maplist $txt] + # remove <img> tags leaving (possible) alt text + set txt [regsub -- {<img *src="[^"]*" *(alt="([^"]*)")? */?>} $txt {\2}] + # remove <a href=""> variable tag + set txt [regsub -- {<a[^>]*>} $txt {}] + return $txt + } +} + + +# tk systray -- +# This procedure creates an icon display in the platform-specific system tray. +# +# Subcommands: +# +# create - create systray icon. +# Arguments: +# -image - Tk image to display. +# -text - string to display in tooltip over image. +# -button1 - Tcl proc to invoke on <Button-1> event. +# -button3 - Tcl proc to invoke on <Button-3> event. +# +# configure - change one of the systray properties. +# Arguments (Any or all can be called): +# -image - Tk image to update. +# -text - string to update. +# -button1 - Tcl proc to change for <Button-1> event. +# -button3 - Tcl proc to change for <Button-3> event. +# +# destroy - destroy systray icon. +# Arguments: +# none. +proc ::tk::systray::create {args} { + variable _created + variable _options + variable _current + variable _ico + + if {$_created} { + return -code error -errorcode {TK SYSTRAY CREATE} "only one system tray icon supported per interpeter" + } + _check_options $args 0 + if {![dict exists $args -image]} { + return -code error -errorcode {TK SYSTRAY CREATE} "missing required option \"-image\"" + } + set values [dict merge $_options $args] + try { + switch -- [tk windowingsystem] { + "win32" { + set _ico [_systray add -image [dict get $values -image] \ + -text [dict get $values -text] \ + -callback [list ::tk::systray::_win_callback %m]] + } + "x11" { + _systray ._tray -image [dict get $values -image] -visible true + _balloon ._tray [dict get $values -text] + bind ._tray <Button-1> [dict get $values -button1] + bind ._tray <Button-3> [dict get $values -button3] + } + "aqua" { + _systray create [dict get $values -image] [dict get $values -text] \ + [dict get $values -button1] [dict get $values -button3] + } + } + } on ok {} { + set _current $values + set _created 1 + return + } on error {msg opts} { + return -code error -errorcode [dict get $opts -errorcode] $msg + } +} + +# Modify the systray icon. +proc ::tk::systray::configure {args} { + variable _created + variable _options + variable _current + variable _ico + + if {!$_created} { + return -code error -errorcode {TK SYSTRAY CREATE} "systray not created" + } + _check_options $args 1 + if {[llength $args] == 0} { + return $_current + } elseif {[llength $args] == 1} { + return [dict get $_current [lindex $args 0]] + } + set values [dict merge $_current $args] + try { + switch -- [tk windowingsystem] { + "win32" { + if {[dict exists $args -image]} { + _systray modify $_ico -image [dict get $args -image] + } + if {[dict exists $args -text]} { + _systray modify $_ico -text [dict get $args -text] + } + } + "x11" { + if {[dict exists $args -image]} { + ._tray configure -image [dict get $args -image] + } + if {[dict exists $args -text]} { + _balloon ._tray [dict get $args -text] + } + if {[dict exists $args -button1]} { + bind ._tray <Button-1> [dict get $args -button1] + } + if {[dict exists $args -button3]} { + bind ._tray <Button-3> [dict get $args -button3] + } + } + "aqua" { + foreach {key opt} {image -image text \ + -text b1_callback -button1 b3_callback -button3} { + if {[dict exists $args $opt]} { + _systray modify $key [dict get $args $opt] + } + } + } + } + } on ok {} { + set _current $values + return + } on error {msg opts} { + return -code error -errorcode [dict get $opts -errorcode] $msg + } +} + +# Remove the systray icon. +proc ::tk::systray::destroy {} { + variable _created + variable _current + variable _ico + + if {!$_created} { + return -code error -errorcode {TK SYSTRAY DESTROY} "systray not created" + } + switch -- [tk windowingsystem] { + "win32" { + _systray delete $_ico + set _ico "" + } + "x11" { + ::destroy ._tray + } + "aqua" { + _systray destroy + } + } + set _created 0 + set _current {} + return +} + +# Check systray icon existence. +proc tk::systray::exists {} { + variable _created + return $_created +} + +# Check systray options +proc ::tk::systray::_check_options {argsList singleOk} { + variable _options + + set len [llength $argsList] + while {[llength $argsList] > 0} { + set opt [lindex $argsList 0] + if {![dict exists $_options $opt]} { + tailcall return -code error -errorcode {TK SYSTRAY OPTION} \ + "unknown option \"$opt\": must be -image, -text, -button1 or -button3" + } + if {[llength $argsList] == 1 && !($len == 1 && $singleOk)} { + tailcall return -code error -errorcode {TK SYSTRAY OPTION} \ + "missing value for option \"$opt\"" + } + set argsList [lrange $argsList 2 end] + } +} + +# tk sysnotify -- +# This procedure implements a platform-specific system notification alert. +# +# Arguments: +# title - main text of alert. +# message - body text of alert. + +proc ::tk::sysnotify::sysnotify {title message} { + + switch -- [tk windowingsystem] { + "win32" { + if {!$::tk::systray::_created} { + error "must create a system tray icon with the \"tk systray\" command first" + } + _sysnotify notify $::tk::systray::_ico $title $message + } + "x11" { + if {[info commands ::tk::sysnotify::_sysnotify] eq ""} { + _notifywindow $title $message + } else { + _sysnotify $title $message + } + } + "aqua" { + _sysnotify $title $message + } + } + return +} + +#Add these commands to the tk command ensemble: tk systray, tk sysnotify +#Thanks to Christian Gollwitzer for the guidance here +namespace ensemble configure tk -map \ + [dict merge [namespace ensemble configure tk -map] \ + {systray ::tk::systray sysnotify ::tk::sysnotify::sysnotify}] + |