Browse Source

Implemented screen_get_usable_rect for X11 Display Server

Helps with fixing issues with scrolling popups not respecting screen
decorations on the display server side.

Reproduction steps for a simple use case:
- Start the editor project list
- Open the language selection popup

Support for multi-screen:
Handling decorations is supported in different ways depending on the
information the window manager provides:
- _GTK_WORKAREAS is used when available from the WM to get accurate rect
for the different screens directly (available on Gnome).
- Alternatively, strut information is used to calculate available space
for a given desktop manually (XFCE, KDE).
- As last resort _NET_WORKAREA is used. It provides one full rect for all
screens, which doesn't handle decorations on the secondary screen in all
cases.
PouleyKetchoupp 5 years ago
parent
commit
85a934954d
1 changed files with 257 additions and 11 deletions
  1. 257 11
      platform/linuxbsd/display_server_x11.cpp

+ 257 - 11
platform/linuxbsd/display_server_x11.cpp

@@ -735,21 +735,267 @@ Rect2i DisplayServerX11::screen_get_usable_rect(int p_screen) const {
 		p_screen = window_get_current_screen();
 		p_screen = window_get_current_screen();
 	}
 	}
 
 
-	// Using Xinerama Extension
-	int event_base, error_base;
-	const Bool ext_okay = XineramaQueryExtension(x11_display, &event_base, &error_base);
-	if (!ext_okay) {
-		return Rect2i(0, 0, 0, 0);
+	int screen_count = get_screen_count();
+
+	// Check if screen is valid.
+	ERR_FAIL_INDEX_V(p_screen, screen_count, Rect2i(0, 0, 0, 0));
+
+	bool is_multiscreen = screen_count > 1;
+
+	// Use full monitor size as fallback.
+	Rect2i rect = _screen_get_rect(p_screen);
+
+	// There's generally only one screen reported by xlib even in multi-screen setup,
+	// in this case it's just one virtual screen composed of all physical monitors.
+	int x11_screen_count = ScreenCount(x11_display);
+	Window x11_window = RootWindow(x11_display, p_screen < x11_screen_count ? p_screen : 0);
+
+	Atom type;
+	int format = 0;
+	unsigned long remaining = 0;
+
+	// Find active desktop for the root window.
+	unsigned int desktop_index = 0;
+	Atom desktop_prop = XInternAtom(x11_display, "_NET_CURRENT_DESKTOP", True);
+	if (desktop_prop != None) {
+		unsigned long desktop_len = 0;
+		unsigned char *desktop_data = nullptr;
+		if (XGetWindowProperty(x11_display, x11_window, desktop_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &desktop_len, &remaining, &desktop_data) == Success) {
+			if ((format == 32) && (desktop_len > 0) && desktop_data) {
+				desktop_index = (unsigned int)desktop_data[0];
+			}
+			if (desktop_data) {
+				XFree(desktop_data);
+			}
+		}
 	}
 	}
 
 
