menu.tcl (38077B)
1 # menu.tcl -- 2 # 3 # This file defines the default bindings for Tk menus and menubuttons. 4 # It also implements keyboard traversal of menus and implements a few 5 # other utility procedures related to menus. 6 # 7 # Copyright (c) 1992-1994 The Regents of the University of California. 8 # Copyright (c) 1994-1997 Sun Microsystems, Inc. 9 # Copyright (c) 1998-1999 by Scriptics Corporation. 10 # Copyright (c) 2007 Daniel A. Steffen <das@users.sourceforge.net> 11 # 12 # See the file "license.terms" for information on usage and redistribution 13 # of this file, and for a DISCLAIMER OF ALL WARRANTIES. 14 # 15 16 #------------------------------------------------------------------------- 17 # Elements of tk::Priv that are used in this file: 18 # 19 # cursor - Saves the -cursor option for the posted menubutton. 20 # focus - Saves the focus during a menu selection operation. 21 # Focus gets restored here when the menu is unposted. 22 # grabGlobal - Used in conjunction with tk::Priv(oldGrab): if 23 # tk::Priv(oldGrab) is non-empty, then tk::Priv(grabGlobal) 24 # contains either an empty string or "-global" to 25 # indicate whether the old grab was a local one or 26 # a global one. 27 # inMenubutton - The name of the menubutton widget containing 28 # the mouse, or an empty string if the mouse is 29 # not over any menubutton. 30 # menuBar - The name of the menubar that is the root 31 # of the cascade hierarchy which is currently 32 # posted. This is null when there is no menu currently 33 # being pulled down from a menu bar. 34 # oldGrab - Window that had the grab before a menu was posted. 35 # Used to restore the grab state after the menu 36 # is unposted. Empty string means there was no 37 # grab previously set. 38 # popup - If a menu has been popped up via tk_popup, this 39 # gives the name of the menu. Otherwise this 40 # value is empty. 41 # postedMb - Name of the menubutton whose menu is currently 42 # posted, or an empty string if nothing is posted 43 # A grab is set on this widget. 44 # relief - Used to save the original relief of the current 45 # menubutton. 46 # window - When the mouse is over a menu, this holds the 47 # name of the menu; it's cleared when the mouse 48 # leaves the menu. 49 # tearoff - Whether the last menu posted was a tearoff or not. 50 # This is true always for unix, for tearoffs for Mac 51 # and Windows. 52 # activeMenu - This is the last active menu for use 53 # with the <<MenuSelect>> virtual event. 54 # activeItem - This is the last active menu item for 55 # use with the <<MenuSelect>> virtual event. 56 #------------------------------------------------------------------------- 57 58 #------------------------------------------------------------------------- 59 # Overall note: 60 # This file is tricky because there are five different ways that menus 61 # can be used: 62 # 63 # 1. As a pulldown from a menubutton. In this style, the variable 64 # tk::Priv(postedMb) identifies the posted menubutton. 65 # 2. As a torn-off menu copied from some other menu. In this style 66 # tk::Priv(postedMb) is empty, and menu's type is "tearoff". 67 # 3. As an option menu, triggered from an option menubutton. In this 68 # style tk::Priv(postedMb) identifies the posted menubutton. 69 # 4. As a popup menu. In this style tk::Priv(postedMb) is empty and 70 # the top-level menu's type is "normal". 71 # 5. As a pulldown from a menubar. The variable tk::Priv(menubar) has 72 # the owning menubar, and the menu itself is of type "normal". 73 # 74 # The various binding procedures use the state described above to 75 # distinguish the various cases and take different actions in each 76 # case. 77 #------------------------------------------------------------------------- 78 79 #------------------------------------------------------------------------- 80 # The code below creates the default class bindings for menus 81 # and menubuttons. 82 #------------------------------------------------------------------------- 83 84 bind Menubutton <FocusIn> {} 85 bind Menubutton <Enter> { 86 tk::MbEnter %W 87 } 88 bind Menubutton <Leave> { 89 tk::MbLeave %W 90 } 91 bind Menubutton <1> { 92 if {$tk::Priv(inMenubutton) ne ""} { 93 tk::MbPost $tk::Priv(inMenubutton) %X %Y 94 } 95 } 96 bind Menubutton <Motion> { 97 tk::MbMotion %W up %X %Y 98 } 99 bind Menubutton <B1-Motion> { 100 tk::MbMotion %W down %X %Y 101 } 102 bind Menubutton <ButtonRelease-1> { 103 tk::MbButtonUp %W 104 } 105 bind Menubutton <space> { 106 tk::MbPost %W 107 tk::MenuFirstEntry [%W cget -menu] 108 } 109 bind Menubutton <<Invoke>> { 110 tk::MbPost %W 111 tk::MenuFirstEntry [%W cget -menu] 112 } 113 114 # Must set focus when mouse enters a menu, in order to allow 115 # mixed-mode processing using both the mouse and the keyboard. 116 # Don't set the focus if the event comes from a grab release, 117 # though: such an event can happen after as part of unposting 118 # a cascaded chain of menus, after the focus has already been 119 # restored to wherever it was before menu selection started. 120 121 bind Menu <FocusIn> {} 122 123 bind Menu <Enter> { 124 set tk::Priv(window) %W 125 if {[%W cget -type] eq "tearoff"} { 126 if {"%m" ne "NotifyUngrab"} { 127 if {[tk windowingsystem] eq "x11"} { 128 tk_menuSetFocus %W 129 } 130 } 131 } 132 tk::MenuMotion %W %x %y %s 133 } 134 135 bind Menu <Leave> { 136 tk::MenuLeave %W %X %Y %s 137 } 138 bind Menu <Motion> { 139 tk::MenuMotion %W %x %y %s 140 } 141 bind Menu <ButtonPress> { 142 tk::MenuButtonDown %W 143 } 144 bind Menu <ButtonRelease> { 145 tk::MenuInvoke %W 1 146 } 147 bind Menu <space> { 148 tk::MenuInvoke %W 0 149 } 150 bind Menu <<Invoke>> { 151 tk::MenuInvoke %W 0 152 } 153 bind Menu <Return> { 154 tk::MenuInvoke %W 0 155 } 156 bind Menu <Escape> { 157 tk::MenuEscape %W 158 } 159 bind Menu <<PrevChar>> { 160 tk::MenuLeftArrow %W 161 } 162 bind Menu <<NextChar>> { 163 tk::MenuRightArrow %W 164 } 165 bind Menu <<PrevLine>> { 166 tk::MenuUpArrow %W 167 } 168 bind Menu <<NextLine>> { 169 tk::MenuDownArrow %W 170 } 171 bind Menu <KeyPress> { 172 tk::TraverseWithinMenu %W %A 173 break 174 } 175 176 # The following bindings apply to all windows, and are used to 177 # implement keyboard menu traversal. 178 179 if {[tk windowingsystem] eq "x11"} { 180 bind all <Alt-KeyPress> { 181 tk::TraverseToMenu %W %A 182 } 183 184 bind all <F10> { 185 tk::FirstMenu %W 186 } 187 } else { 188 bind Menubutton <Alt-KeyPress> { 189 tk::TraverseToMenu %W %A 190 } 191 192 bind Menubutton <F10> { 193 tk::FirstMenu %W 194 } 195 } 196 197 # ::tk::MbEnter -- 198 # This procedure is invoked when the mouse enters a menubutton 199 # widget. It activates the widget unless it is disabled. Note: 200 # this procedure is only invoked when mouse button 1 is *not* down. 201 # The procedure ::tk::MbB1Enter is invoked if the button is down. 202 # 203 # Arguments: 204 # w - The name of the widget. 205 206 proc ::tk::MbEnter w { 207 variable ::tk::Priv 208 209 if {$Priv(inMenubutton) ne ""} { 210 MbLeave $Priv(inMenubutton) 211 } 212 set Priv(inMenubutton) $w 213 if {[$w cget -state] ne "disabled" && [tk windowingsystem] ne "aqua"} { 214 $w configure -state active 215 } 216 } 217 218 # ::tk::MbLeave -- 219 # This procedure is invoked when the mouse leaves a menubutton widget. 220 # It de-activates the widget, if the widget still exists. 221 # 222 # Arguments: 223 # w - The name of the widget. 224 225 proc ::tk::MbLeave w { 226 variable ::tk::Priv 227 228 set Priv(inMenubutton) {} 229 if {![winfo exists $w]} { 230 return 231 } 232 if {[$w cget -state] eq "active" && [tk windowingsystem] ne "aqua"} { 233 $w configure -state normal 234 } 235 } 236 237 # ::tk::MbPost -- 238 # Given a menubutton, this procedure does all the work of posting 239 # its associated menu and unposting any other menu that is currently 240 # posted. 241 # 242 # Arguments: 243 # w - The name of the menubutton widget whose menu 244 # is to be posted. 245 # x, y - Root coordinates of cursor, used for positioning 246 # option menus. If not specified, then the center 247 # of the menubutton is used for an option menu. 248 249 proc ::tk::MbPost {w {x {}} {y {}}} { 250 global errorInfo 251 variable ::tk::Priv 252 253 if {[$w cget -state] eq "disabled" || $w eq $Priv(postedMb)} { 254 return 255 } 256 set menu [$w cget -menu] 257 if {$menu eq ""} { 258 return 259 } 260 set tearoff [expr {[tk windowingsystem] eq "x11" \ 261 || [$menu cget -type] eq "tearoff"}] 262 if {[string first $w $menu] != 0} { 263 return -code error -errorcode {TK MENUBUTTON POST_NONCHILD} \ 264 "can't post $menu: it isn't a descendant of $w" 265 } 266 set cur $Priv(postedMb) 267 if {$cur ne ""} { 268 MenuUnpost {} 269 } 270 if {$::tk_strictMotif} { 271 set Priv(cursor) [$w cget -cursor] 272 $w configure -cursor arrow 273 } 274 if {[tk windowingsystem] ne "aqua"} { 275 set Priv(relief) [$w cget -relief] 276 $w configure -relief raised 277 } else { 278 $w configure -state active 279 } 280 281 set Priv(postedMb) $w 282 set Priv(focus) [focus] 283 $menu activate none 284 GenerateMenuSelect $menu 285 286 # If this looks like an option menubutton then post the menu so 287 # that the current entry is on top of the mouse. Otherwise post 288 # the menu just below the menubutton, as for a pull-down. 289 290 update idletasks 291 if {[catch { 292 switch [$w cget -direction] { 293 above { 294 set x [winfo rootx $w] 295 set y [expr {[winfo rooty $w] - [winfo reqheight $menu]}] 296 # if we go offscreen to the top, show as 'below' 297 if {$y < [winfo vrooty $w]} { 298 set y [expr {[winfo vrooty $w] + [winfo rooty $w] + [winfo reqheight $w]}] 299 } 300 PostOverPoint $menu $x $y 301 } 302 below { 303 set x [winfo rootx $w] 304 set y [expr {[winfo rooty $w] + [winfo height $w]}] 305 # if we go offscreen to the bottom, show as 'above' 306 set mh [winfo reqheight $menu] 307 if {($y + $mh) > ([winfo vrooty $w] + [winfo vrootheight $w])} { 308 set y [expr {[winfo vrooty $w] + [winfo vrootheight $w] + [winfo rooty $w] - $mh}] 309 } 310 PostOverPoint $menu $x $y 311 } 312 left { 313 set x [expr {[winfo rootx $w] - [winfo reqwidth $menu]}] 314 set y [expr {(2 * [winfo rooty $w] + [winfo height $w]) / 2}] 315 set entry [MenuFindName $menu [$w cget -text]] 316 if {$entry eq ""} { 317 set entry 0 318 } 319 if {[$w cget -indicatoron]} { 320 if {$entry == [$menu index last]} { 321 incr y [expr {-([$menu yposition $entry] \ 322 + [winfo reqheight $menu])/2}] 323 } else { 324 incr y [expr {-([$menu yposition $entry] \ 325 + [$menu yposition [expr {$entry+1}]])/2}] 326 } 327 } 328 PostOverPoint $menu $x $y 329 if {$entry ne "" \ 330 && [$menu entrycget $entry -state] ne "disabled"} { 331 $menu activate $entry 332 GenerateMenuSelect $menu 333 } 334 } 335 right { 336 set x [expr {[winfo rootx $w] + [winfo width $w]}] 337 set y [expr {(2 * [winfo rooty $w] + [winfo height $w]) / 2}] 338 set entry [MenuFindName $menu [$w cget -text]] 339 if {$entry eq ""} { 340 set entry 0 341 } 342 if {[$w cget -indicatoron]} { 343 if {$entry == [$menu index last]} { 344 incr y [expr {-([$menu yposition $entry] \ 345 + [winfo reqheight $menu])/2}] 346 } else { 347 incr y [expr {-([$menu yposition $entry] \ 348 + [$menu yposition [expr {$entry+1}]])/2}] 349 } 350 } 351 PostOverPoint $menu $x $y 352 if {$entry ne "" \ 353 && [$menu entrycget $entry -state] ne "disabled"} { 354 $menu activate $entry 355 GenerateMenuSelect $menu 356 } 357 } 358 default { 359 if {[$w cget -indicatoron]} { 360 if {$y eq ""} { 361 set x [expr {[winfo rootx $w] + [winfo width $w]/2}] 362 set y [expr {[winfo rooty $w] + [winfo height $w]/2}] 363 } 364 PostOverPoint $menu $x $y [MenuFindName $menu [$w cget -text]] 365 } else { 366 PostOverPoint $menu [winfo rootx $w] [expr {[winfo rooty $w]+[winfo height $w]}] 367 } 368 } 369 } 370 } msg opt]} { 371 # Error posting menu (e.g. bogus -postcommand). Unpost it and 372 # reflect the error. 373 374 MenuUnpost {} 375 return -options $opt $msg 376 } 377 378 set Priv(tearoff) $tearoff 379 if {$tearoff != 0} { 380 focus $menu 381 if {[winfo viewable $w]} { 382 SaveGrabInfo $w 383 grab -global $w 384 } 385 } 386 } 387 388 # ::tk::MenuUnpost -- 389 # This procedure unposts a given menu, plus all of its ancestors up 390 # to (and including) a menubutton, if any. It also restores various 391 # values to what they were before the menu was posted, and releases 392 # a grab if there's a menubutton involved. Special notes: 393 # 1. It's important to unpost all menus before releasing the grab, so 394 # that any Enter-Leave events (e.g. from menu back to main 395 # application) have mode NotifyGrab. 396 # 2. Be sure to enclose various groups of commands in "catch" so that 397 # the procedure will complete even if the menubutton or the menu 398 # or the grab window has been deleted. 399 # 400 # Arguments: 401 # menu - Name of a menu to unpost. Ignored if there 402 # is a posted menubutton. 403 404 proc ::tk::MenuUnpost menu { 405 variable ::tk::Priv 406 set mb $Priv(postedMb) 407 408 # Restore focus right away (otherwise X will take focus away when 409 # the menu is unmapped and under some window managers (e.g. olvwm) 410 # we'll lose the focus completely). 411 412 catch {focus $Priv(focus)} 413 set Priv(focus) "" 414 415 # Unpost menu(s) and restore some stuff that's dependent on 416 # what was posted. 417 418 after cancel [array get Priv menuActivatedTimer] 419 unset -nocomplain Priv(menuActivated) 420 after cancel [array get Priv menuDeactivatedTimer] 421 unset -nocomplain Priv(menuDeactivated) 422 423 catch { 424 if {$mb ne ""} { 425 set menu [$mb cget -menu] 426 $menu unpost 427 set Priv(postedMb) {} 428 if {$::tk_strictMotif} { 429 $mb configure -cursor $Priv(cursor) 430 } 431 if {[tk windowingsystem] ne "aqua"} { 432 $mb configure -relief $Priv(relief) 433 } else { 434 $mb configure -state normal 435 } 436 } elseif {$Priv(popup) ne ""} { 437 $Priv(popup) unpost 438 set Priv(popup) {} 439 } elseif {[$menu cget -type] ne "menubar" && [$menu cget -type] ne "tearoff"} { 440 # We're in a cascaded sub-menu from a torn-off menu or popup. 441 # Unpost all the menus up to the toplevel one (but not 442 # including the top-level torn-off one) and deactivate the 443 # top-level torn off menu if there is one. 444 445 while {1} { 446 set parent [winfo parent $menu] 447 if {[winfo class $parent] ne "Menu" || ![winfo ismapped $parent]} { 448 break 449 } 450 $parent activate none 451 $parent postcascade none 452 GenerateMenuSelect $parent 453 set type [$parent cget -type] 454 if {$type eq "menubar" || $type eq "tearoff"} { 455 break 456 } 457 set menu $parent 458 } 459 if {[$menu cget -type] ne "menubar"} { 460 $menu unpost 461 } 462 } 463 } 464 465 if {($Priv(tearoff) != 0) || $Priv(menuBar) ne ""} { 466 # Release grab, if any, and restore the previous grab, if there 467 # was one. 468 if {$menu ne ""} { 469 set grab [grab current $menu] 470 if {$grab ne ""} { 471 grab release $grab 472 } 473 } 474 RestoreOldGrab 475 if {$Priv(menuBar) ne ""} { 476 if {$::tk_strictMotif} { 477 $Priv(menuBar) configure -cursor $Priv(cursor) 478 } 479 set Priv(menuBar) {} 480 } 481 if {[tk windowingsystem] ne "x11"} { 482 set Priv(tearoff) 0 483 } 484 } 485 } 486 487 # ::tk::MbMotion -- 488 # This procedure handles mouse motion events inside menubuttons, and 489 # also outside menubuttons when a menubutton has a grab (e.g. when a 490 # menu selection operation is in progress). 491 # 492 # Arguments: 493 # w - The name of the menubutton widget. 494 # upDown - "down" means button 1 is pressed, "up" means 495 # it isn't. 496 # rootx, rooty - Coordinates of mouse, in (virtual?) root window. 497 498 proc ::tk::MbMotion {w upDown rootx rooty} { 499 variable ::tk::Priv 500 501 if {$Priv(inMenubutton) eq $w} { 502 return 503 } 504 set new [winfo containing $rootx $rooty] 505 if {$new ne $Priv(inMenubutton) \ 506 && ($new eq "" || [winfo toplevel $new] eq [winfo toplevel $w])} { 507 if {$Priv(inMenubutton) ne ""} { 508 MbLeave $Priv(inMenubutton) 509 } 510 if {$new ne "" \ 511 && [winfo class $new] eq "Menubutton" \ 512 && ([$new cget -indicatoron] == 0) \ 513 && ([$w cget -indicatoron] == 0)} { 514 if {$upDown eq "down"} { 515 MbPost $new $rootx $rooty 516 } else { 517 MbEnter $new 518 } 519 } 520 } 521 } 522 523 # ::tk::MbButtonUp -- 524 # This procedure is invoked to handle button 1 releases for menubuttons. 525 # If the release happens inside the menubutton then leave its menu 526 # posted with element 0 activated. Otherwise, unpost the menu. 527 # 528 # Arguments: 529 # w - The name of the menubutton widget. 530 531 proc ::tk::MbButtonUp w { 532 variable ::tk::Priv 533 534 set menu [$w cget -menu] 535 set tearoff [expr {[tk windowingsystem] eq "x11" || \ 536 ($menu ne "" && [$menu cget -type] eq "tearoff")}] 537 if {($tearoff != 0) && $Priv(postedMb) eq $w \ 538 && $Priv(inMenubutton) eq $w} { 539 MenuFirstEntry [$Priv(postedMb) cget -menu] 540 } else { 541 MenuUnpost {} 542 } 543 } 544 545 # ::tk::MenuMotion -- 546 # This procedure is called to handle mouse motion events for menus. 547 # It does two things. First, it resets the active element in the 548 # menu, if the mouse is over the menu. Second, if a mouse button 549 # is down, it posts and unposts cascade entries to match the mouse 550 # position. 551 # 552 # Arguments: 553 # menu - The menu window. 554 # x - The x position of the mouse. 555 # y - The y position of the mouse. 556 # state - Modifier state (tells whether buttons are down). 557 558 proc ::tk::MenuMotion {menu x y state} { 559 variable ::tk::Priv 560 if {$menu eq $Priv(window)} { 561 set activeindex [$menu index active] 562 if {[$menu cget -type] eq "menubar"} { 563 if {[info exists Priv(focus)] && $menu ne $Priv(focus)} { 564 $menu activate @$x,$y 565 GenerateMenuSelect $menu 566 } 567 } else { 568 $menu activate @$x,$y 569 GenerateMenuSelect $menu 570 } 571 set index [$menu index @$x,$y] 572 if {[info exists Priv(menuActivated)] \ 573 && $index ne "none" \ 574 && $index ne $activeindex} { 575 set mode [option get $menu clickToFocus ClickToFocus] 576 if {[string is false $mode]} { 577 set delay [expr {[$menu cget -type] eq "menubar" ? 0 : 50}] 578 if {[$menu type $index] eq "cascade"} { 579 set Priv(menuActivatedTimer) \ 580 [after $delay [list $menu postcascade active]] 581 } else { 582 set Priv(menuDeactivatedTimer) \ 583 [after $delay [list $menu postcascade none]] 584 } 585 } 586 } 587 } 588 } 589 590 # ::tk::MenuButtonDown -- 591 # Handles button presses in menus. There are a couple of tricky things 592 # here: 593 # 1. Change the posted cascade entry (if any) to match the mouse position. 594 # 2. If there is a posted menubutton, must grab to the menubutton; this 595 # overrrides the implicit grab on button press, so that the menu 596 # button can track mouse motions over other menubuttons and change 597 # the posted menu. 598 # 3. If there's no posted menubutton (e.g. because we're a torn-off menu 599 # or one of its descendants) must grab to the top-level menu so that 600 # we can track mouse motions across the entire menu hierarchy. 601 # 602 # Arguments: 603 # menu - The menu window. 604 605 proc ::tk::MenuButtonDown menu { 606 variable ::tk::Priv 607 608 if {![winfo viewable $menu]} { 609 return 610 } 611 if {[$menu index active] eq "none"} { 612 if {[$menu cget -type] ne "menubar" } { 613 set Priv(window) {} 614 } 615 return 616 } 617 $menu postcascade active 618 if {$Priv(postedMb) ne "" && [winfo viewable $Priv(postedMb)]} { 619 grab -global $Priv(postedMb) 620 } else { 621 while {[$menu cget -type] eq "normal" \ 622 && [winfo class [winfo parent $menu]] eq "Menu" \ 623 && [winfo ismapped [winfo parent $menu]]} { 624 set menu [winfo parent $menu] 625 } 626 627 if {$Priv(menuBar) eq {}} { 628 set Priv(menuBar) $menu 629 if {$::tk_strictMotif} { 630 set Priv(cursor) [$menu cget -cursor] 631 $menu configure -cursor arrow 632 } 633 if {[$menu type active] eq "cascade"} { 634 set Priv(menuActivated) 1 635 } 636 } 637 638 # Don't update grab information if the grab window isn't changing. 639 # Otherwise, we'll get an error when we unpost the menus and 640 # restore the grab, since the old grab window will not be viewable 641 # anymore. 642 643 if {$menu ne [grab current $menu]} { 644 SaveGrabInfo $menu 645 } 646 647 # Must re-grab even if the grab window hasn't changed, in order 648 # to release the implicit grab from the button press. 649 650 if {[tk windowingsystem] eq "x11"} { 651 grab -global $menu 652 } 653 } 654 } 655 656 # ::tk::MenuLeave -- 657 # This procedure is invoked to handle Leave events for a menu. It 658 # deactivates everything unless the active element is a cascade element 659 # and the mouse is now over the submenu. 660 # 661 # Arguments: 662 # menu - The menu window. 663 # rootx, rooty - Root coordinates of mouse. 664 # state - Modifier state. 665 666 proc ::tk::MenuLeave {menu rootx rooty state} { 667 variable ::tk::Priv 668 set Priv(window) {} 669 if {[$menu index active] eq "none"} { 670 return 671 } 672 if {[$menu type active] eq "cascade" \ 673 && [winfo containing $rootx $rooty] eq \ 674 [$menu entrycget active -menu]} { 675 return 676 } 677 $menu activate none 678 GenerateMenuSelect $menu 679 } 680 681 # ::tk::MenuInvoke -- 682 # This procedure is invoked when button 1 is released over a menu. 683 # It invokes the appropriate menu action and unposts the menu if 684 # it came from a menubutton. 685 # 686 # Arguments: 687 # w - Name of the menu widget. 688 # buttonRelease - 1 means this procedure is called because of 689 # a button release; 0 means because of keystroke. 690 691 proc ::tk::MenuInvoke {w buttonRelease} { 692 variable ::tk::Priv 693 694 if {$buttonRelease && $Priv(window) eq ""} { 695 # Mouse was pressed over a menu without a menu button, then 696 # dragged off the menu (possibly with a cascade posted) and 697 # released. Unpost everything and quit. 698 699 $w postcascade none 700 $w activate none 701 event generate $w <<MenuSelect>> 702 MenuUnpost $w 703 return 704 } 705 if {[$w type active] eq "cascade"} { 706 $w postcascade active 707 set menu [$w entrycget active -menu] 708 MenuFirstEntry $menu 709 } elseif {[$w type active] eq "tearoff"} { 710 ::tk::TearOffMenu $w 711 MenuUnpost $w 712 } elseif {[$w cget -type] eq "menubar"} { 713 $w postcascade none 714 set active [$w index active] 715 set isCascade [string equal [$w type $active] "cascade"] 716 717 # Only de-activate the active item if it's a cascade; this prevents 718 # the annoying "activation flicker" you otherwise get with 719 # checkbuttons/commands/etc. on menubars 720 721 if { $isCascade } { 722 $w activate none 723 event generate $w <<MenuSelect>> 724 } 725 726 MenuUnpost $w 727 728 # If the active item is not a cascade, invoke it. This enables 729 # the use of checkbuttons/commands/etc. on menubars (which is legal, 730 # but not recommended) 731 732 if { !$isCascade } { 733 uplevel #0 [list $w invoke $active] 734 } 735 } else { 736 set active [$w index active] 737 if {$Priv(popup) eq "" || $active ne "none"} { 738 MenuUnpost $w 739 } 740 uplevel #0 [list $w invoke active] 741 } 742 } 743 744 # ::tk::MenuEscape -- 745 # This procedure is invoked for the Cancel (or Escape) key. It unposts 746 # the given menu and, if it is the top-level menu for a menu button, 747 # unposts the menu button as well. 748 # 749 # Arguments: 750 # menu - Name of the menu window. 751 752 proc ::tk::MenuEscape menu { 753 set parent [winfo parent $menu] 754 if {[winfo class $parent] ne "Menu"} { 755 MenuUnpost $menu 756 } elseif {[$parent cget -type] eq "menubar"} { 757 MenuUnpost $menu 758 RestoreOldGrab 759 } else { 760 MenuNextMenu $menu left 761 } 762 } 763 764 # The following routines handle arrow keys. Arrow keys behave 765 # differently depending on whether the menu is a menu bar or not. 766 767 proc ::tk::MenuUpArrow {menu} { 768 if {[$menu cget -type] eq "menubar"} { 769 MenuNextMenu $menu left 770 } else { 771 MenuNextEntry $menu -1 772 } 773 } 774 775 proc ::tk::MenuDownArrow {menu} { 776 if {[$menu cget -type] eq "menubar"} { 777 MenuNextMenu $menu right 778 } else { 779 MenuNextEntry $menu 1 780 } 781 } 782 783 proc ::tk::MenuLeftArrow {menu} { 784 if {[$menu cget -type] eq "menubar"} { 785 MenuNextEntry $menu -1 786 } else { 787 MenuNextMenu $menu left 788 } 789 } 790 791 proc ::tk::MenuRightArrow {menu} { 792 if {[$menu cget -type] eq "menubar"} { 793 MenuNextEntry $menu 1 794 } else { 795 MenuNextMenu $menu right 796 } 797 } 798 799 # ::tk::MenuNextMenu -- 800 # This procedure is invoked to handle "left" and "right" traversal 801 # motions in menus. It traverses to the next menu in a menu bar, 802 # or into or out of a cascaded menu. 803 # 804 # Arguments: 805 # menu - The menu that received the keyboard 806 # event. 807 # direction - Direction in which to move: "left" or "right" 808 809 proc ::tk::MenuNextMenu {menu direction} { 810 variable ::tk::Priv 811 812 # First handle traversals into and out of cascaded menus. 813 814 if {$direction eq "right"} { 815 set count 1 816 set parent [winfo parent $menu] 817 set class [winfo class $parent] 818 if {[$menu type active] eq "cascade"} { 819 $menu postcascade active 820 set m2 [$menu entrycget active -menu] 821 if {$m2 ne ""} { 822 MenuFirstEntry $m2 823 } 824 return 825 } else { 826 set parent [winfo parent $menu] 827 while {$parent ne "."} { 828 if {[winfo class $parent] eq "Menu" \ 829 && [$parent cget -type] eq "menubar"} { 830 tk_menuSetFocus $parent 831 MenuNextEntry $parent 1 832 return 833 } 834 set parent [winfo parent $parent] 835 } 836 } 837 } else { 838 set count -1 839 set m2 [winfo parent $menu] 840 if {[winfo class $m2] eq "Menu"} { 841 $menu activate none 842 GenerateMenuSelect $menu 843 tk_menuSetFocus $m2 844 845 $m2 postcascade none 846 847 if {[$m2 cget -type] ne "menubar"} { 848 return 849 } 850 } 851 } 852 853 # Can't traverse into or out of a cascaded menu. Go to the next 854 # or previous menubutton, if that makes sense. 855 856 set m2 [winfo parent $menu] 857 if {[winfo class $m2] eq "Menu" && [$m2 cget -type] eq "menubar"} { 858 tk_menuSetFocus $m2 859 MenuNextEntry $m2 -1 860 return 861 } 862 863 set w $Priv(postedMb) 864 if {$w eq ""} { 865 return 866 } 867 set buttons [winfo children [winfo parent $w]] 868 set length [llength $buttons] 869 set i [expr {[lsearch -exact $buttons $w] + $count}] 870 while {1} { 871 while {$i < 0} { 872 incr i $length 873 } 874 while {$i >= $length} { 875 incr i -$length 876 } 877 set mb [lindex $buttons $i] 878 if {[winfo class $mb] eq "Menubutton" \ 879 && [$mb cget -state] ne "disabled" \ 880 && [$mb cget -menu] ne "" \ 881 && [[$mb cget -menu] index last] ne "none"} { 882 break 883 } 884 if {$mb eq $w} { 885 return 886 } 887 incr i $count 888 } 889 MbPost $mb 890 MenuFirstEntry [$mb cget -menu] 891 } 892 893 # ::tk::MenuNextEntry -- 894 # Activate the next higher or lower entry in the posted menu, 895 # wrapping around at the ends. Disabled entries are skipped. 896 # 897 # Arguments: 898 # menu - Menu window that received the keystroke. 899 # count - 1 means go to the next lower entry, 900 # -1 means go to the next higher entry. 901 902 proc ::tk::MenuNextEntry {menu count} { 903 if {[$menu index last] eq "none"} { 904 return 905 } 906 set length [expr {[$menu index last]+1}] 907 set quitAfter $length 908 set active [$menu index active] 909 if {$active eq "none"} { 910 set i 0 911 } else { 912 set i [expr {$active + $count}] 913 } 914 while {1} { 915 if {$quitAfter <= 0} { 916 # We've tried every entry in the menu. Either there are 917 # none, or they're all disabled. Just give up. 918 919 return 920 } 921 while {$i < 0} { 922 incr i $length 923 } 924 while {$i >= $length} { 925 incr i -$length 926 } 927 if {[catch {$menu entrycget $i -state} state] == 0} { 928 if {$state ne "disabled" && \ 929 ($i!=0 || [$menu cget -type] ne "tearoff" \ 930 || [$menu type 0] ne "tearoff")} { 931 break 932 } 933 } 934 if {$i == $active} { 935 return 936 } 937 incr i $count 938 incr quitAfter -1 939 } 940 $menu activate $i 941 GenerateMenuSelect $menu 942 943 if {[$menu type $i] eq "cascade" && [$menu cget -type] eq "menubar"} { 944 set cascade [$menu entrycget $i -menu] 945 if {$cascade ne ""} { 946 # Here we auto-post a cascade. This is necessary when 947 # we traverse left/right in the menubar, but undesirable when 948 # we traverse up/down in a menu. 949 $menu postcascade $i 950 MenuFirstEntry $cascade 951 } 952 } 953 } 954 955 # ::tk::MenuFind -- 956 # This procedure searches the entire window hierarchy under w for 957 # a menubutton that isn't disabled and whose underlined character 958 # is "char" or an entry in a menubar that isn't disabled and whose 959 # underlined character is "char". 960 # It returns the name of that window, if found, or an 961 # empty string if no matching window was found. If "char" is an 962 # empty string then the procedure returns the name of the first 963 # menubutton found that isn't disabled. 964 # 965 # Arguments: 966 # w - Name of window where key was typed. 967 # char - Underlined character to search for; 968 # may be either upper or lower case, and 969 # will match either upper or lower case. 970 971 proc ::tk::MenuFind {w char} { 972 set char [string tolower $char] 973 set windowlist [winfo child $w] 974 975 foreach child $windowlist { 976 # Don't descend into other toplevels. 977 if {[winfo toplevel $w] ne [winfo toplevel $child]} { 978 continue 979 } 980 if {[winfo class $child] eq "Menu" && \ 981 [$child cget -type] eq "menubar"} { 982 if {$char eq ""} { 983 return $child 984 } 985 set last [$child index last] 986 for {set i [$child cget -tearoff]} {$i <= $last} {incr i} { 987 if {[$child type $i] eq "separator"} { 988 continue 989 } 990 set char2 [string index [$child entrycget $i -label] \ 991 [$child entrycget $i -underline]] 992 if {$char eq [string tolower $char2] || $char eq ""} { 993 if {[$child entrycget $i -state] ne "disabled"} { 994 return $child 995 } 996 } 997 } 998 } 999 } 1000 1001 foreach child $windowlist { 1002 # Don't descend into other toplevels. 1003 if {[winfo toplevel $w] ne [winfo toplevel $child]} { 1004 continue 1005 } 1006 switch -- [winfo class $child] { 1007 Menubutton { 1008 set char2 [string index [$child cget -text] \ 1009 [$child cget -underline]] 1010 if {$char eq [string tolower $char2] || $char eq ""} { 1011 if {[$child cget -state] ne "disabled"} { 1012 return $child 1013 } 1014 } 1015 } 1016 1017 default { 1018 set match [MenuFind $child $char] 1019 if {$match ne ""} { 1020 return $match 1021 } 1022 } 1023 } 1024 } 1025 return {} 1026 } 1027 1028 # ::tk::TraverseToMenu -- 1029 # This procedure implements keyboard traversal of menus. Given an 1030 # ASCII character "char", it looks for a menubutton with that character 1031 # underlined. If one is found, it posts the menubutton's menu 1032 # 1033 # Arguments: 1034 # w - Window in which the key was typed (selects 1035 # a toplevel window). 1036 # char - Character that selects a menu. The case 1037 # is ignored. If an empty string, nothing 1038 # happens. 1039 1040 proc ::tk::TraverseToMenu {w char} { 1041 variable ::tk::Priv 1042 if {![winfo exists $w] || $char eq ""} { 1043 return 1044 } 1045 while {[winfo class $w] eq "Menu"} { 1046 if {[$w cget -type] eq "menubar"} { 1047 break 1048 } elseif {$Priv(postedMb) eq ""} { 1049 return 1050 } 1051 set w [winfo parent $w] 1052 } 1053 set w [MenuFind [winfo toplevel $w] $char] 1054 if {$w ne ""} { 1055 if {[winfo class $w] eq "Menu"} { 1056 tk_menuSetFocus $w 1057 set Priv(window) $w 1058 SaveGrabInfo $w 1059 grab -global $w 1060 TraverseWithinMenu $w $char 1061 } else { 1062 MbPost $w 1063 MenuFirstEntry [$w cget -menu] 1064 } 1065 } 1066 } 1067 1068 # ::tk::FirstMenu -- 1069 # This procedure traverses to the first menubutton in the toplevel 1070 # for a given window, and posts that menubutton's menu. 1071 # 1072 # Arguments: 1073 # w - Name of a window. Selects which toplevel 1074 # to search for menubuttons. 1075 1076 proc ::tk::FirstMenu w { 1077 variable ::tk::Priv 1078 set w [MenuFind [winfo toplevel $w] ""] 1079 if {$w ne ""} { 1080 if {[winfo class $w] eq "Menu"} { 1081 tk_menuSetFocus $w 1082 set Priv(window) $w 1083 SaveGrabInfo $w 1084 grab -global $w 1085 MenuFirstEntry $w 1086 } else { 1087 MbPost $w 1088 MenuFirstEntry [$w cget -menu] 1089 } 1090 } 1091 } 1092 1093 # ::tk::TraverseWithinMenu 1094 # This procedure implements keyboard traversal within a menu. It 1095 # searches for an entry in the menu that has "char" underlined. If 1096 # such an entry is found, it is invoked and the menu is unposted. 1097 # 1098 # Arguments: 1099 # w - The name of the menu widget. 1100 # char - The character to look for; case is 1101 # ignored. If the string is empty then 1102 # nothing happens. 1103 1104 proc ::tk::TraverseWithinMenu {w char} { 1105 if {$char eq ""} { 1106 return 1107 } 1108 set char [string tolower $char] 1109 set last [$w index last] 1110 if {$last eq "none"} { 1111 return 1112 } 1113 for {set i 0} {$i <= $last} {incr i} { 1114 if {[catch {set char2 [string index \ 1115 [$w entrycget $i -label] [$w entrycget $i -underline]]}]} { 1116 continue 1117 } 1118 if {$char eq [string tolower $char2]} { 1119 if {[$w type $i] eq "cascade"} { 1120 $w activate $i 1121 $w postcascade active 1122 event generate $w <<MenuSelect>> 1123 set m2 [$w entrycget $i -menu] 1124 if {$m2 ne ""} { 1125 MenuFirstEntry $m2 1126 } 1127 } else { 1128 MenuUnpost $w 1129 uplevel #0 [list $w invoke $i] 1130 } 1131 return 1132 } 1133 } 1134 } 1135 1136 # ::tk::MenuFirstEntry -- 1137 # Given a menu, this procedure finds the first entry that isn't 1138 # disabled or a tear-off or separator, and activates that entry. 1139 # However, if there is already an active entry in the menu (e.g., 1140 # because of a previous call to tk::PostOverPoint) then the active 1141 # entry isn't changed. This procedure also sets the input focus 1142 # to the menu. 1143 # 1144 # Arguments: 1145 # menu - Name of the menu window (possibly empty). 1146 1147 proc ::tk::MenuFirstEntry menu { 1148 if {$menu eq ""} { 1149 return 1150 } 1151 tk_menuSetFocus $menu 1152 if {[$menu index active] ne "none"} { 1153 return 1154 } 1155 set last [$menu index last] 1156 if {$last eq "none"} { 1157 return 1158 } 1159 for {set i 0} {$i <= $last} {incr i} { 1160 if {([catch {set state [$menu entrycget $i -state]}] == 0) \ 1161 && $state ne "disabled" && [$menu type $i] ne "tearoff"} { 1162 $menu activate $i 1163 GenerateMenuSelect $menu 1164 # Only post the cascade if the current menu is a menubar; 1165 # otherwise, if the first entry of the cascade is a cascade, 1166 # we can get an annoying cascading effect resulting in a bunch of 1167 # menus getting posted (bug 676) 1168 if {[$menu type $i] eq "cascade" && [$menu cget -type] eq "menubar"} { 1169 set cascade [$menu entrycget $i -menu] 1170 if {$cascade ne ""} { 1171 $menu postcascade $i 1172 MenuFirstEntry $cascade 1173 } 1174 } 1175 return 1176 } 1177 } 1178 } 1179 1180 # ::tk::MenuFindName -- 1181 # Given a menu and a text string, return the index of the menu entry 1182 # that displays the string as its label. If there is no such entry, 1183 # return an empty string. This procedure is tricky because some names 1184 # like "active" have a special meaning in menu commands, so we can't 1185 # always use the "index" widget command. 1186 # 1187 # Arguments: 1188 # menu - Name of the menu widget. 1189 # s - String to look for. 1190 1191 proc ::tk::MenuFindName {menu s} { 1192 set i "" 1193 if {![regexp {^active$|^last$|^none$|^[0-9]|^@} $s]} { 1194 catch {set i [$menu index $s]} 1195 return $i 1196 } 1197 set last [$menu index last] 1198 if {$last eq "none"} { 1199 return 1200 } 1201 for {set i 0} {$i <= $last} {incr i} { 1202 if {![catch {$menu entrycget $i -label} label]} { 1203 if {$label eq $s} { 1204 return $i 1205 } 1206 } 1207 } 1208 return "" 1209 } 1210 1211 # ::tk::PostOverPoint -- 1212 # This procedure posts a given menu such that a given entry in the 1213 # menu is centered over a given point in the root window. It also 1214 # activates the given entry. 1215 # 1216 # Arguments: 1217 # menu - Menu to post. 1218 # x, y - Root coordinates of point. 1219 # entry - Index of entry within menu to center over (x,y). 1220 # If omitted or specified as {}, then the menu's 1221 # upper-left corner goes at (x,y). 1222 1223 proc ::tk::PostOverPoint {menu x y {entry {}}} { 1224 if {$entry ne ""} { 1225 if {$entry == [$menu index last]} { 1226 incr y [expr {-([$menu yposition $entry] \ 1227 + [winfo reqheight $menu])/2}] 1228 } else { 1229 incr y [expr {-([$menu yposition $entry] \ 1230 + [$menu yposition [expr {$entry+1}]])/2}] 1231 } 1232 incr x [expr {-[winfo reqwidth $menu]/2}] 1233 } 1234 1235 if {[tk windowingsystem] eq "win32"} { 1236 # osVersion is not available in safe interps 1237 set ver 5 1238 if {[info exists ::tcl_platform(osVersion)]} { 1239 scan $::tcl_platform(osVersion) %d ver 1240 } 1241 1242 # We need to fix some problems with menu posting on Windows, 1243 # where, if the menu would overlap top or bottom of screen, 1244 # Windows puts it in the wrong place for us. We must also 1245 # subtract an extra amount for half the height of the current 1246 # entry. To be safe we subtract an extra 10. 1247 # NOTE: this issue appears to have been resolved in the Window 1248 # manager provided with Vista and Windows 7. 1249 if {$ver < 6} { 1250 set yoffset [expr {[winfo screenheight $menu] \ 1251 - $y - [winfo reqheight $menu] - 10}] 1252 if {$yoffset < [winfo vrooty $menu]} { 1253 # The bottom of the menu is offscreen, so adjust upwards 1254 incr y [expr {$yoffset - [winfo vrooty $menu]}] 1255 } 1256 # If we're off the top of the screen (either because we were 1257 # originally or because we just adjusted too far upwards), 1258 # then make the menu popup on the top edge. 1259 if {$y < [winfo vrooty $menu]} { 1260 set y [winfo vrooty $menu] 1261 } 1262 } 1263 } 1264 $menu post $x $y 1265 if {$entry ne "" && [$menu entrycget $entry -state] ne "disabled"} { 1266 $menu activate $entry 1267 GenerateMenuSelect $menu 1268 } 1269 } 1270 1271 # ::tk::SaveGrabInfo -- 1272 # Sets the variables tk::Priv(oldGrab) and tk::Priv(grabStatus) to record 1273 # the state of any existing grab on the w's display. 1274 # 1275 # Arguments: 1276 # w - Name of a window; used to select the display 1277 # whose grab information is to be recorded. 1278 1279 proc tk::SaveGrabInfo w { 1280 variable ::tk::Priv 1281 set Priv(oldGrab) [grab current $w] 1282 if {$Priv(oldGrab) ne ""} { 1283 set Priv(grabStatus) [grab status $Priv(oldGrab)] 1284 } 1285 } 1286 1287 # ::tk::RestoreOldGrab -- 1288 # Restores the grab to what it was before TkSaveGrabInfo was called. 1289 # 1290 1291 proc ::tk::RestoreOldGrab {} { 1292 variable ::tk::Priv 1293 1294 if {$Priv(oldGrab) ne ""} { 1295 # Be careful restoring the old grab, since it's window may not 1296 # be visible anymore. 1297 1298 catch { 1299 if {$Priv(grabStatus) eq "global"} { 1300 grab set -global $Priv(oldGrab) 1301 } else { 1302 grab set $Priv(oldGrab) 1303 } 1304 } 1305 set Priv(oldGrab) "" 1306 } 1307 } 1308 1309 proc ::tk_menuSetFocus {menu} { 1310 variable ::tk::Priv 1311 if {![info exists Priv(focus)] || $Priv(focus) eq ""} { 1312 set Priv(focus) [focus] 1313 } 1314 focus $menu 1315 } 1316 1317 proc ::tk::GenerateMenuSelect {menu} { 1318 variable ::tk::Priv 1319 1320 if {$Priv(activeMenu) eq $menu \ 1321 && $Priv(activeItem) eq [$menu index active]} { 1322 return 1323 } 1324 1325 set Priv(activeMenu) $menu 1326 set Priv(activeItem) [$menu index active] 1327 event generate $menu <<MenuSelect>> 1328 } 1329 1330 # ::tk_popup -- 1331 # This procedure pops up a menu and sets things up for traversing 1332 # the menu and its submenus. 1333 # 1334 # Arguments: 1335 # menu - Name of the menu to be popped up. 1336 # x, y - Root coordinates at which to pop up the 1337 # menu. 1338 # entry - Index of a menu entry to center over (x,y). 1339 # If omitted or specified as {}, then menu's 1340 # upper-left corner goes at (x,y). 1341 1342 proc ::tk_popup {menu x y {entry {}}} { 1343 variable ::tk::Priv 1344 if {$Priv(popup) ne "" || $Priv(postedMb) ne ""} { 1345 tk::MenuUnpost {} 1346 } 1347 tk::PostOverPoint $menu $x $y $entry 1348 if {[tk windowingsystem] eq "x11" && [winfo viewable $menu]} { 1349 tk::SaveGrabInfo $menu 1350 grab -global $menu 1351 set Priv(popup) $menu 1352 set Priv(window) $menu 1353 set Priv(menuActivated) 1 1354 tk_menuSetFocus $menu 1355 } 1356 }