From 4486be1e675fa025bd1592d4b7c23f17372b17b6 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Wed, 2 Sep 2026 20:12:44 +0200 Subject: [PATCH] [Win32] Complete zoom change processing despite failing widgets A zoom change is propagated from the shell through the whole widget tree and is concluded by laying out the shell. Adapting a widget to the new zoom can fail because of resources provided by the application, such as an image or a region that has already been disposed when the adaptation re-applies it. Such a failure is contained per listener by the event table, which reports it to the exception handlers of the display. It is not contained in two cases, each of which leaves parts of the UI rendered with a stale zoom: * If no handler consuming the failure is installed, it is re-thrown by the event table and aborts the propagation to all widgets that have not been processed yet, as well as the concluding layout of the shell and its move to the new monitor. * A failure of a widget's own adaptation that happens before the widget propagates the event further, as done by composites for their children and by tree items for their child items, skips the complete subtree of that widget. The execution of a zoom change, which already knows when the zoom change has been processed by all widgets, therefore also becomes the place where failures are collected. Every widget the event is sent to is isolated: its failure is reported to the exception handlers immediately and, if those do not consume it, propagated only once the zoom change is complete, so that no widget is left behind and the shell is laid out in any case. Contributes to https://github.com/eclipse-platform/eclipse.platform.swt/issues/2432 Assisted-by: Claude Opus 5 --- ...ZoomChangeExceptionHandlingWin32Tests.java | 267 ++++++++++++++++++ .../org/eclipse/swt/widgets/Composite.java | 8 +- .../org/eclipse/swt/widgets/Control.java | 44 ++- .../org/eclipse/swt/widgets/CoolBar.java | 2 +- .../org/eclipse/swt/widgets/Decorations.java | 9 +- .../org/eclipse/swt/widgets/ExpandBar.java | 4 +- .../win32/org/eclipse/swt/widgets/Menu.java | 4 +- .../org/eclipse/swt/widgets/MenuItem.java | 5 +- .../win32/org/eclipse/swt/widgets/Shell.java | 8 +- .../org/eclipse/swt/widgets/TabFolder.java | 4 +- .../win32/org/eclipse/swt/widgets/Table.java | 8 +- .../org/eclipse/swt/widgets/ToolBar.java | 2 +- .../win32/org/eclipse/swt/widgets/Tree.java | 8 +- .../org/eclipse/swt/widgets/TreeItem.java | 34 ++- .../win32/org/eclipse/swt/widgets/Widget.java | 50 ++++ 15 files changed, 396 insertions(+), 61 deletions(-) create mode 100644 bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ZoomChangeExceptionHandlingWin32Tests.java diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ZoomChangeExceptionHandlingWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ZoomChangeExceptionHandlingWin32Tests.java new file mode 100644 index 00000000000..cf20db1fae6 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/ZoomChangeExceptionHandlingWin32Tests.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) 2026 Vector Informatik 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 + *******************************************************************************/ +package org.eclipse.swt.widgets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.*; + +/** + * Tests that a failure while processing a zoom change for a single widget does + * not abort the zoom change processing of all other widgets, as reported in + * issue + * #2432. + *

