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