TearoffTabBar -Notebook Style Access to Floating Palettes

WJG (17 January 2005)

Notebook widgets provide a quick and easily way to switch between forms. But, what if we want that sort visual style of access to windows, rather than pages?

As I don't have to get my daughter from school yet and I don't start work till tonight and the missus has gone out for lunch with her mates and.. Well, the truth is that I don't want to prune our trees on such a cold and damp day. So, I've spent the morning putting the bits together for such a widget. I'm sure that, indeed I hope that, other Tickler's out there will play-n-hack with this code. Next step, modify the palette window creation proc to produce a floating roll-up palette (See [L1 ]).


WJG (21 January 2005) Now the tabs can be dragged and repositioned along along the container. Raised tab can also be highlighted. Double-B1 click the main body of the to post a popup-menu/palette, Double-1 click the tab icon to post a floating palettte. When a floating palette is posted, the tab will be hidden. Close the palette and the tab reappears.


(PBO 26 aug 2005) Fixed a bug related to withdrawing a previous tab that was detached. Now tabs (and detached windows) are no more lost when you detach several of them (when clicking red boxes).


 ############################################
 #
 # tearofftabbar.tcl
 # ------------------------
 #
 # Copyright (C) 2005 William J Giddings
 # email: [email protected]
 #
 ############################################
 #
 # Description:
 # -----------
 # Provide a horizontal notebook tab-bar megawidget that allows
 # floating palettes to appear rather than book pages. Conceptually
 # this is similar to a standard menu button but the author
 # is now able to embed other resources into the palette.
 #
 # Creation:
 # --------
 # TearoffTabBar pathName ?option value...?
 #
 # Standard Options:
 # ----------------
 # -relief            (default = flat)
 # -borderwidth       (default = 0)
 #
 # Widget Specific Options:
 # -----------------------
 # -lbevel             Tab left bevel (default = 2)
 # -rbevel             Tab right bevel (default = 2)
 # -font               Tab font (default = Ariel 10)
 # -height             Set base tab height (default = 20)
 # -width              Set base tab width (default = 75)
 # -shift              Set rise/drop of tags (default = +/-3)
 #
 # Returns:
 # --------
 # Pathname of the tabbar container.
 #
 # Widget Commands:
 # --------
 # pathName add        Add new tab to the bar. Returns pathname of the palette created.
 # pathName raisetab   Raise tab to topmost position.
 # pathName lowertab   Lowet tab.
 #
 # Bindings:
 # -----------------------------------#
 # Tab                 Button-1    Raise tab and show palette. Lower previously selected tab.
 # Palette             Focus-In    Raise associated tab.
 #
 # Example:
 # -------
 # This module includes a demo proceedure. Delete and/or comment out as required.
 #
 # Note:
 # ----
 # Work still in progress.
 #
 # Future enhancements:
 # -------------------
 # 1) Improve/complete this notes section.
 # 2) Combine with floating-palettes.
 # 4) Means of tracking available palettes per tabbar widget.
 #
 ############################################

 package require Tk

 namespace eval TearoffTabBar {}

 proc TearoffTabBar { pathName args} {
     #-------
     # create private namespace and set defaults
     #-------
     namespace eval ${pathName} {
         set height 20
         set width 75
         set relief flat
         set borderwidth 0
         set font {Ariel 10}
         set tabbg SystemButtonFace
         set tabhighlight #ddddcc
         set lbevel 2          ;# tab drawing parameter
         set rbevel 15
     set shift 3
     set image ""
     set lastx 0           ;# mouse inpt
     set lasty 0
     set lasttab ""        ;# hide/lower previous items
     set tabmin_x ""        ;# used in testing during dragging the tab in the tabbar
     set tabmax_x ""
 }
 #-------
 # parse args
 #-------
 foreach {arg val} $args {
     puts "$arg : $val"
     switch -- $arg {
         -height -
         -relief -
         -borderwidth -
         -lbevel -
         -rbevel -
         -font { set ${pathName}::[string trimleft $arg -] $val }
     }
 }
 #-------
 # create container
 #-------
 canvas $pathName \
     -height [set ${pathName}::height] \
     -relief [set ${pathName}::relief] \
     -borderwidth [set ${pathName}::borderwidth]
 #-------
 # Here comes the overloaded widget proc:
 #-------
 rename $pathName _$pathName      ;# keep the original widget command
 proc $pathName {cmd args} {
     set self [lindex [info level 0] 0] ;# get name I was called with
     switch -- $cmd {
         add          {eval TearoffTabBar::add $self $args}
         raisetab     {eval TearoffTabBar::raisetab $self $args}
         lowertab     {eval TearoffTabBar::lowertab $self $args}
         default      {uplevel 1 _$self $cmd $args}
     }
 }
 return $pathName
 }
 #-------
 # add new items to the bar
 #-------
 # currently creating tab at disired location, this perhaps causing problems
 # create tab, then move to xpos

 proc TearoffTabBar::add {pathName args} {
 #-------
 # set some defaults
 #-------
 set xpos 1
 set height [set ${pathName}::height]
 set width 75
 set title "NEW"
 set font {Ariel 10}
 set tabbg SystemButtonFace
 set image ""
 set palettewidth 100
 set paletteheight 150
 #-------
 # parse args
 #-------
 foreach {arg val} $args {
     switch -- $arg {
         -tag -
         -xpos -
         -height -
         -width -
         -title -
         -image -
         -font  { set [string trimleft $arg -] $val}
         -tabbg -
         -palettewidth -
         -paletteheight -
         -tabbackground { set tabbg $val}
     }
 }
 #-------
 # add local vars, make this easier to read
 #-------
 set lbevel [set ${pathName}::lbevel]
 set rbevel [set ${pathName}::rbevel]
 #-------
 # draw the tab
 #-------
 set tmp $xpos
 set xpos 0
 # 1) background polygon
 $pathName create polygon \
     0 $height 0 $lbevel \
     0 $lbevel $lbevel 0 \
     $lbevel 0 [expr $width - $rbevel] 0 \
     [expr $width - $rbevel] 0 $width $rbevel \
     $width $height  \
     -fill  $tabbg \
     -outline $tabbg \
     -tag "$tag $tag.tab"
 #2) tab outline
 #2a) left line
 #
 #|
 #|
 #|
 $pathName create line \
     $xpos $height \
     $xpos $lbevel \
     -fill white \
     -tag $tag
 #2b) left bevel
 #/
 #|
 #|
 #|
 $pathName create line \
     $xpos $lbevel \
     [expr $xpos + $lbevel] 0 \
     -fill white \
     -tag $tag
 #2c) top line
 #/-------------
 #|
 #|
 #|
 $pathName create line \
     [expr $xpos + $lbevel] 0  \
     [expr $xpos + $width - $rbevel] 0 \
     -fill white \
     -tag $tag
 #2d) right bevel
 #/-------------\
 #|              \
 #|               \
 #|
 $pathName create line \
     [expr $xpos + $width - $rbevel] 0 \
     [expr $xpos + $width] $rbevel \
     -fill #888888 \
     -tag $tag
 #2e) right line
 #/-------------\
 #|              \
 #|               \
 #|                |
 $pathName create line \
         [expr $xpos + $width] $rbevel \
         [expr $xpos + $width] $height  \
         -fill #888888 \
         -tag $tag
 #3) add icon
 if {$image != "" } { \
     $pathName create image \
         [expr $xpos + 4] 11 \
         -image $image \
         -anchor w \
         -tag "$tag $tag.image"
 }
 # 4) add text
 $pathName create text \
     [expr $xpos + 22] 11 \
     -text $title \
     -anchor w \
     -font $font \
     -tag "$tag $tag.text"
 #---------
 # shuffle the tabs down
 # --------
 $pathName move $tag $tmp 2
 #---------
 # add bindings
 #---------
 #-------
 # select & raise tab
 #-------
 $pathName bind $tag <ButtonPress-1> {
    set tags [lindex [%W gettags current] 0]
    #hide previous palette
    if { [set %W::lasttab] != "" } {
         withdrawIfNeeded [string tolower .[set %W::lasttab]]
    }
    %W raise $tags
    %W raisetab $tags
    set %W::lasttab $tags
    set %W::lastx %x
    set %W::lasty %y
    update idletasks
    set %W::x %x
    #minmax %W
    #puts "$tags %x min [set %W::tabminx] max [set %W::tabmaxx]"
 }
 #-------
 # show palette
 #-------
 $pathName bind $tag <Double-ButtonPress-1> {
    #hide previous palette
    if { [set %W::lasttab] != "" } {
         withdrawIfNeeded [string tolower .[set %W::lasttab]]
    }
    %W raise [set %W::lasttab]
    %W raisetab [set %W::lasttab]
    TearoffTabBar::_placepalette %W
 }
 #-------
 # show palette, allow movement
 #-------
 $pathName bind $tag.image <Double-ButtonPress-1> {
     #hide previous palette
     if { [set %W::lasttab] != "" } {
         withdrawIfNeeded [string tolower .[set %W::lasttab]]
     }
     %W raise [set %W::lasttab]
     %W raisetab [set %W::lasttab]
     TearoffTabBar::_placepalette %W
     wm overrideredirect [string tolower .[set %W::lasttab]] 0
     [string tolower .[set %W::lasttab]].fra config -relief flat
     focus -force [string tolower .[set %W::lasttab]]
     %W move [set %W::lasttab] 0 20
 }

 #---------
 # drag tab to different location
 #---------
  $pathName bind $tag <Button1-Motion> {
      set tags [lindex [%W gettags current] 0]
      drag.canvas.item %W $tags %x -1
      #test to see if torn-ff
 }
 #-------
 # show palette
 #-------
 # $pathName bind $tag <ButtonRelease-1> {
    # wm deiconify [string tolower .[set %W::lasttab]]
    # #TearoffTabBar::_placepalette %W
    # set %W::lastx %x
    # set %W::lasty %y
 # }
 #-------
 # create palette
 #-------
 set title [string tolower $title]
 toplevel .$title
 wm transient .$title .
 wm title .$title "Palette: $title"
 wm protocol .$title WM_DELETE_WINDOW \
         "wm overrideredirect .$title 1
         wm withdraw .$title
         .$title.fra config -relief raised
         $pathName move [string totitle $title] 0 -20
         "
 wm withdraw .$title
 wm geometry .$title ${palettewidth}x${paletteheight}
 wm overrideredirect .$title 1
 bind .$title <FocusIn> ".ttb raisetab $title"
 return .$title
 }

 proc withdrawIfNeeded {w} {
     if {[wm overrideredirect $w] == 1} {
         #not for detached palettes
         wm withdraw $w
     }
 }

 proc drag.canvas.item {w item x y} {
     #test for locked axis, -1 = locked
     if {$x} {
         set dx [expr {$x - [set ${w}::lastx]}]
     } else  {
         set dx 0
     }
     if {$y} {
         set dy [expr {$y - [set ${w}::lasty]}]
     } else  {
         set dy 0
     }
     #test before moving
     if {[inside $w $item $dx $dy]} {
         puts inside
         $w move $item $dx $dy
     }
     set ${w}::lastx $x
     set ${w}::lasty $y
 }

 proc inside {w item dx dy} {
     #canvas extents
     set can(minx) 2
     set can(miny) 2
     set can(maxx) [expr [winfo width $w ] - 3 ]
     set can(maxy) [expr [winfo height $w ] - 0 ]
     #item coords
     set item [$w coords $item]
     #check min values
     foreach {x y} $item {
         set x [expr $x + $dx]
         set y [expr $y + $dy]
         if {$x < $can(minx)} {
             return 0
         }
         if {$y < $can(miny)} {
             return 0
         }
         if {$x > $can(maxx)} {
             return 0
         }
         if {$y > $can(maxy)} {
             return 0
         }
     }
     #puts $item
     return 1
 }

 #-------
 # move tag up 5 pixels
 #-------
 proc TearoffTabBar::raisetab {pathName tag} {
 catch {  $pathName lowertab [set ${pathName}::lasttab] }
 $pathName raise $tag
 $pathName move $tag 0 -[set ${pathName}::shift]
 set ${pathName}::lasttab $tag
 $pathName itemconfigure $tag.tab -fill [set ${pathName}::tabhighlight]
 }
 #-------
 # move tag down 5 pixels
 #-------
 proc TearoffTabBar::lowertab {pathName tag} {
 $pathName move $tag 0 [set ${pathName}::shift]
 $pathName lower $tag [set ${pathName}::lasttab]
 $pathName itemconfigure $tag.tab -fill [set ${pathName}::tabbg]
 }
 #-------
 # position palette window, called by tab bindings
 #-------
 proc TearoffTabBar::_placepalette {w} {
 set cc ""
 set aa [lindex [$w gettags current] 0 ]
 foreach item [$w coords $aa ] {
     set item [string trimright $item ".0"]
     lappend cc $item
 }
 set maxx 0
 foreach {x y} $cc {
     if {$x > $maxx} {
         set maxx $x
     }
 }
 set aa [string tolower $aa]
 wm geometry .$aa +[expr [winfo rootx $w] + $maxx -90]+[expr [winfo rooty $w]+25]
 wm deiconify .$aa
 update
 }
 ################################################################################
 # test block
 ################################################################################
 proc demo {} {
 pack [TearoffTabBar .ttb -rbevel 15] -fill x
 pack [text .txt -font {Ariel 12} ] -fill both -expand 1
 set x 5
 #-------
 # create some initial graphics
 #-------
 image create photo im_red -data R0lGODlhDAAMAJEAAP////8AAAAAAAAAACwAAAAADAAMAAACCoyPqcvtD6OclBUAOw==
 image create photo im_green -data R0lGODlhDAAMAJEAAP///wD/AAAAAAAAACwAAAAADAAMAAACCoyPqcvtD6OclBUAOw==
 image create photo im_blue -data R0lGODlhDAAMAJEAAP///wAA/wAAAAAAACwAAAAADAAMAAACCoyPqcvtD6OclBUAOw==

  # Scotland Wales Ireland Eire
  foreach item {England Scotland Wales Ireland Eire} {
     .ttb add -title $item -font {Ariel 8} -xpos $x -width 90 -tag $item -image im_red
     .ttb lowertab $item
     incr x 75
     #add some palette widgets
     pack [frame .[string tolower $item].fra -height 150 -width 100 -relief raised -borderwidth 2] -fill both -expand 1
     pack [label .[string tolower $item].fra.lab1 -text $item -width 15 -borderwidth 2 -relief ridge]
     pack [label .[string tolower $item].fra.lab2 -text $item -width 15 -borderwidth 2 -relief ridge]
     pack [label .[string tolower $item].fra.lab3 -text $item -width 15 -borderwidth 2 -relief ridge]
  }
  .txt insert end \
  "Tearoff Tabbar.
  Click on a tab...
      Drag left/right to move.
      Double-Click for popup-palette.
      Double-Click icon for floating palette, tab will vanish.
      Close floating palette to restore tab."
 }
 demo