Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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());

Expand All @@ -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;
}
Expand Down Expand Up @@ -335,7 +334,7 @@ public void dispose()

context.dispose();
}

public final EditorActivationState getEditorActivationState()
{
return editorActivationState;
Expand All @@ -345,7 +344,7 @@ public final Control getControl()
{
return top;
}

public void setFocus()
{
getControl().setFocus();
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
* <ol>
* <li>Action toolbar - always gets full preferred width (highest priority)</li>
* <li>View toolbar minimum - always shows at least 1 item + chevron</li>
* <li>Title - gets remaining space, truncated with ellipsis if needed</li>
* <li>View toolbar expansion - expands into unused title space</li>
* </ol>
*/
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);
}
Comment on lines +112 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
}
var titleSize = titleLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
var viewSize = viewToolbarWrapper.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
int yTitle = marginHeight + (availableHeight - titleSize.y) / 2;
int yAction = marginHeight + (availableHeight - actionSize.y) / 2;
int yView = marginHeight + (availableHeight - viewSize.y) / 2;
// title at left
titleLabel.setBounds(marginWidth, yTitle, titleWidth, titleSize.y);
// Action toolbar at right
int actionX = bounds.width - marginWidth - actionWidth;
actionToolbar.setBounds(actionX, yAction, actionWidth, actionSize.y);
// Update the view toolbar wrapper's layout with the actual available
// width
if (viewToolbarWrapper.getLayout() instanceof ToolBarPlusChevronLayout layout)
{
layout.setMaxWidth(viewActualWidth);
}
// View toolbar between title and action toolbar (right-aligned within
// its space)
int viewX = actionX - HORIZONTAL_SPACING - viewActualWidth;
viewToolbarWrapper.setBounds(viewX, yView, viewActualWidth, viewSize.y);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposition :

  • for the height, each one of the 3 should to be centered compared to its own height, here actionSize.y was used for more than the action Toolbar, and viewToolbar not centered. I think this fix the "horizontally cut title" and the not centered viewToolBar in Payment.
  • create the viewToolBar after setMaxWidth. I think setMaxWidth itselft does not retrigger a layout update.

}

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;
}
}
Loading