Skip to content

Commit 78cb838

Browse files
[MOD] GUI, layout: Preserve width/height of all panels
1 parent f6e99b1 commit 78cb838

9 files changed

Lines changed: 304 additions & 17 deletions

File tree

basex-core/src/main/java/org/basex/gui/GUIOptions.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ public final class GUIOptions extends Options {
6464
public static final Comment C_WINDOWS = new Comment("Windows");
6565

6666
/** GUI layout. */
67-
public static final StringOption VIEWS = new StringOption("VIEWS", GUIConstants.VIEWS);
67+
public static final StringOption LAYOUT = new StringOption("LAYOUT", GUIConstants.VIEWS);
68+
/** Width of the project view (pixels). */
69+
public static final NumberOption PROJECTSIZE = new NumberOption("PROJECTSIZE", 250);
6870

6971
/** GUI height. */
7072
public static final NumbersOption GUISIZE = new NumbersOption("GUISIZE", 1004, 748);

basex-core/src/main/java/org/basex/gui/layout/BaseXSplit.java

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.basex.gui.layout;
22

33
import java.awt.*;
4+
import java.util.function.*;
45

56
/**
67
* Project specific Split panel implementation.
@@ -22,6 +23,18 @@ public final class BaseXSplit extends BaseXBack implements LayoutManager {
2223
private double[] hiddenSize;
2324
/** Cached sizes (when panel is hidden). */
2425
private double[] cachedSize;
26+
/** Resize listener. */
27+
private Consumer<double[]> resized;
28+
29+
/** Index of the panel with a fixed pixel size ({@code -1}: proportional). */
30+
private int anchor = -1;
31+
/** Target pixel size of the anchored panel. */
32+
private int anchorSize;
33+
/** Minimum pixel size for the anchored panel and its neighbors. */
34+
private int anchorMin;
35+
36+
/** Panels have actually been resized during the current drag. */
37+
private boolean dragged;
2538

2639
/**
2740
* Constructor.
@@ -56,6 +69,42 @@ public void init(final double[] vis, final double[] hidden) {
5669
hiddenSize = hidden;
5770
}
5871

72+
/**
73+
* Sets the proportional panel sizes (sum must be 1.0).
74+
* @param sizes proportional sizes
75+
*/
76+
public void sizes(final double[] sizes) {
77+
propSize = sizes;
78+
}
79+
80+
/**
81+
* Registers a listener.
82+
* @param listener resize listener
83+
*/
84+
public void resized(final Consumer<double[]> listener) {
85+
resized = listener;
86+
}
87+
88+
/**
89+
* Anchors a panel to a fixed pixel size.
90+
* @param index panel index to anchor
91+
* @param size target pixel size
92+
* @param min minimum pixel size
93+
*/
94+
public void anchor(final int index, final int size, final int min) {
95+
anchor = index;
96+
anchorSize = size;
97+
anchorMin = min;
98+
}
99+
100+
/**
101+
* Returns the current pixel size of the anchored panel.
102+
* @return pixel size
103+
*/
104+
public int anchorSize() {
105+
return anchorSize;
106+
}
107+
59108
/**
60109
* Sets proportional panel sizes (sum must be 1.0).
61110
* @param show show/hide flag
@@ -84,6 +133,7 @@ public void visible(final boolean show) {
84133
void startDrag(final double p) {
85134
dragPos = p;
86135
dragSize = propSize.clone();
136+
dragged = false;
87137
}
88138

89139
/**
@@ -92,26 +142,68 @@ void startDrag(final double p) {
92142
* @param p current position
93143
*/
94144
void drag(final BaseXSplitSep sep, final double p) {
95-
if(dragSize == null) startDrag(p);
96-
97145
final Component[] m = getComponents();
98146
final int r = propSize.length;
99147
int q = 0;
100148
for(int n = 0; n < r - 1; ++n) {
101149
if(m[(n << 1) + 1] == sep) q = n + 1;
102150
}
103151
final double v = (dragPos - p) / (horizontal ? getWidth() : getHeight());
152+
final double min = anchor >= 0 ? (double) anchorMin / splitSize() : 0.0001;
104153
for(int i = 0; i < q; ++i) {
105-
if(dragSize[i] - v / q < 0.0001) return;
154+
if(dragSize[i] - v / q < min) return;
106155
}
107156
for(int i = q; i < r; ++i) {
108-
if(dragSize[i] + v / (r - q) < 0.0001) return;
157+
if(dragSize[i] + v / (r - q) < min) return;
109158
}
110159
for(int i = 0; i < q; ++i) propSize[i] = dragSize[i] - v / q;
111160
for(int i = q; i < r; ++i) propSize[i] = dragSize[i] + v / (r - q);
161+
dragged = true;
112162
revalidate();
113163
}
114164

165+
/**
166+
* Finishes a splitter drag: snapshots the anchored pixel size and notifies the listener once.
167+
*/
168+
void endDrag() {
169+
if(dragged) {
170+
// remember the dragged pixel size of the anchored panel
171+
if(anchor >= 0) {
172+
anchorSize = (int) Math.round(propSize[anchor] * splitSize());
173+
}
174+
if(resized != null) resized.accept(propSize);
175+
}
176+
dragged = false;
177+
}
178+
179+
/**
180+
* Computes the proportional size of the anchored panel for the given available space.
181+
* The panel keeps its target pixel size, but shrinks down to {@code min} pixels (and no
182+
* further) when space runs short, always reserving {@code min} pixels for its neighbors.
183+
* @param target target pixel size
184+
* @param min minimum pixel size
185+
* @param size available space in pixels
186+
* @return proportional size (between 0 and 1)
187+
*/
188+
static double anchorFraction(final int target, final int min, final int size) {
189+
if(size <= 0) return 0;
190+
int a = Math.min(target, size - min);
191+
a = Math.max(a, Math.min(min, size));
192+
return (double) a / size;
193+
}
194+
195+
/**
196+
* Returns the available space for the panels (total size minus the visible separators).
197+
* @return size in pixels
198+
*/
199+
private int splitSize() {
200+
int seps = propSize.length - 1;
201+
for(final double d : propSize) {
202+
if(d == 0) --seps;
203+
}
204+
return (horizontal ? getWidth() : getHeight()) - seps * SEPARATOR_SIZE;
205+
}
206+
115207
@Override
116208
public void addLayoutComponent(final String name, final Component comp) { }
117209

@@ -150,6 +242,21 @@ public void layoutContainer(final Container parent) {
150242

151243
// set bounds of all components
152244
final int sz = (horizontal ? w : h) - c * SEPARATOR_SIZE;
245+
246+
// enforce a fixed pixel size for the anchored panel (skip while hidden or dragging)
247+
if(anchor >= 0 && !dragged && propSize[anchor] != 0 && sz > 0) {
248+
final double frac = anchorFraction(anchorSize, anchorMin, sz);
249+
// distribute the remaining space among the other panels, keeping their relative sizes
250+
double rest = 0;
251+
for(int i = 0; i < propSize.length; i++) {
252+
if(i != anchor) rest += propSize[i];
253+
}
254+
for(int i = 0; i < propSize.length; i++) {
255+
propSize[i] = i == anchor ? frac :
256+
rest > 0 ? propSize[i] / rest * (1 - frac) : (1 - frac) / (propSize.length - 1);
257+
}
258+
}
259+
153260
double posD = 0;
154261
boolean invisible = false;
155262
for(c = 0; c < cl; c++) {

basex-core/src/main/java/org/basex/gui/layout/BaseXSplitSep.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public void mousePressed(final MouseEvent e) {
3232
public void mouseDragged(final MouseEvent e) {
3333
((BaseXSplit) getParent()).drag(BaseXSplitSep.this, pos(e));
3434
}
35+
@Override
36+
public void mouseReleased(final MouseEvent e) {
37+
((BaseXSplit) getParent()).endDrag();
38+
}
3539

3640
private double pos(final MouseEvent e) {
3741
final Point p = getLocationOnScreen();

basex-core/src/main/java/org/basex/gui/view/ViewComponent.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
* @author Christian Gruen
1010
*/
1111
interface ViewComponent {
12+
/**
13+
* Returns the proportional size (weight) of this component within its parent layout.
14+
* @return weight
15+
*/
16+
double weight();
17+
18+
/**
19+
* Assigns the proportional size (weight) of this component within its parent layout.
20+
* @param weight weight
21+
*/
22+
void weight(double weight);
23+
24+
/**
25+
* Formats a weight for the layout string (rounded to four decimal digits).
26+
* @param weight weight
27+
* @return string representation
28+
*/
29+
static String format(final double weight) {
30+
return Double.toString(Math.round(weight * 10_000) / 10_000.0);
31+
}
32+
1233
/**
1334
* Checks if the view layout is visible.
1435
* @return true if layout is visible

basex-core/src/main/java/org/basex/gui/view/ViewContainer.java

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public ViewContainer(final GUI gui, final View... view) {
6767
for(int v = 0; v < vl; ++v) views[v] = new ViewPanel(view[v]);
6868

6969
// build layout or use default if something goes wrong
70-
if(!buildLayout(gui.gopts.get(GUIOptions.VIEWS)) && !buildLayout(VIEWS)) {
70+
if(!buildLayout(gui.gopts.get(GUIOptions.LAYOUT)) && !buildLayout(VIEWS)) {
7171
Util.errln(Util.className(this) + ": could not build layout.");
7272
}
7373
}
@@ -84,16 +84,26 @@ public void updateViews() {
8484
if(ls.equals(layoutString)) return;
8585

8686
// remember new layout
87-
gui.gopts.set(GUIOptions.VIEWS, layout.layoutString(true));
87+
persistLayout();
8888
layoutString = ls;
8989

9090
// rebuild views
9191
removeAll();
92+
layout.persister = this::persistLayout;
9293
layout.addTo(this);
9394
validate();
9495
repaint();
9596
}
9697

98+
/**
99+
* Stores the current layout, including the proportional view sizes, in the GUI options.
100+
* Invoked on structural changes and whenever the user drags a splitter (see
101+
* {@link ViewLayout#persister}).
102+
*/
103+
private void persistLayout() {
104+
gui.gopts.set(GUIOptions.LAYOUT, layout.layoutString(true));
105+
}
106+
97107
@Override
98108
public void paintComponent(final Graphics g) {
99109
super.paintComponent(g);
@@ -120,6 +130,8 @@ void dropPanel() {
120130
if(source == null) return;
121131

122132
if(location != null) {
133+
// the model already holds the current sizes (kept in sync on every splitter drag),
134+
// so the moved components carry their weights through the restructuring
123135
final ViewComponent comp = layout.delete(source);
124136
if(comp instanceof final ViewLayout vl) layout = vl;
125137

@@ -315,24 +327,42 @@ private boolean buildLayout(final String string) {
315327
layout = null;
316328
int nv = 0;
317329
final Stack<ViewLayout> layouts = new Stack<>();
318-
final StringTokenizer tokens = new StringTokenizer(string);
319-
while(tokens.hasMoreTokens()) {
320-
final String token = tokens.nextToken();
321-
if(Strings.eq(token, "H", "V")) {
330+
// collect all tokens (allows look-ahead for the optional weight tokens)
331+
final ArrayList<String> tokens = new ArrayList<>();
332+
final StringTokenizer st = new StringTokenizer(string);
333+
while(st.hasMoreTokens()) tokens.add(st.nextToken());
334+
335+
final int ts = tokens.size();
336+
for(int t = 0; t < ts; t++) {
337+
final String token = tokens.get(t);
338+
final ViewComponent comp;
339+
if("-".equals(token)) {
340+
layouts.pop();
341+
continue;
342+
} else if(Strings.eq(token, "H", "V")) {
322343
final ViewLayout view = new ViewLayout("H".equals(token));
323344
if(layouts.isEmpty()) {
324345
layout = view;
325346
} else {
326347
layouts.peek().add(view);
327348
}
328349
layouts.add(view);
329-
} else if("-".equals(token)) {
330-
layouts.pop();
350+
comp = view;
331351
} else {
332352
final ViewPanel view = getView(token);
333353
if(view == null) return false;
334354
layouts.peek().add(view);
335355
++nv;
356+
comp = view;
357+
}
358+
// parse the optional proportional size that may follow a layout or view token
359+
if(t + 1 < ts) {
360+
final String next = tokens.get(t + 1);
361+
final char ch = next.charAt(0);
362+
if(ch >= '0' && ch <= '9' || ch == '.') {
363+
comp.weight(Double.parseDouble(next));
364+
++t;
365+
}
336366
}
337367
}
338368
return nv == views.length;

0 commit comments

Comments
 (0)