From 4733652dd53d9a8bb1e2dc981d599e1e5cbf956b Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sat, 7 May 2022 22:39:19 -0500 Subject: [PATCH] Updated titlebar options and top-level titlebar. Fixed bugs with SizeFrame. Ensure that the resize does not go past screen/area boundaries. Also ensure that the widget does not jump, by ensuring we have a local and not global rect. We also fixed that if the resize goes below the minimum size values of the widget, we don't drag the widget. Added top-level titlebar. This has been tested to work on X11 (with numerous workarounds), and has various tricks to set window flags without hiding and re-showing the window. Fixed bugs with the menu size event. Properly keep the override cursor outside the application boundaries, and handle clicks outside as well. It also limits the resizing to the current MDI area's bounds. Still need to provide fixes for moving/resizing windows on Wayland. --- ISSUES.md | 2 +- example/shared.py | 6 + example/titlebar.py | 1019 +++++++++++++++++++++++++++++++------------ 3 files changed, 756 insertions(+), 271 deletions(-) diff --git a/ISSUES.md b/ISSUES.md index e74ddbd..c4a4add 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -392,7 +392,7 @@ def main(): ### Title Bar Customization -The system title bar cannot be customized extensively, since it depends on either the application style or the system theme for how it renders. For a comprehensive example on how to create your own, custom title bar, with fully functional minimize, maximize, shade, unshade, context help, keep above, window title, and a context menu, see [titlebar.py](/example/titlebar.py). This is a drop-in replacement for the title bar on `QMdiSubWindow` which also lets you customize the placement of where the windows minimize to, but could also be modified for `QMainWindow` or `QDialog`. +The system title bar cannot be customized extensively, since it depends on either the application style or the system theme for how it renders. For a comprehensive example on how to create your own, custom title bar, with fully functional minimize, maximize, shade, unshade, context help, keep above, window title, and a context menu, see [titlebar.py](/example/titlebar.py). This is a drop-in replacement for the title bar on `QMdiSubWindow` and `QMaindWindow` which also lets you customize the placement of where the windows minimize to, but could also be modified for `QDialog`. Custom Title Bar diff --git a/example/shared.py b/example/shared.py index 60041d1..b6f7602 100644 --- a/example/shared.py +++ b/example/shared.py @@ -421,8 +421,11 @@ def get_compat_definitions(args): ns.SubWindow = ns.WindowType.SubWindow ns.WindowContextHelpButtonHint = ns.WindowType.WindowContextHelpButtonHint ns.WindowShadeButtonHint = ns.WindowType.WindowShadeButtonHint + ns.BypassWindowManagerHint = ns.WindowType.BypassWindowManagerHint + ns.X11BypassWindowManagerHint = ns.WindowType.X11BypassWindowManagerHint ns.FramelessWindowHint = ns.WindowType.FramelessWindowHint ns.WindowStaysOnTopHint = ns.WindowType.WindowStaysOnTopHint + ns.WindowStaysOnBottomHint = ns.WindowType.WindowStaysOnBottomHint ns.WindowNoState = ns.WindowState.WindowNoState ns.WindowMinimized = ns.WindowState.WindowMinimized ns.WindowMaximized = ns.WindowState.WindowMaximized @@ -699,8 +702,11 @@ def get_compat_definitions(args): ns.SubWindow = QtCore.Qt.SubWindow ns.WindowContextHelpButtonHint = QtCore.Qt.WindowContextHelpButtonHint ns.WindowShadeButtonHint = QtCore.Qt.WindowShadeButtonHint + ns.BypassWindowManagerHint = QtCore.Qt.BypassWindowManagerHint + ns.X11BypassWindowManagerHint = QtCore.Qt.X11BypassWindowManagerHint ns.FramelessWindowHint = QtCore.Qt.FramelessWindowHint ns.WindowStaysOnTopHint = QtCore.Qt.WindowStaysOnTopHint + ns.WindowStaysOnBottomHint = QtCore.Qt.WindowStaysOnBottomHint ns.WindowNoState = QtCore.Qt.WindowNoState ns.WindowMinimized = QtCore.Qt.WindowMinimized ns.WindowMaximized = QtCore.Qt.WindowMaximized diff --git a/example/titlebar.py b/example/titlebar.py index 369810c..364e47e 100644 --- a/example/titlebar.py +++ b/example/titlebar.py @@ -67,14 +67,30 @@ have `NoFrame` without a border, but should be a `Box` with a border. Any other more elaborate style, like a `Panel`, won't be rendered correctly. + + The top-level titlebar can have a few issues. ''' import enum +import os import shared import sys from pathlib import Path +# Determine if we're running on the Wayland display manager. +# Do this above the argument parser, since we might modify +# XDG_SESSION_TYPE. +IS_WAYLAND = 'WAYLAND_DISPLAY' in os.environ +IS_XWAYLAND = os.environ.get('XDG_SESSION_TYPE', 'xwayland') +IS_X11 = os.environ.get('XDG_SESSION_TYPE', 'x11') + +# TODO(ahuszagh) Need to determine if we're running: +# weston +# kwin_wayland +# gnome? +# Probably going to need to check if that process is running. + parser = shared.create_parser() parser.add_argument( '--minimize-location', @@ -89,6 +105,26 @@ parser.add_argument( choices=range(0, 6), default=1, ) +parser.add_argument( + '--default-window-frame', + help='use the default title bars', + action='store_true', +) +parser.add_argument( + '--status-bar', + help='use a top-level status bar', + action='store_true', +) +parser.add_argument( + '--window-help', + help='add a top-level context help button', + action='store_true', +) +parser.add_argument( + '--window-shade', + help='add a top-level shade/unshade button', + action='store_true', +) args, unknown = shared.parse_args(parser) QtCore, QtGui, QtWidgets = shared.import_qt(args) compat = shared.get_compat_definitions(args) @@ -97,10 +133,17 @@ ICON_MAP = shared.get_icon_map(args, compat) # 100ms between repaints, so we avoid over-repainting. # Allows us to avoid glitchy motion during drags/ REPAINT_TIMER = 100 +TRACK_TIMER = 20 CLICK_TIMER = 20 # Make the titlebar size too large, so we can get the real value with min. TITLEBAR_HEIGHT = 2**16 +# Add a warning if we're using Wayland with a custom titlebar. +if not args.default_window_frame and IS_WAYLAND: + print('WARNING: Wayland does not support custom title bars.', file=sys.stderr) + print('Applications in Wayland cannot set their own position.', file=sys.stderr) + print('Defaulting to the system title bar instead.', file=sys.stderr) + class MinimizeLocation(enum.IntEnum): '''Location where to place minimized widgets.''' @@ -123,6 +166,10 @@ class WindowEdge(enum.IntEnum): BottomRight = 8 MINIMIZE_LOCATION = getattr(MinimizeLocation, args.minimize_location) +TOP_EDGES = (WindowEdge.Top, WindowEdge.TopLeft, WindowEdge.TopRight) +BOTTOM_EDGES = (WindowEdge.Bottom, WindowEdge.BottomLeft, WindowEdge.BottomRight) +LEFT_EDGES = (WindowEdge.Left, WindowEdge.TopLeft, WindowEdge.BottomLeft) +RIGHT_EDGES = (WindowEdge.Right, WindowEdge.TopRight, WindowEdge.BottomRight) def standard_icon(widget, icon): '''Get a standard icon.''' @@ -321,6 +368,237 @@ class SettingTabs(QtWidgets.QTabWidget): if ok: edit.setText(font.family()) +# RESIZE HELPERS + +# TODO(ahuszagh) Might be able to move windows on Weston +# Check if WAYLAND_DISPLAY is something like `weston-0`. +# weston_view_set_initial_position +# weston_view_set_position + +def border_size(self): + '''Get the size of the border, regardless if present.''' + return QtCore.QSize(2 * self._border, 2 * self._border) + +def minimized_content_size(self): + '''Get the minimum content size of the widget.''' + return self._titlebar_size + +def minimized_size(self): + '''Get the minimum size of the widget, with the size grips hidden.''' + + size = self.minimized_content_size + if self._border: + size = size + self.border_size + return size + +def minimum_size(self): + '''Get the minimum size for the widget.''' + + size = self.minimized_size + if getattr(self, '_sizegrip', None) is not None and self._sizegrip.isVisible(): + # Don't modify in place: percolates later. + size = size + self._sizegrip_size + + if getattr(self, '_statusbar', None) is not None and self._statusbar.isVisible(): + size = size + self._statusbar_size + + return size + +def get_larger_size(x, y): + '''Get the larger of the two sizes, for both the height and width.''' + return QtCore.QSize(max(x.width(), y.width()), max(x.height(), y.height())) + +def set_minimum_size(self): + '''Sets the minimum size of the window and the titlebar, with clobbering.''' + + self._old_minimum_size = self.minimumSize() + self._titlebar.set_minimum_size() + self._titlebar_size = self._titlebar.minimumSize() + self.setMinimumSize(self.minimum_size) + +def set_larger_minimum_size(self): + '''Sets the minimum size of the window and the titlebar, without clobbering.''' + + if self._old_minimum_size is not None: + self.setMinimumSize(self._old_minimum_size) + self._titlebar.set_minimum_size() + self._titlebar_size = self._titlebar.minimumSize() + size = get_larger_size(self.minimum_size, self.minimumSize()) + self.setMinimumSize(size) + +def move_to(self, position): + '''Move the window to the desired position''' + + # Also updates the stored previous subwindow position, if applicable. + # This means shading/unshading uses the new position of the window, + # but the old sizes, rather than jump the window back. + # NOTICE: this fails on Wayland + self.move(position) + rect = self._titlebar._window_rect + if rect is not None: + rect.moveTo(position) + +def shade(self, size, grip_type): + '''Shade the window, hiding the main widget and size grip.''' + + self._widget.hide() + if getattr(self, f'_{grip_type}') is not None: + getattr(self, f'_{grip_type}').hide() + self.set_minimum_size() + self.resize(size) + +def unshade(self, rect, grip_type): + '''Unshade the window, showing the main widget and size grip.''' + + self._widget.show() + if getattr(self, f'_{grip_type}') is not None: + getattr(self, f'_{grip_type}').show() + self.set_larger_minimum_size() + self.setGeometry(rect) + +def start_drag(self, event, window_type): + '''Start the window drag state.''' + setattr(self, f'_{window_type}_drag', event.pos()) + +def handle_drag(self, event, window, window_type): + '''Handle the window drag event.''' + + position = event.pos() - getattr(self, f'_{window_type}_drag') + window.move_to(window.mapToParent(position)) + +def end_drag(self, window_type): + '''End the window drag state.''' + setattr(self, f'_{window_type}_drag', None) + +def start_move(self, widget, window_type): + '''Start the window move state.''' + + setattr(self, f'_{window_type}_move', widget) + widget.menu_move_to(QtGui.QCursor.pos()) + +def handle_move(self, position, window_type): + '''Handle the window move event.''' + getattr(self, f'_{window_type}_move').menu_move_to(position) + +def end_move(self, window_type): + '''End the window move state.''' + setattr(self, f'_{window_type}_move', None) + +def start_resize(self, widget, window_type): + '''Start the window resize state.''' + + setattr(self, f'_{window_type}_resize', widget) + self.set_cursor(compat.SizeFDiagCursor) + widget.menu_size_to(QtGui.QCursor.pos()) + +def handle_resize(self, position, window_type): + '''Handle the window resize event.''' + getattr(self, f'_{window_type}_resize').menu_size_to(position) + +def end_resize(self, window_type): + '''End the window resize state.''' + + if getattr(self, f'_{window_type}_resize') is not None: + setattr(self, f'_{window_type}_resize', None) + self.restore_cursor() + self.releaseMouse() + +def start_frame(self, frame, window_type): + '''Start the window frame resize state.''' + setattr(self, f'_{window_type}_frame', frame) + +def handle_frame(self, obj, event, window_type): + '''Handle the window frame resize event.''' + self.frame_event(obj, event, window_type) + +def end_frame(self, window_type): + '''End the window frame resize state.''' + setattr(self, f'_{window_type}_frame', None) + +# EVENT HANDLES + +def window_resize_event(self, event): + '''Ensure titlebar text elides normally.''' + + # Need to trigger the titlebar title resize. Need to handle it + # here, since the SizeFrame resizes won't always trigger a + # Label::resizeEvent, which can cause the text to stay elided. + title_timer = self._titlebar._title._timer + title_timer.start(REPAINT_TIMER) + + super(type(self), self).resizeEvent(event) + +def window_show_event(self, event, grip_type): + '''Set the minimum size policies once the widgets are shown.''' + + # Until shown, the size grip has inaccurate sizes. + # Set the minimum size policy of the widget. + # The show event occurs just after everything is shown, + # so the widget sizes (and isVisible) are accurate. + self._titlebar_size = self._titlebar.minimumSize() + if getattr(self, f'_{grip_type}') is not None: + grip_size = getattr(self, f'_{grip_type}').sizeHint() + setattr(self, f'_{grip_type}_size', QtCore.QSize(0, grip_size.height())) + size = get_larger_size(self.minimum_size, self.minimumSize()) + self.setMinimumSize(size) + + super(type(self), self).showEvent(event) + +def window_mouse_double_click_event(self, event): + '''Override the mouse double click, and don't call the press event.''' + + # By default, the flowchart for titlebar double clicks is as follows: + # 1. If minimized, restore + # 2. If maximized, restore + # 3. If no state and can shade, shade + # 4. If no state and cannot shade, maximize + # 5. If shaded, unshade. + widget = self._titlebar + if not widget.underMouse() or event.button() != compat.LeftButton: + return super(type(self), self).mouseDoubleClickEvent(event) + if widget._is_shaded: + widget.unshade() + elif widget.isMinimized() or widget.isMaximized(): + widget.restore() + elif widget._has_shade: + widget.shade() + else: + widget.maximize() + +def window_mouse_press_event(self, event, window, window_type): + '''Override a mouse click on the titlebar to allow a move.''' + + widget = self._titlebar + if widget.underMouse(): + # `self.window().subwindow_move` cannot be set, since we're inside + # the global event filter here. We handle conflicts here, + # so only one of the 4 states can be set. We can't move + # minimized widgets, so don't try. + is_left = event.button() == compat.LeftButton + is_minimized = self.isMinimized() and not widget._is_shaded + has_frame = getattr(window, f'{window_type}_frame') is not None + if is_left and not is_minimized and not has_frame: + start_drag(self.window(), event, window_type) + elif event.button() == compat.RightButton: + position = shared.single_point_global_position(args, event) + shared.execute(args, widget._main_menu, position) + return super(type(self), self).mousePressEvent(event) + +def window_mouse_move_event(self, event, window, window_type): + '''Reposition the window on the move event.''' + + if getattr(window, f'{window_type}_frame') is not None: + end_drag(window, window_type) + if getattr(window, f'{window_type}_drag') is not None: + handle_drag(window, event, self, window_type) + return super(type(self), self).mouseMoveEvent(event) + +def window_mouse_release_event(self, event, window, window_type): + '''End the drag event.''' + + end_drag(window, window_type) + return super(type(self), self).mouseReleaseEvent(event) + # WINDOW WIDGETS class Label(QtWidgets.QLabel): @@ -384,14 +662,17 @@ class TitleButton(QtWidgets.QToolButton): class Titlebar(QtWidgets.QFrame): '''Custom instance of a QTitlebar''' - def __init__(self, subwindow, parent=None, flags=None): + def __init__(self, window, parent=None, flags=None): super().__init__(parent) # Get and set some properties. self.setProperty('isTitlebar', True) - self._subwindow = subwindow + self._window = window + self._window_type = 'window' + if isinstance(self._window, SubWindow): + self._window_type = 'subwindow' self._state = compat.WindowNoState - self._subwindow_rect = None + self._window_rect = None self._has_help = False self._has_shade = False self._is_shaded = False @@ -437,7 +718,7 @@ class Titlebar(QtWidgets.QFrame): self._top_action = action('Stay on &Top', self, checkable=True) self._top_action.toggled.connect(self.toggle_keep_above) self._close_action = action('&Close', self, close_icon(self)) - self._close_action.triggered.connect(self._subwindow.close) + self._close_action.triggered.connect(self._window.close) self._main_menu.addActions([ self._restore_action, self._move_action, @@ -480,7 +761,7 @@ class Titlebar(QtWidgets.QFrame): self._min.clicked.connect(self.minimize) self._max.clicked.connect(self.maximize) self._restore.clicked.connect(self.restore) - self._close.clicked.connect(self._subwindow.close) + self._close.clicked.connect(self._window.close) if self._has_help: self._help.clicked.connect(self.help) if self._has_shade: @@ -526,15 +807,15 @@ class Titlebar(QtWidgets.QFrame): self._title.setText(title) def isNormal(self): - '''Get if the titlebar and therefore subwindow has no state.''' + '''Get if the titlebar and therefore window has no state.''' return self._state == compat.WindowNoState def isMinimized(self): - '''Get if the titlebar and therefore subwindow is minimized.''' + '''Get if the titlebar and therefore window is minimized.''' return self._state == compat.WindowMinimized def isMaximized(self): - '''Get if the titlebar and therefore subwindow is maximized.''' + '''Get if the titlebar and therefore window is maximized.''' return self._state == compat.WindowMaximized # QT EVENTS @@ -567,7 +848,7 @@ class Titlebar(QtWidgets.QFrame): def menu_move(self): '''Start a manually trigger move.''' - self.window().start_move(self) + start_move(self.window(), self, self._window_type) def menu_move_to(self, global_position): ''' @@ -582,7 +863,7 @@ class Titlebar(QtWidgets.QFrame): y = position.y() rect.moveBottomLeft(QtCore.QPoint(x, y)) - window = self._subwindow + window = self._window window.move_to(window.mapToParent(rect.topLeft())) def size_timer(self): @@ -594,35 +875,46 @@ class Titlebar(QtWidgets.QFrame): def menu_size(self): '''Start a manually triggered resize event.''' - self.window().start_resize(self) + + window = self.window() + # Want to intercept all mouse events until the size event finishes. + window.grabMouse() + start_resize(window, self, self._window_type) def menu_size_to(self, global_position): ''' - Size the subwindow so that the position is in the center bottom + Size the window so that the position is in the center bottom of the title bar. The position is given in global coordinates. ''' - window = self._subwindow - position = self.mapFromGlobal(global_position) + window = self._window + point = window.mapToParent(self.mapFromGlobal(global_position)) rect = window.geometry() - rect.setBottomRight(window.mapToParent(position)) + + # If we have a subwindow, need to limit to the MDI area rect. + if self._window.window() != self._window: + area_rect = self._window.mdiArea().contentsRect() + point.setX(min(point.x(), area_rect.right())) + point.setY(min(point.y(), area_rect.bottom())) + + rect.setBottomRight(point) window.resize(rect.size()) # Ensure we trigger the elide resize timer. self._title._timer.start(REPAINT_TIMER) def minimize(self): - '''Minimize the current subwindow.''' + '''Minimize the current window.''' if self.isNormal(): - self._subwindow_rect = self._subwindow.geometry() + self._window_rect = self._window.geometry() self.set_minimized() self.set_shaded() # Toggle state self._state = compat.WindowMinimized self._is_shaded = False - self._subwindow.minimize(self._subwindow.minimized_size) + self._window.minimize(self._window.minimized_size) # Toggle the menu actions # Minimized windows should not be movable, resizable, or minimizable. @@ -632,16 +924,14 @@ class Titlebar(QtWidgets.QFrame): self._min_action.setEnabled(False) self._max_action.setEnabled(True) - self._subwindow.mdiArea().minimize(self._subwindow) - def maximize(self): - '''Maximize the current subwindow.''' + '''Maximize the current window.''' if self.isNormal(): - self._subwindow_rect = self._subwindow.geometry() + self._window_rect = self._window.geometry() elif self.isMinimized() and not self._is_shaded: - self._subwindow.mdiArea().unminimize(self._subwindow) - size = self._subwindow.maximum_size + self._window.unminimize() + size = self._window.maximum_size rect = QtCore.QRect(0, 0, size.width(), size.height()) self.set_maximized() self.set_unshaded() @@ -649,7 +939,7 @@ class Titlebar(QtWidgets.QFrame): # Toggle state self._state = compat.WindowMaximized self._is_shaded = False - self._subwindow.maximize(rect) + self._window.maximize(rect) # Toggle the menu actions self._restore_action.setEnabled(True) @@ -659,17 +949,17 @@ class Titlebar(QtWidgets.QFrame): self._max_action.setEnabled(False) def restore(self): - '''Restore the current subwindow (set to no state).''' + '''Restore the current window (set to no state).''' if self.isMinimized() and not self._is_shaded: - self._subwindow.mdiArea().unminimize(self._subwindow) + self._window.unminimize() self.set_restored() self.set_unshaded() # Toggle state self._state = compat.WindowNoState self._is_shaded = False - self._subwindow.restore(self._subwindow_rect) + self._window.restore(self._window_rect) # Toggle the menu actions self._restore_action.setEnabled(False) @@ -679,10 +969,10 @@ class Titlebar(QtWidgets.QFrame): self._max_action.setEnabled(True) def shade(self): - '''Shade the current subwindow.''' + '''Shade the current window.''' # Shaded windows are treated as if they have minimized state, and - # if the window is maximized, it sets the previous subwindow rect + # if the window is maximized, it sets the previous window rect # to the maximized geometry. self.set_shaded() self.set_minimized() @@ -690,10 +980,10 @@ class Titlebar(QtWidgets.QFrame): # Toggle state self._state = compat.WindowMinimized self._is_shaded = True - self._subwindow_rect = self._subwindow.geometry() - width = self._subwindow.width() - height = self._subwindow.minimized_size.height() - self._subwindow.shade(QtCore.QSize(width, height)) + self._window_rect = self._window.geometry() + width = self._window.width() + height = self._window.minimized_size.height() + self._window.shade(QtCore.QSize(width, height)) # Toggle the menu actions # Shaded windows should be movable, but not resizable or minimizable. @@ -704,10 +994,10 @@ class Titlebar(QtWidgets.QFrame): self._max_action.setEnabled(True) def unshade(self): - '''Unshade the current subwindow.''' + '''Unshade the current window.''' if self.isMinimized() and not self._is_shaded: - self._subwindow.mdiArea().unminimize(self._subwindow) + self._window.unminimize() # If the window is minimized, it restores to the previous # window state and position. @@ -717,7 +1007,7 @@ class Titlebar(QtWidgets.QFrame): # Toggle state self._state = compat.WindowNoState self._is_shaded = False - self._subwindow.unshade(self._subwindow_rect) + self._window.unshade(self._window_rect) # Toggle the menu actions # Unshaded windows have no state: they are restored. @@ -729,7 +1019,29 @@ class Titlebar(QtWidgets.QFrame): def toggle_keep_above(self, checked): '''Toggle whether to keep the window above others.''' - self._subwindow.setWindowFlag(compat.WindowStaysOnTopHint, checked) + + # If we have a top-level widget, changing the window + # flags causes `setParent` to be called, causing the + # widget to hide and then re-appear. This causes major + # visual delay, so we just ignore the hide event, then + # set the flags, re-show the window, and unignore hides. + # Finally, this can change the geometry of the window, + # so we need to store the geometry and reset it. + if self._window.window() == self._window: + self._window._ignore_hide = True + rect = self.window().geometry() + + flags = self._window.windowFlags() + if checked: + flags |= compat.WindowStaysOnTopHint + else: + flags &= ~compat.WindowStaysOnTopHint + self._window.setWindowFlags(flags) + + if self._window.window() == self._window: + self._window._ignore_hide = False + self.window().show() + self.window().setGeometry(rect) def help(self): '''Enter what's this mode.''' @@ -928,13 +1240,13 @@ class SizeFrame(QtCore.QObject): # might be a subwindow. If it has a parent, then it's a subwindow # and we need to map our coordinates. point = QtCore.QPoint(self._window.x(), self._window.y()) - if self._window.parent() is not None: + if self._window.window() != self._window: point = self._window.parent().mapToGlobal(point) return point def frame_geometry(self): - '''Calculate the frame geometry of our window in global coordinates.''' + '''Calculate the frame geometry of our window in global coordinates.''' return QtCore.QRect(self.top_left(), self._window.frameSize()) def update_cursor(self, position): @@ -1011,11 +1323,37 @@ class SizeFrame(QtCore.QObject): elif self._press_edge == WindowEdge.BottomRight: rect.setBottomRight(position) + # Ensure we don't drag the widgets if we go below min sizes. if rect.width() < self._window.minimumWidth(): - rect.setLeft(self._window.x()) + if self._press_edge in LEFT_EDGES: + rect.setLeft(rect.right() - self._window.minimumWidth()) + elif self._press_edge in RIGHT_EDGES: + rect.setRight(rect.left() + self._window.minimumWidth()) if rect.height() < self._window.minimumHeight(): - rect.setTop(self._window.y()) - self._window.setGeometry(rect) + if self._press_edge in TOP_EDGES: + rect.setTop(rect.bottom() - self._window.minimumHeight()) + elif self._press_edge in BOTTOM_EDGES: + rect.setBottom(rect.top() + self._window.minimumHeight()) + + # Calculate our rect for our widget. + size = rect.size() + point = rect.topLeft() + if self._window.window() != self._window: + point = self._window.parent().mapFromGlobal(point) + local_rect = QtCore.QRect(point, size) + + # If we have a subwindow, need to limit to the MDI area rect. + if self._window.window() != self._window: + area_rect = self._window.mdiArea().contentsRect() + # Need to calculate our shifts here. + dx1 = max(local_rect.left(), area_rect.left()) - local_rect.left() + dy1 = max(local_rect.top(), area_rect.top()) - local_rect.top() + dx2 = min(local_rect.right(), area_rect.right()) - local_rect.right() + dy2 = min(local_rect.bottom(), area_rect.bottom()) - local_rect.bottom() + rect.adjust(dx1, dy1, dx2, dy2) + local_rect.adjust(dx1, dy1, dx2, dy2) + + self._window.setGeometry(local_rect) self._band.setGeometry(rect) def mouse_press(self, event): @@ -1044,7 +1382,15 @@ class SizeFrame(QtCore.QObject): self.update_cursor(self._window.mapToGlobal(position)) class SubWindow(QtWidgets.QMdiSubWindow): - '''Custom subwindow instance''' + '''Base subclass for a QMdiSubwindow.''' + + def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)): + super().__init__(parent) + self.setWindowFlags(self.windowFlags() | flags) + super().setWidget(QtWidgets.QWidget()) + +class DefaultSubWindow(SubWindow): + '''Default subwindow with a window frame.''' def __init__( self, @@ -1052,9 +1398,18 @@ class SubWindow(QtWidgets.QMdiSubWindow): flags=QtCore.Qt.WindowType(0), sizegrip=False, ): - super().__init__(parent) - self.setWindowFlags(self.windowFlags() | flags) - super().setWidget(QtWidgets.QWidget()) + super().__init__(parent, flags=flags) + +class FramelessSubWindow(SubWindow): + '''Custom subwindow instance without a window frame.''' + + def __init__( + self, + parent=None, + flags=QtCore.Qt.WindowType(0), + sizegrip=False, + ): + super().__init__(parent, flags=flags | compat.FramelessWindowHint) # Create our widgets. Sizeframe and sizegrip are mutually exclusive. self._central = QtWidgets.QFrame(super().widget()) @@ -1067,6 +1422,7 @@ class SubWindow(QtWidgets.QMdiSubWindow): self._border = args.border_width self._titlebar_size = QtCore.QSize() self._sizegrip_size = QtCore.QSize() + self._old_minimum_size = None if sizegrip: self._sizegrip = QtWidgets.QSizeGrip(self._central) else: @@ -1110,35 +1466,22 @@ class SubWindow(QtWidgets.QMdiSubWindow): @property def border_size(self): '''Get the size of the border, regardless if present.''' - return QtCore.QSize(2 * self._border, 2 * self._border) + return border_size(self) @property def minimized_content_size(self): '''Get the minimum content size of the widget.''' - return self._titlebar_size - size = self._titlebar_size - if self._border: - size = size + self.border_size - return size + return minimized_content_size(self) @property def minimized_size(self): '''Get the minimum size of the widget, with the size grips hidden.''' - - size = self.minimized_content_size - if self._border: - size = size + self.border_size - return size + return minimized_size(self) @property def minimum_size(self): '''Get the minimum size for the widget.''' - - size = self.minimized_size - if self._sizegrip is not None and self._sizegrip.isVisible(): - # Don't modify in place: percolates later. - size = size + self._sizegrip_size - return size + return minimum_size(self) @property def maximum_size(self): @@ -1149,21 +1492,15 @@ class SubWindow(QtWidgets.QMdiSubWindow): def move_to(self, position): '''Move the window to the desired position''' - - # Also updates the stored previous subwindow position, if applicable. - # This means shading/unshading uses the new position of the window, - # but the old sizes, rather than jump the window back. - self.move(position) - rect = self._titlebar._subwindow_rect - if rect is not None: - rect.moveTo(position) + move_to(self, position) def set_minimum_size(self): - '''Sets the minimum size of the window and the titlebar.''' + '''Sets the minimum size of the window and the titlebar, with clobbering.''' + set_minimum_size(self) - self._titlebar.set_minimum_size() - self._titlebar_size = self._titlebar.minimumSize() - self.setMinimumSize(self.minimum_size) + def set_larger_minimum_size(self): + '''Sets the minimum size of the window and the titlebar, without clobbering.''' + set_larger_minimum_size(self) def minimize(self, size): '''Minimize the window, hiding the main widget and size grip.''' @@ -1173,6 +1510,7 @@ class SubWindow(QtWidgets.QMdiSubWindow): self._sizegrip.hide() self.set_minimum_size() self.resize(size) + self.mdiArea().minimize(self) def maximize(self, rect): '''Maximize the window, showing the main widget and hiding size grip.''' @@ -1180,7 +1518,7 @@ class SubWindow(QtWidgets.QMdiSubWindow): self._widget.show() if self._sizegrip is not None: self._sizegrip.hide() - self.set_minimum_size() + self.set_larger_minimum_size() self.setGeometry(rect) def restore(self, rect): @@ -1189,111 +1527,46 @@ class SubWindow(QtWidgets.QMdiSubWindow): self._widget.show() if self._sizegrip is not None: self._sizegrip.show() - self.set_minimum_size() + self.set_larger_minimum_size() self.setGeometry(rect) def shade(self, size): '''Shade the window, hiding the main widget and size grip.''' - - self._widget.hide() - if self._sizegrip is not None: - self._sizegrip.hide() - self.set_minimum_size() - self.resize(size) + shade(self, size, 'sizegrip') def unshade(self, rect): '''Unshade the window, showing the main widget and size grip.''' + unshade(self, rect, 'sizegrip') - self._widget.show() - if self._sizegrip is not None: - self._sizegrip.show() - self.set_minimum_size() - self.setGeometry(rect) + def unminimize(self): + '''Unminimize a minimized subwindow.''' + self.mdiArea().unminimize(self) # QT EVENTS def resizeEvent(self, event): '''Handle widget resize events here.''' - - # Need to trigger the titlebar title resize. Need to handle it - # here, since the SizeFrame resizes won't always trigger a - # Label::resizeEvent, which can cause the text to stay elided. - title_timer = self._titlebar._title._timer - title_timer.start(REPAINT_TIMER) - - super().resizeEvent(event) + window_resize_event(self, event) def showEvent(self, event): '''Set the minimum size policies once the widgets are shown.''' - - # Until shown, the size grip has inaccurate sizes. - # Set the minimum size policy of the widget. - # The show event occurs just after everything is shown, - # so the widget sizes (and isVisible) are accurate. - self._titlebar_size = self._titlebar.minimumSize() - if self._sizegrip is not None: - sizegrip_size = self._sizegrip.sizeHint() - self._sizegrip_size = QtCore.QSize(0, sizegrip_size.height()) - self.setMinimumSize(self.minimum_size) - - super().showEvent(event) + window_show_event(self, event, 'sizegrip') def mouseDoubleClickEvent(self, event): '''Override the mouse double click, and don't call the press event.''' - - # By default, the flowchart for titlebar double clicks is as follows: - # 1. If minimized, restore - # 2. If maximized, restore - # 3. If no state and can shade, shade - # 4. If no state and cannot shade, maximize - # 5. If shaded, unshade. - widget = self._titlebar - if not widget.underMouse() or event.button() != compat.LeftButton: - return super().mouseDoubleClickEvent(event) - if widget._is_shaded: - widget.unshade() - elif widget.isMinimized() or widget.isMaximized(): - widget.restore() - elif widget._has_shade: - widget.shade() - else: - widget.maximize() + window_mouse_double_click_event(self, event) def mousePressEvent(self, event): '''Override a mouse click on the titlebar to allow a move.''' - - widget = self._titlebar - window = self.window() - if widget.underMouse(): - # `self.window()._move` cannot be set, since we're inside - # the global event filter here. We handle conflicts here, - # so only one of the 4 states can be set. We can't move - # minimized widgets, so don't try. - is_left = event.button() == compat.LeftButton - is_minimized = self.isMinimized() and not self._titlebar._is_shaded - has_frame = window._frame is not None - if is_left and not is_minimized and not has_frame: - self.window().start_drag(event) - elif event.button() == compat.RightButton: - position = shared.single_point_global_position(args, event) - shared.execute(args, widget._main_menu, position) - return super().mousePressEvent(event) + return window_mouse_press_event(self, event, self.window(), 'subwindow') def mouseMoveEvent(self, event): '''Reposition the window on the move event.''' - - window = self.window() - if window._frame is not None: - window.end_drag() - if window._drag is not None: - self.window().handle_drag(self, event) - return super().mouseMoveEvent(event) + return window_mouse_move_event(self, event, self.window(), 'subwindow') def mouseReleaseEvent(self, event): '''End the drag event.''' - - self.window().end_drag() - return super().mouseReleaseEvent(event) + return window_mouse_release_event(self, event, self.window(), 'subwindow') # QT-LIKE PROPERTIES @@ -1329,7 +1602,7 @@ class SubWindow(QtWidgets.QMdiSubWindow): def isMaximized(self): '''Overload since we use a custom maximized for our subwindow.''' - return self._titlebar.isMaximized() or super().isMaximized() + return self._titlebar.isMaximized() class MdiArea(QtWidgets.QMdiArea): '''Override the QMdiArea for window minimization and background color.''' @@ -1429,21 +1702,34 @@ class MdiArea(QtWidgets.QMdiArea): point = shift_row(point, width) class Window(QtWidgets.QMainWindow): - '''Main window with a custom event filter for all events.''' + '''Base subclass for a QMainWindow.''' def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)): super().__init__(parent, flags) - self.centralwidget = QtWidgets.QWidget(self) - self.layout = QtWidgets.QVBoxLayout(self.centralwidget) - self.setCentralWidget(self.centralwidget) + # Tracking for move and resize events. + # Click and drag title bar move. + self._subwindow_drag = None + # Context menu move. + self._subwindow_move = None + # Context menu resize. + self._subwindow_resize = None + # SizeFrame resize. + self._subwindow_frame = None + + def setup(self): + '''Setup the main UI.''' + + subwindow_class = FramelessSubWindow + if args.default_window_frame: + subwindow_class = DefaultSubWindow + self.resize(1068, 824) self.setWindowTitle('Custom SubWindow Style.') flags = compat.SubWindow - flags |= compat.FramelessWindowHint - self.area = MdiArea(self.centralwidget) - self.window1 = SubWindow(flags=flags, sizegrip=True) + self.area = MdiArea(self._widget) + self.window1 = subwindow_class(flags=flags, sizegrip=True) self.window1.setWindowTitle('Short Title') self.area.addSubWindow(self.window1) self.table = LargeTable(self.window1.widget()) @@ -1452,8 +1738,7 @@ class Window(QtWidgets.QMainWindow): flags = compat.SubWindow flags |= compat.WindowContextHelpButtonHint flags |= compat.WindowShadeButtonHint - flags |= compat.FramelessWindowHint - self.window2 = SubWindow(flags=flags) + self.window2 = subwindow_class(flags=flags) self.window2.setWindowTitle('Example of a very, very long title') self.area.addSubWindow(self.window2) self.tree = SortableTree(self.window2.widget()) @@ -1461,23 +1746,42 @@ class Window(QtWidgets.QMainWindow): flags = compat.SubWindow flags |= compat.WindowShadeButtonHint - flags |= compat.FramelessWindowHint - self.window3 = SubWindow(flags=flags, sizegrip=True) + self.window3 = subwindow_class(flags=flags, sizegrip=True) self.window3.setWindowTitle('Medium length title') self.area.addSubWindow(self.window3) - self.layout.addWidget(self.area) + self._widget.layout().addWidget(self.area) self.tab = SettingTabs(self.window3.widget()) self.window3.layout().addWidget(self.tab) - # Tracking for move and resize events. - # Click and drag title bar move. - self._drag = None - # Context menu move. - self._move = None - # Context menu resize. - self._resize = None - # SizeFrame resize. - self._frame = None + # PROPERTIES + + @property + def maximum_size(self): + '''Get the maximum size for the window.''' + # Unused since we use the window flags anyway. + return self.maximumSize() + + @property + def subwindow_drag(self): + '''Get if the subwindow is in a drag state.''' + return self._subwindow_drag + + @property + def subwindow_move(self): + '''Get if the subwindow is in a move state.''' + return self._subwindow_move + + @property + def subwindow_resize(self): + '''Get if the subwindow is in a resize state.''' + return self._subwindow_resize + + @property + def subwindow_frame(self): + '''Get if the subwindow is in a frame state.''' + return self._subwindow_frame + + # ACTIONS def set_cursor(self, cursor): '''Temporarily set the application cursor to the override cursor.''' @@ -1491,67 +1795,7 @@ class Window(QtWidgets.QMainWindow): app = QtWidgets.QApplication.instance() app.restoreOverrideCursor() - def start_drag(self, event): - '''Start the drag state.''' - self._drag = event.pos() - - def handle_drag(self, subwindow, event): - '''Handle the drag event.''' - subwindow.move_to(subwindow.mapToParent(event.pos() - self._drag)) - - def end_drag(self): - '''End the drag state.''' - self._drag = None - - def start_move(self, widget): - '''Start the move state.''' - - self._move = widget - self._move.menu_move_to(QtGui.QCursor.pos()) - - def handle_move(self, obj, event): - '''Handle the move event.''' - - position = shared.single_point_global_position(args, event) - self._move.menu_move_to(position) - - def end_move(self): - '''End the move state.''' - self._move = None - - def start_resize(self, widget): - '''Start the resize state.''' - - self._resize = widget - self.set_cursor(compat.SizeFDiagCursor) - self._resize.menu_size_to(QtGui.QCursor.pos()) - - def handle_resize(self, obj, event): - '''Handle the resize event.''' - - position = shared.single_point_global_position(args, event) - self._resize.menu_size_to(position) - - def end_resize(self): - '''End the resize state.''' - - if self._resize is not None: - self._resize = None - self.restore_cursor() - - def start_frame(self, subwindow): - '''Start the frame resize state.''' - self._frame = subwindow - - def handle_frame(self, obj, event): - '''Handle the frame resize event.''' - self.window_frame_event(obj, event) - - def end_frame(self): - '''End the frame resize state.''' - self._frame = None - - def resolve_window_state(self): + def resolve_state(self): '''Handle theoretically possible conflicts in window state.''' # The _drag, _move, _resize, and _frame options are @@ -1559,33 +1803,46 @@ class Window(QtWidgets.QMainWindow): # Since we use timers for `_move` and `_resize`, it's **possible** # multiple might be active here, but it's unlikely. So, we handle # those cases by playing favorites. _frame > _resize > _move > _drag. - if self._frame is not None: - self.end_resize() - if self._resize is not None: - self.end_move() - if self._move is not None: - self.end_drag() + # We deal with the window-level widgets first, then the subwindow-level + # widgets next. We use `getattr(obj, attr, None)` for the window-level + # widgets since they might not be present (if using Wayland). + if getattr(self, f'_window_frame', None) is not None: + end_resize(self, 'window') + if getattr(self, f'_window_resize', None) is not None: + end_move(self, 'window') + if getattr(self, f'_window_move', None) is not None: + end_drag(self, 'window') + if getattr(self, f'_window_drag', None) is not None: + end_frame(self, 'window') + if getattr(self, f'_subwindow_frame') is not None: + end_resize(self, 'subwindow') + if getattr(self, f'_subwindow_resize') is not None: + end_move(self, 'subwindow') + if getattr(self, f'_subwindow_move') is not None: + end_drag(self, 'subwindow') - def window_move_event(self, obj, event): + def move_event(self, _, event, window_type): '''Handle window move events.''' if event.type() == compat.MouseMove: - self.handle_move(obj, event) + position = shared.single_point_global_position(args, event) + handle_move(self, position, window_type) elif event.type() == compat.MouseButtonPress: - self.end_move() + end_move(self, window_type) - def window_resize_event(self, obj, event): + def resize_event(self, _, event, window_type): '''Handle window resize events.''' if event.type() == compat.MouseMove: - self.handle_resize(obj, event) + position = shared.single_point_global_position(args, event) + handle_resize(self, position, window_type) elif event.type() == compat.MouseButtonPress: - self.end_resize() + end_resize(self, window_type) - def window_frame_event(self, subwindow, event): + def frame_event(self, window, event, window_type): '''Handle size adjustments using the window frame.''' - frame = subwindow._sizeframe + frame = getattr(window, '_sizeframe', None) # Uses size grips, return early. if frame is None: return @@ -1606,42 +1863,264 @@ class Window(QtWidgets.QMainWindow): # Store if the frame state is active. if frame.is_active: - self.start_frame(frame) + start_frame(self, frame, window_type) else: - self.end_frame() + end_frame(self, window_type) + + # QT EVENTS def eventFilter(self, obj, event): '''Custom event filter to handle move and resize events.''' - self.resolve_window_state() - if self._move is not None: + self.resolve_state() + if getattr(self, 'window_move', None) is not None: # Cannot occur while the size frame is active. - self.window_move_event(obj, event) - elif self._resize is not None: - self.window_resize_event(obj, event) + self.move_event(obj, event, 'window') + elif getattr(self, 'window_resize', None) is not None: + self.resize_event(obj, event, 'window') + elif isinstance(obj, Window) and not obj.isMinimized(): + handle_frame(self, obj, event, 'window') + elif self.subwindow_move is not None: + # Cannot occur while the size frame is active. + self.move_event(obj, event, 'subwindow') + elif self.subwindow_resize is not None: + self.resize_event(obj, event, 'subwindow') elif isinstance(obj, SubWindow) and not obj.isMinimized(): - self.handle_frame(obj, event) + handle_frame(self, obj, event, 'subwindow') return super().eventFilter(obj, event) - def enterEvent(self, event): - '''Reset the resize mouse on an enter event.''' +class DefaultWindow(Window): + '''Default main window with a window frame.''' - if self._resize is not None: - self.set_cursor(compat.SizeFDiagCursor) - return super().enterEvent(event) + def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)): + if args.window_help: + flags |= compat.WindowContextHelpButtonHint + if args.window_shade: + flags |= compat.WindowShadeButtonHint + super().__init__(parent, flags) - def leaveEvent(self, event): - '''Reset the resize mouse on an enter event.''' + self._central = QtWidgets.QFrame(self) + self._layout = QtWidgets.QVBoxLayout(self._central) + self.setCentralWidget(self._central) + self._widget = QtWidgets.QWidget(self._central) + self._widget.setLayout(QtWidgets.QVBoxLayout()) + self._central.layout().addWidget(self._widget, 10) - if self._resize is not None: - self.restore_cursor() - return super().leaveEvent(event) + if args.status_bar: + self._statusbar = QtWidgets.QStatusBar(self._central) + self.setStatusBar(self._statusbar) + + self.setup() + +class FramelessWindow(Window): + '''Main window with a custom event filter for all events.''' + + def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)): + # On X11, the `WindowStaysOnTopHint` hint supposedly doesn't + # work unless you bypass the window manager, but this seems + # to no longer be true. There's major downsides to bypassing + # the window manager, so it's not worth it anyway. + flags |= compat.FramelessWindowHint + if args.window_help: + flags |= compat.WindowContextHelpButtonHint + if args.window_shade: + flags |= compat.WindowShadeButtonHint + super().__init__(parent, flags) + + # Create our widgets. Sizeframe and sizegrip are mutually exclusive. + self._central = QtWidgets.QFrame(self) + self._layout = QtWidgets.QVBoxLayout(self._central) + self.setCentralWidget(self._central) + self._titlebar = Titlebar(self, self._central, flags) + self._widget = QtWidgets.QWidget(self._central) + self._widget.setLayout(QtWidgets.QVBoxLayout()) + self._sizeframe = None + self._statusbar = None + self._border = args.border_width + self._titlebar_size = QtCore.QSize() + self._statusbar_size = QtCore.QSize() + self._old_minimum_size = None + if args.status_bar: + self._statusbar = QtWidgets.QStatusBar(self._central) + self.setStatusBar(self._statusbar) + else: + self._sizeframe = SizeFrame(self, border_width=5) + + self._central.layout().setSpacing(0) + self._central.layout().addWidget(self._titlebar, 0, compat.AlignTop) + self._central.layout().addWidget(self._widget, 10) + + # Tracking for move and resize events. + # Click and drag title bar move. + self._window_drag = None + # Context menu move. + self._window_move = None + # Context menu resize. + self._window_resize = None + # SizeFrame resize. + self._window_frame = None + + # For toggling window flags, which calls `setParent`, hiding the window. + # Since an immediate show causes an unminimize/re-minimize, this + # causes a serious visual lag. + self._ignore_hide = False + + # Set the border properties. + self._central.layout().setContentsMargins(QtCore.QMargins(0, 0, 0, 0)) + self._central.setProperty('isWindow', True) + if self._border > 0: + self._central.setProperty('windowFrame', min(self._border, 5)) + self._central.setFrameShape(compat.Box) + self._central.setFrameShadow(compat.Raised) + + # Ensure our titlebar gets highest priority. + self._titlebar.raise_() + self._widget.lower() + + self.setup() + + # HACKS + + def hide(self): + '''Override the hide event to ignore it if desired.''' + + if self._ignore_hide: + return + super().hide() + + def setVisible(self, value): + '''Override the hide event to ignore it if desired.''' + + if self._ignore_hide and not value: + return + super().setVisible(value) + + # PROPERTIES + + @property + def window_drag(self): + '''Get if the window is in a drag state.''' + return self._window_drag + + @property + def window_move(self): + '''Get if the window is in a move state.''' + return self._window_move + + @property + def window_resize(self): + '''Get if the window is in a resize state.''' + return self._window_resize + + @property + def window_frame(self): + '''Get if the window is in a frame state.''' + return self._window_frame + + @property + def border_size(self): + '''Get the size of the border, regardless if present.''' + return border_size(self) + + @property + def minimized_content_size(self): + '''Get the minimum content size of the widget.''' + return minimized_content_size(self) + + @property + def minimized_size(self): + '''Get the minimum size of the widget, with the size grips hidden.''' + return minimized_size(self) + + @property + def minimum_size(self): + '''Get the minimum size for the widget.''' + return minimum_size(self) + + # QT-LIKE PROPERTIES + + def windowTitle(self): + '''Get the window title from the titlebar.''' + return self._titlebar.windowTitle() + + def setWindowTitle(self, title): + '''Get the window title from the titlebar.''' + self._titlebar.setWindowTitle(title) + + # RESIZE + + def move_to(self, position): + '''Move the window to the desired position''' + move_to(self, position) + + def set_minimum_size(self): + '''Sets the minimum size of the window and the titlebar, with clobbering.''' + set_minimum_size(self) + + def set_larger_minimum_size(self): + '''Sets the minimum size of the window and the titlebar, without clobbering.''' + set_larger_minimum_size(self) + + def minimize(self, _): + '''Minimize the window, using the actual OS to handle that.''' + self.showMinimized() + + def maximize(self, _): + '''Minimize the window, using the actual OS to handle that.''' + self.showMaximized() + + def restore(self, _): + '''Restore the window, showing the main widget and size grip.''' + # Must have been handled by the window manager, so we're good here. + self.showNormal() + + def shade(self, size): + '''Shade the window, hiding the main widget and size grip.''' + shade(self, size, 'statusbar') + + def unshade(self, rect): + '''Unshade the window, showing the main widget and size grip.''' + unshade(self, rect, 'statusbar') + + def unminimize(self): + '''Unminimize a minimized window (unimplemented).''' + + # QT EVENTS + + def resizeEvent(self, event): + '''Handle widget resize events here.''' + window_resize_event(self, event) + + def showEvent(self, event): + '''Call `activateWindow` if we bypass the X11 window manager.''' + window_show_event(self, event, 'statusbar') + + def mouseDoubleClickEvent(self, event): + '''Override the mouse double click, and don't call the press event.''' + window_mouse_double_click_event(self, event) + + def mousePressEvent(self, event): + '''Override a mouse click on the titlebar to allow a move.''' + return window_mouse_press_event(self, event, self, 'window') + + def mouseMoveEvent(self, event): + '''Reposition the window on the move event.''' + return window_mouse_move_event(self, event, self, 'window') + + def mouseReleaseEvent(self, event): + '''End the drag event.''' + return window_mouse_release_event(self, event, self, 'window') def main(): 'Application entry point' - app, window = shared.setup_app(args, unknown, compat, window_class=Window) + window_class = FramelessWindow + # Wayland does not allow windows to reposition themselves: therefore, + # we cannot use the custom titlebar at the application level. + if args.default_window_frame or IS_WAYLAND: + window_class = DefaultWindow + app, window = shared.setup_app(args, unknown, compat, window_class=window_class) app.installEventFilter(window) shared.set_stylesheet(args, app, compat)