Posted to tcl by patthoyts at Tue Dec 29 23:14:13 GMT 2009view pretty

# Replace the standard notebook tab with one that includes a close
# button. 
# In future versions of ttk this will be supported more directly when
# the identify command will be able to identify parts of the tab.

package require Tk

namespace eval ::ButtonNotebook {
}

# Tk 8.6 has the Visual Styles element engine on windows. If this is
# available we use it to get proper windows close buttons.
#
proc ::ButtonNotebook::CreateElements {} {
    if {[lsearch -exact [ttk::style element names] close] == -1} {
        if {[catch {
            # WINDOW WP_SMALLCLOSEBUTTON (19)
            # WINDOW WP_MDICLOSEBUTTON (20)
            # WINDOW WP_MDIRESTOREBUTTON (22)
            #ttk::style element create close vsapi \
            #    WINDOW 20 {disabled 4 {active pressed} 3 active 2 {} 1}
            ttk::style element create close vsapi \
                EXPLORERBAR 2 {disabled 4 {active pressed} 3 active 2 {} 1}
            ttk::style element create detach vsapi \
                WINDOW 22 {disabled 4 {active pressed} 3 active 2 {} 1}
        }]} then {
            # No XP element engine - use images...
            CreateImageElements
        }
    }
}

proc ::ButtonNotebook::CreateImageElements {} {
    # Create two image based elements to provide buttons to close the
    # tabs or to detach a tab and turn it into a toplevel.
    namespace eval ::img {}
    set imgdir [file join [file dirname [info script]] images]
    image create photo ::img::close -file [file join $imgdir xhn.gif]
    image create photo ::img::closepressed -file [file join $imgdir xhd.gif]
    image create photo ::img::closeactive -file [file join $imgdir xhu.gif]
    image create photo ::img::detach -file [file join $imgdir dhn.gif]
    image create photo ::img::detachup -file [file join $imgdir dhu.gif]
    image create photo ::img::detachdown -file [file join $imgdir dhd.gif]
    if {[lsearch -exact [ttk::style element names] close] == -1} {
        if {[catch {
            ttk::style element create close image \
                [list ::img::close \
                     {active user3} ::img::close \
                     {active pressed !disabled} ::img::closepressed \
                     {active !disabled} ::img::closeactive] \
                -border 3 -sticky {}
            ttk::style element create detach image \
                [list ::img::detach \
                     {active pressed !disabled} ::img::detachdown \
                     {active !disabled} ::img::detachup] \
                -border 3 -sticky {}
        } err]} { puts stderr $err }
    }
}

proc ::ButtonNotebook::Init {{pertab 0}} {
    CreateElements

    # This places the buttons on the right end of the tab area -- but in
    # Tk 8.5 we cannot identify these elements.
    if {!$pertab} {
        ttk::style layout ButtonNotebook {
            ButtonNotebook.client -sticky nswe
            ButtonNotebook.close -side right -sticky ne
            ButtonNotebook.detach -side right -sticky ne
        }
    }

    # This places the button elements on each tab which uses quite a
    # lot of space but we can identify the elements. Changes to the 
    # widget state affect all the button elements though.
    if {$pertab} {
        ttk::style layout ButtonNotebook {
            ButtonNotebook.client -sticky nswe
        }
        ttk::style layout ButtonNotebook.Tab {
            ButtonNotebook.tab -sticky nswe -children {
                ButtonNotebook.focus -side top -sticky nswe -children {
                    ButtonNotebook.padding -side right -sticky nswe -children {
                        ButtonNotebook.close -side right -sticky {}
                    }
                    ButtonNotebook.label -side left -sticky {}
                }
            }
        }
        #if {$::tcl_platform(platform) eq "windows"} {} ??
        ttk::style configure ButtonNotebook.Tab -width -8
        ttk::style configure ButtonNotebook.Tab -padding {8 0 0 0}
    }

    bind TNotebook <ButtonPress-1> {+::ButtonNotebook::Press %W %x %y}
    bind TNotebook <Motion> {+::ButtonNotebook::Drag %W %x %y %X %Y}
    bind TNotebook <ButtonRelease-1> {+::ButtonNotebook::Release %W %x %y %X %Y}
    bind TNotebook <<ThemeChanged>> [namespace code [list Init $pertab]]
}

# Hook in some event extras:
# set the state to pressed if button down over a button element.
proc ::ButtonNotebook::Press {w x y} {
    set e [$w identify $x $y]
    if {[string match "*close" $e] || [string match "*detach" $e]} {
        set index [$w index @$x,$y]
        set tw [lindex [$w tabs] $index]
        if {[winfo class $tw] ne "Noclose"} {
            $w state pressed
        }
    } else {
        upvar #0 [namespace current]::$w state
        if {![info exists state]} {
            # FIX ME: dragging tabs isnt working so well at the moment
            #set state(drag) 1
            #set state(drag_index) [$w index @$x,$y]
            #set state(drag_under)  $state(drag_index)
            #set state(drag_from_x) $x
            #set state(draw_from_y) $y
            #set state(drag_indic) [ttk::label $w._indic -text v]
        }
    }
}