+ * Such failures are usually caused by resources provided by the application, + * such as an image that has already been disposed when the widget is adapted to + * the new zoom. They must be reported to the exception handlers of the + * {@link Display}, but every other widget must still be adapted and the shell + * must still be laid out afterwards. + */ +@ExtendWith(PlatformSpecificExecutionExtension.class) +@ExtendWith(WithMonitorSpecificScalingExtension.class) +class ZoomChangeExceptionHandlingWin32Tests { + + private static final String FAILURE_MESSAGE = "Intentional zoom change failure"; + + private Display display; + private Shell shell; + private int newZoom; + // Not declared as java.util.List, as List is the SWT widget in this package + private final ArrayList reportedExceptions = new ArrayList<>(); + + @BeforeEach + void setUp() { + display = Display.getDefault(); + display.setRuntimeExceptionHandler(reportedExceptions::add); + shell = new Shell(display); + newZoom = shell.nativeZoom * 2; + } + + @AfterEach + void tearDown() { + if (!shell.isDisposed()) { + shell.dispose(); + } + } + + @Test + void testFailingControlDoesNotAbortRescalingOfSiblingsWhenProcessedSynchronously() { + Composite composite = new Composite(shell, SWT.NONE); + Button failingChild = createFailingButton(composite); + Button subsequentChild = new Button(composite, SWT.PUSH); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, subsequentChild.getAutoscalingZoom(), + "Sibling of a failing control must still be adapted to the new zoom"); + assertEquals(newZoom, failingChild.getAutoscalingZoom(), + "Failing control must still be adapted to the new zoom"); + assertReportedFailures(1); + } + + @Test + void testFailingControlDoesNotAbortRescalingOfSiblingsWhenProcessedAsynchronously() { + shell.setLayout(new CountingLayout()); + Composite composite = new Composite(shell, SWT.NONE); + composite.setLayout(new CountingLayout()); + Button failingChild = createFailingButton(composite); + Button subsequentChild = new Button(composite, SWT.PUSH); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, subsequentChild.getAutoscalingZoom(), + "Sibling of a failing control must still be adapted to the new zoom"); + assertEquals(newZoom, failingChild.getAutoscalingZoom(), + "Failing control must still be adapted to the new zoom"); + assertReportedFailures(1); + } + + @Test + void testFailingControlDoesNotAbortRescalingOfOtherComposites() { + Composite failingComposite = new Composite(shell, SWT.NONE); + createFailingButton(failingComposite); + Composite subsequentComposite = new Composite(shell, SWT.NONE); + Button subsequentChild = new Button(subsequentComposite, SWT.PUSH); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, subsequentChild.getAutoscalingZoom(), + "Control in a composite following a failing one must still be adapted to the new zoom"); + assertReportedFailures(1); + } + + @Test + void testFailingCompositeDoesNotAbortRescalingOfItsChildren() { + Composite failingComposite = new Composite(shell, SWT.NONE); + // Adapting a control to a new zoom re-applies its region, which fails if the + // application has disposed that region in the meantime + Region region = new Region(display); + region.add(new Rectangle(0, 0, 10, 10)); + failingComposite.setRegion(region); + region.dispose(); + Button child = new Button(failingComposite, SWT.PUSH); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, child.getAutoscalingZoom(), + "Children of a failing composite must still be adapted to the new zoom"); + assertEquals(1, reportedExceptions.size(), + "Unexpected number of failures reported to the display: " + reportedExceptions); + } + + @Test + void testFailingControlDoesNotAbortRescalingOnMonitorChange() { + // The processing on a monitor change is the path used in production, in which + // the shell is not adapted as a task of the zoom change itself + CountingLayout layout = new CountingLayout(); + shell.setLayout(layout); + createFailingButton(shell); + Button subsequentChild = new Button(shell, SWT.PUSH); + layout.layoutCount = 0; + + DPITestUtil.changeDPIZoomOnMonitorChange(shell, newZoom); + + assertEquals(newZoom, subsequentChild.getAutoscalingZoom(), + "Sibling of a failing control must still be adapted to the new zoom"); + // One layout is caused by resizing the shell for the new zoom, the other one + // concludes the zoom change + assertEquals(2, layout.layoutCount, "The shell must be laid out once per zoom change despite the failure"); + assertReportedFailures(1); + } + + @Test + void testFailingControlDoesNotPreventShellLayout() { + CountingLayout layout = new CountingLayout(); + shell.setLayout(layout); + createFailingButton(shell); + layout.layoutCount = 0; + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertTrue(layout.layoutCount > 0, "The shell must be laid out even if a control failed to be adapted"); + assertReportedFailures(1); + } + + @Test + void testFailingItemDoesNotAbortRescalingOfRemainingItemsAndColumns() { + Table table = new Table(shell, SWT.NONE); + TableColumn column = new TableColumn(table, SWT.NONE); + TableItem failingItem = new TableItem(table, SWT.NONE); + failingItem.addListener(SWT.ZoomChanged, event -> { + throw new IllegalStateException(FAILURE_MESSAGE); + }); + TableItem subsequentItem = new TableItem(table, SWT.NONE); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, subsequentItem.getAutoscalingZoom(), + "Item following a failing item must still be adapted to the new zoom"); + assertEquals(newZoom, column.getAutoscalingZoom(), + "Columns must still be adapted to the new zoom if an item failed"); + assertReportedFailures(1); + } + + @Test + void testFailingItemDoesNotAbortRescalingOfItsChildItems() { + Tree tree = new Tree(shell, SWT.NONE); + new TreeColumn(tree, SWT.NONE); + new TreeColumn(tree, SWT.NONE); + TreeItem failingItem = new TreeItem(tree, SWT.NONE); + // Adapting an item to a new zoom re-applies its images, which fails if the + // application has disposed one of them in the meantime + Image image = new Image(display, 16, 16); + failingItem.setImage(1, image); + image.dispose(); + TreeItem childItem = new TreeItem(failingItem, SWT.NONE); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, childItem.getAutoscalingZoom(), + "Child items of a failing item must still be adapted to the new zoom"); + assertEquals(1, reportedExceptions.size(), + "Unexpected number of failures reported to the display: " + reportedExceptions); + } + + @Test + void testFailingMenuItemDoesNotAbortRescalingOfRemainingMenuItems() { + // A pop-up menu is used instead of a menu bar, as the latter is adapted twice: + // explicitly as the menu bar and again as part of the decoration's menus + Menu menu = new Menu(shell, SWT.POP_UP); + shell.setMenu(menu); + MenuItem failingItem = new MenuItem(menu, SWT.PUSH); + failingItem.addListener(SWT.ZoomChanged, event -> { + throw new IllegalStateException(FAILURE_MESSAGE); + }); + MenuItem subsequentItem = new MenuItem(menu, SWT.PUSH); + + DPITestUtil.changeDPIZoom(shell, newZoom); + + assertEquals(newZoom, subsequentItem.getAutoscalingZoom(), + "Menu item following a failing menu item must still be adapted to the new zoom"); + assertReportedFailures(1); + } + + @Test + void testFailureIsReportedToDisplayHandlerAndPropagatedIfItIsNotConsumed() { + display.setRuntimeExceptionHandler(DefaultExceptionHandler.RUNTIME_EXCEPTION_HANDLER); + Composite composite = new Composite(shell, SWT.NONE); + createFailingButton(composite); + Button subsequentChild = new Button(composite, SWT.PUSH); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> DPITestUtil.changeDPIZoom(shell, newZoom)); + + assertEquals(FAILURE_MESSAGE, exception.getMessage(), + "A failure that is not consumed by the exception handler must still be propagated"); + assertEquals(newZoom, subsequentChild.getAutoscalingZoom(), + "Sibling of a failing control must be adapted to the new zoom before the failure is propagated"); + } + + private Button createFailingButton(Composite parent) { + Button button = new Button(parent, SWT.PUSH); + button.addListener(SWT.ZoomChanged, event -> { + throw new IllegalStateException(FAILURE_MESSAGE); + }); + return button; + } + + private void assertReportedFailures(int expectedCount) { + assertEquals(expectedCount, reportedExceptions.size(), + "Unexpected number of failures reported to the display: " + reportedExceptions); + for (RuntimeException reportedException : reportedExceptions) { + assertEquals(FAILURE_MESSAGE, reportedException.getMessage()); + } + } + + private static final class CountingLayout extends Layout { + int layoutCount; + + @Override + protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { + return new Point(wHint == SWT.DEFAULT ? 100 : wHint, hHint == SWT.DEFAULT ? 100 : hHint); + } + + @Override + protected void layout(Composite composite, boolean flushCache) { + layoutCount++; + } + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Composite.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Composite.java index 3cec613b140..b3286f8aec7 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Composite.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Composite.java @@ -1965,7 +1965,13 @@ public String toString() { @Override void handleDPIChange(Event event, float scalingFactor) { - super.handleDPIChange(event, scalingFactor); + try { + super.handleDPIChange(event, scalingFactor); + } catch (Error | RuntimeException ex) { + // A failure of the adaptation of this composite must not prevent the whole + // subtree from being adapted + stashZoomChangeFailure(event, ex); + } for (Control child : getChildren()) { child.sendZoomChangedEvent(event, getShell()); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java index a162c1ce6ea..3a25cb142ba 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Control.java @@ -3399,7 +3399,7 @@ public boolean setAutoscalingMode(AutoscalingMode autoscalingMode) { Event zoomChangedEvent = createZoomChangedEvent(newZoom, false); startZoomChangeTask(zoomChangedEvent); try { - notifyListeners(SWT.ZoomChanged, zoomChangedEvent); + notifyZoomChanged(this, zoomChangedEvent); } finally { completeZoomChangeTask(zoomChangedEvent, getShell()); } @@ -6035,6 +6035,14 @@ static class DPIChangeExecution { private final Event event; private final AtomicInteger taskCount = new AtomicInteger(); private boolean asyncExec; + /** + * Collects the failures of the zoom change processing of individual widgets for + * the complete zoom change. A failure is reported to the exception handlers of + * the {@link Display} immediately, but is only propagated once the zoom change + * has been processed by all widgets, so that a single failing widget does not + * leave the remaining ones rendered with a stale zoom. + */ + private final ExceptionStash exceptions = new ExceptionStash(); private DPIChangeExecution(Event event, boolean asyncExec) { this.event = event; @@ -6056,16 +6064,33 @@ private void startTask() { } /** - * Completes a task registered with {@link #startTask()} and lays out the given - * shell if it was the last outstanding task of this zoom change. + * Completes a task registered with {@link #startTask()}. If it was the last + * outstanding task, the given shell is laid out and the collected failures are + * propagated, as the zoom change has then been processed by all widgets. */ private void completeTask(Shell shell) { // Deliberately not requiring the count to be exactly zero: if the accounting - // of tasks was ever off, laying out the shell once too often is preferable to - // not laying it out at all - if (taskCount.decrementAndGet() <= 0 && event.doit && !shell.isDisposed()) { - shell.layout(true, true); + // of tasks was ever off, completing the zoom change once too often is + // preferable to not completing it at all + if (taskCount.decrementAndGet() > 0) { + return; + } + try { + if (event.doit && !shell.isDisposed()) { + shell.layout(true, true); + } + } catch (Error | RuntimeException ex) { + exceptions.stash(ex); } + exceptions.close(); + } + + /** + * Collects a failure of the zoom change processing of a single widget, see + * {@link Widget#stashZoomChangeFailure(Event, Throwable)}. + */ + void stash(Throwable throwable) { + exceptions.stash(throwable); } /** @@ -6147,8 +6172,9 @@ void sendZoomChangedEvent(Event event, Shell shell) { startZoomChangeTask(event); dpiExecData.process(this, () -> { try { - if (!this.isDisposed() && event.doit) { - notifyListeners(SWT.ZoomChanged, event); + if (event.doit) { + // The disposal of this control is handled by the send itself + notifyZoomChanged(this, event); } } finally { completeZoomChangeTask(event, shell); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/CoolBar.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/CoolBar.java index 338a557f8e5..6fe28dee6b0 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/CoolBar.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/CoolBar.java @@ -1193,7 +1193,7 @@ void handleDPIChange(Event event, float scalingFactor) { Control control = item.control; if (control != null && !control.isDisposed()) { - control.notifyListeners(SWT.ZoomChanged, event); + notifyZoomChanged(control, event); item.setControl(control); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Decorations.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Decorations.java index 7162e5efc7a..1699bfae35f 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Decorations.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Decorations.java @@ -1700,16 +1700,11 @@ LRESULT WM_WINDOWPOSCHANGING (long wParam, long lParam) { void handleDPIChange(Event event, float scalingFactor) { super.handleDPIChange(event, scalingFactor); - Menu menuBar = getMenuBar(); - if (menuBar != null && !menuBar.isDisposed()) { - menuBar.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(getMenuBar(), event); if (menus != null) { for (Menu menu : menus) { - if (menu != null && !menu.isDisposed()) { - menu.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(menu, event); } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ExpandBar.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ExpandBar.java index 39aa8d40e5a..d605158a480 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ExpandBar.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ExpandBar.java @@ -873,9 +873,7 @@ LRESULT wmScroll (ScrollBar bar, boolean update, long hwnd, int msg, long wParam void handleDPIChange(Event event, float scalingFactor) { super.handleDPIChange(event, scalingFactor); for (ExpandItem item : getItems()) { - if (item != null && !item.isDisposed()) { - item.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(item, event); } layoutItems(0, true); redraw(); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Menu.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Menu.java index 8c359cf8878..df14d1c1aad 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Menu.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Menu.java @@ -1366,9 +1366,7 @@ LRESULT wmTimer (long wParam, long lParam) { void handleDPIChange(Event event, float scalingFactor) { super.handleDPIChange(event, scalingFactor); for (MenuItem item : getItems()) { - if (item != null && !item.isDisposed()) { - item.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(item, event); } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/MenuItem.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/MenuItem.java index e91bcb25908..9f563fc1777 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/MenuItem.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/MenuItem.java @@ -1460,9 +1460,6 @@ void handleDPIChange(Event event, float scalingFactor) { updateImage(); } // Refresh the sub menu - Menu subMenu = getMenu(); - if (subMenu != null && !subMenu.isDisposed()) { - subMenu.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(getMenu(), event); } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java index 10c54c64c41..86b287b3534 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Shell.java @@ -2547,8 +2547,12 @@ void handleMonitorSpecificDpiChange(int newNativeZoom, Rectangle newBoundsInPixe lastDpiChangeEvent = zoomChangedEvent; startZoomChangeTask(zoomChangedEvent); try { - notifyListeners(SWT.ZoomChanged, zoomChangedEvent); - this.setBoundsInPixels(newBoundsInPixels.x, newBoundsInPixels.y, newBoundsInPixels.width, newBoundsInPixels.height); + notifyZoomChanged(this, zoomChangedEvent); + // The shell must be moved to the new monitor even if the zoom change + // processing of some widget failed + if (!isDisposed()) { + setBoundsInPixels(newBoundsInPixels.x, newBoundsInPixels.y, newBoundsInPixels.width, newBoundsInPixels.height); + } } finally { completeZoomChangeTask(zoomChangedEvent, this); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TabFolder.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TabFolder.java index 105ac66fa7a..cc052649274 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TabFolder.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TabFolder.java @@ -1132,9 +1132,7 @@ void handleDPIChange(Event event, float scalingFactor) { imageList = null; } for (int i = 0; i < getItemCount(); i++) { - if (items[i] != null && !items[i].isDisposed()) { - items[i].notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(items[i], event); } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java index 91189e8b012..187a18586f1 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Table.java @@ -7347,14 +7347,10 @@ void handleDPIChange(Event event, float scalingFactor) { setItemHeight(-1); for (TableItem item : getItems()) { - if (item != null && !item.isDisposed()) { - item.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(item, event); } for (TableColumn tableColumn : getColumns()) { - if (tableColumn != null && !tableColumn.isDisposed()) { - tableColumn.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(tableColumn, event); } if (getColumns().length == 0 && scrollWidth != 0) { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java index 366959d31bf..bfefdc86ed6 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/ToolBar.java @@ -1706,7 +1706,7 @@ record ToolItemData(ToolItem toolItem, TBBUTTON button) { // at the end seperatorWidth[i] = item.getWidth(); } - item.notifyListeners(SWT.ZoomChanged, event); + notifyZoomChanged(item, event); // Capture the button data AFTER handling the zoom change. The zoom refresh // may update the item's image-list slot (iBitmap), so capturing the button // beforehand could re-add it with a stale image index, resulting in the diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Tree.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Tree.java index 86bd60e7465..b92fae3333a 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Tree.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Tree.java @@ -8269,14 +8269,10 @@ void handleDPIChange(Event event, float scalingFactor) { setItemHeight(-1); for (TreeColumn treeColumn : getColumns()) { - if (treeColumn != null && !treeColumn.isDisposed()) { - treeColumn.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(treeColumn, event); } for (TreeItem item : getItems()) { - if (item != null && !item.isDisposed()) { - item.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(item, event); } calculateAndApplyIndentSize(); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TreeItem.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TreeItem.java index 7fbeb06fe0e..c4046b5a241 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TreeItem.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/TreeItem.java @@ -1815,25 +1815,29 @@ String getNameText () { @Override void handleDPIChange(Event event, float scalingFactor) { super.handleDPIChange(event, scalingFactor); - if (images != null) { - for (int i = 1; i < images.length; i++) { - setImage(i, images[i]); + try { + if (images != null) { + for (int i = 1; i < images.length; i++) { + setImage(i, images[i]); + } } - } - if (font != null) { - setFont(font); - } - Font[] cellFonts = cellFont; - if (cellFonts != null) { - for (int index = 0; index < cellFonts.length; index++) { - Font cellFont = cellFonts[index]; - cellFonts[index] = cellFont == null ? null : Font.win32_new(cellFont, nativeZoom); + if (font != null) { + setFont(font); + } + Font[] cellFonts = cellFont; + if (cellFonts != null) { + for (int index = 0; index < cellFonts.length; index++) { + Font cellFont = cellFonts[index]; + cellFonts[index] = cellFont == null ? null : Font.win32_new(cellFont, nativeZoom); + } } + } catch (Error | RuntimeException ex) { + // A failure of the adaptation of this item, e.g. because one of its images has + // already been disposed, must not prevent the child items from being adapted + stashZoomChangeFailure(event, ex); } for (TreeItem item : getItems()) { - if (item != null && !item.isDisposed()) { - item.notifyListeners(SWT.ZoomChanged, event); - } + notifyZoomChanged(item, event); } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Widget.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Widget.java index d501c6da139..0602d3d2ce7 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Widget.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Widget.java @@ -2705,6 +2705,56 @@ void handleDPIChange(Event event, float scalingFactor) { this.nativeZoom = event.detail; } +/** + * Sends the given zoom changed event to the given widget, if it is still valid, + * isolating a failure of its processing as described in + * {@link #stashZoomChangeFailure(Event, Throwable)}. + */ +static void notifyZoomChanged (Widget widget, Event event) { + if (widget == null || widget.isDisposed ()) return; + try { + widget.notifyListeners (SWT.ZoomChanged, event); + } catch (Error | RuntimeException ex) { + stashZoomChangeFailure (event, ex); + } +} + +/** + * Collects a failure of the zoom change processing of a single widget in the + * execution of the given zoom changed event. + *

+ * Such a failure must not abort the processing of the zoom change by the widgets + * it is propagated to afterwards, as this would leave them rendered with a stale + * zoom. It is therefore reported to the exception handlers of the + * {@link Display} immediately and, if those do not consume it, propagated only + * once the zoom change has been processed completely. + *

+ *

+ * A widget that adapts itself before propagating the event further, as + * composites do for their children, is supposed to isolate the re-application + * of resources owned by the application, such as images, regions and fonts, + * since that is the code which can fail on state outside the control of SWT. + * SWT's own native bookkeeping is deliberately not isolated: if it fails, that + * widget's own propagation to its sub-widgets is skipped, as continuing it over + * an inconsistent native state is no improvement. Every other widget is adapted + * in either case, since the failure is collected by the sender of the event. + *

+ *

+ * A failure raised by a listener has already been reported by the + * {@link EventTable} and is only reported again if that report did not consume + * it. + *

+ */ +static void stashZoomChangeFailure (Event event, Throwable throwable) { + if (event.data instanceof Control.DPIChangeExecution execution) { + execution.stash (throwable); + } else if (throwable instanceof RuntimeException runtimeException) { + throw runtimeException; + } else { + throw (Error) throwable; + } +} + int getSystemMetrics(int nIndex) { return OS.GetSystemMetricsForDpi(nIndex, DPIUtil.mapZoomToDPI(nativeZoom)); }