diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AbstractFinanceView.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AbstractFinanceView.java
index 7c873cd0c4..1c374fbd7a 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AbstractFinanceView.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AbstractFinanceView.java
@@ -92,12 +92,12 @@ protected final void updateTitle(String title)
if (!this.title.isDisposed())
{
String escaped = TextUtil.tooltip(title);
- boolean isEqual = escaped.equals(this.title.getText());
this.titleText = title;
this.title.setText(escaped);
- if (!isEqual)
- this.title.getParent().layout(true);
+ this.title.setData(AdaptiveHeaderLayout.KEY_ORIGINAL_TITLE, escaped);
+ this.title.setToolTipText(null);
+ this.title.getParent().layout(true);
}
}
@@ -232,7 +232,9 @@ private final Control createHeader(Composite parent)
titleText = getDefaultTitle();
title = new Label(header, SWT.NONE);
title.setData(UIConstants.CSS.CLASS_NAME, UIConstants.CSS.HEADING1);
- title.setText(TextUtil.tooltip(titleText));
+ var escaped = TextUtil.tooltip(titleText);
+ title.setText(escaped);
+ title.setData(AdaptiveHeaderLayout.KEY_ORIGINAL_TITLE, escaped);
title.setForeground(Colors.SIDEBAR_TEXT);
title.setBackground(header.getBackground());
@@ -254,11 +256,8 @@ private final Control createHeader(Composite parent)
// add buttons only after (!) creation of tool bar to avoid flickering
addButtons(actionToolBar);
- // layout
- GridLayoutFactory.fillDefaults().numColumns(3).margins(5, 5).applyTo(header);
- GridDataFactory.fillDefaults().applyTo(title);
- GridDataFactory.fillDefaults().grab(true, false).align(SWT.END, SWT.CENTER).applyTo(wrapper);
- GridDataFactory.fillDefaults().applyTo(tb2);
+ // use adaptive layout instead of grid layout
+ header.setLayout(new AdaptiveHeaderLayout());
return header;
}
@@ -335,7 +334,7 @@ public void dispose()
context.dispose();
}
-
+
public final EditorActivationState getEditorActivationState()
{
return editorActivationState;
@@ -345,7 +344,7 @@ public final Control getControl()
{
return top;
}
-
+
public void setFocus()
{
getControl().setFocus();
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AdaptiveHeaderLayout.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AdaptiveHeaderLayout.java
new file mode 100644
index 0000000000..6aece1bf38
--- /dev/null
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/AdaptiveHeaderLayout.java
@@ -0,0 +1,257 @@
+package name.abuchen.portfolio.ui.editor;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
+import org.eclipse.swt.widgets.ToolBar;
+
+/**
+ * A layout manager that adaptively allocates space between title, view toolbar,
+ * and action toolbar. Priority order:
+ *
+ * - Action toolbar - always gets full preferred width (highest priority)
+ * - View toolbar minimum - always shows at least 1 item + chevron
+ * - Title - gets remaining space, truncated with ellipsis if needed
+ * - View toolbar expansion - expands into unused title space
+ *
+ */
+public class AdaptiveHeaderLayout extends Layout
+{
+ public static final String KEY_ORIGINAL_TITLE = "originalTitle"; //$NON-NLS-1$
+
+ private static final int HORIZONTAL_SPACING = 5;
+ private static final int VERTICAL_MARGIN = 5;
+ private static final String ELLIPSIS = "…"; //$NON-NLS-1$
+
+ private final int marginWidth;
+ private final int marginHeight;
+
+ public AdaptiveHeaderLayout()
+ {
+ this(HORIZONTAL_SPACING, VERTICAL_MARGIN);
+ }
+
+ public AdaptiveHeaderLayout(int marginWidth, int marginHeight)
+ {
+ this.marginWidth = marginWidth;
+ this.marginHeight = marginHeight;
+ }
+
+ @Override
+ protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache)
+ {
+ Control[] children = composite.getChildren();
+ if (children.length != 3)
+ throw new IllegalArgumentException(
+ "AdaptiveHeaderLayout expects exactly 3 children: title, viewToolbarWrapper, actionToolbar"); //$NON-NLS-1$
+
+ var titleLabel = (Label) children[0];
+ var viewToolbarWrapper = (Composite) children[1];
+ var actionToolbar = (ToolBar) children[2];
+
+ // Calculate preferred sizes
+ var titleSize = titleLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
+ var viewSize = viewToolbarWrapper.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
+ var actionSize = actionToolbar.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
+
+ // Width is sum of all preferred widths plus margins
+ var width = titleSize.x + viewSize.x + actionSize.x + 2 * marginWidth + 2 * HORIZONTAL_SPACING;
+
+ // Height is maximum of all heights plus margins
+ var height = Math.max(titleSize.y, Math.max(viewSize.y, actionSize.y)) + 2 * marginHeight;
+
+ if (wHint != SWT.DEFAULT)
+ width = Math.min(width, wHint);
+ if (hHint != SWT.DEFAULT)
+ height = Math.min(height, hHint);
+
+ return new Point(width, height);
+ }
+
+ @Override
+ protected void layout(Composite composite, boolean flushCache)
+ {
+ Control[] children = composite.getChildren();
+ if (children.length != 3)
+ throw new IllegalArgumentException(
+ "AdaptiveHeaderLayout expects exactly 3 children: title, viewToolbarWrapper, actionToolbar"); //$NON-NLS-1$
+
+ var titleLabel = (Label) children[0];
+ var viewToolbarWrapper = (Composite) children[1];
+ var actionToolbar = (ToolBar) children[2];
+
+ var bounds = composite.getClientArea();
+ var availableWidth = bounds.width - 2 * marginWidth;
+ var availableHeight = bounds.height - 2 * marginHeight;
+
+ // Step 1: Reserve space for action toolbar (always gets full preferred
+ // width)
+ var actionSize = actionToolbar.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
+ int actionWidth = actionSize.x;
+
+ // Step 2: Calculate minimum space needed for view toolbar (1 item +
+ // chevron)
+ int viewMinWidth = calculateMinViewToolbarWidth(viewToolbarWrapper);
+
+ // Step 3: Calculate remaining space available for title
+ int spacingTotal = 2 * HORIZONTAL_SPACING;
+ int remainingWidth = availableWidth - actionWidth - viewMinWidth - spacingTotal;
+
+ // Step 4: Calculate actual title width (may be truncated)
+ int titleWidth = calculateTitleWidth(titleLabel, Math.max(0, remainingWidth), flushCache);
+
+ // Step 5: Give any leftover space back to view toolbar
+ int extraSpace = remainingWidth - titleWidth;
+ int viewActualWidth = viewMinWidth + Math.max(0, extraSpace);
+
+ // Step 6: Position all components
+ int y = marginHeight + (availableHeight - actionSize.y) / 2;
+
+ // title at left
+ titleLabel.setBounds(marginWidth, y, titleWidth, actionSize.y);
+
+ // Action toolbar at right
+ int actionX = bounds.width - marginWidth - actionWidth;
+ actionToolbar.setBounds(actionX, y, actionWidth, actionSize.y);
+
+ // View toolbar between title and action toolbar (right-aligned within
+ // its space)
+ int viewX = actionX - HORIZONTAL_SPACING - viewActualWidth;
+ viewToolbarWrapper.setBounds(viewX, marginHeight, viewActualWidth, availableHeight);
+
+ // Update the view toolbar wrapper's layout with the actual available
+ // width
+ if (viewToolbarWrapper.getLayout() instanceof ToolBarPlusChevronLayout layout)
+ {
+ layout.setMaxWidth(viewActualWidth);
+ }
+ }
+
+ private int calculateMinViewToolbarWidth(Composite viewToolbarWrapper)
+ {
+ // Find the toolbar inside the wrapper
+ ToolBar toolBar = findToolBar(viewToolbarWrapper);
+ if (toolBar == null)
+ return 50; // Fallback minimum width
+
+ // Get the first toolbar item's width + chevron space
+ var items = toolBar.getItems();
+ if (items.length == 0)
+ return 50;
+
+ int firstItemWidth = items[0].getBounds().width;
+ if (firstItemWidth == 0)
+ {
+ // if bounds aren't computed yet, use a reasonable estimate
+ firstItemWidth = 50;
+ }
+
+ // add space for chevron (approximately 16px + padding)
+ return firstItemWidth + 20;
+ }
+
+ private int calculateTitleWidth(Label titleLabel, int availableWidth, boolean flushCache)
+ {
+ if (availableWidth <= 0)
+ return 0;
+
+ var originalText = getTitleText(titleLabel);
+
+ var currentLabel = titleLabel.getText();
+ if (currentLabel.endsWith(ELLIPSIS))
+ titleLabel.setText(originalText);
+
+ var preferredSize = titleLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
+
+ // if the preferred size fits, use it
+ if (preferredSize.x <= availableWidth)
+ return preferredSize.x;
+
+ // Otherwise, we need to truncate the text
+ if (originalText == null || originalText.isEmpty())
+ return 0;
+
+ // Measure text width and truncate if necessary
+ GC gc = new GC(titleLabel);
+ try
+ {
+ int ellipsisWidth = gc.textExtent(ELLIPSIS).x;
+ int availableForText = availableWidth - ellipsisWidth;
+
+ if (availableForText <= 0)
+ return 0;
+
+ var truncatedText = truncateText(gc, originalText, availableForText);
+ var displayText = truncatedText.isEmpty() ? "" : truncatedText + ELLIPSIS; //$NON-NLS-1$
+
+ if (!displayText.equals(titleLabel.getText()))
+ {
+ titleLabel.setText(displayText);
+ titleLabel.setToolTipText(originalText);
+ }
+
+ return gc.textExtent(displayText).x;
+ }
+ finally
+ {
+ gc.dispose();
+ }
+ }
+
+ private String getTitleText(Label titleLabel)
+ {
+ var originalTitle = titleLabel.getData(KEY_ORIGINAL_TITLE);
+ if (originalTitle instanceof String s)
+ return s;
+ else
+ return titleLabel.getText();
+ }
+
+ private String truncateText(GC gc, String text, int availableWidth)
+ {
+ if (availableWidth <= 0)
+ return ""; //$NON-NLS-1$
+
+ var textWidth = gc.textExtent(text).x;
+ if (textWidth <= availableWidth)
+ return text;
+
+ // binary search for the longest substring that fits
+ int left = 0;
+ int right = text.length();
+ var bestFit = ""; //$NON-NLS-1$
+
+ while (left <= right)
+ {
+ int mid = (left + right) / 2;
+ String candidate = text.substring(0, mid);
+ int candidateWidth = gc.textExtent(candidate).x;
+
+ if (candidateWidth <= availableWidth)
+ {
+ bestFit = candidate;
+ left = mid + 1;
+ }
+ else
+ {
+ right = mid - 1;
+ }
+ }
+
+ return bestFit;
+ }
+
+ private ToolBar findToolBar(Composite composite)
+ {
+ for (Control child : composite.getChildren())
+ {
+ if (child instanceof ToolBar toolBar)
+ return toolBar;
+ }
+ return null;
+ }
+}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/ToolBarPlusChevronLayout.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/ToolBarPlusChevronLayout.java
index 7a818bac69..a676dccdef 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/ToolBarPlusChevronLayout.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/ToolBarPlusChevronLayout.java
@@ -35,6 +35,7 @@
private int alignment = SWT.LEFT;
private ImageHyperlink chevron;
private Menu chevronMenu;
+ private int maxWidth = -1; // -1 means no limit
private List invisible = new ArrayList<>();
@@ -72,6 +73,15 @@ public void linkActivated(HyperlinkEvent e)
});
}
+ /**
+ * Sets the maximum width available for the toolbar. If set, the layout will
+ * use this width instead of the composite's bounds width.
+ */
+ public void setMaxWidth(int maxWidth)
+ {
+ this.maxWidth = maxWidth;
+ }
+
@Override
protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache)
{
@@ -87,6 +97,10 @@ protected void layout(Composite composite, boolean flushCache)
ToolBar toolBar = getToolBar(composite);
Rectangle availableBounds = composite.getBounds();
+
+ // Use maxWidth if set, otherwise use the composite's bounds width
+ int availableWidth = maxWidth > 0 ? maxWidth : availableBounds.width;
+
Point chevronSize = this.chevron.computeSize(SWT.DEFAULT, SWT.DEFAULT);
// check which toolbar items are visible
@@ -106,9 +120,9 @@ protected void layout(Composite composite, boolean flushCache)
// b) if the current item uses up the space for the chevron that
// will have to be shown for the next item
- if ((width + itemBounds.width > availableBounds.width) // a
+ if ((width + itemBounds.width > availableWidth) // a
|| ((index + 1 < items.length) && // b
- (width + itemBounds.width + chevronSize.x > availableBounds.width)))
+ (width + itemBounds.width + chevronSize.x > availableWidth)))
{
// the tool item is not visible anymore
for (int jj = index; jj < items.length; jj++)
@@ -133,9 +147,11 @@ protected void layout(Composite composite, boolean flushCache)
if (chevron.isVisible())
chevron.setVisible(false);
- // all items are visible - give the tool bar the full space, the
- // alignment is up to the tool bar itself
- toolBar.setBounds(0, 0, availableBounds.width, availableBounds.height);
+ // all items are visible - give the tool bar the available space
+ if (alignment == SWT.LEFT)
+ toolBar.setBounds(0, 0, availableWidth, availableBounds.height);
+ else
+ toolBar.setBounds(availableWidth - width, 0, width, availableBounds.height);
}
else
{
@@ -143,20 +159,20 @@ protected void layout(Composite composite, boolean flushCache)
{
// due to the padding issues on Linux, make the tool bar always
// as big as possible
- chevron.setBounds(availableBounds.width - chevronSize.x, (availableBounds.height - chevronSize.y) / 2,
+ chevron.setBounds(availableWidth - chevronSize.x, (availableBounds.height - chevronSize.y) / 2,
chevronSize.x, chevronSize.y);
- toolBar.setBounds(0, 0, availableBounds.width - chevronSize.x, availableBounds.height);
+ toolBar.setBounds(0, 0, availableWidth - chevronSize.x, availableBounds.height);
}
else
{
- int x = alignment == SWT.LEFT ? width : availableBounds.width - chevronSize.x;
+ int x = alignment == SWT.LEFT ? width : availableWidth - chevronSize.x;
chevron.setBounds(x, (availableBounds.height - chevronSize.y) / 2, chevronSize.x, chevronSize.y);
if (alignment == SWT.LEFT)
toolBar.setBounds(0, 0, width, availableBounds.height);
else
- toolBar.setBounds(availableBounds.width - chevronSize.x - width, 0, width, availableBounds.height);
+ toolBar.setBounds(availableWidth - chevronSize.x - width, 0, width, availableBounds.height);
}
if (!chevron.isVisible())