[1] | 1 | # ---------------------------------------------------------------------- |
---|
| 2 | # COMPONENT: tooltip - help information that pops up beneath a widget |
---|
| 3 | # |
---|
| 4 | # This file provides support for tooltips, which are little bits |
---|
| 5 | # of help information that pop up beneath a widget. |
---|
| 6 | # |
---|
| 7 | # Tooltips can be registered for various widgets as follows: |
---|
| 8 | # |
---|
| 9 | # Rappture::Tooltip::for .w "Some help text." |
---|
| 10 | # Rappture::Tooltip::for .x.y "Some more help text." |
---|
| 11 | # |
---|
| 12 | # Tooltips can also be popped up as an error cue beneath a widget |
---|
| 13 | # with bad information: |
---|
| 14 | # |
---|
| 15 | # Rappture::Tooltip::cue .w "Bad data in this widget." |
---|
| 16 | # |
---|
| 17 | # ====================================================================== |
---|
| 18 | # AUTHOR: Michael McLennan, Purdue University |
---|
[11] | 19 | # Copyright (c) 2004-2005 |
---|
| 20 | # Purdue Research Foundation, West Lafayette, IN |
---|
[1] | 21 | # ====================================================================== |
---|
| 22 | package require Itk |
---|
| 23 | |
---|
| 24 | option add *Tooltip.background white widgetDefault |
---|
| 25 | option add *Tooltip.outline black widgetDefault |
---|
| 26 | option add *Tooltip.borderwidth 1 widgetDefault |
---|
| 27 | option add *Tooltip.font -*-helvetica-medium-r-normal-*-*-120-* widgetDefault |
---|
[11] | 28 | option add *Tooltip.wrapLength 4i widgetDefault |
---|
[1] | 29 | |
---|
| 30 | itcl::class Rappture::Tooltip { |
---|
| 31 | inherit itk::Toplevel |
---|
| 32 | |
---|
| 33 | itk_option define -outline outline Outline "" |
---|
| 34 | itk_option define -icon icon Icon "" |
---|
| 35 | itk_option define -message message Message "" |
---|
| 36 | |
---|
| 37 | constructor {args} { # defined below } |
---|
| 38 | |
---|
| 39 | public method show {where} |
---|
| 40 | public method hide {} |
---|
| 41 | |
---|
| 42 | public proc for {widget args} |
---|
[11] | 43 | public proc text {widget args} |
---|
[1] | 44 | private common catalog ;# maps widget => message |
---|
| 45 | |
---|
[11] | 46 | public proc tooltip {option args} |
---|
[1] | 47 | private common pending "" ;# after ID for pending "tooltip show" |
---|
| 48 | |
---|
| 49 | public proc cue {option args} |
---|
| 50 | |
---|
| 51 | bind RapptureTooltip <Enter> \ |
---|
| 52 | [list ::Rappture::Tooltip::tooltip pending %W] |
---|
| 53 | bind RapptureTooltip <Leave> \ |
---|
| 54 | [list ::Rappture::Tooltip::tooltip cancel] |
---|
| 55 | bind RapptureTooltip <ButtonPress> \ |
---|
| 56 | [list ::Rappture::Tooltip::tooltip cancel] |
---|
| 57 | bind RapptureTooltip <KeyPress> \ |
---|
| 58 | [list ::Rappture::Tooltip::tooltip cancel] |
---|
| 59 | |
---|
| 60 | private common icons |
---|
| 61 | set dir [file dirname [info script]] |
---|
| 62 | set icons(cue) [image create photo -file [file join $dir images cue24.gif]] |
---|
| 63 | } |
---|
| 64 | |
---|
| 65 | itk::usual Tooltip { |
---|
| 66 | keep -background -outline -cursor -font |
---|
| 67 | } |
---|
| 68 | |
---|
| 69 | # ---------------------------------------------------------------------- |
---|
| 70 | # CONSTRUCTOR |
---|
| 71 | # ---------------------------------------------------------------------- |
---|
| 72 | itcl::body Rappture::Tooltip::constructor {args} { |
---|
| 73 | wm overrideredirect $itk_component(hull) yes |
---|
| 74 | wm withdraw $itk_component(hull) |
---|
| 75 | |
---|
| 76 | component hull configure -borderwidth 1 -background black |
---|
| 77 | itk_option remove hull.background hull.borderwidth |
---|
| 78 | |
---|
| 79 | itk_component add icon { |
---|
| 80 | label $itk_interior.icon -anchor n |
---|
| 81 | } |
---|
| 82 | |
---|
| 83 | itk_component add text { |
---|
| 84 | label $itk_interior.text -justify left |
---|
| 85 | } { |
---|
| 86 | usual |
---|
| 87 | keep -wraplength |
---|
| 88 | } |
---|
| 89 | pack $itk_component(text) -expand yes -fill both -ipadx 4 -ipady 4 |
---|
| 90 | |
---|
| 91 | eval itk_initialize $args |
---|
| 92 | } |
---|
| 93 | |
---|
| 94 | # ---------------------------------------------------------------------- |
---|
[43] | 95 | # USAGE: show @<x>,<y>|<widget>+/-<x>,<y> |
---|
[1] | 96 | # |
---|
| 97 | # Clients use this to pop up the tooltip on the screen. The position |
---|
[43] | 98 | # should be either a <widget> name with an optional offset +/-<x>,<y> |
---|
[11] | 99 | # (tooltip pops up beneath widget by default), or a specific root |
---|
| 100 | # window coordinate of the form @x,y. |
---|
[1] | 101 | # |
---|
| 102 | # If the -message has the form "@command", then the command is executed |
---|
| 103 | # now, just before the tooltip is popped up, to build the message |
---|
| 104 | # on-the-fly. |
---|
| 105 | # ---------------------------------------------------------------------- |
---|
| 106 | itcl::body Rappture::Tooltip::show {where} { |
---|
[11] | 107 | set hull $itk_component(hull) |
---|
[43] | 108 | set signx "+" |
---|
| 109 | set signy "+" |
---|
[11] | 110 | |
---|
[1] | 111 | if {[regexp {^@([0-9]+),([0-9]+)$} $where match x y]} { |
---|
| 112 | set xpos $x |
---|
| 113 | set ypos $y |
---|
[43] | 114 | } elseif {[regexp {^(.*)([-+])([0-9]+),([-+]?)([0-9]+)$} $where match win signx x signy y]} { |
---|
| 115 | if {$signy == ""} { set signy $signx } |
---|
| 116 | set xpos [expr {[winfo rootx $win] + $x}] |
---|
| 117 | set ypos [expr {[winfo rooty $win] + $y}] |
---|
[1] | 118 | } elseif {[winfo exists $where]} { |
---|
| 119 | set xpos [expr {[winfo rootx $where]+10}] |
---|
| 120 | set ypos [expr {[winfo rooty $where]+[winfo height $where]}] |
---|
| 121 | } else { |
---|
[43] | 122 | error "bad position \"$where\": should be widget+x,y, or @x,y" |
---|
[1] | 123 | } |
---|
| 124 | |
---|
| 125 | if {[string index $itk_option(-message) 0] == "@"} { |
---|
| 126 | set cmd [string range $itk_option(-message) 1 end] |
---|
| 127 | if {[catch $cmd mesg] != 0} { |
---|
| 128 | bgerror $mesg |
---|
| 129 | return |
---|
| 130 | } |
---|
| 131 | } else { |
---|
| 132 | set mesg $itk_option(-message) |
---|
| 133 | } |
---|
| 134 | |
---|
[26] | 135 | # if there's no message to show, forget it |
---|
| 136 | if {[string length $mesg] == 0} { |
---|
| 137 | return |
---|
| 138 | } |
---|
| 139 | |
---|
[11] | 140 | # strings can't be too big, or they'll go off screen! |
---|
| 141 | if {[string length $mesg] > 1000} { |
---|
| 142 | set mesg "[string range $mesg 0 1000]..." |
---|
| 143 | } |
---|
| 144 | set pos 0 |
---|
| 145 | ::for {set i 0} {$pos >= 0 && $i < 5} {incr i} { |
---|
| 146 | incr pos |
---|
| 147 | set pos [string first \n $mesg $pos] |
---|
| 148 | } |
---|
| 149 | if {$pos > 0} { |
---|
| 150 | set mesg "[string range $mesg 0 $pos]..." |
---|
| 151 | } |
---|
[1] | 152 | $itk_component(text) configure -text $mesg |
---|
| 153 | |
---|
[11] | 154 | # |
---|
| 155 | # Make sure the tooltip doesn't go off screen. Then, put it up. |
---|
| 156 | # |
---|
[43] | 157 | update idletasks |
---|
| 158 | if {$signx == "+"} { |
---|
| 159 | if {$xpos+[winfo reqwidth $hull] > [winfo screenwidth $hull]} { |
---|
| 160 | set xpos [expr {[winfo screenwidth $hull]-[winfo reqwidth $hull]}] |
---|
| 161 | } |
---|
| 162 | if {$xpos < 0} { set xpos 0 } |
---|
| 163 | } else { |
---|
| 164 | if {$xpos-[winfo reqwidth $hull] < 0} { |
---|
| 165 | set xpos [expr {[winfo screenwidth $hull]-[winfo reqwidth $hull]}] |
---|
| 166 | } |
---|
| 167 | set xpos [expr {[winfo screenwidth $hull]-$xpos}] |
---|
[11] | 168 | } |
---|
[1] | 169 | |
---|
[43] | 170 | if {$signy == "+"} { |
---|
| 171 | if {$ypos+[winfo reqheight $hull] > [winfo screenheight $hull]} { |
---|
| 172 | set ypos [expr {[winfo screenheight $hull]-[winfo reqheight $hull]}] |
---|
| 173 | } |
---|
| 174 | if {$ypos < 0} { set ypos 0 } |
---|
| 175 | } else { |
---|
| 176 | if {$ypos-[winfo reqheight $hull] < 0} { |
---|
| 177 | set ypos [expr {[winfo screenheight $hull]-[winfo reqheight $hull]}] |
---|
| 178 | } |
---|
| 179 | set ypos [expr {[winfo screenheight $hull]-$ypos}] |
---|
[11] | 180 | } |
---|
| 181 | |
---|
[43] | 182 | wm geometry $hull $signx$xpos$signy$ypos |
---|
[11] | 183 | update |
---|
| 184 | |
---|
| 185 | wm deiconify $hull |
---|
| 186 | raise $hull |
---|
[1] | 187 | } |
---|
| 188 | |
---|
| 189 | # ---------------------------------------------------------------------- |
---|
| 190 | # USAGE: hide |
---|
| 191 | # |
---|
| 192 | # Takes down the tooltip, if it is showing on the screen. |
---|
| 193 | # ---------------------------------------------------------------------- |
---|
| 194 | itcl::body Rappture::Tooltip::hide {} { |
---|
| 195 | wm withdraw $itk_component(hull) |
---|
| 196 | } |
---|
| 197 | |
---|
| 198 | # ---------------------------------------------------------------------- |
---|
| 199 | # USAGE: for <widget> <text> |
---|
| 200 | # |
---|
| 201 | # Used to register the tooltip <text> for a particular <widget>. |
---|
| 202 | # This sets up bindings on the widget so that, when the mouse pointer |
---|
| 203 | # lingers over the widget, the tooltip pops up automatically after |
---|
| 204 | # a small delay. When the mouse pointer leaves the widget or the |
---|
| 205 | # user clicks on the widget, it cancels the tip. |
---|
| 206 | # |
---|
| 207 | # If the <text> has the form "@command", then the command is executed |
---|
| 208 | # just before the tip pops up to build the message on-the-fly. |
---|
| 209 | # ---------------------------------------------------------------------- |
---|
| 210 | itcl::body Rappture::Tooltip::for {widget text} { |
---|
| 211 | set catalog($widget) $text |
---|
| 212 | |
---|
| 213 | set btags [bindtags $widget] |
---|
| 214 | set i [lsearch $btags RapptureTooltip] |
---|
| 215 | if {$i < 0} { |
---|
| 216 | set i [lsearch $btags [winfo class $widget]] |
---|
| 217 | if {$i < 0} {set i 0} |
---|
| 218 | set btags [linsert $btags $i RapptureTooltip] |
---|
| 219 | bindtags $widget $btags |
---|
| 220 | } |
---|
| 221 | } |
---|
| 222 | |
---|
| 223 | # ---------------------------------------------------------------------- |
---|
[11] | 224 | # USAGE: text <widget> ?<text>? |
---|
| 225 | # |
---|
| 226 | # Used to query or set the text used for the tooltip for a widget. |
---|
| 227 | # This is done automatically when you call the "for" proc, but it |
---|
| 228 | # is sometimes handy to query or change the text later. |
---|
| 229 | # ---------------------------------------------------------------------- |
---|
| 230 | itcl::body Rappture::Tooltip::text {widget args} { |
---|
| 231 | if {[llength $args] == 0} { |
---|
| 232 | if {[info exists catalog($widget)]} { |
---|
| 233 | return $catalog($widget) |
---|
| 234 | } |
---|
| 235 | return "" |
---|
| 236 | } elseif {[llength $args] == 1} { |
---|
| 237 | set str [lindex $args 0] |
---|
| 238 | set catalog($widget) $str |
---|
| 239 | } else { |
---|
| 240 | error "wrong # args: should be \"text widget ?str?\"" |
---|
| 241 | } |
---|
| 242 | } |
---|
| 243 | |
---|
| 244 | # ---------------------------------------------------------------------- |
---|
| 245 | # USAGE: tooltip pending <widget> ?@<x>,<y>|+<x>,<y>? |
---|
| 246 | # USAGE: tooltip show <widget> ?@<x>,<y>|+<x>,<y>? |
---|
[1] | 247 | # USAGE: tooltip cancel |
---|
| 248 | # |
---|
| 249 | # This is invoked automatically whenever the user clicks somewhere |
---|
| 250 | # inside or outside of the editor. If the <X>,<Y> coordinate is |
---|
| 251 | # outside the editor, then we assume the user is done and wants to |
---|
| 252 | # take the editor down. Otherwise, we do nothing, and let the entry |
---|
| 253 | # bindings take over. |
---|
| 254 | # ---------------------------------------------------------------------- |
---|
[11] | 255 | itcl::body Rappture::Tooltip::tooltip {option args} { |
---|
[1] | 256 | switch -- $option { |
---|
| 257 | pending { |
---|
[11] | 258 | if {[llength $args] < 1 || [llength $args] > 2} { |
---|
| 259 | error "wrong # args: should be \"tooltip pending widget ?@x,y?\"" |
---|
| 260 | } |
---|
| 261 | set widget [lindex $args 0] |
---|
| 262 | set loc [lindex $args 1] |
---|
| 263 | |
---|
[1] | 264 | if {![info exists catalog($widget)]} { |
---|
| 265 | error "can't find tooltip for $widget" |
---|
| 266 | } |
---|
| 267 | if {$pending != ""} { |
---|
| 268 | after cancel $pending |
---|
| 269 | } |
---|
[11] | 270 | set pending [after 1500 [itcl::code tooltip show $widget $loc]] |
---|
[1] | 271 | } |
---|
| 272 | show { |
---|
[11] | 273 | if {[llength $args] < 1 || [llength $args] > 2} { |
---|
| 274 | error "wrong # args: should be \"tooltip pending widget ?@x,y?\"" |
---|
| 275 | } |
---|
| 276 | set widget [lindex $args 0] |
---|
| 277 | set loc [lindex $args 1] |
---|
| 278 | |
---|
[1] | 279 | if {[winfo exists $widget]} { |
---|
| 280 | .rappturetooltip configure -message $catalog($widget) |
---|
[11] | 281 | if {[string index $loc 0] == "@"} { |
---|
| 282 | .rappturetooltip show $loc |
---|
[43] | 283 | } elseif {[regexp {^[-+]} $loc]} { |
---|
[11] | 284 | .rappturetooltip show $widget$loc |
---|
| 285 | } else { |
---|
| 286 | .rappturetooltip show $widget |
---|
| 287 | } |
---|
[1] | 288 | } |
---|
| 289 | } |
---|
| 290 | cancel { |
---|
| 291 | if {$pending != ""} { |
---|
| 292 | after cancel $pending |
---|
| 293 | set pending "" |
---|
| 294 | } |
---|
| 295 | .rappturetooltip hide |
---|
| 296 | } |
---|
| 297 | default { |
---|
| 298 | error "bad option \"$option\": should be show, pending, cancel" |
---|
| 299 | } |
---|
| 300 | } |
---|
| 301 | } |
---|
| 302 | |
---|
| 303 | # ---------------------------------------------------------------------- |
---|
| 304 | # USAGE: cue <location> <message> |
---|
| 305 | # USAGE: cue hide |
---|
| 306 | # |
---|
| 307 | # Clients use this to show a <message> in a tooltip cue at the |
---|
| 308 | # specified <location>, which can be a widget name or a root coordinate |
---|
| 309 | # at @x,y. |
---|
| 310 | # ---------------------------------------------------------------------- |
---|
| 311 | itcl::body Rappture::Tooltip::cue {option args} { |
---|
| 312 | if {"hide" == $option} { |
---|
| 313 | grab release .rappturetoolcue |
---|
| 314 | .rappturetoolcue hide |
---|
| 315 | } elseif {[regexp {^@[0-9]+,[0-9]+$} $option] || [winfo exists $option]} { |
---|
| 316 | if {[llength $args] != 1} { |
---|
| 317 | error "wrong # args: should be \"cue location message\"" |
---|
| 318 | } |
---|
| 319 | set loc $option |
---|
| 320 | set mesg [lindex $args 0] |
---|
| 321 | |
---|
| 322 | .rappturetoolcue configure -message $mesg |
---|
| 323 | .rappturetoolcue show $loc |
---|
| 324 | |
---|
| 325 | # |
---|
| 326 | # Add a binding to all widgets so that any keypress will |
---|
| 327 | # take this cue down. |
---|
| 328 | # |
---|
| 329 | set cmd [bind all <KeyPress>] |
---|
| 330 | if {![regexp {Rappture::Tooltip::cue} $cmd]} { |
---|
| 331 | bind all <KeyPress> "+[list ::Rappture::Tooltip::cue hide]" |
---|
| 332 | bind all <KeyPress-Return> "+ " |
---|
| 333 | } |
---|
| 334 | |
---|
| 335 | # |
---|
| 336 | # If nobody has the pointer, then grab it. Otherwise, |
---|
| 337 | # we assume the pop-up editor or someone like that has |
---|
| 338 | # the grab, so we don't need to impose a grab here. |
---|
| 339 | # |
---|
| 340 | if {"" == [grab current]} { |
---|
| 341 | update |
---|
| 342 | while {[catch {grab set -global .rappturetoolcue}]} { |
---|
| 343 | after 100 |
---|
| 344 | } |
---|
| 345 | } |
---|
| 346 | } else { |
---|
| 347 | error "bad option \"$option\": should be hide, a widget name, or @x,y" |
---|
| 348 | } |
---|
| 349 | } |
---|
| 350 | |
---|
| 351 | # ---------------------------------------------------------------------- |
---|
| 352 | # CONFIGURATION OPTION: -icon |
---|
| 353 | # ---------------------------------------------------------------------- |
---|
| 354 | itcl::configbody Rappture::Tooltip::icon { |
---|
| 355 | if {"" == $itk_option(-icon)} { |
---|
| 356 | $itk_component(icon) configure -image "" |
---|
| 357 | pack forget $itk_component(icon) |
---|
| 358 | } else { |
---|
| 359 | $itk_component(icon) configure -image $itk_option(-icon) |
---|
| 360 | pack $itk_component(icon) -before $itk_component(text) \ |
---|
| 361 | -side left -fill y |
---|
| 362 | } |
---|
| 363 | } |
---|
| 364 | |
---|
| 365 | # ---------------------------------------------------------------------- |
---|
| 366 | # CONFIGURATION OPTION: -outline |
---|
| 367 | # ---------------------------------------------------------------------- |
---|
| 368 | itcl::configbody Rappture::Tooltip::outline { |
---|
| 369 | component hull configure -background $itk_option(-outline) |
---|
| 370 | } |
---|
| 371 | |
---|
| 372 | # create a tooltip widget to show tool tips |
---|
| 373 | Rappture::Tooltip .rappturetooltip |
---|
| 374 | |
---|
[14] | 375 | # any click on any widget takes down the tooltip |
---|
| 376 | bind all <Leave> [list ::Rappture::Tooltip::tooltip cancel] |
---|
| 377 | bind all <ButtonPress> [list ::Rappture::Tooltip::tooltip cancel] |
---|
| 378 | |
---|
[1] | 379 | # create a tooltip widget to show error cues |
---|
| 380 | Rappture::Tooltip .rappturetoolcue \ |
---|
| 381 | -icon $Rappture::Tooltip::icons(cue) \ |
---|
| 382 | -background black -outline #333333 -foreground white |
---|
| 383 | |
---|
| 384 | # when cue is up, it has a grab, and any click brings it down |
---|
| 385 | bind .rappturetoolcue <ButtonPress> [list ::Rappture::Tooltip::cue hide] |
---|