chiark / gitweb /
build system: debian/copyright file
[chiark-tcl-applet.git] / applet.tcl
1 # General purpose code for being a tray applet
2
3 # Copyright 2016,2020 Ian Jackson
4 # SPDX-License-Identifier: GPL-3.0-or-later
5 # There is NO WARRANTY.
6
7 package require Tclx
8 package require tktray
9
10 #----- general machinery -----
11
12 # Interface:
13 #
14 #  tk::tktray widget is called .i
15 #
16 # Tooltip:
17 #
18 #   Caller may call
19 #      applet::setup-tooltip ON-VISIBLE ON-INVISIBLE
20 #   to make applet have a tooltip.
21 #
22 #   ON-VISIBLE and ON-INVISIBLE will be globally eval'd
23 #   when the tooltip becomes visible and invisible.
24 #
25 #   Caller should call
26 #      applet::tooltip-set TEXT-MAYBE-MULTILINE
27 #   whenever they like.
28
29 # Button presses
30 #
31 #    Caller may bind .i.i <ButtonPress-$b>
32 #
33 #    Alternatively caller may call  applet::setup-button-menu $b
34 #    which will generate a menu .m$b which the user can configure
35 #    and which will automatically be posted and unposted etc.
36 #    In this case the caller should arrange that all of their
37 #    menus, when an item is selected, call
38 #      applet::msel
39 #
40 # Icon:
41 #
42 #  Caller should call:
43 #      applet::setimage IMAGE
44 #  as necessary.
45 #
46 # Alternatively of icon, it may provide other arrangements for
47 # using the provided subwindow.  Such a caller should call
48 #      applet::setup-subwindow ON-DESTROYING ON-READY
49 #  Then the main code will call ON-DESTROYING just before
50 #  destroying the inner window and recreating it, and
51 #  [concat ON-READY [list ORIENTATION]]
52 #  just after.  The inner window to use is called .i.i.b.
53 #
54 #  This uses variables, in the applet namespace,
55 #      w h border_colour border_width deforient
56 #  These should be set before setup-subwindow is called and not
57 #  modified thereafter.
58 #
59 #  The user of the subwindow machinery may call
60 #      applet::subwindow-need-recreate
61 #  if for any reason the inner window should be destroyed and recreated.
62 #
63 # Alternatively, it may request that a subprocess be spawned
64 # repeatedly with the xid of a suitable window.
65 #      applet::setup-subproc GET-CMDLINE
66 #  Then the main code will call [concat GET_CMDLINE [list XID ORIENTATION]]
67 #  to get the command line to run.
68 #
69 #  This also uses the same variables as setup-subwindow.
70
71 wm withdraw .
72
73 tktray::icon .i -class example
74 .i configure -docked 1
75
76 fconfigure stdout -buffering none
77 fconfigure stderr -buffering none
78
79
80 namespace eval applet {
81
82 # used by both menus and tooltips
83 variable posted 0
84
85 #----- menus -----
86
87 proc setup-button-menu {b} {
88     bind .i.i <ButtonPress> { applet::menubuttonpressed %b %X %Y }
89     menu .m$b -tearoff 0
90 }
91
92 proc menubuttonpressed {b x y} {
93     variable posted
94     tooltip-cancel
95     if {$posted == $b} {
96         debug::debug "unpost $posted toggle"
97         .m$posted unpost
98         set posted 0
99     } elseif {[winfo exists .m$b]} {
100         if {$posted} {
101             .m$posted unpost
102             debug::debug "unpost $posted other"
103         }
104         debug::debug "post $b"
105         set posted $b
106         .m$b post $x $y
107     }
108 }
109
110 proc msel {} {
111     variable posted
112     set posted 0
113 }
114
115 #----- tooltips -----
116
117 variable tooltip_on_vis {}
118 variable tooltip_on_invis {}
119
120 proc tooltip-starttimer {state x y} {
121     variable tooltip_after
122     variable posted
123     variable tooltip_inwindow
124     if {$state || $posted || !$tooltip_inwindow} { tooltip-cancel; return }
125     catch { after cancel $tooltip_after }
126     set tooltip_after [after 500 applet::tooltip-show $x $y]
127 }
128
129 proc tooltip-cancel {} {
130     variable tooltip_after
131     variable tooltip_on_invis
132     catch { after cancel $tooltip_after }
133     catch { unset $tooltip_after }
134     wm withdraw .tt
135     uplevel #0 $tooltip_on_invis
136 }
137
138 set tooltip_inwindow 0
139
140 proc tooltip-enter {state x y} {
141     variable tooltip_inwindow
142     set tooltip_inwindow 1
143     tooltip-starttimer $state $x $y
144 }
145
146 proc tooltip-leave {} {
147     variable tooltip_inwindow
148     set tooltip_inwindow 0
149     tooltip-cancel
150 }
151
152 proc setup-tooltip {on_vis on_invis} {
153     foreach v {vis invis} {
154         variable tooltip_on_$v [set on_$v]
155     }
156     bind .i <Enter> { applet::tooltip-enter %s %X %Y }
157     bind .i <Leave> { applet::tooltip-leave }
158     bind .i <ButtonRelease> { 
159         applet::tooltip-cancel
160         applet::tooltip-starttimer %s %X %Y
161     }
162     bind .i <Motion> { applet::tooltip-starttimer %s %X %Y }
163     toplevel .tt -background black
164     wm withdraw .tt
165     wm overrideredirect .tt 1
166     label .tt.t -justify left -background {#EEE1B3}
167     pack .tt.t -padx 1 -pady 1
168     tooltip-set {}
169 }
170
171 proc tooltip-set {s} {
172     .tt.t configure -text $s
173 }
174
175 proc tooltip-show {x y} {
176     variable tooltip_on_vis
177     incr x 9
178     incr y 9
179     wm geometry .tt +$x+$y
180     wm deiconify .tt
181     raise .tt
182     uplevel #0 $tooltip_on_vis
183 }
184
185 #----- simple images -----
186
187 proc setimage {image} {
188     .i configure -image $image
189 }
190
191 #----- subwindow -----
192
193 variable subwindow_on_destroying
194 variable subwindow_on_ready
195
196 variable w 50
197 variable h 50
198 variable deforient horizontal
199 variable border_colour darkblue
200 variable border_width 1
201 variable tray_width X
202 variable tray_height X
203 variable orientation vertical
204
205 proc subwindow-need-recreate {evtype why} {
206     variable orientation
207     variable innerwindow_after
208     variable tray_width
209     variable tray_height
210     debug::debug "IW-EVENT $evtype $why [winfo reqwidth .i] [winfo reqheight .i] [winfo width .i] [winfo height .i] $orientation"
211     switch -exact $orientation {
212         horizontal { set szv height }
213         vertical { set szv width }
214         unknown { return }
215     }
216     set new_sz [winfo req$szv .i]
217     if {![string compare $new_sz [set tray_$szv]]} {
218         return
219     }
220     set tray_$szv $new_sz
221 #    switch -exact -- $evtype 35 { return }
222     if {[info exists innerwindow_after]} return
223     set innerwindow_after [after idle applet::innerwindow-resetup]
224 }
225
226 proc innerwindow-resetup {} {
227     variable innerwindow_after
228     variable subwindow_on_destroying
229     variable subwindow_on_ready
230     variable border_colour
231     variable border_width
232     variable deforient
233     variable orientation
234     unset innerwindow_after
235
236     debug::debug RESETUP
237
238     if {![winfo exists .i.i]} return
239     destroy [frame .i.i.make-exist]
240
241     uplevel #0 $subwindow_on_destroying
242     catch { destroy .i.i.b }
243
244     set orientation [.i orientation]
245     debug::debug "orientation $orientation"
246     if {![string compare $orientation unknown]} {
247         set orientation $deforient
248     }
249     .i configure -image applet::innerwindow-ph-$orientation
250
251     frame .i.i.b -background $border_colour -bd $border_width
252     pack .i.i.b -fill both -side left -expand 1
253
254     uplevel #0 $subwindow_on_ready [list $orientation]
255 }
256
257 proc setup-subwindow {on_destroying on_ready} {
258     variable w
259     variable h
260
261     foreach v {on_destroying on_ready} {
262         variable subwindow_$v [set $v]
263     }
264
265     image create photo applet::innerwindow-ph-horizontal -width $w -height 2
266     image create photo applet::innerwindow-ph-vertical -width 2 -height $h
267     .i configure -image applet::innerwindow-ph-horizontal
268
269     destroy [frame .i.make-exist]
270     #destroy [frame .i.i.make-exist]
271     bind .i <<IconConfigure>> { 
272         applet::subwindow-need-recreate %T "%T i=%i k=%K N=%N R=%R S=%S k=%k m=%m d=%d s=%s a=%a b=%b c=%c f=%f w,h=%w,%h o=%o p=%p t=%t x,y=%x,%y B=%B D=%D E=%E P=%P W=%W X,Y=%X,%Y"
273     }
274 }
275
276 #----- subprocess -----
277
278 variable subproc none
279 variable ratelimit {}
280
281 proc setup-subproc {get_cmdline} {
282     variable subproc_get_cmdline $get_cmdline
283     setup-subwindow applet::subproc-destroying applet::subproc-ready
284 }
285
286 proc subproc-destroying {} {
287     variable subproc
288     debug::debug "DESTROYING $subproc"
289
290     catch { destroy .i.i.b.c }
291
292     switch -exact $subproc {
293         none { }
294         old { }
295         default { kill $subproc; set subproc old }
296     }
297 }
298
299 proc subproc-ready {orientation} {
300     variable subproc
301     variable subproc_orientation $orientation
302     debug::debug "READY $subproc"
303
304     frame .i.i.b.c -container 1 -background orange
305     pack .i.i.b.c -fill both -side left -expand 1
306
307     switch -exact $subproc {
308         none {
309             run-child
310         }
311         old {
312             # wait for it to die
313         }
314         default {
315             error "unexpected state $subproc"
316         }
317     }
318     debug::debug "READY-done $subproc"
319 }
320
321 proc run-child {} {
322     variable subproc
323     variable ratelimit
324     variable subproc_get_cmdline
325     variable subproc_orientation
326
327     set id [winfo id .i.i.b.c]
328     set cmd [uplevel #0 $subproc_get_cmdline [list $id $subproc_orientation]]
329
330     debug::debug "RUN-CHILD $subproc"
331     set now [clock seconds]
332     lappend ratelimit $now
333     while {[lindex $ratelimit 0] < {$now - 10}} {
334         set ratelimit [lrange $ratelimit 1 end]
335     }
336     if {[llength $ratelimit] > 10} {
337         puts stderr "crashing repeatedly, quitting $ratelimit"
338         exit 127
339     }
340
341     set subproc none
342     set subproc [subproc::fork applet::child-died {
343         execl [lindex $cmd 0] [lrange $cmd 1 end]
344     }]
345     debug::debug "FORKED $subproc"
346 }
347
348 proc child-died {how how2} {
349     debug::debug "DIED $how $how2"
350     variable subproc
351     switch -exact $subproc {
352         old {
353             set subproc none
354             run-child
355         }
356         default {
357             set subproc none
358             subwindow-need-recreate child-died child-died
359         }
360     }
361 }
362
363 }