diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/DPITestUtil.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/DPITestUtil.java index f105f6bec12..8e10a50a086 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/DPITestUtil.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/DPITestUtil.java @@ -14,7 +14,6 @@ package org.eclipse.swt.widgets; import java.time.*; -import java.util.concurrent.atomic.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.widgets.Control.*; @@ -30,19 +29,24 @@ public static void changeDPIZoom (Shell shell, int nativeZoom) { DPIUtil.setDeviceZoom(nativeZoom); Event event = shell.createZoomChangedEvent(nativeZoom, true); shell.sendZoomChangedEvent(event, shell); - DPIChangeExecution data = (DPIChangeExecution) event.data; - waitForDPIChange(shell, TIMEOUT_MILLIS, data.taskCount); + waitForDPIChange(shell, (DPIChangeExecution) event.data); } - private static void waitForDPIChange(Shell shell, int timeout, AtomicInteger scalingCounter) { - waitForPassCondition(shell, timeout, scalingCounter); + /** + * Performs a zoom change like the operating system does when the shell is moved + * to a monitor with a different scaling, i.e. by processing the zoom change for + * the shell instead of sending the zoom changed event to it. + */ + public static void changeDPIZoomOnMonitorChange (Shell shell, int nativeZoom) { + shell.handleMonitorSpecificDpiChange(nativeZoom, shell.getBoundsInPixels()); + waitForDPIChange(shell, (DPIChangeExecution) shell.lastDpiChangeEvent.data); } - private static void waitForPassCondition(Shell shell, int timeout, AtomicInteger scalingCounter) { - final Instant timeOut = Instant.now().plusMillis(timeout); + private static void waitForDPIChange(Shell shell, DPIChangeExecution execution) { + final Instant timeOut = Instant.now().plusMillis(TIMEOUT_MILLIS); final Display display = shell == null ? Display.getDefault() : shell.getDisplay(); - while (Instant.now().isBefore(timeOut) && scalingCounter.get() != 0) { + while (Instant.now().isBefore(timeOut) && !execution.isComplete()) { if (!display.isDisposed()) { display.readAndDispatch(); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/WidgetWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/WidgetWin32Tests.java index e7222e0371e..233bcebfd2a 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/WidgetWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/widgets/WidgetWin32Tests.java @@ -331,4 +331,38 @@ public void testCaretInStyledTextAfterZooming() { } + @Test + public void testShellIsLaidOutOnceWhenZoomChangeIsProcessedSynchronously() { + Display display = Display.getDefault(); + // A child shell processes the zoom change synchronously, i.e. all its children + // are adapted within the propagation of the zoom changed event + Shell parentShell = new Shell(display); + Shell shell = new Shell(parentShell, SWT.SHELL_TRIM); + CountingLayout layout = new CountingLayout(); + shell.setLayout(layout); + for (int i = 0; i < 3; i++) { + new Button(shell, SWT.PUSH); + } + int scaledZoom = shell.nativeZoom * 2; + + layout.layoutCount = 0; + DPITestUtil.changeDPIZoomOnMonitorChange(shell, scaledZoom); + + assertEquals("The shell must be laid out exactly once for a single zoom change", 1, layout.layoutCount); + } + + 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++; + } + } + } \ No newline at end of file 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 8aac323cd12..d9b0a9df257 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 @@ -3397,7 +3397,12 @@ public boolean setAutoscalingMode(AutoscalingMode autoscalingMode) { if (nativeZoom != newZoom) { nativeZoom = newZoom; Event zoomChangedEvent = createZoomChangedEvent(newZoom, false); - notifyListeners(SWT.ZoomChanged, zoomChangedEvent); + startZoomChangeTask(zoomChangedEvent); + try { + notifyListeners(SWT.ZoomChanged, zoomChangedEvent); + } finally { + completeZoomChangeTask(zoomChangedEvent, getShell()); + } } return true; } @@ -5141,9 +5146,7 @@ Event createZoomChangedEvent(int zoom, boolean asyncExec) { event.widget = this; event.detail = zoom; event.doit = true; - DPIChangeExecution dpiChangeExecution = new DPIChangeExecution(); - dpiChangeExecution.asyncExec = asyncExec; - event.data = dpiChangeExecution; + event.data = new DPIChangeExecution(event, asyncExec); return event; } @@ -6023,9 +6026,51 @@ LRESULT wmScrollChild (long wParam, long lParam) { return null; } +/** + * The processing of a single zoom change, which is performed in tasks that may be + * executed asynchronously. The zoom change is complete, and the shell is laid out, + * once all its tasks have been completed. + */ static class DPIChangeExecution { - AtomicInteger taskCount = new AtomicInteger(); - private boolean asyncExec = true; + private final Event event; + private final AtomicInteger taskCount = new AtomicInteger(); + private boolean asyncExec; + + private DPIChangeExecution(Event event, boolean asyncExec) { + this.event = event; + this.asyncExec = asyncExec; + } + + /** + * Registers a task of this zoom change, which must be completed with + * {@link #completeTask(Shell)}. + *

+ * Everything propagating the zoom changed event to widgets that are adapted in + * a task of their own must hold a task itself, as the zoom change would + * otherwise be considered complete as soon as the first of those widgets has + * been adapted. + *

+ */ + private void startTask() { + taskCount.incrementAndGet(); + } + + /** + * Completes a task registered with {@link #startTask()} and lays out the given + * shell if it was the last outstanding task of this zoom change. + */ + private void completeTask(Shell shell) { + if (taskCount.decrementAndGet() <= 0 && event.doit && !shell.isDisposed()) { + shell.layout(true, true); + } + } + + /** + * Returns whether all tasks of this zoom change have been completed. + */ + boolean isComplete() { + return taskCount.get() <= 0; + } private void process(Control control, Runnable operation) { boolean currentAsyncExec = asyncExec; @@ -6044,14 +6089,6 @@ private void process(Control control, Runnable operation) { // DPI change handling is finished asyncExec = currentAsyncExec; } - - private void increment() { - taskCount.incrementAndGet(); - } - - private boolean decrement() { - return taskCount.decrementAndGet() <= 0; - } } private static class DPIChangeProcessingCallback { @@ -6099,26 +6136,39 @@ static void schedule(Control control, Runnable runnable) { void sendZoomChangedEvent(Event event, Shell shell) { if (event.data instanceof DPIChangeExecution dpiExecData) { - dpiExecData.increment(); + startZoomChangeTask(event); dpiExecData.process(this, () -> { try { if (!this.isDisposed() && event.doit) { notifyListeners(SWT.ZoomChanged, event); } } finally { - if (shell.isDisposed()) { - return; - } - if (dpiExecData.decrement()) { - if (event.doit) { - shell.layout(true, true); - } - } + completeZoomChangeTask(event, shell); } }); } } +/** + * Registers a task of the zoom change the given event belongs to, see + * {@link DPIChangeExecution#startTask()}. + */ +static void startZoomChangeTask(Event event) { + if (event.data instanceof DPIChangeExecution execution) { + execution.startTask(); + } +} + +/** + * Completes a task of the zoom change the given event belongs to, see + * {@link DPIChangeExecution#completeTask(Shell)}. + */ +static void completeZoomChangeTask(Event event, Shell shell) { + if (event.data instanceof DPIChangeExecution execution) { + execution.completeTask(shell); + } +} + private boolean isAutoscalingDisabled() { return autoscalingMode != AutoscalingMode.ENABLED; } 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 547063df0ce..10c54c64c41 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 @@ -2535,7 +2535,7 @@ LRESULT WM_DPICHANGED (long wParam, long lParam) { return LRESULT.ONE; } -private void handleMonitorSpecificDpiChange(int newNativeZoom, Rectangle newBoundsInPixels) { +void handleMonitorSpecificDpiChange(int newNativeZoom, Rectangle newBoundsInPixels) { DPIUtil.setDeviceZoom (newNativeZoom); // Do not process DPI change for child shells asynchronous to avoid relayouting when // repositioning the child shell to a different monitor upon opening @@ -2545,8 +2545,13 @@ private void handleMonitorSpecificDpiChange(int newNativeZoom, Rectangle newBoun lastDpiChangeEvent.doit = false; } lastDpiChangeEvent = zoomChangedEvent; - notifyListeners(SWT.ZoomChanged, zoomChangedEvent); - this.setBoundsInPixels(newBoundsInPixels.x, newBoundsInPixels.y, newBoundsInPixels.width, newBoundsInPixels.height); + startZoomChangeTask(zoomChangedEvent); + try { + notifyListeners(SWT.ZoomChanged, zoomChangedEvent); + this.setBoundsInPixels(newBoundsInPixels.x, newBoundsInPixels.y, newBoundsInPixels.width, newBoundsInPixels.height); + } finally { + completeZoomChangeTask(zoomChangedEvent, this); + } } @Override