Skip to content

Commit a01737f

Browse files
andyclaude
andcommitted
Merge wip/sym-float: tool panes share the column and scroll, Symmetry no longer drawn over the viewer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTQ7mniRtDdhU7VF9tEVBD
2 parents ce79876 + a9155a9 commit a01737f

7 files changed

Lines changed: 308 additions & 5 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,15 @@ if(PYTHON3_EXECUTABLE)
20872087
set_tests_properties(apps_pbc_ux PROPERTIES TIMEOUT 900
20882088
SKIP_RETURN_CODE 77)
20892089

2090+
# The default tool panes of a calculation not yet run (Symmetry last) stay
2091+
# docked, clear of the viewer, at 1024x600, 1366x768 and 1400x900.
2092+
add_test(NAME apps_tool_column
2093+
COMMAND ${PYTHON3_EXECUTABLE}
2094+
${CMAKE_CURRENT_SOURCE_DIR}/tests/apps/tool_column_test.py
2095+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/apps)
2096+
set_tests_properties(apps_tool_column PROPERTIES TIMEOUT 900
2097+
SKIP_RETURN_CODE 77)
2098+
20902099
# Every app's main window and every codereg Theory/Runtype dialog fits a
20912100
# small screen (#189): measured on private Xvfb screens, no window may
20922101
# extend past the screen. Same installed tree and isolation as `apps`.

