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