-	int count;
-	XineramaScreenInfo *xsi = XineramaQueryScreens(x11_display, &count);
+	bool use_simple_method = true;
+
+	// First check for GTK work area, which is more accurate for multi-screen setup.
+	if (is_multiscreen) {
+		// Use already calculated work area when available.
+		Atom gtk_workareas_prop = XInternAtom(x11_display, "_GTK_WORKAREAS", False);
+		if (gtk_workareas_prop != None) {
+			char gtk_workarea_prop_name[32];
+			snprintf(gtk_workarea_prop_name, 32, "_GTK_WORKAREAS_D%d", desktop_index);
+			Atom gtk_workarea_prop = XInternAtom(x11_display, gtk_workarea_prop_name, True);
+			if (gtk_workarea_prop != None) {
+				unsigned long workarea_len = 0;
+				unsigned char *workarea_data = nullptr;
+				if (XGetWindowProperty(x11_display, x11_window, gtk_workarea_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &workarea_len, &remaining, &workarea_data) == Success) {
+					if ((format == 32) && (workarea_len % 4 == 0) && workarea_data) {
+						long *rect_data = (long *)workarea_data;
+						for (uint32_t data_offset = 0; data_offset < workarea_len; data_offset += 4) {
+							Rect2i workarea_rect;
+							workarea_rect.position.x = rect_data[data_offset];
+							workarea_rect.position.y = rect_data[data_offset + 1];
+							workarea_rect.size.x = rect_data[data_offset + 2];
+							workarea_rect.size.y = rect_data[data_offset + 3];
+
+							// Intersect with actual monitor size to find the correct area,
+							// because areas are not in the same order as screens from Xinerama.
+							if (rect.grow(-1).intersects(workarea_rect)) {
+								rect = rect.intersection(workarea_rect);
+								XFree(workarea_data);
+								return rect;
+							}
+						}
+					}
+				}
+				if (workarea_data) {
+					XFree(workarea_data);
+				}
+			}
+		}
 
 
-	// Check if screen is valid
-	ERR_FAIL_INDEX_V(p_screen, count, Rect2i(0, 0, 0, 0));
+		// Fallback to calculating work area by hand from struts.
+		Atom client_list_prop = XInternAtom(x11_display, "_NET_CLIENT_LIST", True);
+		if (client_list_prop != None) {
+			unsigned long clients_len = 0;
+			unsigned char *clients_data = nullptr;
+			if (XGetWindowProperty(x11_display, x11_window, client_list_prop, 0, LONG_MAX, False, XA_WINDOW, &type, &format, &clients_len, &remaining, &clients_data) == Success) {
+				if ((format == 32) && (clients_len > 0) && clients_data) {
+					Window *windows_data = (Window *)clients_data;
+
+					Rect2i desktop_rect;
+					bool desktop_valid = false;
+
+					// Get full desktop size.
+					{
+						Atom desktop_geometry_prop = XInternAtom(x11_display, "_NET_DESKTOP_GEOMETRY", True);
+						if (desktop_geometry_prop != None) {
+							unsigned long geom_len = 0;
+							unsigned char *geom_data = nullptr;
+							if (XGetWindowProperty(x11_display, x11_window, desktop_geometry_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &geom_len, &remaining, &geom_data) == Success) {
+								if ((format == 32) && (geom_len >= 2) && geom_data) {
+									desktop_valid = true;
+									long *size_data = (long *)geom_data;
+									desktop_rect.size.x = size_data[0];
+									desktop_rect.size.y = size_data[1];
+								}
+							}
+							if (geom_data) {
+								XFree(geom_data);
+							}
+						}
+					}
+
+					// Get full desktop position.
+					if (desktop_valid) {
+						Atom desktop_viewport_prop = XInternAtom(x11_display, "_NET_DESKTOP_VIEWPORT", True);
+						if (desktop_viewport_prop != None) {
+							unsigned long viewport_len = 0;
+							unsigned char *viewport_data = nullptr;
+							if (XGetWindowProperty(x11_display, x11_window, desktop_viewport_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &viewport_len, &remaining, &viewport_data) == Success) {
+								if ((format == 32) && (viewport_len >= 2) && viewport_data) {
+									desktop_valid = true;
+									long *pos_data = (long *)viewport_data;
+									desktop_rect.position.x = pos_data[0];
+									desktop_rect.position.y = pos_data[1];
+								}
+							}
+							if (viewport_data) {
+								XFree(viewport_data);
+							}
+						}
+					}
+
+					if (desktop_valid) {
+						use_simple_method = false;
+
+						for (unsigned long win_index = 0; win_index < clients_len; ++win_index) {
+							// Remove strut size from desktop size to get a more accurate result.
+							bool strut_found = false;
+							unsigned long strut_len = 0;
+							unsigned char *strut_data = nullptr;
+							Atom strut_partial_prop = XInternAtom(x11_display, "_NET_WM_STRUT_PARTIAL", True);
+							if (strut_partial_prop != None) {
+								if (XGetWindowProperty(x11_display, windows_data[win_index], strut_partial_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &strut_len, &remaining, &strut_data) == Success) {
+									strut_found = true;
+								}
+							}
+							// Fallback to older strut property.
+							if (!strut_found) {
+								Atom strut_prop = XInternAtom(x11_display, "_NET_WM_STRUT", True);
+								if (strut_prop != None) {
+									if (XGetWindowProperty(x11_display, windows_data[win_index], strut_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &strut_len, &remaining, &strut_data) == Success) {
+										strut_found = true;
+									}
+								}
+							}
+							if (strut_found && (format == 32) && (strut_len >= 4) && strut_data) {
+								long *struts = (long *)strut_data;
+
+								long left = struts[0];
+								long right = struts[1];
+								long top = struts[2];
+								long bottom = struts[3];
+
+								long left_start_y, left_end_y, right_start_y, right_end_y;
+								long top_start_x, top_end_x, bottom_start_x, bottom_end_x;
+
+								if (strut_len >= 12) {
+									left_start_y = struts[4];
+									left_end_y = struts[5];
+									right_start_y = struts[6];
+									right_end_y = struts[7];
+									top_start_x = struts[8];
+									top_end_x = struts[9];
+									bottom_start_x = struts[10];
+									bottom_end_x = struts[11];
+								} else {
+									left_start_y = 0;
+									left_end_y = desktop_rect.size.y;
+									right_start_y = 0;
+									right_end_y = desktop_rect.size.y;
+									top_start_x = 0;
+									top_end_x = desktop_rect.size.x;
+									bottom_start_x = 0;
+									bottom_end_x = desktop_rect.size.x;
+								}
+
+								const Point2i &pos = desktop_rect.position;
+								const Size2i &size = desktop_rect.size;
+
+								Rect2i left_rect(pos.x, pos.y + left_start_y, left, left_end_y - left_start_y);
+								if (left_rect.size.x > 0) {
+									Rect2i intersection = rect.intersection(left_rect);
+									if (!intersection.has_no_area() && intersection.size.x < rect.size.x) {
+										rect.position.x = left_rect.size.x;
+										rect.size.x = rect.size.x - intersection.size.x;
+									}
+								}
+
+								Rect2i right_rect(pos.x + size.x - right, pos.y + right_start_y, right, right_end_y - right_start_y);
+								if (right_rect.size.x > 0) {
+									Rect2i intersection = rect.intersection(right_rect);
+									if (!intersection.has_no_area() && right_rect.size.x < rect.size.x) {
+										rect.size.x = intersection.position.x - rect.position.x;
+									}
+								}
+
+								Rect2i top_rect(pos.x + top_start_x, pos.y, top_end_x - top_start_x, top);
+								if (top_rect.size.y > 0) {
+									Rect2i intersection = rect.intersection(top_rect);
+									if (!intersection.has_no_area() && intersection.size.y < rect.size.y) {
+										rect.position.y = top_rect.size.y;
+										rect.size.y = rect.size.y - intersection.size.y;
+									}
+								}
+
+								Rect2i bottom_rect(pos.x + bottom_start_x, pos.y + size.y - bottom, bottom_end_x - bottom_start_x, bottom);
+								if (bottom_rect.size.y > 0) {
+									Rect2i intersection = rect.intersection(bottom_rect);
+									if (!intersection.has_no_area() && right_rect.size.y < rect.size.y) {
+										rect.size.y = intersection.position.y - rect.position.y;
+									}
+								}
+							}
+							if (strut_data) {
+								XFree(strut_data);
+							}
+						}
+					}
+				}
+			}
+			if (clients_data) {
+				XFree(clients_data);
+			}
+		}
+	}
+
+	// Single screen or fallback for multi screen.
+	if (use_simple_method) {
+		// Get desktop available size from the global work area.
+		Atom workarea_prop = XInternAtom(x11_display, "_NET_WORKAREA", True);
+		if (workarea_prop != None) {
+			unsigned long workarea_len = 0;
+			unsigned char *workarea_data = nullptr;
+			if (XGetWindowProperty(x11_display, x11_window, workarea_prop, 0, LONG_MAX, False, XA_CARDINAL, &type, &format, &workarea_len, &remaining, &workarea_data) == Success) {
+				if ((format == 32) && (workarea_len >= ((desktop_index + 1) * 4)) && workarea_data) {
+					long *rect_data = (long *)workarea_data;
+					int data_offset = desktop_index * 4;
+					Rect2i workarea_rect;
+					workarea_rect.position.x = rect_data[data_offset];
+					workarea_rect.position.y = rect_data[data_offset + 1];
+					workarea_rect.size.x = rect_data[data_offset + 2];
+					workarea_rect.size.y = rect_data[data_offset + 3];
+
+					// Intersect with actual monitor size to get a proper approximation in multi-screen setup.
+					if (!is_multiscreen) {
+						rect = workarea_rect;
+					} else if (rect.intersects(workarea_rect)) {
+						rect = rect.intersection(workarea_rect);
+					}
+				}
+			}
+			if (workarea_data) {
+				XFree(workarea_data);
+			}
+		}
+	}
 
 
-	Rect2i rect = Rect2i(xsi[p_screen].x_org, xsi[p_screen].y_org, xsi[p_screen].width, xsi[p_screen].height);
-	XFree(xsi);
 	return rect;
 	return rect;
 }
 }