‎docs/claude/wx-viewer/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Read this before changing any dialog, panel, sizer, grid, ewx control or the Ope
1818
- [`ComputeMoCmd` caches a grid before computing it; a failed Compute must remove it (#239)](mo-grid-cache-keeps-a-failed-compute.md)
1919
- [A property panel is built when it is on screen, not when wxAUI Show()s it in a hidden floating frame](mo-diagram-builds-when-a-calculation-opens.md)
2020
- [A property panel's SetSizeHints() lifts its docked pane's floor to the content height](property-panel-sizehints-lift-the-dock-floor.md)
21+
- [Tool panes that do not fit the right-hand column get no height and are drawn over the viewer](tool-column-shares-height-when-panes-do-not-fit.md)
2122
- [The redraw sensor must not be drained inside `schedule()`: `SoWxEventHandler::setUpCallbacks()` only wakes idle](open-inventors-redraw-sensor-is-a-one.md)
2223
- [`ECCE_USE_COIN` (default ON) builds the viewer against Coin3D; `=OFF` keeps the vendored core for one release](ecce-use-coin-builds-the-viewer-against-coin3d.md)
2324
- [Coin SORTED_LAYERS_BLEND is the default lobe transparency (6 passes, alpha canvas); slow scenes fall back to SCREEN_DOOR](coin-sorted-layers-blend-is-the-default-lobe-transparency.md)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
type: pitfall
3+
title: "Tool panes that do not fit the right-hand column get no height and are drawn over the viewer; fitToolColumn shrinks their floors and their controls scroll"
4+
area: wx-viewer
5+
paths: [src/apps/builder/BuilderPanels.C, src/apps/builder/Builder.C, src/apps/builder/Builder.H, tests/apps/tool_column_test.py]
6+
issues: []
7+
---
8+
The tools a calculation not yet run opens with (Open structures, Build,
9+
Coordinates or Selection, Symmetry, Log) need more height than the capped
10+
Builder window has on 1024x600 up to 1920x1080 screens. wxAUI lays out the
11+
panes that fit at their `min_size` and gives the rest height 0. wxGTK keeps
12+
a window at least its own min size (`SetSizeHints` in each panel's
13+
`Create`), and AUI does not move a pane it gave no room. The window
14+
then stays where it was created, so Symmetry was drawn over the viewer.
15+
`makeRoomFor` only handles a pane opened from the Tools menu.
16+
17+
`addToolPanel` moves each tool panel's controls into a vertically scrolled
18+
child (`scrollToolContent`; lists, tables and the Periodic Builder already
19+
scroll) and records the pane's floor in `p_paneFloor`. After every layout
20+
(`updatePanes`, and after a frame resize via `OnSize`), `fitToolColumn`
21+
compares the sum of the right-hand column's floors with the height its
22+
panes got. If they do not fit, it scales every floor by the same factor
23+
(at least 40 px). If they do, it puts the floors back. `paneInfoForSave`
24+
saves the full floor. `contentFixedHeight` looks through the scrolled
25+
child for the content height.
26+
27+
Test: `tests/apps/tool_column_test.py` (`apps_tool_column`).

‎src/apps/builder/Builder.C‎

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2677,6 +2677,14 @@ void Builder::helpBuilderMenuitemClick( wxCommandEvent& event )
26772677
void Builder::OnSize( wxSizeEvent& event )
26782678
{
26792679
BuilderGUI::OnSize(event);
2680+
// AUI lays out a resize itself; the tool column's floors follow after.
2681+
if (!p_fitPending) {
2682+
p_fitPending = true;
2683+
CallAfter([this]() {
2684+
p_fitPending = false;
2685+
if (fitToolColumn()) updatePanes();
2686+
});
2687+
}
26802688
}
26812689

26822690

@@ -3586,12 +3594,16 @@ wxString Builder::paneInfoForSave(wxAuiPaneInfo &pane)
35863594
{
35873595
map<wxWindow*, FoldState>::iterator it = p_folded.find(pane.window);
35883596
if (it == p_folded.end()) {
3597+
wxAuiPaneInfo copy(pane);
35893598
if (p_tabHidden.count(pane.window)) {
3590-
wxAuiPaneInfo shown(pane);
3591-
shown.Show(true);
3592-
return p_mgr.SavePaneInfo(shown);
3599+
copy.Show(true);
35933600
}
3594-
return p_mgr.SavePaneInfo(pane);
3601+
// A floor lowered for this screen is not part of the layout.
3602+
map<wxWindow*, int>::const_iterator floor = p_paneFloor.find(pane.window);
3603+
if (floor != p_paneFloor.end() && copy.min_size.y < floor->second) {
3604+
copy.MinSize(wxSize(copy.min_size.x, floor->second));
3605+
}
3606+
return p_mgr.SavePaneInfo(copy);
35953607
}
35963608
wxAuiPaneInfo copy(pane);
35973609
if (p_tabHidden.count(pane.window)) {
@@ -5643,9 +5655,51 @@ static int contentMinWidth(wxWindow *window)
56435655
// A fixed pane cannot be resized by the user, so its height is what its
56445656
// sizer needs in the current font. A degenerate answer from a panel
56455657
// not yet laid out falls back to the old flat 150.
5658+
static const char TOOL_SCROLL_NAME[] = "ecceToolScroll";
5659+
5660+
static wxWindow *toolScroller(wxWindow *window)
5661+
{
5662+
if (!window) return 0;
5663+
for (wxWindow *child : window->GetChildren()) {
5664+
if (child->GetName() == TOOL_SCROLL_NAME) return child;
5665+
}
5666+
return 0;
5667+
}
5668+
5669+
// Moves a tool panel's controls into a vertically scrolled window of its
5670+
// own, so that its window has no height floor: a column too short for
5671+
// every open pane can then shrink it (fitToolColumn) and it scrolls.
5672+
// Without that, wxGTK keeps a window at least its min size and the pane
5673+
// AUI could not fit was drawn where it was created, over the viewer.
5674+
static void scrollToolContent(wxWindow *panel)
5675+
{
5676+
wxSizer *content = panel->GetSizer();
5677+
if (!content || toolScroller(panel)) return;
5678+
panel->Layout();
5679+
const wxSize need = content->GetMinSize();
5680+
wxScrolledWindow *scroll = new wxScrolledWindow(
5681+
panel, wxID_ANY, wxDefaultPosition, panel->GetClientSize(),
5682+
wxVSCROLL | wxTAB_TRAVERSAL | wxNO_BORDER, TOOL_SCROLL_NAME);
5683+
std::vector<wxWindow*> kids(panel->GetChildren().begin(),
5684+
panel->GetChildren().end());
5685+
for (wxWindow *child : kids) {
5686+
if (child != scroll && !child->IsTopLevel()) child->Reparent(scroll);
5687+
}
5688+
panel->SetSizer(0, false);
5689+
scroll->SetSizer(content);
5690+
scroll->SetScrollRate(0, 10);
5691+
scroll->SetMinSize(wxSize(need.x, 1));
5692+
wxBoxSizer *outer = new wxBoxSizer(wxVERTICAL);
5693+
outer->Add(scroll, 1, wxEXPAND);
5694+
panel->SetSizer(outer);
5695+
panel->SetMinSize(wxSize(need.x, -1));
5696+
}
5697+
56465698
static int contentFixedHeight(wxWindow *window)
56475699
{
5648-
wxSizer *sizer = window ? window->GetSizer() : 0;
5700+
wxWindow *scroll = toolScroller(window);
5701+
wxSizer *sizer = scroll ? scroll->GetSizer()
5702+
: window ? window->GetSizer() : 0;
56495703
if (!sizer) {
56505704
return FIXED_PANE_HEIGHT_FALLBACK;
56515705
}
@@ -5709,6 +5763,13 @@ void Builder::addToolPanel(wxWindow *panel, const string& name,
57095763
pinfo.MinSize(wxSize(minWidth, 150));
57105764
}
57115765

5766+
// Lists, tables and the Periodic Builder scroll by themselves.
5767+
if (name != NAME_TOOL_CONTEXT && name != NAME_TOOL_ATOM_TABLE &&
5768+
name != NAME_TOOL_RESIDUE_TABLE && name != NAME_TOOL_PBC) {
5769+
scrollToolContent(panel);
5770+
}
5771+
p_paneFloor[panel] = pinfo.min_size.y;
5772+
57125773
// NOTE: the OptionsButton() caption button (ewxAUI addition) has no
57135774
// stock wx3.2 wxAuiPaneInfo equivalent and is dropped here - see
57145775
// EwxAuiCompat.H.

‎src/apps/builder/Builder.H‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ class Builder: public BuilderGUI,
411411
void updatePanes(bool allowSwitch=false);
412412
void initShownPanels();
413413
void makeRoomFor(const wxString& name);
414+
bool fitToolColumn();
414415
void setColumnTab(int tab, bool save=true);
415416
void selectDetail(const string& name);
416417
void setDetail(wxWindow *win);
@@ -558,6 +559,10 @@ class Builder: public BuilderGUI,
558559
// Property panes folded to their caption bar, with what to restore.
559560
struct FoldState { wxSize best; wxSize min; bool resizable; int proportion; };
560561
map<wxWindow*, FoldState> p_folded;
562+
// Each tool pane's own height floor; fitToolColumn() lowers the
563+
// pane's min_size below it while the column is too short.
564+
map<wxWindow*, int> p_paneFloor;
565+
bool p_fitPending = false;
561566
void toggleFold(wxWindow *win);
562567
void unfoldPane(wxAuiPaneInfo &pane);
563568
wxString paneInfoForSave(wxAuiPaneInfo &pane);

‎src/apps/builder/BuilderPanels.C‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ void Builder::updatePanes(bool allowSwitch)
570570
PanelGuard guard(p_panelBuildDepth);
571571
syncColumn(allowSwitch);
572572
p_mgr.Update();
573+
fitToolColumn();
573574
updating = false;
574575
if (again) {
575576
again = false;
@@ -1039,6 +1040,57 @@ void Builder::makeRoomFor(const wxString& name)
10391040
}
10401041

10411042

1043+
/**
1044+
* Shares the right-hand column among its tool panes when their own height
1045+
* floors do not fit in it. AUI lays out the panes that fit and gives the
1046+
* rest no height (Symmetry, the last default pane, on a 1400x900 screen);
1047+
* each pane's floor is scaled down by the same factor instead, and its
1048+
* controls scroll. On a screen with room every pane keeps its floor.
1049+
* Lays out again when it changes a floor; returns whether it did.
1050+
*/
1051+
bool Builder::fitToolColumn()
1052+
{
1053+
// Below this a pane is a caption with a scroll bar.
1054+
static const int SHRUNK_FLOOR = 40;
1055+
bool changed = false;
1056+
for (int round = 0; round < 3; ++round) {
1057+
vector<wxAuiPaneInfo*> column;
1058+
int avail = 0, need = 0;
1059+
wxAuiPaneInfoArray &panes = p_mgr.GetAllPanes();
1060+
for (size_t i = 0; i < panes.GetCount(); ++i) {
1061+
wxAuiPaneInfo &pane = panes.Item(i);
1062+
if (!pane.IsShown() || pane.IsFloating() || pane.IsToolbar() ||
1063+
!pane.window || pane.dock_direction != wxAUI_DOCK_RIGHT ||
1064+
pane.dock_layer < 1 || p_folded.count(pane.window) ||
1065+
!p_paneFloor.count(pane.window)) {
1066+
continue;
1067+
}
1068+
column.push_back(&pane);
1069+
avail += std::max(0, pane.rect.height);
1070+
need += p_paneFloor[pane.window];
1071+
}
1072+
// Not laid out yet: a frame not yet sized would shrink everything.
1073+
if (avail <= 0) break;
1074+
bool now = false;
1075+
for (size_t i = 0; i < column.size(); ++i) {
1076+
wxAuiPaneInfo &pane = *column[i];
1077+
const int floor = p_paneFloor[pane.window];
1078+
const int want = need <= avail ? floor :
1079+
std::min(floor, std::max(SHRUNK_FLOOR,
1080+
(int)((long)floor * avail / need)));
1081+
if (pane.min_size.y != want) {
1082+
pane.MinSize(wxSize(pane.min_size.x, want));
1083+
now = true;
1084+
}
1085+
}
1086+
if (!now) break;
1087+
changed = true;
1088+
p_mgr.Update();
1089+
}
1090+
return changed;
1091+
}
1092+
1093+
10421094
// ---- test hook ------------------------------------------------------
10431095

10441096
/**

‎tests/apps/tool_column_test.py‎

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/usr/bin/env python3
2+
"""The Builder's default tool panes stay docked on small screens, headless.
3+
4+
tests/apps/tool_column_test.py [--png DIR] [--sizes 1024x600,...]
5+
6+
A calculation that has not been run opens with the building tools: Open
7+
structures, Build, Coordinates or Selection, Symmetry and the Log. On a
8+
laptop screen these do not fit the right-hand column at their full height;
9+
the last of them (Symmetry) used to get no height at all and its window was
10+
drawn where it was created, over the 3-D viewer. For each screen size and
11+
the classic and list + detail layouts every shown tool pane must be docked,
12+
a child of the frame, inside the window, at least MIN_HEIGHT high, and clear
13+
of the viewer.
14+
15+
Same installed tree and isolation as run_tests.py (ECCE_TEST_HOME,
16+
ECCE_TEST_WRAPPERS); exit 77 (skip) without an install or Xvfb.
17+
"""
18+
import argparse
19+
import os
20+
import sys
21+
22+
HERE = os.path.dirname(os.path.abspath(__file__))
23+
sys.path.insert(0, HERE)
24+
25+
import apps # noqa: E402
26+
import fixture # noqa: E402
27+
import pbc_ux_test # noqa: E402
28+
29+
SIZES = ("1024x600", "1366x768", "1400x900")
30+
MODES = ("classic", "detail")
31+
TOOLS = ["Open_structures", "Build", "Coordinates", "Selection", "Symmetry",
32+
"Atom_Table"]
33+
MIN_HEIGHT = 30
34+
35+
36+
def rects(st):
37+
"""{pane name: (x, y, w, h)} of every shown pane."""
38+
out = {}
39+
for line in st.get("raw", []):
40+
name = line.split('"')[1]
41+
x, y, w, h = (int(v) for v in line.split('"')[2].split()[1:5])
42+
out[name] = (x, y, w, h)
43+
return out
44+
45+
46+
def problems(st, label):
47+
if st is None:
48+
return ["%s: no state written" % label]
49+
found = []
50+
sym = st["tools"].get("Symmetry")
51+
if not sym or not sym["shown"]:
52+
found.append("%s: Symmetry not shown (%s)"
53+
% (label, ", ".join(st["shown"])))
54+
cw, ch = st["client"]
55+
viewer = rects(st).get("Viewer")
56+
for name, t in sorted(st["tools"].items()):
57+
if not t["shown"]:
58+
continue
59+
where = "%s, %s %dx%d+%d+%d" % (label, name, t["w"], t["h"],
60+
t["x"], t["y"])
61+
if t["floating"]:
62+
found.append(where + ": floating")
63+
if not t["child"]:
64+
found.append(where + ": window left the frame")
65+
if t["h"] < MIN_HEIGHT:
66+
found.append(where + ": no room in the column")
67+
if (t["x"] < 0 or t["y"] < 0 or t["x"] + t["w"] > cw or
68+
t["y"] + t["h"] > ch):
69+
found.append(where + ": outside the %dx%d window" % (cw, ch))
70+
if viewer:
71+
vx, vy, vw, vh = viewer
72+
if (t["x"] < vx + vw and vx < t["x"] + t["w"] and
73+
t["y"] < vy + vh and vy < t["y"] + t["h"]):
74+
found.append(where + ": covers the viewer")
75+
return found
76+
77+
78+
def main():
79+
parser = argparse.ArgumentParser(description=__doc__)
80+
parser.add_argument("--png", help="write one screenshot per case here")
81+
parser.add_argument("--sizes", default=",".join(SIZES))
82+
args = parser.parse_args()
83+
84+
import isolate
85+
import xdisplay
86+
87+
for var in ("ECCE_DATASERVER_PORT", "ECCE_BROKER_PORT"):
88+
os.environ.pop(var, None)
89+
os.environ["ECCE_REALUSER"] = fixture.USER
90+
if not apps.installed("builder"):
91+
print("SKIP no installed builder")
92+
return 77
93+
settings = isolate.apply(apps.INSTALL)
94+
isolate.killLeftovers(settings["ECCE_REALUSERHOME"])
95+
os.environ["ECCE_NO_REAP"] = "1"
96+
97+
failures, checks = [], 0
98+
for size in args.sizes.split(","):
99+
xdisplay.SCREEN = size + "x24"
100+
with xdisplay.Display() as display:
101+
log = []
102+
apps.startServices(display, log)
103+
try:
104+
if not all(apps.serviceState(display).values()):
105+
print("FAIL services did not come up: %s" % log)
106+
return 1
107+
fixture.ensureRealUserAccount()
108+
restore = fixture.settleUpgradeNotices()
109+
try:
110+
url, error = pbc_ux_test.install("tool-column",
111+
pbc_ux_test.notRun)
112+
if error:
113+
print("FAIL cannot install fixture: " + error)
114+
return 1
115+
auth = fixture.authFile(
116+
os.path.join(fixture.stateHome(), ".ECCE",
117+
"auth.pipe"), user=fixture.USER)
118+
for mode in MODES:
119+
checks += 1
120+
label = "%s, %s layout" % (size, mode)
121+
script = ["columntab 0"]
122+
if args.png:
123+
os.makedirs(args.png, exist_ok=True)
124+
script.append("xshot " + os.path.join(
125+
args.png, "tools-%s-%s.png" % (size, mode)))
126+
script.append("panestate state " + " ".join(TOOLS))
127+
work, found, out = pbc_ux_test.runScene(
128+
display, "tools-%s-%s" % (size, mode), script,
129+
url, ("-pipe", auth, "-context", url),
130+
env={"ECCE_PANEL_MODE": mode})
131+
found += problems(pbc_ux_test.readState(
132+
os.path.join(work, "state.txt")), label)
133+
print("%s %s" % ("FAIL" if found else "ok ", label))
134+
for p in found:
135+
print(" " + p)
136+
failures += found
137+
fixture.remove("tool-column")
138+
finally:
139+
restore()
140+
finally:
141+
apps.stopServices(display)
142+
print("%s %d cases, %d problems" % ("FAIL" if failures else "PASS",
143+
checks, len(failures)))
144+
return 1 if failures else 0
145+
146+
147+
if __name__ == "__main__":
148+
sys.exit(main())

0 commit comments

Comments
 (0)