proc ::ButtonNotebook::Drag {w x y rootX rootY} {
    upvar #0 [namespace current]::$w state

    # Use user3 to prevent activation of a non-closing close element
    set contained [lindex [$w tabs] [$w index @$x,$y]]
    if {[winfo class $contained] eq "Noclose"} {
        $w state user3
    } else { $w state !user3 }

    if {[info exists state]} {
        if {[winfo containing $rootX $rootY] eq $w} {
            set index [$w index @$x,$y]
            if {$index != $state(drag_under)} {
                place $state(drag_indic) -anchor nw -x $x -y 0
                set state(drag_under) $index
            }
        }
    }
}

# On release, do the button action if any.
proc ::ButtonNotebook::Release {w x y rootX rootY} {
    $w state !pressed
    set e [$w identify $x $y]
    set index [$w index @$x,$y]
    if {[string match "*close" $e]} {
        set tw [lindex [$w tabs] $index]
        if {[winfo class $tw] ne "Noclose"} {
            $w forget $index
            event generate $w <<NotebookClosedTab>>
        }
    } elseif {[string match "*detach" $e]} {
        Detach $w $index
    } else {
        upvar #0 [namespace current]::$w state
        if {[info exists state]} {
            set dropwin [winfo containing $rootX $rootY]
            if {$dropwin eq {}} {
                Detach $w $state(drag_index)
            } elseif {$dropwin eq $w && $index != $state(drag_index)} {
                Move $w $state(drag_index) $index
            }
            destroy $state(drag_indic)
            unset state
        }
    }
}

# Move a tab from old index to new index position.
proc ::ButtonNotebook::Move {notebook old_index new_index} {
    set tab [lindex [$notebook tabs] $old_index]
    set title [$notebook tab $old_index -text]
    $notebook forget $old_index
    if {[string is integer -strict $new_index]} {
        incr new_index -1
        if {$new_index < 0} {set new_index 0}
        if {$new_index > [llength [$notebook tabs]]} { set new_index end }
    } else {
        set new_index end
    }
    $notebook insert $new_index $tab -text $title
}

# Turn a tab into a toplevel (must be a tk::frame)
proc ::ButtonNotebook::Detach {notebook index} {
    set tab [lindex [$notebook tabs] $index]
    set title [$notebook tab $index -text]
    $notebook forget $index
    wm manage $tab
    wm title $tab $title
    wm protocol $tab WM_DELETE_WINDOW \
        [namespace code [list Attach $notebook $tab $index]]
    bind $tab <Configure> \
        [namespace code [list Debug $notebook "Configure %wx%h %x,%y"]]
    bind $tab <Expose> [namespace code [list Debug $notebook "Expose"]]
    bind $tab <Activate> [namespace code [list Debug $notebook "Activate"]]
    bind $tab <Deactivate> [namespace code [list Debug $notebook "Deactivate"]]
    bind $tab <ButtonPress> [namespace code [list Debug $notebook "Button"]]
    bind $tab <Visibility> \
        [namespace code [list Debug $notebook "Visibility %s"]]
    
    event generate $tab <<DetachedTab>>
}
proc ::ButtonNotebook::Debug {notebook msg} {
    if {[winfo exists $notebook.page0.text]} {
        $notebook.page0.text insert end $msg\n {}
        $notebook.page0.text see end
    }
}

# Attach a toplevel to the notebook
proc ::ButtonNotebook::Attach {notebook tab {index end}} {
    set title [wm title $tab]
    wm forget $tab
    if {[catch {
        if {[catch {$notebook insert $index $tab -text $title} err]} {
            $notebook add $tab -text $title
        }
        $notebook select $tab
    } err]} {
        puts stderr "AttachWindow: $err"
        wm manage $w
        wm title $w $title
    }
}
proc ::ButtonNotebook::Test {} {
    variable tabtest
    set dlg [toplevel .test[incr tabtest]]
    wm title $dlg "Notebook test"
    wm withdraw $dlg
    set nb [ttk::notebook $dlg.nb -style ButtonNotebook]
    frame $nb.page0 -background red -width 100 -height 100
    frame $nb.page1 -background blue -width 100 -height 100
    frame $nb.page2 -background green -width 100 -height 100 -class Noclose
    frame $nb.page3 -background tomato -width 100 -height 100
    $nb add $nb.page0 -text One
    $nb add $nb.page1 -text Two
    $nb add $nb.page2 -text Three
    $nb add $nb.page3 -text "Some really long label."

    set txt [text $nb.page0.text -height 10 -width 10]
    set vs [scrollbar $nb.page0.vs -command [list $txt yview]]
    $txt configure -yscrollcommand [list $vs set]
    grid $txt $vs -sticky news
    grid rowconfigure $nb.page0 0 -weight 1
    grid columnconfigure $nb.page0 0 -weight 1

    grid $dlg.nb -sticky news
    grid rowconfigure $dlg 0 -weight 1
    grid columnconfigure $dlg 0 -weight 1
    
    bind TNotebook <Motion> [string map [list %txt $txt] {
        %txt insert end [%W identify %x %y] {} "\n" {}
        %txt see end
    }]
    bind $dlg <Control-F2> {console show}
    wm withdraw .
    wm protocol $dlg WM_DELETE_WINDOW {exit}
    wm geometry $dlg 320x240
    wm deiconify $dlg
}

::ButtonNotebook::Init 1

# The following line causes the Test procedure to be run if this file
# is run standalone.
puts [winfo class .]
if {[string match "Tab*" [winfo class .]]} {
    ::ButtonNotebook::Test
    tkwait window .
}