0
votes

I'd like to build a tabbed display like the ttk::notebook widget, something I've done before in JavaScript and now would like to move to Tcl/Tk. I read on the Tk Docs site that the notebook tab is made for a small number of tabs of small size, and that one would need to customize that a bit anyway to get the close button on each tab and similar functionalities.

In JavaScript, the "frame" for each tab had the same geometry and was positioned absolutely at the same point within the parent container; and only one tab frame was visible at any one time. When a tab button was clicked, the visibility of the current tab was set to hidden and the new tab to visible.

To do the same in Tk, should all tabs be "gridded" in the same row and column, and the stacking order changed when a new tab button is clicked; or, since Tk retains the geometry of a removed frame, should only one tab be gridded at a time and replaced each time a tab button is clicked? Or is there a better approach?

Each tab has an editable text widget and I need to capture any changes if the user edits the text, leaves the focus in the text widget, and clicks a new tab button.

Thank you.


package require Tk
ttk::style theme use clam
ttk::style configure TScrollbar -arrowsize 20

wm geometry . "[winfo screenwidth .]x[winfo screenheight .]+0+0"
wm title . "Notebook Test"

ttk::label .main_l -text "Main Label"

grid .main_l -row 0 -column 0 -sticky ew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 1 -weight 1

foreach n { 3 2 1 } {
  ttk::frame .f$n -relief solid
  tk::label .f$n.l -text "Tab $n"
  tk::text .f$n.t -yscrollcommand ".f$n.v set" \
                  -background white -padx 10 -pady 10 -font { Georgia 12 } \
                  -wrap word -borderwidth 10 -relief flat

  ttk::scrollbar .f$n.v -orient vertical -command ".f$n.t yview"

  grid .f$n -row 1 -column 0 -sticky nsew
  grid .f$n.l -in .f$n -row 0 -columnspan 2 -sticky ew
  grid .f$n.t -in .f$n -row 1 -column 0 -sticky nsew
  grid .f$n.v -in .f$n -row 1 -column 1 -sticky ns

  grid rowconfigure .f$n 1 -weight 1
  grid columnconfigure .f$n 0 -weight 1

  set tab_foc($n) .f$n
}

focus .f1
set cur_tab 1

bind . <Alt-KeyPress-1> { navigate 1 }
bind . <Alt-KeyPress-2> { navigate 2 }
bind . <Alt-KeyPress-3> { navigate 3 }

proc navigate { n } {
  global cur_tab tab_foc
  set tab_foc($cur_tab) [focus]
  set cur_tab $n
  focus $tab_foc($n)
  raise .f$n
}

I attempted a simple example using the ttk::notebook styled to have no tabs and using the select method of the notebook to change tabs. There is a bit of flickering when the tabs are changed, that is not present using the previous code above. Perhaps, the notebook uses grid remove and it behaves like display:block and display:none versus visibility:visible and visibility:hidden in JavaScript, in that display alters the layout and can cause flicker while visibility doesn't change layout. I don't know for sure, of course.

package require Tk
ttk::style theme use clam
ttk::style configure TScrollbar -arrowsize 20
ttk::style layout Plain.TNotebook.Tab null

wm geometry . "[winfo screenwidth .]x[winfo screenheight .]+0+0"
wm title . "Notebook Test"

set nb [ttk::notebook .pnb -style Plain.TNotebook]
ttk::label .main_l -text "Main Label"
grid .main_l -row 0 -column 0 -sticky ew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 1 -weight 1
grid $nb -row 1 -column 0 -sticky nsew

foreach n { 1 2 3 } {
  set f [ ttk::frame $nb.f$n -relief solid ]
  tk::label $f.l -text "Tab $n"
  tk::text $f.t -yscrollcommand "$f.v set" \
                 -background white -padx 10 -pady 10 \
                 -font { Georgia 12 } -wrap word -borderwidth 10 -relief flat

  ttk::scrollbar $f.v -orient vertical -command "$f.t yview"

  $nb add $f -sticky nsew
  grid rowconfigure $f 1 -weight 1
  grid columnconfigure $f 0 -weight 1
  grid $f.l -in $f -row 0 -columnspan 2 -sticky ew
  grid $f.t -in $f -row 1 -column 0 -sticky nsew
  grid $f.v -in $f -row 1 -column 1 -sticky ns
}

$nb select $nb.f1

bind . <Alt-KeyPress-1> { navigate 1 }
bind . <Alt-KeyPress-2> { navigate 2 }
bind . <Alt-KeyPress-3> { navigate 3 }

proc navigate { n } {
  global nb
  $nb select $nb.f$n
}

I also tried using place and still experience the flickering. Perhaps, I've coded something wrong.

package require Tk
ttk::style theme use clam
ttk::style configure TScrollbar -arrowsize 20

wm geometry . "[winfo screenwidth .]x[winfo screenheight .]+0+0"
wm title . "Notebook Test"

ttk::label .main_l -text "Main Label"
place .main_l -relx 0.0 -rely 0.0  -relwidth 1.0 -height 30

