diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java index 470aaea7163..d05cd871520 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/ConsoleTests.java @@ -16,17 +16,24 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import org.eclipse.core.commands.Command; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.tests.DebugTestExtension; import org.eclipse.debug.tests.TestUtil; import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.IWorkbenchPage; @@ -36,10 +43,14 @@ import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.IConsoleConstants; import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.IConsoleView; import org.eclipse.ui.console.IOConsole; import org.eclipse.ui.console.IOConsoleOutputStream; import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.TextConsole; +import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.internal.console.ConsoleManager; +import org.eclipse.ui.internal.console.ConsoleZoomInHandler; import org.eclipse.ui.texteditor.IWorkbenchActionDefinitionIds; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -233,4 +244,409 @@ public void testIOConsoleAvailable() throws Exception { consoleInput.available(); } } + + /** + * Zooming in must only change the font size of the console currently + * shown in the active console view; a different console (of a different + * type, with its own natural font) that was already open must not be + * affected. + */ + @Test + public void testZoomOnlyAffectsActiveConsoleType(TestInfo testInfo) throws Exception { + String activeType = uniqueConsoleType("active"); //$NON-NLS-1$ + String otherType = uniqueConsoleType("other"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole activeConsole = new MessageConsole("Zoom Active Console", activeType, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + MessageConsole otherConsole = new MessageConsole("Zoom Other Console", otherType, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { activeConsole, otherConsole }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(activeConsole); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int activeInitialHeight = getFontHeight(activeConsole); + int otherInitialHeight = getFontHeight(otherConsole); + + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + + assertEquals(activeInitialHeight + 1, getFontHeight(activeConsole), + "zooming should increase the font size of the console shown in the active console view"); //$NON-NLS-1$ + assertEquals(otherInitialHeight, getFontHeight(otherConsole), + "zooming must not affect a console of a different type that was already open"); //$NON-NLS-1$ + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + } + } + + /** + * A font change coming from somewhere else (e.g. a preference page) must + * be able to override the current zoom instead of being silently + * reverted. + *
+ * The very first external font change on a freshly zoomed console is + * reasserted once (to cope with consoles, such as {@code ProcessConsole}, + * that set their own font asynchronously right after being zoomed), so + * this test performs such a change first and confirms it is reverted, + * before confirming that a second, later change is genuinely honored. + *
+ */ + @Test + public void testPreferenceFontChangeOverridesZoom(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("pref"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole console = new MessageConsole("Zoom Preference Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + Font reassertedFont = null; + Font preferenceFont = null; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int naturalHeight = getFontHeight(console); + + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + int zoomedHeight = getFontHeight(console); + assertEquals(naturalHeight + 1, zoomedHeight, "zoom should have increased the font size"); //$NON-NLS-1$ + + // simulate the console re-asserting its own natural font once, right + // after being zoomed (mirrors e.g. ProcessConsole's asynchronous font + // initialization) - this first external change is expected to be + // reverted back to the current zoom level + reassertedFont = withHeight(console.getFont(), naturalHeight); + console.setFont(reassertedFont); + processUiEvents(); + assertEquals(zoomedHeight, getFontHeight(console), + "the console's own re-assertion of its natural font should have been reverted back to the current zoom level"); //$NON-NLS-1$ + + // simulate an actual, deliberate font change from a preference page: + // this must be accepted as the new base font, overriding the zoom + int preferenceHeight = naturalHeight + 5; + preferenceFont = withHeight(console.getFont(), preferenceHeight); + console.setFont(preferenceFont); + processUiEvents(); + assertEquals(preferenceHeight, getFontHeight(console), + "a later, deliberate font change (e.g. from a preference page) should override the current zoom"); //$NON-NLS-1$ + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + if (reassertedFont != null && !reassertedFont.isDisposed()) { + reassertedFont.dispose(); + } + if (preferenceFont != null && !preferenceFont.isDisposed()) { + preferenceFont.dispose(); + } + } + } + + /** + * The current zoom level (base font height and delta) must be persisted + * to the console plug-in's preferences as soon as a zoom step is applied. + */ + @Test + public void testZoomLevelIsPersisted(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("persist"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole console = new MessageConsole("Zoom Persistence Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int naturalHeight = getFontHeight(console); + + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + + String persisted = getPersistedZoomState(); + String expectedEntry = type + "=" + naturalHeight + "|" + 1; //$NON-NLS-1$ //$NON-NLS-2$ + assertThat(persisted).as("persisted zoom state should contain an entry for the zoomed console type") //$NON-NLS-1$ + .contains(expectedEntry); + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + } + } + + /** + * A console added later, of a type that has already been zoomed, must + * immediately start at the current zoom level for that type - even + * though it was never itself shown in the active console view. + */ + @Test + public void testNewConsoleOfAlreadyZoomedTypeInheritsZoom(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("inherit"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole firstConsole = new MessageConsole("Zoom Inherit Console 1", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] firstConsoles = { firstConsole }; + MessageConsole secondConsole = null; + try { + consoleManager.addConsoles(firstConsoles); + consoleManager.showConsoleView(firstConsole); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int naturalHeight = getFontHeight(firstConsole); + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + int zoomedHeight = getFontHeight(firstConsole); + assertEquals(naturalHeight + 1, zoomedHeight, "zoom should have increased the font size"); //$NON-NLS-1$ + + // a second console of the SAME type, added afterwards and never shown + // in the console view, should still start at the current zoom level + secondConsole = new MessageConsole("Zoom Inherit Console 2", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + consoleManager.addConsoles(new IConsole[] { secondConsole }); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + assertEquals(zoomedHeight, getFontHeight(secondConsole), + "a newly added console of an already zoomed type should immediately start at the current zoom level"); //$NON-NLS-1$ + } finally { + if (secondConsole != null) { + consoleManager.removeConsoles(new IConsole[] { secondConsole }); + } + consoleManager.removeConsoles(firstConsoles); + activePage.hideView(consoleViewPart); + } + } + + /** + * Zooming out must decrease the font size, mirroring zoom in. + */ + @Test + public void testZoomOutDecreasesFontSize(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("zoomOut"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole console = new MessageConsole("Zoom Out Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int naturalHeight = getFontHeight(console); + + // zoom in twice, then out once: should settle one step above natural + ConsoleZoomInHandler.applyZoom(consoleView, 1); + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + assertEquals(naturalHeight + 2, getFontHeight(console), "two zoom-in steps should increase the font size by 2"); //$NON-NLS-1$ + + ConsoleZoomInHandler.applyZoom(consoleView, -1); + processUiEvents(); + assertEquals(naturalHeight + 1, getFontHeight(console), "a zoom-out step should decrease the font size"); //$NON-NLS-1$ + + // zoom back down to (and past) the natural size + ConsoleZoomInHandler.applyZoom(consoleView, -1); + ConsoleZoomInHandler.applyZoom(consoleView, -1); + processUiEvents(); + assertEquals(naturalHeight - 1, getFontHeight(console), + "zooming out below the natural size should be possible"); //$NON-NLS-1$ + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + } + } + + /** + * The font height must never go below the minimum, nor above the maximum + * supported font size, regardless of how large a zoom delta is applied. + */ + @Test + public void testZoomClampsAtMinimumAndMaximumFontSize(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("clamp"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole console = new MessageConsole("Zoom Clamp Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int minFontSize = getStaticIntField("MIN_FONT_SIZE"); //$NON-NLS-1$ + int maxFontSize = getStaticIntField("MAX_FONT_SIZE"); //$NON-NLS-1$ + + ConsoleZoomInHandler.applyZoom(consoleView, 1000); + processUiEvents(); + assertEquals(maxFontSize, getFontHeight(console), "zooming in by a huge delta should clamp at the maximum font size"); //$NON-NLS-1$ + + ConsoleZoomInHandler.applyZoom(consoleView, -10000); + processUiEvents(); + assertEquals(minFontSize, getFontHeight(console), "zooming out by a huge delta should clamp at the minimum font size"); //$NON-NLS-1$ + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + } + } + + /** + * When a console is removed, its custom zoom font must be disposed so it + * is not leaked (see also the {@code SWT Resource was not properly + * disposed} regression this guards against). + */ + @Test + public void testRemovingConsoleDisposesZoomFont(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("disposeOnRemove"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + IConsoleView consoleView = (IConsoleView) consoleViewPart; + + MessageConsole console = new MessageConsole("Zoom Dispose Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + ConsoleZoomInHandler.applyZoom(consoleView, 1); + processUiEvents(); + + Field zoomFontAttributeField = ConsoleZoomInHandler.class.getDeclaredField("ZOOM_FONT_ATTRIBUTE"); //$NON-NLS-1$ + zoomFontAttributeField.setAccessible(true); + String zoomFontAttribute = (String) zoomFontAttributeField.get(null); + + Object attribute = console.getAttribute(zoomFontAttribute); + assertThat(attribute).as("a custom zoom font should have been created").isInstanceOf(Font.class); //$NON-NLS-1$ + Font zoomFont = (Font) attribute; + assertFalse(zoomFont.isDisposed(), "the zoom font must not be disposed while its console is still open"); //$NON-NLS-1$ + + consoleManager.removeConsoles(consoles); + processUiEvents(); + + assertTrue(zoomFont.isDisposed(), "the zoom font must be disposed once its console is removed"); //$NON-NLS-1$ + } finally { + activePage.hideView(consoleViewPart); + } + } + + /** + * Executing the actual registered zoom-in command (as a keybinding would) + * while the console view is the active part must zoom the console it is + * currently showing. + */ + @Test + public void testZoomCommandExecutesWhileConsoleViewActive(TestInfo testInfo) throws Exception { + String type = uniqueConsoleType("command"); //$NON-NLS-1$ + + IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IViewPart consoleViewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW); + + MessageConsole console = new MessageConsole("Zoom Command Console", type, null, //$NON-NLS-1$ + StandardCharsets.UTF_8.name(), true); + IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + IConsole[] consoles = { console }; + try { + consoleManager.addConsoles(consoles); + consoleManager.showConsoleView(console); + activePage.activate(consoleViewPart); + TestUtil.waitForJobs(testInfo.getDisplayName(), ConsoleManager.CONSOLE_JOB_FAMILY, 100, 3000); + processUiEvents(); + + int naturalHeight = getFontHeight(console); + + IHandlerService handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class); + handlerService.executeCommand("org.eclipse.ui.console.command.fontZoomIn", null); //$NON-NLS-1$ + processUiEvents(); + + assertEquals(naturalHeight + 1, getFontHeight(console), + "executing the zoom-in command should increase the font size of the active console"); //$NON-NLS-1$ + } finally { + consoleManager.removeConsoles(consoles); + activePage.hideView(consoleViewPart); + } + } + + private static int getStaticIntField(String name) throws ReflectiveOperationException { + Field field = ConsoleZoomInHandler.class.getDeclaredField(name); + field.setAccessible(true); + return field.getInt(null); + } + + private static String uniqueConsoleType(String suffix) { + return "org.eclipse.debug.tests.console.zoomTest." + suffix + "." + System.nanoTime(); //$NON-NLS-1$ //$NON-NLS-2$ + } + + private static int getFontHeight(TextConsole console) { + Font font = console.getFont(); + FontData[] fontData = font.getFontData(); + return fontData[0].getHeight(); + } + + private static Font withHeight(Font font, int height) { + FontData fontData = font.getFontData()[0]; + return new Font(font.getDevice(), fontData.getName(), height, fontData.getStyle()); + } + + private static void processUiEvents() { + Display display = Display.getCurrent(); + if (display != null) { + while (display.readAndDispatch()) { + // drain pending UI work (e.g. asyncExec calls) triggered by the zoom handler + } + } + } + + /** + * Reads the raw, persisted per-console-type zoom state string via + * reflection, since {@code ConsoleZoomInHandler} intentionally does not + * expose its preference key as public API. + */ + private static String getPersistedZoomState() throws ReflectiveOperationException { + Field prefKeyField = ConsoleZoomInHandler.class.getDeclaredField("PREF_ZOOM_FONT_HEIGHTS"); //$NON-NLS-1$ + prefKeyField.setAccessible(true); + String prefKey = (String) prefKeyField.get(null); + IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(ConsolePlugin.getUniqueIdentifier()); + return preferences.get(prefKey, ""); //$NON-NLS-1$ + } } diff --git a/debug/org.eclipse.ui.console/plugin.properties b/debug/org.eclipse.ui.console/plugin.properties index 7f084b3ff55..5dc60ccbc69 100644 --- a/debug/org.eclipse.ui.console/plugin.properties +++ b/debug/org.eclipse.ui.console/plugin.properties @@ -37,3 +37,7 @@ command.copy_without_escapes.name = Copy Text Without ANSI Escapes command.copy_without_escapes.description = Copy the console content to clipboard, removing the escape sequences command.enable_disable.name = Enable / Disable ANSI Support command.enable_disable.description = Enable / disable ANSI Support +command.console.fontZoomIn.name = Zoom In Console Font +command.console.fontZoomIn.description = Increase the console font size +command.console.fontZoomOut.name = Zoom Out Console Font +command.console.fontZoomOut.description = Decrease the console font size diff --git a/debug/org.eclipse.ui.console/plugin.xml b/debug/org.eclipse.ui.console/plugin.xml index cf75e8936ce..48cd9f4e917 100644 --- a/debug/org.eclipse.ui.console/plugin.xml +++ b/debug/org.eclipse.ui.console/plugin.xml @@ -91,6 +91,36 @@ M4 = Platform-specific fourth key sequence="M1+M2+Insert"> --> ++ * Zoom is tracked per {@link IConsole#getType() console type} (since types can + * have different natural font sizes) and applied to every registered console + * of that type, as well as to ones added later. Font changes coming from + * elsewhere (e.g. a preference page) are never reverted; they are instead + * adopted as the new base font, resetting the zoom delta to zero. + *
+ */ +public class ConsoleZoomInHandler extends AbstractHandler { + + /** + * Key used to remember, on the console itself, the custom font created for + * zooming, so it can be reused/replaced and eventually disposed. + */ + static final String ZOOM_FONT_ATTRIBUTE = ConsoleZoomInHandler.class.getName() + ".zoomFont"; //$NON-NLS-1$ + + /** + * Key used to remember, on the console itself, that a mismatching font change + * was already reasserted once for it; see {@link #onFontChanged(TextConsole)}. + */ + private static final String REENFORCED_ATTRIBUTE = ConsoleZoomInHandler.class.getName() + ".reenforced"; //$NON-NLS-1$ + + private static final int MIN_FONT_SIZE = 6; + private static final int MAX_FONT_SIZE = 72; + private static final int STEP = 1; + + /** + * Key used, in the per-type persisted zoom string, for consoles that report a + *null {@link IConsole#getType() type}.
+ */
+ private static final String DEFAULT_TYPE_KEY = "$default$"; //$NON-NLS-1$
+
+ private static final String TYPE_ENTRY_SEPARATOR = ":"; //$NON-NLS-1$
+ private static final String TYPE_VALUE_SEPARATOR = "="; //$NON-NLS-1$
+ private static final String BASE_DELTA_SEPARATOR = "|"; //$NON-NLS-1$
+
+ /**
+ * Preference key under which the per console type zoom state is persisted,
+ * as type1=base1|delta1:type2=base2|delta2. There is
+ * intentionally no preference page for this.
+ */
+ private static final String PREF_ZOOM_FONT_HEIGHTS = ConsolePlugin.getUniqueIdentifier() + ".zoomFontHeights"; //$NON-NLS-1$
+
+ /**
+ * The base (natural, un-zoomed) font height and the zoom delta currently
+ * applied on top of it, for consoles of a given type.
+ */
+ private record ZoomState(int base, int delta) {
+ int height() {
+ return base + delta;
+ }
+ }
+
+ /**
+ * Shared zoom bookkeeping, keyed by {@link IConsole#getType() console type}.
+ * A missing entry means that type has never been zoomed.
+ */
+ private static final Mapnull if it cannot be determined. Must run on the UI thread.
+ *
+ * @param textConsole the console
+ * @return the font height in points, or null
+ */
+ private static Integer getFontHeight(TextConsole textConsole) {
+ Font font = textConsole.getFont();
+ if (font == null || font.isDisposed()) {
+ return null;
+ }
+ FontData[] fontData = font.getFontData();
+ if (fontData == null || fontData.length == 0) {
+ return null;
+ }
+ return Integer.valueOf(fontData[0].getHeight());
+ }
+
+ /**
+ * Applies the given font height to the console, preserving its font
+ * family/style, and disposes the previous zoom font. Must run on the UI
+ * thread; re-dispatches itself otherwise.
+ *
+ * @param textConsole the console to update
+ * @param height the font height to apply, in points
+ */
+ private static void applyHeight(TextConsole textConsole, int height) {
+ if (Display.getCurrent() == null) {
+ Display.getDefault().asyncExec(() -> applyHeight(textConsole, height));
+ return;
+ }
+
+ // make sure this console's font is (still) being watched, in case it was
+ // registered before the zoom handler class got loaded, or the listener
+ // was otherwise not yet attached
+ textConsole.addPropertyChangeListener(FONT_ENFORCER);
+
+ Font currentFont = textConsole.getFont();
+ if (currentFont == null || currentFont.isDisposed()) {
+ return;
+ }
+ FontData[] fontData = currentFont.getFontData();
+ if (fontData == null || fontData.length == 0 || fontData[0].getHeight() == height) {
+ return;
+ }
+ // getFontData() already returns a fresh array/copy that is safe to mutate;
+ // only the height is changed so that every other attribute (including any
+ // platform-specific data) is preserved as-is
+ for (FontData fd : fontData) {
+ fd.setHeight(height);
+ }
+
+ Object oldAttribute = textConsole.getAttribute(ZOOM_FONT_ATTRIBUTE);
+ Font oldZoomFont = oldAttribute instanceof Font f ? f : null;
+
+ Font newZoomFont = new Font(currentFont.getDevice(), fontData);
+ // remember/apply before disposing the old one, in case they are the same object
+ textConsole.setAttribute(ZOOM_FONT_ATTRIBUTE, newZoomFont);
+ textConsole.setFont(newZoomFont);
+
+ if (oldZoomFont != null && !oldZoomFont.isDisposed()) {
+ disposeLater(oldZoomFont);
+ }
+ }
+
+ /**
+ * Disposes the custom zoom font remembered on the given console, if any.
+ * Dispatches to the UI thread if necessary.
+ *
+ * @param textConsole the console whose zoom font should be disposed
+ */
+ private static void disposeZoomFont(TextConsole textConsole) {
+ Object attribute = textConsole.getAttribute(ZOOM_FONT_ATTRIBUTE);
+ if (!(attribute instanceof Font font) || font.isDisposed()) {
+ return;
+ }
+ if (Display.getCurrent() == null) {
+ // see applyHeight(...) above for why asyncExec (not syncExec) is used
+ Display.getDefault().asyncExec(() -> disposeZoomFont(textConsole));
+ return;
+ }
+ disposeLater(font);
+ textConsole.setAttribute(ZOOM_FONT_ATTRIBUTE, null);
+ }
+
+ /**
+ * Disposes the given font on a later UI cycle rather than immediately.
+ * + * A font that was just replaced on a console (e.g. via + * {@link TextConsole#setFont(Font)}) may still be referenced for a little + * while by the viewer's internal rendering caches (e.g. + * {@code StyledText}/{@code TextLayout} keep per-line layouts that are only + * refreshed on their next repaint). Disposing it synchronously can therefore + * cause a later, asynchronously dispatched repaint to fail with an + * {@code IllegalArgumentException} ("Argument not valid") when it tries to + * use the now-disposed font. Deferring the actual disposal by one UI cycle + * gives any such pending repaint a chance to pick up the new font first. + *
+ * + * @param font the font to dispose; must be called on the UI thread + */ + private static void disposeLater(Font font) { + Display.getCurrent().asyncExec(() -> { + if (!font.isDisposed()) { + font.dispose(); + } + }); + } +} + diff --git a/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleZoomOutHandler.java b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleZoomOutHandler.java new file mode 100644 index 00000000000..aef9296a733 --- /dev/null +++ b/debug/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleZoomOutHandler.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2026 Advantest Europe GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Raghunandana Murthappa + *******************************************************************************/ +package org.eclipse.ui.internal.console; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.Status; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.handlers.HandlerUtil; + +/** + * Command handler to decrease the font size of the console currently shown in + * the active console view. + * + * @see ConsoleZoomInHandler + */ +public class ConsoleZoomOutHandler extends AbstractHandler { + private static final int STEP = 1; + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + IWorkbenchPart part = HandlerUtil.getActivePart(event); + ConsoleZoomInHandler.applyZoom(part, -STEP); + return Status.OK_STATUS; + } +}