Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

scrolling notebook tabs

51 views
Skip to first unread message

Brad Lanam

unread,
Jan 2, 2020, 5:48:48 PM1/2/20
to
Here is one possible implementation.

It uses a -maxtabs option, rather than trying to calculate how many tabs fit
within the available width.

The arrows are implemented as actual notebook tabs.
This has the disadvantage that it's not a drop-in replacement if numeric
indices to the notebook tabs are in use.

This was just an exercise, based on ticket:
https://core.tcl-lang.org/tk/info/2782346fffffffff
I do not plan on maintaining this code, so feel free to take ownership.

There may still be bugs with respect to hidden tabs.

#!/usr/bin/tclsh
#
# This code is in the public domain.
#

package require Tk

package require tksvg

proc ::scrollnotebook { nm args } {
::scrollnb new $nm {*}$args
return $nm
}

proc ::snbinit { } {
if { [info exists ::snb::initialized] } {
return
}

namespace eval ::snb {
set imgleft [image create photo -format svg -data {
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="z.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="44.555556"
inkscape:cx="7.9999972"
inkscape:cy="8.0000026"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1023"
inkscape:window-height="594"
inkscape:window-x="159"
inkscape:window-y="71"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(131.69759,-128.34964)">
<g
aria-label="⏷"
transform="matrix(0,1.0351751,-0.96602014,0,0,0)"
style="font-style:normal;font-weight:normal;font-size:12.09316635px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#dd7000;fill-opacity:1;stroke:none;stroke-width:0.30232921"
id="text817">
<path
d="m 135.8314,123.90797 q 0.23219,0 0.23219,0.18059 0,0.0258 -0.0258,0.0774 l -4.1794,7.86863 q -0.0774,0.15479 -0.15479,0.15479 -0.0774,0 -0.1548,-0.15479 l -4.17939,-7.84283 q 0,-0.0516 0,-0.10319 0,-0.18059 0.25798,-0.18059 z"
style="font-size:25.79875755px;fill:#dd7000;fill-opacity:1;stroke-width:0.30232921"
id="path814"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>
}]

set imgright [image create photo -format svg -data {
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="tree-arrow-right-n.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.25"
inkscape:cx="8.0655738"
inkscape:cy="8"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1023"
inkscape:window-height="594"
inkscape:window-x="159"
inkscape:window-y="71"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(131.69759,-128.34964)">
<g
aria-label="⏷"
transform="matrix(0,1.0351751,0.96602014,0,0,0)"
style="font-style:normal;font-weight:normal;font-size:12.09316635px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#dd7000;fill-opacity:1;stroke:none;stroke-width:0.30232921"
id="text817">
<path
d="m 135.8314,-132.18937 q 0.23219,0 0.23219,0.18059 0,0.0258 -0.0258,0.0774 l -4.1794,7.86862 q -0.0774,0.15479 -0.15479,0.15479 -0.0774,0 -0.1548,-0.15479 l -4.17939,-7.84282 q 0,-0.0516 0,-0.1032 0,-0.18059 0.25798,-0.18059 z"
style="font-size:25.79875755px;fill:#dd7000;fill-opacity:1;stroke-width:0.30232921"
id="path9" />
</g>
</g>
</svg>
}]

set initialized true
}
}

proc ::snbhandler { snb args } {
$snb {*}$args
}

