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