TIP 454: Automatically Resize Frames After Last Child Removed

Login
Bounty program for improvements to Tcl and certain Tcl packages.
Author:         Harald Oehlmann <Harald.Oehlmann@elmicron.de>
Author:         Harald Oehlmann <oehhar@users.sourceforge.net>
Author:         Francois Vogel <fvogelnew1@free.fr>
State:          Accepted
Type:           Project
Vote:           Done
Created:        21-Sep-2016
Post-History:   
Keywords:       Tk
Tcl-Version:    8.6.6

Abstract

A frame-like widget has 1x1 required size if created. If children are added by pack/grid and the last children is unpacked/grid, the frame-like widget does not return to the 1x1 required size. Instead, it keeps the size of the last packed item. It should automatically or under control resize to the initial requested size of 1x1.

Rationale

A frame keeping a size without reason just feels like a bug and mostly leads to unwanted results.

Mostly, it looks just ugly, but there are critical use-cases, specially in scrolled frames.

When the BWidget autoscroll package is used, which displays scrollbars on demand, the scrollbars do not disappear if the contents is gone. And there is nothing, a programmer can do, as the Configure event does not fire on the scrolled frame widget.

Another example is the scrolledwindow example by Emiliano in ticket 2863003fff https://core.tcl.tk/tk/info/12006979562649c9 , where the solution 2 specific part may be removed (or is ignored).

A typical workaround is to configure the width/height manually after the last children was unmapped. Unfortunately, this fact may not be determined for example by scrolling widgets etc. An eventual Configure binding is not firing.

Example

Here is an example to ilustrate the issue. It consisting of a simple scrolling megawidget. The megawidget exposes a frame where a user may pack or grid other widgets and the scrollbar is adjusted following the changing content. This works well when widgets are added or removed. Only removing the last client will not update the scrollbar. With the proposed patch applied, it will update the scrollbar also when the last user widget is removed.

Please paste the code below to a wish console or execute it. On startup it shows on the console: requested frame height: 1

Then press the "+" button to add a user widget. The console output is: + requested frame height: 100 Technically, the frame ".c.f.i1" was packed into the client frame ".c.f". The client frame ".c.f" changes its requested size to hold the new child, which invokes the Convigure event and adjustes the scrolling region of the canvas. The new scrolling region is shown graphically by the scrollbar.

Then press the "-" button to remove the user widget. The console output is: - So, the child widget ".c.f.i1" is destroyed, but the frame ".c.f" does not rechange its requested size to 1x1 (initial value) but stays at 100x100 showing an empty plane. The scrollbar is not updated and the megawidget has no possibility to adjust that (expect additional user action to inform that the last child was removed).

One may also try to add two childs and to remove them. It gets clear, that the widget is resized on removel if it is not the last widget.

With the proposed patch applied, the removal of the last widget would restore the initial frame size of 1x1 which would invoke the Configure event and the scrollbar would be adjusted.

wm geometry . 90x90
# Button to add box on scrolling canvas
set itemNo 0
pack [button .b1 -command newBox -text +] -side left -fill y
proc newBox {} {
    puts +
    incr ::itemNo
    pack  [frame .c.f.i$::itemNo -borderwidth 4 -relief raised -bg red -width 100 -height 100] -side top
}
# Button to remove box on scrolling canvas
pack [button .b2 -command removeBox -text -] -side left -fill y
proc removeBox {} {
    puts -
    if {$::itemNo == 0} {return}
    destroy .c.f.i$::itemNo
    incr ::itemNo -1
}

# This is the scrolling megawidget which exposes frame .c.f for users to pack or grid clients
# It has no knowledge, when the user adds or removes clients, e.g. when +/- is pressed
pack [scrollbar .s -command {.c yview}] -side right -fill y
pack [canvas .c -borderwidth 0 -yscrollcommand {.s set}] -side left -fill both
frame .c.f
.c create window 0 0 -window .c.f -anchor nw -tags win
proc frameConfigure {} {
    set y [winfo reqheight .c.f]
    puts "requested frame height: $y"
    .c configure -scrollregion [list 0 0 100 $y]
}
frameConfigure
bind .c.f <Configure> frameConfigure

