From f15d089af13852462aa7a863a7b0ce6310c11ee4 Mon Sep 17 00:00:00 2001 From: Deepika Udayagiri Date: Wed, 7 Oct 2026 12:40:22 +0530 Subject: [PATCH] Show pinned editors first on editor tabs and in chevron drop-down list when enabled. --- .../renderers/swt/BasicPartList.java | 41 ++++- .../renderers/swt/CTabRendering.java | 12 +- .../renderers/swt/StackRenderer.java | 167 +++++++++++++++++- .../ide/dialogs/IDEEditorsPreferencePage.java | 3 +- .../ui/internal/WorkbenchMessages.java | 3 +- .../ui/internal/WorkbookEditorsHandler.java | 22 ++- .../dialogs/EditorsPreferencePage.java | 28 ++- .../eclipse/ui/internal/messages.properties | 3 +- .../renderers/swt/StackRendererTest.java | 85 ++++++++- .../internal/WorkbookEditorsHandlerTest.java | 96 +++++++++- 10 files changed, 440 insertions(+), 20 deletions(-) diff --git a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java index 63f57378ec1..3a900cbb4e5 100644 --- a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java +++ b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/internal/workbench/renderers/swt/BasicPartList.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -25,6 +25,7 @@ import org.eclipse.e4.ui.model.application.ui.MUILabel; import org.eclipse.e4.ui.model.application.ui.advanced.MPlaceholder; import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.workbench.IPresentationEngine; import org.eclipse.e4.ui.workbench.modeling.EPartService; import org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering; import org.eclipse.e4.ui.workbench.renderers.swt.StackRenderer; @@ -126,8 +127,16 @@ public BasicPartList(Shell parent, int shellStyle, int treeStyler, @Override public int category(Object element) { if (element instanceof MPart part) { + boolean pinned = isPinned(part); CTabItem item = BasicPartList.this.renderer.findItemForPart(part); - if (item != null && !item.isShowing()) { + boolean notShowing = item != null && !item.isShowing(); + if (shouldShowPinnedEditorsFirst()) { + if (pinned) { + return notShowing ? -3 : -2; + } + return notShowing ? -1 : 0; + } + if (notShowing) { return -1; } } @@ -170,9 +179,37 @@ private List getInput() { list.add(element); } } + if (shouldShowPinnedEditorsFirst()) { + List pinned = new ArrayList<>(); + List unpinned = new ArrayList<>(); + for (Object element : list) { + if (element instanceof MPart part && isPinned(part)) { + pinned.add(part); + } else { + unpinned.add(element); + } + } + pinned.addAll(unpinned); + return pinned; + } return list; } + private boolean isPinned(MPart part) { + return part.getTags().contains(IPresentationEngine.ADORNMENT_PIN); + } + + private boolean shouldShowPinnedEditorsFirst() { + if (!PartStackUtil.isEditorStack(input)) { + return false; + } + return Platform.getPreferencesService().getBoolean( + CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT, + CTabRendering.SHOW_PINNED_EDITORS_FIRST, + CTabRendering.SHOW_PINNED_EDITORS_FIRST_DEFAULT, + null); + } + public void setInput() { getTableViewer().setInput(getInput()); selectFirstMatch(); diff --git a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/CTabRendering.java b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/CTabRendering.java index 2f312f68f0d..feb094ce2cc 100644 --- a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/CTabRendering.java +++ b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/CTabRendering.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2020 IBM Corporation and others. + * Copyright (c) 2010, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -91,6 +91,16 @@ public class CTabRendering extends CTabFolderRenderer implements ICTabRendering, */ public static final boolean SHOW_DIRTY_INDICATOR_ON_TABS_DEFAULT = true; + /** + * A named preference for whether to show pinned editors first in the drop-down list + */ + public static final String SHOW_PINNED_EDITORS_FIRST = "SHOW_PINNED_EDITORS_FIRST"; //$NON-NLS-1$ + + /** + * Default value for "show pinned editors first" preference + */ + public static final boolean SHOW_PINNED_EDITORS_FIRST_DEFAULT = false; + private static int MIN_VIEW_CHARS = 1; private static int MAX_VIEW_CHARS = Integer.MAX_VALUE; diff --git a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRenderer.java b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRenderer.java index 3b3ea4523d5..6f9cc69bf04 100644 --- a/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRenderer.java +++ b/bundles/org.eclipse.e4.ui.workbench.renderers.swt/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRenderer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2025 IBM Corporation and others. + * Copyright (c) 2008, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -26,6 +26,7 @@ import jakarta.inject.Named; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -72,6 +73,8 @@ import org.eclipse.e4.ui.workbench.modeling.EModelService; import org.eclipse.e4.ui.workbench.modeling.EPartService; import org.eclipse.e4.ui.workbench.modeling.ISaveHandler; +import org.eclipse.emf.common.util.ECollections; +import org.eclipse.emf.common.util.EList; import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.LegacyActionTools; import org.eclipse.jface.action.MenuManager; @@ -182,6 +185,8 @@ public class StackRenderer extends LazyStackRenderer { * tabs using the keyboard. */ private static final String INHIBIT_FOCUS = "InhibitFocus"; //$NON-NLS-1$ + private static final String PART_OPEN_INDEX = "part_open_index"; //$NON-NLS-1$ + private static final java.util.concurrent.atomic.AtomicLong PART_SEQUENCE_GEN = new java.util.concurrent.atomic.AtomicLong(); // Minimum characters in for stacks outside the shared area private static int MIN_VIEW_CHARS = 1; @@ -385,14 +390,115 @@ void subscribeTopicTagsChanged(@UIEventTopic(UIEvents.ApplicationElement.TOPIC_T if (UIEvents.isADD(event)) { if (UIEvents.contains(event, UIEvents.EventTags.NEW_VALUE, IPresentationEngine.ADORNMENT_PIN)) { item.setImage(getImage(part)); + handlePartPinStateChanged(part, true); } } else if (UIEvents.isREMOVE(event)) { if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE, IPresentationEngine.ADORNMENT_PIN)) { item.setImage(getImage(part)); + handlePartPinStateChanged(part, false); } } } + private static long getPartOpenSequence(MStackElement element) { + MPart part = element instanceof MPart p ? p : (MPart) ((MPlaceholder) element).getRef(); + if (part != null) { + Object seq = part.getTransientData().get(PART_OPEN_INDEX); + if (seq instanceof Long l) { + return l; + } + } + return 0L; + } + + private static void ensurePartOpenSequence(MStackElement element) { + MPart part = element instanceof MPart p ? p : (MPart) ((MPlaceholder) element).getRef(); + if (part != null && !part.getTransientData().containsKey(PART_OPEN_INDEX)) { + part.getTransientData().put(PART_OPEN_INDEX, PART_SEQUENCE_GEN.incrementAndGet()); + } + } + + private void handlePartPinStateChanged(MPart part, boolean pinned) { + if (!shouldShowPinnedEditorsFirst()) { + return; + } + MUIElement parent = part.getParent(); + if (parent == null && part.getCurSharedRef() != null) { + parent = part.getCurSharedRef().getParent(); + } + if (!(parent instanceof MPartStack stack) || !PartStackUtil.isEditorStack(stack)) { + return; + } + MUIElement elementToMove = part.getCurSharedRef() != null ? part.getCurSharedRef() : part; + List children = stack.getChildren(); + int currentIndex = children.indexOf(elementToMove); + if (currentIndex < 0) { + return; + } + for (MStackElement child : children) { + ensurePartOpenSequence(child); + } + long elementSeq = getPartOpenSequence((MStackElement) elementToMove); + int targetIndex; + if (pinned) { + // Find position among pinned items sorted by open order + int insertIndex = 0; + for (int i = 0; i < children.size(); i++) { + MStackElement child = children.get(i); + if (child == elementToMove) { + continue; + } + MPart childPart = child instanceof MPart p ? p : (MPart) ((MPlaceholder) child).getRef(); + if (childPart != null && isPinned(childPart)) { + if (getPartOpenSequence(child) < elementSeq) { + insertIndex++; + } + } + } + targetIndex = insertIndex; + } else { + // Find position among unpinned items (after all pinned items) sorted by open order + int pinnedCount = 0; + int unpinnedBeforeCount = 0; + for (MStackElement child : children) { + if (child == elementToMove) { + continue; + } + MPart childPart = child instanceof MPart p ? p : (MPart) ((MPlaceholder) child).getRef(); + if (childPart != null && isPinned(childPart)) { + pinnedCount++; + } else if (getPartOpenSequence(child) < elementSeq) { + unpinnedBeforeCount++; + } + } + targetIndex = pinnedCount + unpinnedBeforeCount; + } + if (currentIndex != targetIndex) { + if (children instanceof EList eList) { + ECollections.move(eList, targetIndex, currentIndex); + } else { + children.remove(currentIndex); + if (targetIndex >= children.size()) { + children.add((MStackElement) elementToMove); + } else { + children.add(targetIndex, (MStackElement) elementToMove); + } + } + } + } + + private boolean isPinned(MPart part) { + return part.getTags().contains(IPresentationEngine.ADORNMENT_PIN); + } + + private boolean shouldShowPinnedEditorsFirst() { + return Platform.getPreferencesService().getBoolean( + CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT, + CTabRendering.SHOW_PINNED_EDITORS_FIRST, + CTabRendering.SHOW_PINNED_EDITORS_FIRST_DEFAULT, + null); + } + @Inject @Optional void subscribeTopicChildrenChanged(@UIEventTopic(UIEvents.ElementContainer.TOPIC_CHILDREN) Event event) { @@ -716,6 +822,8 @@ public void init() { dirtyIndicatorListener = e -> { if (CTabRendering.SHOW_DIRTY_INDICATOR_ON_TABS.equals(e.getKey())) { synchronize.asyncExec(this::updateDirtyIndicatorStyle); + } else if (CTabRendering.SHOW_PINNED_EDITORS_FIRST.equals(e.getKey())) { + synchronize.asyncExec(this::reorderStacksForPinnedEditors); } }; preferences.addPreferenceChangeListener(dirtyIndicatorListener); @@ -748,6 +856,57 @@ private void updateDirtyIndicatorStyle() { } } + private void reorderStacksForPinnedEditors() { + MApplication app = context.get(MApplication.class); + if (app == null) { + return; + } + boolean showPinnedFirst = shouldShowPinnedEditorsFirst(); + List stacks = modelService.findElements(app, null, MPartStack.class, null); + for (MPartStack stack : stacks) { + if (!PartStackUtil.isEditorStack(stack)) { + continue; + } + List children = stack.getChildren(); + for (MStackElement child : children) { + ensurePartOpenSequence(child); + } + List desiredOrder = new ArrayList<>(children); + if (showPinnedFirst) { + List pinned = new ArrayList<>(); + List unpinned = new ArrayList<>(); + for (MStackElement child : children) { + MPart part = child instanceof MPart p ? p : (MPart) ((MPlaceholder) child).getRef(); + if (part != null && isPinned(part)) { + pinned.add(child); + } else { + unpinned.add(child); + } + } + pinned.sort(Comparator.comparingLong(StackRenderer::getPartOpenSequence)); + unpinned.sort(Comparator.comparingLong(StackRenderer::getPartOpenSequence)); + desiredOrder.clear(); + desiredOrder.addAll(pinned); + desiredOrder.addAll(unpinned); + } else { + desiredOrder.sort(Comparator.comparingLong(StackRenderer::getPartOpenSequence)); + } + + for (int i = 0; i < desiredOrder.size(); i++) { + MStackElement elem = desiredOrder.get(i); + int cur = children.indexOf(elem); + if (cur != i) { + if (children instanceof EList eList) { + ECollections.move(eList, i, cur); + } else { + children.remove(cur); + children.add(i, elem); + } + } + } + } + } + protected void updateTab(CTabItem cti, MPart part, String attName, Object newValue) { switch (attName) { case UIEvents.UILabel.LABEL: @@ -1150,10 +1309,14 @@ private int calcIndexFor(MElementContainer stack, final MU public void childRendered(final MElementContainer parentElement, MUIElement element) { super.childRendered(parentElement, element); - if (!(((MUIElement) parentElement) instanceof MPartStack) || !(element instanceof MStackElement)) { + if (!(((MUIElement) parentElement) instanceof MPartStack stack) || !(element instanceof MStackElement stackElement)) { return; } + if (PartStackUtil.isEditorStack(stack)) { + ensurePartOpenSequence(stackElement); + } + createTab(parentElement, element); } diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEEditorsPreferencePage.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEEditorsPreferencePage.java index 13d631e8276..d5f7525f3f9 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEEditorsPreferencePage.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEEditorsPreferencePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -61,6 +61,7 @@ protected Control createContents(Composite parent) { createAllowInplaceEditorPref(composite); createUseIPersistablePref(composite); createPromptWhenStillOpenPref(composite); + createShowPinnedEditorsFirstPref(composite); createEditorReuseGroup(composite); createAlignMultiPageEditorTabs(composite); diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java index e92931653d2..ada022a4f20 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbenchMessages.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2005, 2020 IBM Corporation and others. + * Copyright (c) 2005, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -472,6 +472,7 @@ public class WorkbenchMessages extends NLS { public static String WorkbenchPreference_allowInplaceEditingButton; public static String WorkbenchPreference_useIPersistableEditorButton; public static String WorkbenchPreference_promptWhenStillOpenButton; + public static String WorkbenchPreference_showPinnedEditorsFirstButton; public static String WorkbenchPreference_stickyCycleButton; public static String WorkbenchPreference_RunInBackgroundButton; public static String WorkbenchPreference_RunInBackgroundToolTip; diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java index dbb4d2f7bb3..8475424ad9a 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkbookEditorsHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2007, 2019 IBM Corporation and others. + * Copyright (c) 2007, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -98,6 +98,13 @@ private static boolean isMruEnabled() { return preferences.getBoolean(StackRenderer.MRU_KEY, initialMRUValue); } + private static boolean isShowPinnedEditorsFirstEnabled() { + IEclipsePreferences preferences = InstanceScope.INSTANCE + .getNode(CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT); + return preferences.getBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, + CTabRendering.SHOW_PINNED_EDITORS_FIRST_DEFAULT); + } + @Override protected Object getInput(WorkbenchPage page) { List editorReferences = getParts(page); @@ -117,6 +124,19 @@ private List getParts(WorkbenchPage page) { refs.add((EditorReference) ier); } } + if (isShowPinnedEditorsFirstEnabled()) { + List pinned = new ArrayList<>(); + List unpinned = new ArrayList<>(); + for (EditorReference ref : refs) { + if (ref.isPinned()) { + pinned.add(ref); + } else { + unpinned.add(ref); + } + } + pinned.addAll(unpinned); + return pinned; + } return refs; } diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/EditorsPreferencePage.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/EditorsPreferencePage.java index 1aca9a93217..c485a18fee9 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/EditorsPreferencePage.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/EditorsPreferencePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2016 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -18,6 +18,10 @@ import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering; import org.eclipse.jface.action.Action; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; @@ -74,6 +78,8 @@ public class EditorsPreferencePage extends PreferencePage implements IWorkbenchP private Button promptWhenStillOpenEditor; + private Button showPinnedEditorsFirst; + private Button allowInplaceEditor; private ComboFieldEditor multiPageEditorTabPositionComboField; @@ -88,6 +94,7 @@ protected Control createContents(Composite parent) { createAllowInplaceEditorPref(composite); createUseIPersistablePref(composite); createPromptWhenStillOpenPref(composite); + createShowPinnedEditorsFirstPref(composite); createEditorReuseGroup(composite); // ((TabBehaviour)Tweaklets.get(TabBehaviour.KEY)).setPreferenceVisibility(editorReuseGroup, // showMultipleEditorTabs); @@ -136,6 +143,15 @@ protected void createPromptWhenStillOpenPref(Composite composite) { setButtonLayoutData(promptWhenStillOpenEditor); } + protected void createShowPinnedEditorsFirstPref(Composite composite) { + showPinnedEditorsFirst = new Button(composite, SWT.CHECK); + showPinnedEditorsFirst.setText(WorkbenchMessages.WorkbenchPreference_showPinnedEditorsFirstButton); + showPinnedEditorsFirst.setSelection(Platform.getPreferencesService().getBoolean( + CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT, + CTabRendering.SHOW_PINNED_EDITORS_FIRST, CTabRendering.SHOW_PINNED_EDITORS_FIRST_DEFAULT, null)); + setButtonLayoutData(showPinnedEditorsFirst); + } + protected void createAlignMultiPageEditorTabs(Composite parent) { Composite comboComposite = new Composite(parent, SWT.NONE); comboComposite.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).create()); @@ -176,6 +192,7 @@ protected void performDefaults() { useIPersistableEditor.setSelection(store.getDefaultBoolean(IPreferenceConstants.USE_IPERSISTABLE_EDITORS)); promptWhenStillOpenEditor.setSelection(getAPIPreferenceStore() .getDefaultBoolean(IWorkbenchPreferenceConstants.PROMPT_WHEN_SAVEABLE_STILL_OPEN)); + showPinnedEditorsFirst.setSelection(CTabRendering.SHOW_PINNED_EDITORS_FIRST_DEFAULT); reuseEditors.setSelection(store.getDefaultBoolean(IPreferenceConstants.REUSE_EDITORS_BOOLEAN)); reuseEditorsThreshold.loadDefault(); reuseEditorsThreshold.getLabelControl(editorReuseThresholdGroup).setEnabled(reuseEditors.getSelection()); @@ -194,6 +211,15 @@ public boolean performOk() { getAPIPreferenceStore().setValue(IWorkbenchPreferenceConstants.PROMPT_WHEN_SAVEABLE_STILL_OPEN, promptWhenStillOpenEditor.getSelection()); + IEclipsePreferences renderersPrefs = InstanceScope.INSTANCE + .getNode(CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT); + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, showPinnedEditorsFirst.getSelection()); + try { + renderersPrefs.flush(); + } catch (Exception e) { + // ignore + } + // store the reuse editors setting store.setValue(IPreferenceConstants.REUSE_EDITORS_BOOLEAN, reuseEditors.getSelection()); reuseEditorsThreshold.store(); diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties index c1c3b2bdc7c..f9717624595 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/messages.properties @@ -1,5 +1,5 @@ ############################################################################### -# Copyright (c) 2000, 2020 IBM Corporation and others. +# Copyright (c) 2000, 2026 IBM Corporation and others. # # This program and the accompanying materials # are made available under the terms of the Eclipse Public License 2.0 @@ -414,6 +414,7 @@ PreferenceExportWarning_applyAndContinue = &Apply and Continue WorkbenchPreference_allowInplaceEditingButton = Allow in-place &system editors WorkbenchPreference_useIPersistableEditorButton = Restore &editor state on startup WorkbenchPreference_promptWhenStillOpenButton = Pr&ompt to save on close even if still open elsewhere +WorkbenchPreference_showPinnedEditorsFirstButton = Show &pinned editors first then followed by others WorkbenchPreference_stickyCycleButton = Keep &next/previous editor, view and perspectives dialog open WorkbenchPreference_RunInBackgroundButton=Always r&un in background WorkbenchPreference_RunInBackgroundToolTip=Run long operations in the background where possible diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRendererTest.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRendererTest.java index bcd45aab57c..bedaf18ff8a 100644 --- a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRendererTest.java +++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/workbench/renderers/swt/StackRendererTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2023 IBM Corporation and others. + * Copyright (c) 2013, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -32,6 +32,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.e4.core.contexts.IEclipseContext; import org.eclipse.e4.ui.internal.workbench.PartStackUtil; import org.eclipse.e4.ui.internal.workbench.swt.CSSConstants; @@ -43,6 +45,7 @@ import org.eclipse.e4.ui.model.application.ui.basic.MPart; import org.eclipse.e4.ui.model.application.ui.basic.MPartStack; import org.eclipse.e4.ui.model.application.ui.basic.MWindow; +import org.eclipse.e4.ui.workbench.IPresentationEngine; import org.eclipse.e4.ui.model.application.ui.menu.MToolBar; import org.eclipse.e4.ui.services.IStylingEngine; import org.eclipse.e4.ui.services.internal.events.EventBroker; @@ -599,6 +602,86 @@ public void testPartReordering() { assertEquals("Part 1", tabFolder.getItem(1).getText()); } + @Test + public void testPinnedTabReorderedToLeftWhenPreferenceEnabled() { + PartStackUtil.makeEditorStack(partStack); + + MPart part1 = ems.createModelElement(MPart.class); + part1.setLabel("Part 1"); + + MPart part2 = ems.createModelElement(MPart.class); + part2.setLabel("Part 2"); + + MPart part3 = ems.createModelElement(MPart.class); + part3.setLabel("Part 3"); + + partStack.getChildren().add(part1); + partStack.getChildren().add(part2); + partStack.getChildren().add(part3); + + IEclipsePreferences renderersPrefs = InstanceScope.INSTANCE + .getNode(CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT); + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, true); + + try { + contextRule.createAndRunWorkbench(window); + + CTabFolder tabFolder = (CTabFolder) partStack.getWidget(); + assertEquals(3, tabFolder.getItemCount()); + + // Activate part 3 so it is the selected element + partStack.setSelectedElement(part3); + assertEquals(part3, partStack.getSelectedElement()); + + // Pin part 3 (should move to front without stealing focus/selection) + part3.getTags().add(IPresentationEngine.ADORNMENT_PIN); + + // Verify part 3 was moved to index 0 both in model and in CTabFolder widget + assertEquals(part3, partStack.getChildren().get(0)); + assertEquals(part1, partStack.getChildren().get(1)); + assertEquals(part2, partStack.getChildren().get(2)); + + assertEquals("Part 3", tabFolder.getItem(0).getText()); + assertEquals("Part 1", tabFolder.getItem(1).getText()); + assertEquals("Part 2", tabFolder.getItem(2).getText()); + + // Verify part 3 remains the selected element + assertEquals(part3, partStack.getSelectedElement()); + assertEquals("Part 3", tabFolder.getSelection().getText()); + + // Unpin part 3 (should restore back to original position index 2) + part3.getTags().remove(IPresentationEngine.ADORNMENT_PIN); + assertEquals(part1, partStack.getChildren().get(0)); + assertEquals(part2, partStack.getChildren().get(1)); + assertEquals(part3, partStack.getChildren().get(2)); + + assertEquals("Part 1", tabFolder.getItem(0).getText()); + assertEquals("Part 2", tabFolder.getItem(1).getText()); + assertEquals("Part 3", tabFolder.getItem(2).getText()); + + // Pin part 3 again + part3.getTags().add(IPresentationEngine.ADORNMENT_PIN); + assertEquals(part3, partStack.getChildren().get(0)); + assertEquals(part1, partStack.getChildren().get(1)); + assertEquals(part2, partStack.getChildren().get(2)); + + // Disable preference -> should restore original order + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, false); + // Process pending async UI events + while (tabFolder.getDisplay().readAndDispatch()) { + } + assertEquals(part1, partStack.getChildren().get(0)); + assertEquals(part2, partStack.getChildren().get(1)); + assertEquals(part3, partStack.getChildren().get(2)); + + assertEquals("Part 1", tabFolder.getItem(0).getText()); + assertEquals("Part 2", tabFolder.getItem(1).getText()); + assertEquals("Part 3", tabFolder.getItem(2).getText()); + } finally { + renderersPrefs.remove(CTabRendering.SHOW_PINNED_EDITORS_FIRST); + } + } + // helper functions /* diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/WorkbookEditorsHandlerTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/WorkbookEditorsHandlerTest.java index 22bad7811ad..3abeb58a18e 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/WorkbookEditorsHandlerTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/WorkbookEditorsHandlerTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 vogella GmbH. + * Copyright (c) 2022, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,6 +10,8 @@ * * Contributors: * Fabian Pfaff (vogella GmbH) - initial API and implementation + * IBM Corporation - Move pinned editors to left in editor space, top in + * chevron list ******************************************************************************/ package org.eclipse.ui.tests.internal; @@ -26,16 +28,23 @@ import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.e4.ui.workbench.renderers.swt.CTabRendering; +import org.eclipse.e4.ui.workbench.renderers.swt.StackRenderer; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; +import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.internal.WorkbenchPage; +import org.eclipse.ui.internal.WorkbenchPartReference; import org.eclipse.ui.internal.WorkbookEditorsHandler; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.tests.harness.util.CloseTestWindowsRule; @@ -68,15 +77,25 @@ public final void setUp() throws CoreException { @After public final void tearDown() throws Exception { - if (project1 != null) { - project1.delete(true, true, null); - project1 = null; - } - if (project2 != null) { - project2.delete(true, true, null); - project2 = null; + try { + if (project1 != null) { + project1.delete(true, true, null); + project1 = null; + } + if (project2 != null) { + project2.delete(true, true, null); + project2 = null; + } + if (activePage != null) { + activePage.closeAllEditors(false); + } + } finally { + IEclipsePreferences renderersPrefs = InstanceScope.INSTANCE + .getNode(CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT); + renderersPrefs.remove(CTabRendering.SHOW_PINNED_EDITORS_FIRST); + renderersPrefs.remove(StackRenderer.MRU_KEY); + renderersPrefs.flush(); } - activePage.closeAllEditors(false); } @Test @@ -372,6 +391,65 @@ public String getName() { assertEquals("Display text should match name of editor input", editorInputName, handler.tableItemTexts.get(1)); } + @Test + public void testShowPinnedEditorsFirstPreference() throws Exception { + String file1Name = "file1.txt"; + String file2Name = "file2.txt"; + String file3Name = "file3.txt"; + String file4Name = "file4.txt"; + IDE.openEditor(activePage, FileUtil.createFile(file1Name, project1), true); + IEditorPart editor2 = IDE.openEditor(activePage, FileUtil.createFile(file2Name, project1), true); + IDE.openEditor(activePage, FileUtil.createFile(file3Name, project1), true); + IEditorPart editor4 = IDE.openEditor(activePage, FileUtil.createFile(file4Name, project1), true); + + // Pin editors 2 and 4 + WorkbenchPartReference ref2 = (WorkbenchPartReference) ((WorkbenchPage) activePage).getReference(editor2); + ref2.setPinned(true); + WorkbenchPartReference ref4 = (WorkbenchPartReference) ((WorkbenchPage) activePage).getReference(editor4); + ref4.setPinned(true); + + ICommandService cmdService = PlatformUI.getWorkbench().getService(ICommandService.class); + final Command cmd = cmdService.getCommand("org.eclipse.ui.window.openEditorDropDown"); + IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class); + + IEclipsePreferences renderersPrefs = InstanceScope.INSTANCE + .getNode(CTabRendering.PREF_QUALIFIER_ECLIPSE_E4_UI_WORKBENCH_RENDERERS_SWT); + + // 1. In default MRU mode when preference is disabled (false): + // Expects standard MRU activation history order: [file4.txt, file3.txt, file2.txt, file1.txt] + renderersPrefs.putBoolean(StackRenderer.MRU_KEY, true); + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, false); + WorkbookEditorsHandlerTestable handlerMruDisabled = new WorkbookEditorsHandlerTestable(); + cmd.setHandler(handlerMruDisabled); + handlerMruDisabled.execute(handlerService.createExecutionEvent(cmd, null)); + assertEquals(List.of(file4Name, file3Name, file2Name, file1Name), handlerMruDisabled.tableItemTexts); + + // When preference is enabled (true) in MRU mode: + // Pinned editors first in MRU order (file4, file2), followed by unpinned in MRU order (file3, file1) + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, true); + WorkbookEditorsHandlerTestable handlerMruEnabled = new WorkbookEditorsHandlerTestable(); + cmd.setHandler(handlerMruEnabled); + handlerMruEnabled.execute(handlerService.createExecutionEvent(cmd, null)); + assertEquals(List.of(file4Name, file2Name, file3Name, file1Name), handlerMruEnabled.tableItemTexts); + + // 2. In non-MRU (tab index) mode when preference is disabled (false): + // Expects standard tab index order: [file1.txt, file2.txt, file3.txt, file4.txt] + renderersPrefs.putBoolean(StackRenderer.MRU_KEY, false); + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, false); + WorkbookEditorsHandlerTestable handlerNonMruDisabled = new WorkbookEditorsHandlerTestable(); + cmd.setHandler(handlerNonMruDisabled); + handlerNonMruDisabled.execute(handlerService.createExecutionEvent(cmd, null)); + assertEquals(List.of(file1Name, file2Name, file3Name, file4Name), handlerNonMruDisabled.tableItemTexts); + + // When preference is enabled (true) in non-MRU mode: + // Pinned editors first in screen tab order (file2, file4), followed by unpinned in screen tab order (file1, file3) + renderersPrefs.putBoolean(CTabRendering.SHOW_PINNED_EDITORS_FIRST, true); + WorkbookEditorsHandlerTestable handlerNonMruEnabled = new WorkbookEditorsHandlerTestable(); + cmd.setHandler(handlerNonMruEnabled); + handlerNonMruEnabled.execute(handlerService.createExecutionEvent(cmd, null)); + assertEquals(List.of(file2Name, file4Name, file1Name, file3Name), handlerNonMruEnabled.tableItemTexts); + } + class WorkbookEditorsHandlerTestable extends WorkbookEditorsHandler { List selectionTexts; List tableItemTexts;