source: trunk/gui/scripts/tooltip.tcl @ 5347

Last change on this file since 5347 was 3330, checked in by gah, 11 years ago

merge (by hand) with Rappture1.2 branch

File size: 16.0 KB
Line 
1# -*- mode: tcl; indent-tabs-mode: nil -*-
2# ----------------------------------------------------------------------
3#  COMPONENT: tooltip - help information that pops up beneath a widget
4#
5#  This file provides support for tooltips, which are little bits
6#  of help information that pop up beneath a widget.
7#
8#  Tooltips can be registered for various widgets as follows:
9#
10#    Rappture::Tooltip::for .w "Some help text."
11#    Rappture::Tooltip::for .x.y "Some more help text."
12#
13#  Tooltips can also be popped up as an error cue beneath a widget
14#  with bad information:
15#
16#    Rappture::Tooltip::cue .w "Bad data in this widget."
17#
18# ======================================================================
19#  AUTHOR:  Michael McLennan, Purdue University
20#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
21#
22#  See the file "license.terms" for information on usage and
23#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
24# ======================================================================
25package require Itk
26
27option add *Tooltip.background white widgetDefault
28option add *Tooltip.outline black widgetDefault
29option add *Tooltip.borderwidth 1 widgetDefault
30option add *Tooltip.font -*-helvetica-medium-r-normal-*-12-* widgetDefault
31option add *Tooltip.wrapLength 4i widgetDefault
32
33itcl::class Rappture::Tooltip {
34    inherit itk::Toplevel
35
36    itk_option define -outline outline Outline ""
37    itk_option define -icon icon Icon ""
38    itk_option define -message message Message ""
39    itk_option define -log log Log ""
40
41    constructor {args} { # defined below }
42
43    public method show {where}
44    public method hide {}
45
46    private variable _showing 0  ;# time when tooltip popped up on screen
47
48    public proc for {widget text args}
49    public proc text {widget args}
50    private common catalog    ;# maps widget => -message and -log
51
52    public proc tooltip {option args}
53    private common pending "" ;# after ID for pending "tooltip show"
54
55    public proc cue {option args}
56
57    bind RapptureTooltip <Enter> \
58        [list ::Rappture::Tooltip::tooltip pending %W]
59    bind RapptureTooltip <Leave> \
60        [list ::Rappture::Tooltip::tooltip cancel]
61    bind RapptureTooltip <ButtonPress> \
62        [list ::Rappture::Tooltip::tooltip cancel]
63    bind RapptureTooltip <KeyPress> \
64        [list ::Rappture::Tooltip::tooltip cancel]
65}
66
67itk::usual Tooltip {
68    keep -background -outline -cursor -font
69}
70
71# ----------------------------------------------------------------------
72# CONSTRUCTOR
73# ----------------------------------------------------------------------
74itcl::body Rappture::Tooltip::constructor {args} {
75    wm overrideredirect $itk_component(hull) yes
76    wm withdraw $itk_component(hull)
77
78    component hull configure -borderwidth 1 -background black
79    itk_option remove hull.background hull.borderwidth
80
81    itk_component add icon {
82        label $itk_interior.icon -anchor n
83    }
84
85    itk_component add text {
86        label $itk_interior.text -justify left
87    } {
88        usual
89        keep -wraplength
90    }
91    pack $itk_component(text) -expand yes -fill both -ipadx 4 -ipady 4
92
93    eval itk_initialize $args
94}
95
96# ----------------------------------------------------------------------
97# USAGE: show @<x>,<y>|<widget>+/-<x>,<y>
98#
99# Clients use this to pop up the tooltip on the screen.  The position
100# should be either a <widget> name with an optional offset +/-<x>,<y>
101# (tooltip pops up beneath widget by default), or a specific root
102# window coordinate of the form @x,y.
103#
104# If the -message has the form "@command", then the command is executed
105# now, just before the tooltip is popped up, to build the message
106# on-the-fly.
107# ----------------------------------------------------------------------
108itcl::body Rappture::Tooltip::show {where} {
109    set hull $itk_component(hull)
110    set _showing 0
111
112    set signx "+"
113    set signy "+"
114
115    if {[regexp {^@([0-9]+),([0-9]+)$} $where match x y]} {
116        set xpos $x
117        set ypos $y
118    } elseif {[regexp {^(.*)([-+])([0-9]+),([-+]?)([0-9]+)$} $where match win signx x signy y]} {
119        if {$signy == ""} { set signy $signx }
120        set xpos [expr {[winfo rootx $win] + $x}]
121        set ypos [expr {[winfo rooty $win] + $y}]
122    } elseif {[winfo exists $where]} {
123        set xpos [expr {[winfo rootx $where]+10}]
124        set ypos [expr {[winfo rooty $where]+[winfo height $where]}]
125    } else {
126        error "bad position \"$where\": should be widget+x,y, or @x,y"
127    }
128
129    if {[string index $itk_option(-message) 0] == "@"} {
130        set cmd [string range $itk_option(-message) 1 end]
131        if {[catch $cmd mesg] != 0} {
132            bgerror $mesg
133            return
134        }
135    } else {
136        set mesg $itk_option(-message)
137    }
138
139    # if there's no message to show, forget it
140    if {[string length $mesg] == 0} {
141        return
142    }
143
144    # strings can't be too big, or they'll go off screen!
145    set pos 0
146    ::for {set i 0} {$pos >= 0 && $i < 20} {incr i} {
147        incr pos
148        set pos [string first \n $mesg $pos]
149    }
150    if {$pos > 0} {
151        set mesg "[string range $mesg 0 $pos]..."
152    }
153    if {[string length $mesg] > 1000} {
154        set mesg "[string range $mesg 0 1500]..."
155    }
156    $itk_component(text) configure -text $mesg
157
158    #
159    # Make sure the tooltip doesn't go off screen.
160    #
161    update idletasks
162    if {$signx == "+"} {
163        if {$xpos+[winfo reqwidth $hull] > [winfo screenwidth $hull]} {
164            set xpos [expr {[winfo screenwidth $hull]-[winfo reqwidth $hull]}]
165        }
166        if {$xpos < 0} { set xpos 0 }
167    } else {
168        if {$xpos-[winfo reqwidth $hull] < 0} {
169            set xpos [expr {[winfo screenwidth $hull]-[winfo reqwidth $hull]}]
170        }
171        set xpos [expr {[winfo screenwidth $hull]-$xpos}]
172    }
173
174    if {$signy == "+"} {
175        if {$ypos+[winfo reqheight $hull] > [winfo screenheight $hull]} {
176            set ypos [expr {[winfo screenheight $hull]-[winfo reqheight $hull]}]
177        }
178        if {$ypos < 0} { set ypos 0 }
179    } else {
180        if {$ypos-[winfo reqheight $hull] < 0} {
181            set ypos [expr {[winfo screenheight $hull]-[winfo reqheight $hull]}]
182        }
183        set ypos [expr {[winfo screenheight $hull]-$ypos}]
184    }
185
186    #
187    # Will the tooltip pop up under the mouse pointer?  If so, then
188    # it will just disappear.  Doh!  We should figure out a better
189    # place to pop it up.
190    #
191    set px [winfo pointerx $hull]
192    set py [winfo pointery $hull]
193    if {$px >= $xpos && $px <= $xpos+[winfo reqwidth $hull]
194          && $py >= $ypos && $py <= $ypos+[winfo reqheight $hull]} {
195
196        if {$px > [winfo screenwidth $hull]/2} {
197            set signx "-"
198            set xpos [expr {[winfo screenwidth $hull]-$px+4}]
199        } else {
200            set signx "+"
201            set xpos [expr {$px+4}]
202        }
203        if {$py > [winfo screenheight $hull]/2} {
204            set signy "-"
205            set ypos [expr {[winfo screenheight $hull]-$py+4}]
206        } else {
207            set signy "+"
208            set ypos [expr {$py+4}]
209        }
210    }
211
212    #
213    # Finally, put it up.
214    #
215    wm geometry $hull $signx$xpos$signy$ypos
216    update
217
218    wm deiconify $hull
219    raise $hull
220
221    #
222    # If logging is enabled, grab the start time.  We'll need this
223    # info later during the "hide" step to log activity.
224    #
225    if {$itk_option(-log) ne ""} {
226        set _showing [clock seconds]
227    }
228}
229
230# ----------------------------------------------------------------------
231# USAGE: hide
232#
233# Takes down the tooltip, if it is showing on the screen.
234# ----------------------------------------------------------------------
235itcl::body Rappture::Tooltip::hide {} {
236    wm withdraw $itk_component(hull)
237
238    #
239    # If logging is enabled and the time is non-zero, then log
240    # the activity on this tooltip.
241    #
242    if {$itk_option(-log) ne "" && $_showing > 0} {
243        set dt [expr {[clock seconds] - $_showing}]
244        if {$dt > 0} {
245            Rappture::Logger::log tooltip -for $itk_option(-log) -time $dt
246        }
247    }
248    set _showing 0
249}
250
251# ----------------------------------------------------------------------
252# USAGE: for <widget> <text> ?-log <name>?
253#
254# Used to register the tooltip <text> for a particular <widget>.
255# This sets up bindings on the widget so that, when the mouse pointer
256# lingers over the widget, the tooltip pops up automatically after
257# a small delay.  When the mouse pointer leaves the widget or the
258# user clicks on the widget, it cancels the tip.
259#
260# If the <text> has the form "@command", then the command is executed
261# just before the tip pops up to build the message on-the-fly.
262#
263# The -log option turns logging on/off for this tooltip.  Logging is
264# useful when you want to keep track of whether the user is looking at
265# tooltips and for how long.  If the <name> is specified, then any
266# activity on the tooltip is reported with that name on the log line.
267# If the name is not specified or "", then the activity is not logged.
268# ----------------------------------------------------------------------
269itcl::body Rappture::Tooltip::for {widget text args} {
270    Rappture::getopts args params {
271        value -log ""
272    }
273    if {[llength $args] > 0} {
274        error "wrong # args: should be \"for widget text ?-log name?\""
275    }
276
277    set catalog($widget-message) $text
278    set catalog($widget-log) $params(-log)
279
280    set btags [bindtags $widget]
281    set i [lsearch $btags RapptureTooltip]
282    if {$i < 0} {
283        set i [lsearch $btags [winfo class $widget]]
284        if {$i < 0} {set i 0}
285        set btags [linsert $btags $i RapptureTooltip]
286        bindtags $widget $btags
287    }
288}
289
290# ----------------------------------------------------------------------
291# USAGE: text <widget> ?<text>? ?-log name?
292#
293# Used to query or set the text used for the tooltip for a widget.
294# This is done automatically when you call the "for" proc, but it
295# is sometimes handy to query or change the text later.
296# ----------------------------------------------------------------------
297itcl::body Rappture::Tooltip::text {widget args} {
298    if {[llength $args] == 0} {
299        if {[info exists catalog($widget-message)]} {
300            return $catalog($widget-message)
301        }
302        return ""
303    }
304
305    # set the text for the tooltip
306    set str [lindex $args 0]
307    set args [lrange $args 1 end]
308
309    Rappture::getopts args params {
310        value -log ""
311    }
312    if {[llength $args] > 0} {
313        error "wrong # args: should be \"text widget ?str? ?-log name?\""
314    }
315
316    set catalog($widget-message) $str
317    set catalog($widget-log) $params(-log)
318}
319
320# ----------------------------------------------------------------------
321# USAGE: tooltip pending <widget> ?@<x>,<y>|+<x>,<y>?
322# USAGE: tooltip show <widget> ?@<x>,<y>|+<x>,<y>?
323# USAGE: tooltip cancel
324#
325# This is invoked automatically whenever the user clicks somewhere
326# inside or outside of the editor.  If the <X>,<Y> coordinate is
327# outside the editor, then we assume the user is done and wants to
328# take the editor down.  Otherwise, we do nothing, and let the entry
329# bindings take over.
330# ----------------------------------------------------------------------
331itcl::body Rappture::Tooltip::tooltip {option args} {
332    switch -- $option {
333        pending {
334            if {[llength $args] < 1 || [llength $args] > 2} {
335                error "wrong # args: should be \"tooltip pending widget ?@x,y?\""
336            }
337            set widget [lindex $args 0]
338            set loc [lindex $args 1]
339
340            if {![info exists catalog($widget-message)]} {
341                return;                        # No tooltip for widget.
342            }
343            if {$pending != ""} {
344                after cancel $pending
345            }
346            set pending [after 750 [itcl::code tooltip show $widget $loc]]
347        }
348        show {
349            if {[llength $args] < 1 || [llength $args] > 2} {
350                error "wrong # args: should be \"tooltip show tag loc\""
351            }
352            set tag [lindex $args 0]
353            set loc [lindex $args 1]
354
355            # tag name may be .g-axis -- get widget ".g" part
356            set widget $tag
357            if {[regexp {^(\.[^-]+)-[^\.]+$} $widget match wname]} {
358                set widget $wname
359            }
360
361            if {[winfo exists $widget] && [info exists catalog($tag-message)]} {
362                .rappturetooltip configure \
363                    -message $catalog($tag-message) \
364                    -log $catalog($tag-log)
365
366                if {[string index $loc 0] == "@"} {
367                    .rappturetooltip show $loc
368                } elseif {[regexp {^[-+]} $loc]} {
369                    .rappturetooltip show $widget$loc
370                } else {
371                    .rappturetooltip show $widget
372                }
373            }
374        }
375        cancel {
376            if {$pending != ""} {
377                after cancel $pending
378                set pending ""
379            }
380            .rappturetooltip hide
381        }
382        default {
383            error "bad option \"$option\": should be show, pending, cancel"
384        }
385    }
386}
387
388# ----------------------------------------------------------------------
389# USAGE: cue <location> <message>
390# USAGE: cue hide
391#
392# Clients use this to show a <message> in a tooltip cue at the
393# specified <location>, which can be a widget name or a root coordinate
394# at @x,y.
395# ----------------------------------------------------------------------
396itcl::body Rappture::Tooltip::cue {option args} {
397    if {"hide" == $option} {
398        grab release .rappturetoolcue
399        .rappturetoolcue hide
400    } elseif {[regexp {^@[0-9]+,[0-9]+$} $option] || [winfo exists $option]} {
401        if {[llength $args] != 1} {
402            error "wrong # args: should be \"cue location message\""
403        }
404        set loc $option
405        set mesg [lindex $args 0]
406
407        .rappturetoolcue configure -message $mesg
408        .rappturetoolcue show $loc
409
410        #
411        # Add a binding to all widgets so that any keypress will
412        # take this cue down.
413        #
414        set cmd [bind all <KeyPress>]
415        if {![regexp {Rappture::Tooltip::cue} $cmd]} {
416            bind all <KeyPress> "+[list ::Rappture::Tooltip::cue hide]"
417            bind all <KeyPress-Return> "+ "
418        }
419
420        #
421        # If nobody has the pointer, then grab it.  Otherwise,
422        # we assume the pop-up editor or someone like that has
423        # the grab, so we don't need to impose a grab here.
424        #
425        if {"" == [grab current]} {
426            update
427            while {[catch {grab set -global .rappturetoolcue}]} {
428                after 100
429            }
430        }
431    } else {
432        error "bad option \"$option\": should be hide, a widget name, or @x,y"
433    }
434}
435
436# ----------------------------------------------------------------------
437# CONFIGURATION OPTION: -icon
438# ----------------------------------------------------------------------
439itcl::configbody Rappture::Tooltip::icon {
440    if {"" == $itk_option(-icon)} {
441        $itk_component(icon) configure -image ""
442        pack forget $itk_component(icon)
443    } else {
444        $itk_component(icon) configure -image $itk_option(-icon)
445        pack $itk_component(icon) -before $itk_component(text) \
446            -side left -fill y
447    }
448}
449
450# ----------------------------------------------------------------------
451# CONFIGURATION OPTION: -outline
452# ----------------------------------------------------------------------
453itcl::configbody Rappture::Tooltip::outline {
454    component hull configure -background $itk_option(-outline)
455}
456
457# ----------------------------------------------------------------------
458# CONFIGURATION OPTION: -log
459# ----------------------------------------------------------------------
460itcl::configbody Rappture::Tooltip::log {
461    # logging options changed -- reset showing time
462    set _showing 0
463}
464
465# create a tooltip widget to show tool tips
466Rappture::Tooltip .rappturetooltip
467
468# any click on any widget takes down the tooltip
469bind all <Leave> [list ::Rappture::Tooltip::tooltip cancel]
470bind all <ButtonPress> [list ::Rappture::Tooltip::tooltip cancel]
471
472# create a tooltip widget to show error cues
473Rappture::Tooltip .rappturetoolcue \
474    -icon [Rappture::icon cue24] \
475    -background black -outline #333333 -foreground white
476
477# when cue is up, it has a grab, and any click brings it down
478bind .rappturetoolcue <ButtonPress> [list ::Rappture::Tooltip::cue hide]
Note: See TracBrowser for help on using the repository browser.