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