package require Metawidget # name: Tabcontrol # args: args: option-value pairs for metawidget configuration # defines the internal widgets and private variables. initializes the -width # option by calling its member function. metawidget create Tabcontrol { frame $this.tabs -border 2 -relief flat ;# contains all rows and tabs label $this.actv -border 2 -relief raised ;# the active tab is also a frame frame $this.work -border 2 -relief raised ;# the work area frame $this.gbar -border 0 ;# a gray line to hide the lower ;# ...border of the active tab my iWorkY 0 ;# y-coordinate where the work area begins my iAutoW 0 ;# minimum width of $this.tabs, if -width in auto mode my lAllTabs {} ;# list of all defined tab names my sActTab {} ;# name of the currently active tab my wActWin {} ;# the active tab's associated user widget -width 100% bind $this "$this _drawTabs" ;# to update tab geometry if resized } {} -default tabs # name: _drawTabs # args: - # redraws the tabs. called when resized or tabs are configured. basically # calculates the new geometry of the tabs container $this.tabs, of the # the active tab $this.actv and the line that hides its lower border. # the work area's top pixel coordinate is also adjusted. metawidget proc Tabcontrol _drawTabs {} { # position tab container depending on -width option. switch [my sWmode] { pct { place $this.tabs -x 0 -y 0 -relh 1 -relw [my iWidth] -w 0 } abs { place $this.tabs -x 0 -y 0 -relh 1 -w [my iWidth] -relw 0 } req { place $this.tabs -x 0 -y 0 -relh 1 -w [my iAutoW] -relw 0 } } # if an active tab is set, places the $this.actv window exactly over it, # but sizes it slightly bigger. places the $this.gbar frame so that # $this.actv and $this.work look like one window. if { [my sActTab] != {} } { set iX [winfo x [my wTab]] set iY [expr [winfo y [my wRow]]-2] set iW [expr [winfo w [my wTab]]+4] set iH [expr [winfo h [my wRow]]+2] place $this.actv -x $iX -y $iY -w $iW -h $iH place $this.gbar -x [expr $iX+1] -y [expr $iY+$iH-2] -w [expr $iW-2] -h 2 place $this.work -x 0 -y [my iWorkY] -relw 1 -h [expr [winfo height $this.tabs]-[my iWorkY]] } } # name: _findTab # args: sTab: Tab name to retrieve data for # raises an exception, if the tab doesn't exist. otherwise, returns # the tab's properties: widget name, row, user window and command. metawidget proc Tabcontrol _findTab { sTab } { if { ! [myinfo exists aTabs($sTab)] } { error "Tabcontrol $this: No such tab '$sTab'" } return [my aTabs($sTab)] } # name: _calcGeom # args: - # calculates the minimum required with of the container $this.tabs, so that # the labels of all tabs are completely visible. retrieves the 'required with' # of all tab rows and calculates the longest one. used when -width is 'auto'. # also calculates the top border of the work area. metawidget proc Tabcontrol _calcGeom {} { update set iAutoW 0 foreach wWindow [winfo children $this.tabs] { set iWidth [winfo reqw $wWindow] if { $iWidth < $iAutoW } continue set iAutoW $iWidth } my iAutoW [expr $iAutoW+4] set iWorkY 2 foreach wWindow [winfo children $this.tabs] { incr iWorkY [expr [winfo reqh $wWindow]-2] } my iWorkY $iWorkY } # name: _updateActive # args: - # make the active tab look like the original tab (which stays in its row) # remember: all tabs are simply packed into their row frame. to make a tab # look raised, the $this.actv frame is configured to look like the # to-be-raised tab and placed over it slighly bigger by the _drawTabs function. metawidget proc Tabcontrol _updateActive {} { if { [my sActTab] == {} } return $this.actv config -text [[my wTab] cget -text] -image [[my wTab] cget -image] \ -backg [[my wTab] cget -backg] -foreg [[my wTab] cget -foreg] \ -font [[my wTab] cget -font] -cursor [[my wTab] cget -cursor] \ -under [[my wTab] cget -under] -anchor [[my wTab] cget -anchor] \ -padx [[my wTab] cget -padx] -pady [[my wTab] cget -pady] } # name: -width # args: sWidth: new width value # option member. controls the width of the container $this.tabs and its # behaviour when the toplevel is resized. sWidth can be 'auto', an integer, # or an integer followed by a percent sign. sets the two private variables # sWmode and iWidth accordingly. they are used in the _drawTabs member. metawidget proc Tabcontrol -width { sWidth } { if { [regexp {^([0-9]+)%$} $sWidth sDummy iWidth] } { my sWmode pct my iWidth [expr $iWidth/100.] } elseif { [regexp {^[0-9]+$} $sWidth iWidth] } { my sWmode abs my iWidth $iWidth } elseif { $sWidth == "auto" } { my sWmode req my iWidth [my iAutoW] } else { error "Tabcontrol $this: Bad width $sWidth. Must be an integer, integer% or 'auto'" } my -width $sWidth _drawTabs } # name: insert # args: sTab: name of the new tab # iRow: row to place the tab in # args: option-value pairs for tab configuration # defines and displays a new tab. derives the row's widget name and eventually # creates it if it doesn't exist. creates a label for the tab and packs it # into its row. initializes tab properties. metawidget proc Tabcontrol insert { sTab iRow args } { if { [myinfo exists aTabs($sTab)] } { error "Tabcontrol $this: Tab '$sTab' already exists" } set wRow $this.tabs.row$iRow ;# wRow is child of $this.tabs set wTab $wRow.tab$sTab ;# wTab is child of wRow if { ! [myinfo exists lTabs$iRow] } { ;# if wRow doesn't exist frame $wRow -border 0 ;# then create it and my lTabs$iRow {} ;# initialize a list for all its tabs } # create tab widget, simply pack it on the left, create mouse click binding label $wTab -border 2 -relief raised pack $wTab -side left -ipadx 2 -fill both -expand 1 bind $wTab <1> "$this invoke $sTab" my aTabs($sTab) [list $wTab $iRow {} {}] ;# initialize tab prop/s eval tabconfigure {$sTab} -text {$sTab} $args ;# configure it a bit _calcGeom ;# update required width invoke $sTab ;# and raise the new tab # add tab to the all-tabs list and to the row-specific list my lAllTabs [mkw.lextend [my lAllTabs] $sTab] my lTabs$iRow [mkw.lextend [my lTabs$iRow] $sTab] } # name: delete # args: sTab: tab to delete # deletes a tab. displays another tab, if there's one left. metawidget proc Tabcontrol delete { sTab } { mkw.lassign [_findTab $sTab] wTab iRow wWindow pack forget $wWindow destroy $wTab unmy aTabs($sTab) # update all-tabs list and row-specific list my lAllTabs [mkw.lshrink [my lAllTabs] $sTab] my lTabs$iRow [mkw.lshrink [my lTabs$iRow] $sTab] # if there is some other tab left and the deleted tab was active, then # raise the other tab. otherwise hide the $this.actv frame. if { [llength [my lAllTabs]] } { if { $sTab == [my sActTab] } { invoke [lindex [my lAllTabs] 0] } _calcGeom _drawTabs } else { place forget $this.actv place forget $this.gbar my sActTab {} } } # name: tabconfigure # args: sTab: tab to be configured # args: option-value pairs # configures a tab. the two special options -window and -command are stored # in the tab's property list (no immediate action required). all other options # are passed to the tab's widget. metawidget proc Tabcontrol tabconfigure { sTab args } { mkw.lassign [_findTab $sTab] wTab set lArgs [mkw.options $args * {-window *} {-command *}] if { [info exists -window] } { my aTabs($sTab) [lreplace [my aTabs($sTab)] 2 2 ${-window}] } if { [info exists -command] } { my aTabs($sTab) [lreplace [my aTabs($sTab)] 3 3 ${-command}] } eval $wTab config $lArgs _calcGeom _drawTabs _updateActive } # name: tabcget # args: sTab: tab to retrieve option value for # sOption: name of an option # retrieves an option value of a tab. either returns a tab property value for # one of the special options -window and -command, or tries to get it directly # from the tab's widget. metawidget proc Tabcontrol tabcget { sTab sOption } { mkw.lassign [_findTab $sTab] wTab iRow wWindow sCommand set wRow $this.tabs.row$iRow.tab$sTab set wTab $wRow.tab$sTab switch -- $sOption { -window { return $wWindow } -command { return $sCommand } default { return [$wTab cget $sOption] } } } # name: invoke # args: sTab: tab to raise # raises a tab, i.e. makes it look like in the foreground and displays its # associated user window. Also, the row with the active tab is always the lowest # one. therefore the rows are rearranged. metawidget proc Tabcontrol invoke { sTab } { mkw.lassign [_findTab $sTab] wTab iRow wWindow sCommand catch { pack forget [my wActWin ] } ;# hide user-widget from active tab my wTab $wTab ;# update private variables... my wRow [winfo parent $wTab] ;# ...needed later e.g. for _drawTabs my sActTab $sTab my wActWin $wWindow # rearrange rows so that the one with the new active tab is at the bottom. set iY 0 set lRows [mkw.lshrink [lsort [winfo children $this.tabs]] [my wRow]] foreach wRow [linsert $lRows end [my wRow]] { raise $wRow place $wRow -x 0 -y $iY -relw 1 incr iY [expr [winfo reqheight $wRow]-2] } # pack and raise the new tab's user-widget, if it exists if { [winfo exist $wWindow] } { pack $wWindow -in $this.work -padx 2 -pady 2 -fill both -expand 1 raise $wWindow } update _drawTabs _updateActive if { $sCommand != {} } { eval $sCommand ;# eventually execute the tab's command, if there is one } } # name: get # args: sWhat: row number or 'active' or nothing # returns all tabs, the active tab or the tabs of a given row. metawidget proc Tabcontrol get { {sWhat {}} } { if { $sWhat == {} } { return [my lAllTabs] } elseif { $sWhat == "active" } { return [my sActTab] } elseif { [regexp {^[0-9]+$} $sWhat] } { return [my lTabs$sWhat] } else { error "Tabcontrol $this: Bad index $sWhat. Should be active, an integer or nothing." } } # name: bind # args: sTab: tab to set binding for (or get from) # args: arguments for the bind command # creates or retrieves bindings for the given tab. metawidget proc Tabcontrol bind_ { sTab args } { mkw.lassign [_findTab $sTab] wRow wTab eval bind $wTab $args } metawidget command Tabcontrol _drawTabs _drawTabs metawidget command Tabcontrol insert insert metawidget command Tabcontrol delete delete metawidget command Tabcontrol tabconfigure tabconfigure metawidget command Tabcontrol tabcget tabcget metawidget command Tabcontrol invoke invoke metawidget command Tabcontrol get get metawidget command Tabcontrol bind bind_ metawidget option Tabcontrol -width -width proc test {} { global iTabno set iTabno 0 wm minsize . 200 200 [text .code] insert end [info body test] frame .fwid -border 2 -relief groove pack [radiobutton .fwid.auto -text "Set it to Auto mode" -value 0 -command {.tabs config -width auto}] -anchor w pack [radiobutton .fwid.half -text "Half of window width" -value 1 -command {.tabs config -width 50%}] -anchor w pack [radiobutton .fwid.full -text "Full width of window" -value 2 -command {.tabs config -width 100%}] -anchor w pack [radiobutton .fwid.exct -text "Exactly 300 pixels" -value 3 -command {.tabs config -width 300}] -anchor w frame .fcrt pack [button .fcrt.tcre -text "Create a tab" -command { incr iTabno label .lbl$iTabno -text "This is tab $iTabno" -font {Helvetica 24} -bg white .tabs insert Tab$iTabno 1 -window .lbl$iTabno }] -fill x pack [button .fcrt.tdel -text "Delete last tab" -command { .tabs delete Tab$iTabno destroy .lbl$iTabno incr iTabno -1 }] -fill x tabcontrol .tabs -width auto pack .tabs -fill both -expand 1 -padx 10 -pady 10 .tabs insert width 0 -text "Change Width" -window .fwid -underline 7 .tabs insert edit 0 -text "Create Tabs" -window .fcrt -underline 7 .tabs insert source 0 -text "View Source" -window .code -underline 5 .tabs invoke width bind . {.tabs invoke width} bind . {.tabs invoke edit} bind . {.tabs invoke source} } #test