Proposal

The proposal is to change request size of the frame automatically to 1x1 if the last children is unpacked/ungridded/destroyed.

Koen Dankart has passed a patch for this solution containing 3 additional lines of C code. It is available in branch bug-d6b95ce492-alt: http://core.tcl.tk/tk/timeline?r=bug-d6b95ce492-alt&nd&c=2016-09-22+09%3A16%3A21&n=200 .

It just solves the issue by restoring initial size if the last children is unpacked/ungridded.

This is not backward compatible. But that is a side effect of fixing this bug.

Rejected Proposal

Another proposal is to invoke the virtual event <> when the last children is unpacked/ungridded/destroyed.

Emiliano has provided a ticket 2863003fff https://core.tcl.tk/tk/info/2863003fff with the implementation in branch bug-d6b95ce492 https://core.tcl.tk/tk/timeline?r=bug-d6b95ce492&nd&c=2016-09-21+06%3A32%3A55&n=200 :

The virtual event <> is defined which informs the master (a frame-like widget) that it has no child widget any more and that its size is not managed any more by grid/pack.

The program may bind to this event and resize to size 1x1:

   bind .c <<GeometryManager>> "resizeFrame %W"
   proc resizeFrame w {
      $w configure -height 1 -width 1
      $w configure -height 0 -width 0
   }

Discussion:

  • Backward compatible, no visual change to present code

  • Requires additional code to fix the bug (auto-resizing to 1x1)

  • The information of the last size is preserved and may be queried by the script (winfo reqheight)

  • Other sizes may be set on this event

  • May extend the first solution, maybe in another TIP.

  • May be emulated by a Configure event and some code, if the first solution is used.

  • Feels like a workaround to me.

Voted as yes and withdrawn due to implementation and compatibility issues

The TIP was voted yes. (only this section was added after voting)

Nevertheless, it was withdrawn due to the following compatibility issues:

  • flickering

Moen wrote on the core list on 2016-10-26 18:53:

I have to say though that I'm getting less sure about this TIP. I found some comments in the code indicating that the old behaviour was not so much a design choice, but rather an implementation issue. Specifically, this comment in tkGrid.c:1735.

    /*
     * If the master has no slaves anymore, then don't do anything at all:
     * just leave the master's size as-is. Otherwise there is no way to
     * "relinquish" control over the master so another geometry manager can
     * take over.
     */

The current patch for TIP 454 bypasses this by doing the Tk_GeometryRequest() immediately instead of at idle time. The result is that another geometry manager can still take over, but it introduces some flickering (collapse + expand):

  pack [text .t]
  pack forget .t; grid .t
  grid forget .t; pack .t
  • Additional Configure

The patch introduces an additional Configure event where applications may not be aware of. Brian worte on 2016-10-26 19:46:

It turns out that this "flicker" is also what is causing our tests to hang. Our UI is a complex set of nested and tabbed panes where the implementation that manages the panes relies on a complex dance of , and events to make the right things happen. The consequence of adding one more event is causing a hang at a [tkwait visibility $win] where $win never appears. $win is a single child in a frame that is unmanaged and (re)managed based on various conditions.

The hang is easily solved, but that means that the behavior is different. (the difference is not right or wrong, just different*) This difference is also demonstrated in the textWind failures. It could be asserted that these tk tests are confirming that the comment in the tkGrid.c has been faithfully implemented.

I cannot write just one piece of code that works in both 8.6.6 and 8.6.7. I contend that this sort of thing is forbidden in a minor patch release, and questionable in a major patch relase (e.g. 8.7). I'm struggling with this, but I think this kind of change might have to be deferred to 9.0.

*: the change in our code means removing some code. From a perspective of "less is more", the patch is "better".

Due to those issues, the TIP is withdrawn. A way to solve the issue in a compatible and working way is to use Emilianos additional virtual event, as described in the section 'rejected alternatives'.

Additional information and examples

Compatibility

Fixing the issue breaks visual compatibility. Nevertheless, as it is seen as a bug, this is OK.

Reference Implementation

Reference implementations are mentioned in the solution sections.

Copyright

This document has been placed in the public domain.

History