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: + *
    + *
  1. Action toolbar - always gets full preferred width (highest priority)
  2. + *
  3. View toolbar minimum - always shows at least 1 item + chevron
  4. + *
  5. Title - gets remaining space, truncated with ellipsis if needed
  6. + *
  7. View toolbar expansion - expands into unused title space
  8. + *
+ */ +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())