foreach n { 3 2 1 } {
  set f [ ttk::frame .f$n -relief solid ]
  tk::label $f.l -text "Tab $n"
  tk::text $f.t -yscrollcommand "$f.v set" \
                 -background white -padx 10 -pady 10 \
                 -font { Georgia 12 } -wrap word -borderwidth 10 -relief flat

  ttk::scrollbar $f.v -orient vertical -command "$f.t yview"

  place $f -relx 0.0 -y 31 -relwidth 1.0 -relheight 1.0 -height -30
  place $f.l -in $f -relx 0.0 -rely 0.0 -relwidth 1.0 -height 30
  place $f.t -in $f -relx 0.0 -rely 0.0 -y 30 -relwidth 1.0 -width -23 -relheight 1.0 -height -30
  place $f.v -in $f -relx 1.0 -x -22 -rely 0.0 -y 31 -width 22 -relheight 1.0 -height -30

  if { $n > 1 } { place forget $f }
}

set curTab 1

bind . <Alt-KeyPress-1> { navigate 1 }
bind . <Alt-KeyPress-2> { navigate 2 }
bind . <Alt-KeyPress-3> { navigate 3 }

proc navigate { n } {
   global curTab
   place .f$n -relx 0.0 -y 31 -relwidth 1.0 -relheight 1.0 -height -30
   raise .f$n
   place forget .f$curTab
   set curTab $n
}

Using grid alone also has the flickering, but using lower before mapping and executing update idletasks before removing the currently mapped tab, appears to have eliminated or significantly reduced the flickering. That is, attempting to map the tab behind the current tab, such that it is similar to hidden, update idletasks so that it is sized, and then unmap the current tab. Odd to me is that if click in an entry element and then navigate to a different tab and type some text before doing anything else, the text appears in the tab that was removed by grid remove. Why? It is not in the focus cycle any longer but retains the keyboard focus after being removed.

grid .main_l -row 0 -column 0 -sticky ew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 1 -weight 1

foreach n { 3 2 1 } {
  set f [ ttk::frame .f$n -relief solid ]
  tk::label $f.l -text "Tab $n"
  ttk::entry .f$n.e1
  ttk::entry .f$n.e2
  tk::text $f.t -yscrollcommand "$f.v set" \
                 -background white -padx 10 -pady 10 -font { Georgia 12 } -wrap word -borderwidth 10 -relief flat

  ttk::scrollbar $f.v -orient vertical -command "$f.t yview"

  grid .f$n -row 1 -column 0 -sticky nsew
  grid rowconfigure .f$n 1 -weight 1
  grid columnconfigure .f$n 0 -weight 1
  grid .f$n.l -in .f$n -row 0 -columnspan 2 -sticky ew
  grid .f$n.t -in .f$n -row 1 -column 0 -sticky nsew
  grid .f$n.v -in .f$n -row 1 -column 1 -sticky ns
  grid .f$n.e1 -in .f$n -row 0 -column 2
  grid .f$n.e2 -in .f$n -row 1 -column 2

  if { $n > 1 } { grid remove $f }

}

set curTab 1

bind . <Alt-KeyPress-1> { if { $curTab != 1 } { navigate 1 } }
bind . <Alt-KeyPress-2> { if { $curTab != 2 } { navigate 2 } }
bind . <Alt-KeyPress-3> { if { $curTab != 3 } { navigate 3 } }

proc navigate { n } {
   global curTab
   lower .f$n
   grid .f$n
   update idletasks
   grid remove .f$curTab
   set curTab $n
}
1
Not a full answer, but to get focus handling right, you must unmap hidden widgets. The grid remove comnand might help. (Put all the widgets in the same grid cell.) Or maybe this is a job for place? (If the latter, you'll want to watch for <Configure> events in the child widgets.) And in extremis you can make the canvas for sure. A ttk style on the notebook might also do the trick, and surely someone has solved this already?Donal Fellows
The point is, just raise and lower won't work. For sure.Donal Fellows
@DonalFellows Thank you for the guidance. I added a very puerile example to my question, not to disagree but only to ask if I track the window element having the focus at each tab navigation, should that handle the the focus issues; or are they more involved than simply tracking the focus and any selected text and restoring them upon navigation? I noticed that, if a selection is made on more than one tab, although the cursor positions are retained, only one selection is "remembered' when navigating tabs; so, that would need to be tracked also. Does grid remove retain the undo stack?Gary

1 Answers

1
votes

After a bit of research, I found that there's a way to do what you want with the ttk::notebook (which is good; that handles a whole load of trickiness with size computations, etc.) and this is described on the ttk::notebook wiki page:

# Need this style
ttk::style layout Plain.TNotebook.Tab null

# Make the notebook and pages
ttk::notebook .foo -style Plain.TNotebook
.foo add [button .foo.bar -text bar]
.foo add [button .foo.baz -text baz]

# Select a page; you'll need to provide a mechanism for users to switch
.foo select .foo.bar

There are some additional complexities with borders that you might or might not want to do anything about. (Style coding is complicated, alas.)


Otherwise, grid remove is a useful tool as it stops managing a widget (causing it to become unmapped and not focusable) without clearing the settings for how you want to manage it; when you put the widget back in, it will use what it had before. However, you'll still have problems with content widgets being different sizes so every time you change page things run the risk of resizing.

Since you're already getting into doing lots of work when you're doing that, you can probably contemplate using place instead. The main trick there will be to bind <Configure> on each of the pages so that you can find out what size they are (if/when that changes).

Just changing the stacking order with raise and lower does not work. The crucial problem is that that doesn't affect the keyboard focus cycle properly; users will be able to Tab into widgets they can't see and that's very confusing!


In answer to your question in the comments, selections are global within the Tk application by default (or global across all applications on X11; that's the way that PRIMARY selections work). You can give a widget an independent selection by configuring it to not export the selection.