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