::oo::class create ::scrollnb {
constructor { nm args } {
my variable vars

::snbinit

set vars(-maxtabs) 9999999
set vars(tab.count) 0
set vars(first.offset) 1
set vars(last.offset) 1
set vars(show.left.scroll) false
set vars(show.right.scroll) false
set vars(hidden.tabs) [dict create]

set vars(widget) [ttk::notebook ${nm}]
set vars(w.sleft) $vars(widget).scrollleft
set vars(w.sright) $vars(widget).scrollright

set vars(snb) ${nm}_snb
rename $vars(widget) ::$vars(snb)
interp alias {} $vars(widget) {} ::snbhandler [self]

ttk::frame $vars(w.sleft)
$vars(snb) add $vars(w.sleft) -image $::snb::imgleft
$vars(snb) hide $vars(w.sleft)
ttk::frame $vars(w.sright)
$vars(snb) add $vars(w.sright) -image $::snb::imgright
$vars(snb) hide $vars(w.sright)

uplevel 2 [list $vars(widget) configure {*}$args]

set bt [my _addBindTag $vars(widget) snbbt]
bind $bt <Destroy> [list [self] destruct]
bind $bt <<NotebookTabChanged>> [list [self] selecttab]
}

method _addBindTag { w tag } {
if { [lsearch -exact [bindtags $w] $tag$w] == -1 } {
bindtags $w [concat $tag$w [bindtags $w]]
}
return $tag$w
}

method destruct { } {
my variable vars

interp alias {} $vars(widget) {}
[self] destroy
}

method unknown { args } {
my variable vars

uplevel 2 [list $vars(snb) {*}$args]
}

method _processTabs { } {
my variable vars

set tablist [$vars(snb) tabs]
set tcount [$vars(snb) index end]
if { $vars(tab.count) <= 0 } {
return
}

set tfirst 1
set tlast [expr {$tcount - 2}]

# turn off the old left/right
$vars(snb) hide $vars(w.sleft)
$vars(snb) hide $vars(w.sright)
set vars(show.left.scroll) false
set vars(show.right.scroll) false

# adjust first.index and last.index
# based on the new count.
# [.nb index end] will return the total number of tabs including hidden
# tabs.
# note that -maxtabs may have changed.

if { $vars(tab.count) > $vars(-maxtabs) } {
set vars(last.offset) \
[expr {$vars(first.offset)+$vars(-maxtabs)-1}]
if { $vars(first.offset) > 1 } {
set vars(show.left.scroll) true
}
if { $vars(last.offset) < $vars(tab.count) } {
set vars(show.right.scroll) true
}
}

# redisplay the tabs as necessary

set count $tfirst
for { set tidx $tfirst } { $tidx <= $tlast } { incr tidx } {
# if the user has set the tab to hidden, ignore it...
if { [dict exists $vars(hidden.tabs) [lindex $tablist $tidx]] } {
continue
}

if { $count < $vars(first.offset) } {
$vars(snb) hide $tidx
} elseif { $count > $vars(last.offset) } {
$vars(snb) hide $tidx
} else {
$vars(snb) add [lindex $tablist $tidx]
}
incr count
}

# display scrolling arrows as appropriate
if { $vars(show.left.scroll) } {
$vars(snb) insert 0 $vars(w.sleft)
$vars(snb) add $vars(w.sleft)
}
if { $vars(show.right.scroll) } {
$vars(snb) insert end $vars(w.sright)
$vars(snb) add $vars(w.sright)
}
}

method add { w args } {
my variable vars

$vars(snb) add $w {*}$args
if { ! [dict exists $vars(hidden.tabs) $w] } {
incr vars(tab.count)
}
dict unset vars(hidden.tabs) $w
my _processTabs
}

method forget { tabid } {
my variable vars

$vars(snb) forget $tabid
incr vars(tab.count) -1
set tablist [$vars(snb) tabs]
dict unset vars(hidden.tabs) [lindex $tablist [$vars(snb) index $tabid]]
my _processTabs
}

method hide { tabid } {
my variable vars

$vars(snb) hide $tabid
incr vars(tab.count) -1
set tablist [$vars(snb) tabs]
dict set vars(hidden.tabs) [lindex $tablist [$vars(snb) index $tabid]] 1
my _processTabs
}

method insert { pos w args } {
my variable vars

set tablist [$vars(snb) tabs]
if { $w ni $tablist } {
incr vars(tab.count)
}
$vars(snb) insert $pos $w {*}$args
my _processTabs
}

method selecttab { args } {
my variable vars

set sel [$vars(snb) select]
set tablist [$vars(snb) tabs]
set tcount [$vars(snb) index end]
if { $sel eq $vars(w.sleft) } {
if { $vars(first.offset) == 1 } {
$vars(snb) select [lindex $tablist $vars(first.offset)]
} else {
incr vars(first.offset) -1
incr vars(last.offset) -1
}
}
if { $sel eq $vars(w.sright) } {
if { $vars(last.offset) == $tcount - 1 } {
$vars(snb) select [lindex $tablist $vars(last.offset)]
} else {
incr vars(first.offset)
incr vars(last.offset)
}
}
my _processTabs
}

method cget { k } {
my variable vars

if { $k eq "-maxtabs" } {
set rv $vars($k)
} else {
set rv [$vars(snb) cget $k]
}
return $rv
}

method configure { args } {
my variable vars

foreach {k v} $args {
if { $k eq "-maxtabs" } {
set vars($k) $v
my _processTabs
} else {
$vars(snb) configure $k $v
}
}
}
}


# demo

package require Tk

source snb.tcl

::scrollnotebook .nb -maxtabs 4
pack .nb -expand 1 -fill both
foreach k {a b c d e f g h} {
ttk::frame .$k
.nb add .$k -text [string repeat $k 10]
}
.nb hide .c

0 new messages