diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java index 2bfa4ee4980..0d782a21718 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Control.java @@ -1691,8 +1691,9 @@ public Point toControl(int x, int y) { origin_x[0] = origin.x; origin_y[0] = origin.y; } else { - long window = eventWindow(); - GDK.gdk_window_get_origin(window, origin_x, origin_y); + Point origin = getWindowOrigin(); + origin_x[0] = origin.x; + origin_y[0] = origin.y; } x -= origin_x[0]; @@ -1756,8 +1757,9 @@ public Point toDisplay(int x, int y) { origin_x[0] = origin.x; origin_y[0] = origin.y; } else { - long window = eventWindow(); - GDK.gdk_window_get_origin(window, origin_x, origin_y); + Point origin = getWindowOrigin(); + origin_x[0] = origin.x; + origin_y[0] = origin.y; } if ((style & SWT.MIRRORED) != 0) x = getClientWidth() - x; @@ -6919,9 +6921,31 @@ Point getWindowOrigin () { long window = eventWindow (); GDK.gdk_window_get_origin (window, x, y); + Point monitorOrigin = monitorOrigin (); + if (monitorOrigin != null) { + x [0] += monitorOrigin.x; + y [0] += monitorOrigin.y; + } + return new Point (x [0], y [0]); } +/** + * Offset that maps window relative GDK coordinates into the space of the monitor showing the + * receiver, or null when none is needed. Wayland reports no global position, so + * without it {@link Monitor} geometry and control coordinates cannot be compared. + */ +Point monitorOrigin () { + if (GTK.GTK4 || !OS.isWayland ()) return null; + // An unmapped Shell has no GdkWindow; the nearest ancestor that has one is on the same monitor. + long window = 0; + for (Control control = this; control != null; control = control.parent) { + window = gtk_widget_get_window (control.getShell ().topHandle ()); + if (window != 0) break; + } + return display.monitorOrigin (window); +} + /** * Gets the position of the top left corner of the control in root window (display) coordinates. * GTK4 only, do not call on GTK3. diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java index 4a1bc89cce3..3204b5affe2 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Display.java @@ -2002,6 +2002,22 @@ boolean filters (int eventType) { return filterTable.hooks (eventType); } +/** + * Origin of the monitor showing the given GdkWindow, or null when no offset is + * needed. See {@link Control#monitorOrigin()}. + */ +Point monitorOrigin (long window) { + if (GTK.GTK4 || !OS.isWayland () || window == 0) return null; + long displayHandle = GDK.gdk_display_get_default (); + if (displayHandle == 0) return null; + long monitor = GDK.gdk_display_get_monitor_at_window (displayHandle, window); + if (monitor == 0) return null; + GdkRectangle geometry = new GdkRectangle (); + GDK.gdk_monitor_get_geometry (monitor, geometry); + if (geometry.x == 0 && geometry.y == 0) return null; + return new Point (geometry.x, geometry.y); +} + /** * Returns the location of the on-screen pointer relative * to the top left corner of the screen. @@ -2024,7 +2040,7 @@ public Point getCursorLocation() { x[0] = (int)xDouble[0]; y[0] = (int)yDouble[0]; } else { - getWindowPointerPosition(0, x, y, null); + long pointerWindow = getWindowPointerPosition(0, x, y, null); /* * Wayland feature: There is no global x/y coordinates in Wayland for security measures, so they @@ -2045,6 +2061,17 @@ public Point getCursorLocation() { y[0]+= offsetY[0]; tempShell = tempShell.getParent().getShell(); } + /* + * Callers compare this against Control.toDisplay(), so use the same space. + * Prefer the monitor of the window under the pointer, which is the one the + * coordinates are relative to; the active shell is only a fallback. + */ + Point origin = monitorOrigin (pointerWindow); + if (origin == null) origin = tempShell.monitorOrigin (); + if (origin != null) { + x[0] += origin.x; + y[0] += origin.y; + } } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java index 19467e4b2d3..56d5636e86b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Menu.java @@ -447,8 +447,12 @@ void _setVisible (boolean visible) { GTK3.memmove (eventPtr, event, GdkEventButton.sizeof); // Bug in GTK?: testing with SWT_MENU_LOCATION_DEBUGGING=1 shows final_rect.x and // final_rect.y popup menu position is off by 1 compared to this.x and this.y - rect.x = this.x + 1; - rect.y = this.y + 1; + // The rectangle is relative to the shell, so the monitor origin has to go. + Point monitorOrigin = getShell ().monitorOrigin (); + int originX = monitorOrigin != null ? monitorOrigin.x : 0; + int originY = monitorOrigin != null ? monitorOrigin.y : 0; + rect.x = this.x + 1 - originX; + rect.y = this.y + 1 - originY; } // Popup the menu and pin it at the top left corner of the GdkRectangle relative to the GdkWindow GTK3.gtk_menu_popup_at_rect(handle, event.window, rect, GDK.GDK_GRAVITY_NORTH_WEST, diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java index c55f99be603..97c4cc6512f 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Shell.java @@ -1348,10 +1348,26 @@ public Point getLocation() { // TODO: GTK4 GtkWindow no longer has the ability to get position } else { GTK3.gtk_window_get_position (shellHandle, x, y); + applyMonitorOrigin (x, y); } return new Point (x [0], y [0]); } +@Override +Point monitorOrigin () { + // A child Shell is positioned while still hidden, before its own monitor is known. + if (parent != null) return parent.monitorOrigin (); + return super.monitorOrigin (); +} + +/** Shifts a window relative GTK position into display coordinates. */ +void applyMonitorOrigin (int [] x, int [] y) { + Point origin = monitorOrigin (); + if (origin == null) return; + x [0] += origin.x; + y [0] += origin.y; +} + @Override public boolean getMaximized () { checkWidget(); @@ -1601,6 +1617,7 @@ long gtk3_button_press_event (long widget, long event) { long gtk_configure_event (long widget, long event) { int [] x = new int [1], y = new int [1]; GTK3.gtk_window_get_position (shellHandle, x, y); + applyMonitorOrigin (x, y); if (!isVisible ()) { return 0; //We shouldn't handle move/resize events if shell is hidden. @@ -2361,8 +2378,10 @@ void resizeBounds (int width, int height, boolean notify) { GDK.gdk_window_resize (enableWindow, width, height); } } - int boxWidth = width - 2*border; - int boxHeight = height - 2*border; + // GTK rejects negative allocations; a shell smaller than its own border must not + // leak a negative size into gtk_widget_size_allocate(). + int boxWidth = Math.max (0, width - 2*border); + int boxHeight = Math.max (0, height - 2*border); if ((style & SWT.RESIZE) == 0) { GTK.gtk_widget_set_size_request (vboxHandle, boxWidth, boxHeight); } @@ -2415,9 +2434,13 @@ int setBounds (int x, int y, int width, int height, boolean move, boolean resize } if (mapped) positionPopover(); } else if (!GTK.GTK4) { + // GTK positions windows in its own space; x and y arrive in display coordinates. + Point origin = monitorOrigin (); + int gtkX = origin != null ? x - origin.x : x; + int gtkY = origin != null ? y - origin.y : y; int [] x_pos = new int [1], y_pos = new int [1]; GTK3.gtk_window_get_position(shellHandle, x_pos, y_pos); - GTK3.gtk_window_move(shellHandle, x, y); + GTK3.gtk_window_move(shellHandle, gtkX, gtkY); /* * Bug in GTK: gtk_window_get_position () is not always up-to-date right after * gtk_window_move (). The random delays cause problems like bug 445900. @@ -2429,11 +2452,11 @@ int setBounds (int x, int y, int width, int height, boolean move, boolean resize for (int i = 0; i < 1000; i++) { int [] x2_pos = new int [1], y2_pos = new int [1]; GTK3.gtk_window_get_position(shellHandle, x2_pos, y2_pos); - if (x2_pos[0] == x && y2_pos[0] == y) { + if (x2_pos[0] == gtkX && y2_pos[0] == gtkY) { break; } } - if (x_pos [0] != x || y_pos [0] != y) { + if (x_pos [0] != gtkX || y_pos [0] != gtkY) { moved = true; oldX = x; oldY = y; @@ -3611,6 +3634,7 @@ Rectangle getBoundsInPixels () { GDK.gdk_window_get_root_origin(GTK3.gtk_widget_get_window(shellHandle), x, y); } } + if (!GTK.GTK4) applyMonitorOrigin (x, y); GtkAllocation allocation = new GtkAllocation (); GTK.gtk_widget_get_allocation (vboxHandle, allocation); int width = allocation.width; diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java index d16f1e47ae1..18a114a39c2 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java @@ -1054,4 +1054,63 @@ public void test_setLocationII() { } super.test_setLocationII(); } + +@Test +public void test_degenerateSizeDoesNotExpandShell() { + // A bordered popup asked for a height of 0 used to be allocated -1, which GTK rejects, + // so it fell back to its natural size with its content never laid out. See issue 3539. + // GTK4 backs these shells with a GtkPopover, which never went through that box maths + // and enforces a minimum height of its own. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + Shell popup = new Shell(shell, SWT.TOOL | SWT.ON_TOP); + popup.setLayout(new FillLayout()); + Button child = new Button(popup, SWT.PUSH); + child.setText("some content that is clearly taller than two pixels"); + popup.setSize(228, 0); + popup.setVisible(true); + SwtTestUtil.processEvents(); + Rectangle clientArea = popup.getClientArea(); + popup.dispose(); + assertTrue(clientArea.height >= 0, "negative client area: " + clientArea); + assertTrue(clientArea.height <= 2, "shell expanded to its natural size: " + clientArea); +} + +@Test +public void test_toDisplayIsOnTheShellsMonitor() { + // Display coordinates and Monitor geometry have to share one coordinate space, otherwise + // callers that clip a location against a monitor discard it. See issue 3539. + // GTK4 has no root coordinates yet, so toDisplay() is still shell relative there. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + shell.setSize(300, 200); + shell.open(); + SwtTestUtil.processEvents(); + Rectangle monitor = shell.getMonitor().getBounds(); + Point origin = shell.toDisplay(0, 0); + assertTrue(monitor.contains(origin), "toDisplay " + origin + " is outside monitor " + monitor); +} + +@Test +public void test_popupLocationRoundTrip() { + // JFace reuses one popup: position, show, hide, reposition. Every pass has to round-trip, + // including the ones where the shell is still mapped. See issue 3539. + assumeTrue(SwtTestUtil.isGTK && !SwtTestUtil.isGTK4()); + shell.setSize(400, 300); + shell.open(); + SwtTestUtil.processEvents(); + Shell popup = new Shell(shell, SWT.TOOL | SWT.ON_TOP); + popup.setSize(120, 80); + try { + for (int pass = 0; pass < 3; pass++) { + Point location = shell.toDisplay(40 + pass * 10, 40 + pass * 10); + popup.setLocation(location); + popup.setVisible(true); + SwtTestUtil.processEvents(); + assertEquals(location, popup.getLocation(), "pass " + pass); + popup.setVisible(false); + SwtTestUtil.processEvents(); + } + } finally { + popup.dispose(); + } +} }