Added numerous bug fixes for the title bar.
Noted numerous bugs for Wayland, and added a few wworkarounds. Documented how even X11 mode in Wayland does not fully work.main
parent
4733652dd5
commit
79988d236c
|
@ -73,6 +73,7 @@ def parse_args(parser):
|
|||
|
||||
if args.use_x11:
|
||||
os.environ['XDG_SESSION_TYPE'] = 'x11'
|
||||
os.environ['QT_QPA_PLATFORM'] = 'xcb'
|
||||
|
||||
return args, unknown
|
||||
|
||||
|
@ -172,6 +173,7 @@ def get_compat_definitions(args):
|
|||
ns.TextElideMode = QtCore.Qt.TextElideMode
|
||||
ns.CursorShape = QtCore.Qt.CursorShape
|
||||
ns.MouseButton = QtCore.Qt.MouseButton
|
||||
ns.KeyboardModifier = QtCore.Qt.KeyboardModifier
|
||||
ns.SizePolicy = QtWidgets.QSizePolicy.Policy
|
||||
ns.SizeConstraint = QtWidgets.QLayout.SizeConstraint
|
||||
|
||||
|
@ -244,6 +246,8 @@ def get_compat_definitions(args):
|
|||
ns.MouseButtonPress = ns.EventType.MouseButtonPress
|
||||
ns.MouseButtonRelease = ns.EventType.MouseButtonRelease
|
||||
ns.MouseMove = ns.EventType.MouseMove
|
||||
ns.WindowStateChange = ns.EventType.WindowStateChange
|
||||
ns.ActivationChange = ns.EventType.ActivationChange
|
||||
ns.WindowPalette = ns.ColorRole.Window
|
||||
ns.WindowTextPalette = ns.ColorRole.WindowText
|
||||
ns.LightPalette = ns.ColorRole.Light
|
||||
|
@ -445,6 +449,7 @@ def get_compat_definitions(args):
|
|||
ns.WhatsThisCursor = ns.CursorShape.WhatsThisCursor
|
||||
ns.LeftButton = ns.MouseButton.LeftButton
|
||||
ns.RightButton = ns.MouseButton.RightButton
|
||||
ns.NoModifier = ns.KeyboardModifier.NoModifier
|
||||
ns.SizeFixed = ns.SizePolicy.Fixed
|
||||
ns.SizeMinimum = ns.SizePolicy.Minimum
|
||||
ns.SizeMaximum = ns.SizePolicy.Maximum
|
||||
|
@ -531,6 +536,8 @@ def get_compat_definitions(args):
|
|||
ns.MouseButtonPress = QtCore.QEvent.MouseButtonPress
|
||||
ns.MouseButtonRelease = QtCore.QEvent.MouseButtonRelease
|
||||
ns.MouseMove = QtCore.QEvent.MouseMove
|
||||
ns.WindowStateChange = QtCore.QEvent.WindowStateChange
|
||||
ns.ActivationChange = QtCore.QEvent.ActivationChange
|
||||
ns.WindowPalette = QtGui.QPalette.Window
|
||||
ns.WindowTextPalette = QtGui.QPalette.WindowText
|
||||
ns.LightPalette = QtGui.QPalette.Light
|
||||
|
@ -726,6 +733,7 @@ def get_compat_definitions(args):
|
|||
ns.WhatsThisCursor = QtCore.Qt.WhatsThisCursor
|
||||
ns.LeftButton = QtCore.Qt.LeftButton
|
||||
ns.RightButton = QtCore.Qt.RightButton
|
||||
ns.NoModifier = QtCore.Qt.NoModifier
|
||||
ns.SizeFixed = QtWidgets.QSizePolicy.Fixed
|
||||
ns.SizeMinimum = QtWidgets.QSizePolicy.Minimum
|
||||
ns.SizeMaximum = QtWidgets.QSizePolicy.Maximum
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
|
||||
A full-featured, custom titlebar for a subwindow in an MDI area. This
|
||||
uses a frameless window hint with a custom titlebar, and event filter
|
||||
to capture titlebar and frame events. This example can also be easily applied to a top-level window.
|
||||
to capture titlebar and frame events. This example can also be easily
|
||||
applied to a top-level window.
|
||||
|
||||
The custom titlebar supports the following:
|
||||
- Title text
|
||||
|
@ -68,7 +69,37 @@
|
|||
Any other more elaborate style, like a `Panel`, won't be rendered
|
||||
correctly.
|
||||
|
||||
The top-level titlebar can have a few issues.
|
||||
NOTE: you cannot correctly emulate a title bar if the desktop environment
|
||||
is Wayland, even if the app is running in X11 mode. This mostly affects
|
||||
just the top-level title bar (and subwindows almost entirely work),
|
||||
but there are a few small issues for subwindows.
|
||||
|
||||
The top-level title bar can have a few issues on Wayland.
|
||||
- Cannot move the window position. This cannot be done even if you know
|
||||
the compositor (such as kwin).
|
||||
- Cannot use the menu resize due to `QWidget::mouseGrab()`.
|
||||
- This plugin supports grabbing the mouse only for popup windows
|
||||
- The window stops tracking mouse movements past a certain distance.
|
||||
- Attempting to move the window position causes global position to be wrong.
|
||||
- Wayland does not support `Stay on Top` directive.
|
||||
- qt.qpa.wayland: Wayland does not support QWindow::requestActivate()
|
||||
|
||||
A few other issues exist on Wayland.
|
||||
- The menu resize has to guess the mouse position outside of the window bounds.
|
||||
- This cannot be fixed since we cannot use mouse events if the user
|
||||
is outside the main window, nor do hover events trigger.
|
||||
We cannot guess where the user left the main window, since
|
||||
`QCursor::pos` will not be updated until the user moves the
|
||||
mouse within the application, so merely resizing until the
|
||||
actual cursor is within the window won't work.
|
||||
- We cannot intercept mouse events for the menu resize outside the window.
|
||||
- This even occurs when forcing X11 on Wayland.
|
||||
|
||||
# Testing
|
||||
|
||||
The current platforms/desktop environments have been tested:
|
||||
- Gnome (X11, Wayland)
|
||||
- KDE Plasma (X11, Wayland)
|
||||
'''
|
||||
|
||||
import enum
|
||||
|
@ -78,19 +109,6 @@ 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',
|
||||
|
@ -125,6 +143,11 @@ parser.add_argument(
|
|||
help='add a top-level shade/unshade button',
|
||||
action='store_true',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--wayland-testing',
|
||||
help='debug with a custom titlebar on wayland',
|
||||
action='store_true',
|
||||
)
|
||||
args, unknown = shared.parse_args(parser)
|
||||
QtCore, QtGui, QtWidgets = shared.import_qt(args)
|
||||
compat = shared.get_compat_definitions(args)
|
||||
|
@ -137,9 +160,21 @@ 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
|
||||
# QWIDGETSIZE_MAX isn't exported, which is needed to remove fixedSize constraints.
|
||||
QWIDGETSIZE_MAX = (1 << 24) - 1
|
||||
|
||||
# Determine the Linux display server protocol we're using.
|
||||
# Use `XDG_SESSION_TYPE`, since we can override it for X11.
|
||||
IS_WAYLAND = os.environ.get('XDG_SESSION_TYPE') == 'wayland'
|
||||
IS_XWAYLAND = os.environ.get('XDG_SESSION_TYPE') == 'xwayland'
|
||||
IS_X11 = os.environ.get('XDG_SESSION_TYPE') == 'x11'
|
||||
# We can run X11 on Wayland, but this doesn't support certain
|
||||
# features like mouse grabbing, so we don't use it here.
|
||||
IS_TRUE_WAYLAND = 'WAYLAND_DISPLAY' in os.environ
|
||||
USE_WAYLAND_FRAME = IS_WAYLAND and not args.wayland_testing
|
||||
|
||||
# Add a warning if we're using Wayland with a custom titlebar.
|
||||
if not args.default_window_frame and IS_WAYLAND:
|
||||
if not args.default_window_frame and USE_WAYLAND_FRAME:
|
||||
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)
|
||||
|
@ -370,11 +405,6 @@ class SettingTabs(QtWidgets.QTabWidget):
|
|||
|
||||
# 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)
|
||||
|
@ -432,12 +462,28 @@ def move_to(self, 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
|
||||
# NOTICE: this fails on Wayland. Worse, using `QMainWindow::move` on
|
||||
# Wayland causes the cursor position to be incorrect, causing issues
|
||||
# with other events.
|
||||
if IS_WAYLAND and self.window() == self:
|
||||
return
|
||||
|
||||
self.move(position)
|
||||
rect = self._titlebar._window_rect
|
||||
if rect is not None:
|
||||
rect.moveTo(position)
|
||||
|
||||
def set_geometry(self, rect):
|
||||
'''Set the window geometry.'''
|
||||
|
||||
# See `move_to` for documentation.
|
||||
self.resize(rect.size())
|
||||
window_rect = self._titlebar._window_rect
|
||||
if window_rect is not None:
|
||||
window_rect.setSize(rect.size())
|
||||
|
||||
move_to(self, rect.topLeft())
|
||||
|
||||
def shade(self, size, grip_type):
|
||||
'''Shade the window, hiding the main widget and size grip.'''
|
||||
|
||||
|
@ -454,7 +500,7 @@ def unshade(self, rect, grip_type):
|
|||
if getattr(self, f'_{grip_type}') is not None:
|
||||
getattr(self, f'_{grip_type}').show()
|
||||
self.set_larger_minimum_size()
|
||||
self.setGeometry(rect)
|
||||
self.set_geometry(rect)
|
||||
|
||||
def start_drag(self, event, window_type):
|
||||
'''Start the window drag state.'''
|
||||
|
@ -484,32 +530,58 @@ 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):
|
||||
def start_resize(self, window, 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())
|
||||
# NOTE: We can't use a rubber band with mouse tracking,
|
||||
# since mouse events only occurs if the user is holding
|
||||
# down the house. Simulating a mouse click isn't enough,
|
||||
# even if it sends a mouse press without a release.
|
||||
setattr(self, f'_{window_type}_resize', window)
|
||||
self.window().setCursor(compat.SizeFDiagCursor)
|
||||
self.menu_size_to(QtGui.QCursor.pos())
|
||||
|
||||
def handle_resize(self, position, window_type):
|
||||
# Grab the mouse so we can intercept the click event,
|
||||
# and track hover events outside the app. This doesn't
|
||||
# work on Wayland or on macOS.
|
||||
# https://doc.qt.io/qt-5/qwidget.html#grabMouse
|
||||
if not IS_TRUE_WAYLAND and not sys.platform == 'darwin':
|
||||
self.window().grabMouse()
|
||||
|
||||
def handle_resize(self, position):
|
||||
'''Handle the window resize event.'''
|
||||
getattr(self, f'_{window_type}_resize').menu_size_to(position)
|
||||
self.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:
|
||||
window = getattr(self, f'_{window_type}_resize')
|
||||
if window is None:
|
||||
return
|
||||
|
||||
setattr(self, f'_{window_type}_resize', None)
|
||||
self.restore_cursor()
|
||||
self.releaseMouse()
|
||||
window.window().unsetCursor()
|
||||
if not IS_TRUE_WAYLAND and not sys.platform == 'darwin':
|
||||
self.window().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):
|
||||
def handle_frame(self, window, event, window_type):
|
||||
'''Handle the window frame resize event.'''
|
||||
self.frame_event(obj, event, window_type)
|
||||
|
||||
# Check if use size grips, return early.
|
||||
frame = getattr(window, '_sizeframe', None)
|
||||
if frame is None:
|
||||
return
|
||||
self.frame_event(event, frame)
|
||||
|
||||
# Store if the frame state is active.
|
||||
if frame.is_active and not getattr(self, f'_{window_type}_frame'):
|
||||
start_frame(self, frame, window_type)
|
||||
elif not frame.is_active and getattr(self, f'_{window_type}_frame'):
|
||||
end_frame(self, window_type)
|
||||
|
||||
def end_frame(self, window_type):
|
||||
'''End the window frame resize state.'''
|
||||
|
@ -528,6 +600,7 @@ def window_resize_event(self, event):
|
|||
|
||||
super(type(self), self).resizeEvent(event)
|
||||
|
||||
|
||||
def window_show_event(self, event, grip_type):
|
||||
'''Set the minimum size policies once the widgets are shown.'''
|
||||
|
||||
|
@ -570,13 +643,13 @@ def window_mouse_press_event(self, event, window, window_type):
|
|||
|
||||
widget = self._titlebar
|
||||
if widget.underMouse():
|
||||
# `self.window().subwindow_move` cannot be set, since we're inside
|
||||
# `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
|
||||
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:
|
||||
|
@ -587,9 +660,9 @@ def window_mouse_press_event(self, event, window, window_type):
|
|||
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:
|
||||
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:
|
||||
if getattr(window, f'_{window_type}_drag') is not None:
|
||||
handle_drag(window, event, self, window_type)
|
||||
return super(type(self), self).mouseMoveEvent(event)
|
||||
|
||||
|
@ -659,7 +732,7 @@ class TitleButton(QtWidgets.QToolButton):
|
|||
self.setIcon(icon)
|
||||
self.setAutoRaise(True)
|
||||
|
||||
class Titlebar(QtWidgets.QFrame):
|
||||
class TitleBar(QtWidgets.QFrame):
|
||||
'''Custom instance of a QTitlebar'''
|
||||
|
||||
def __init__(self, window, parent=None, flags=None):
|
||||
|
@ -746,13 +819,16 @@ class Titlebar(QtWidgets.QFrame):
|
|||
self._layout.addWidget(self._help, 0, col)
|
||||
col += 1
|
||||
self._layout.addWidget(self._min, 0, col)
|
||||
self._state1_column = col
|
||||
col += 1
|
||||
self._layout.addWidget(self._max, 0, col)
|
||||
self._state2_column = col
|
||||
col += 1
|
||||
if self._has_shade:
|
||||
self._layout.addWidget(self._shade, 0, col)
|
||||
col += 1
|
||||
self._layout.addWidget(self._close, 0, col)
|
||||
self._close_column = col
|
||||
self._restore.hide()
|
||||
if self._has_shade:
|
||||
self._unshade.hide()
|
||||
|
@ -877,31 +953,7 @@ class Titlebar(QtWidgets.QFrame):
|
|||
'''Start a manually triggered resize event.'''
|
||||
|
||||
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 window so that the position is in the center bottom
|
||||
of the title bar. The position is given in global coordinates.
|
||||
'''
|
||||
|
||||
window = self._window
|
||||
point = window.mapToParent(self.mapFromGlobal(global_position))
|
||||
rect = window.geometry()
|
||||
|
||||
# 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)
|
||||
start_resize(window, self._window, self._window_type)
|
||||
|
||||
def minimize(self):
|
||||
'''Minimize the current window.'''
|
||||
|
@ -1041,7 +1093,7 @@ class Titlebar(QtWidgets.QFrame):
|
|||
if self._window.window() == self._window:
|
||||
self._window._ignore_hide = False
|
||||
self.window().show()
|
||||
self.window().setGeometry(rect)
|
||||
self.window().set_geometry(rect)
|
||||
|
||||
def help(self):
|
||||
'''Enter what's this mode.'''
|
||||
|
@ -1052,50 +1104,50 @@ class Titlebar(QtWidgets.QFrame):
|
|||
def set_minimized(self):
|
||||
'''Show the restore and maximize icons.'''
|
||||
|
||||
if self.isNormal():
|
||||
# Restore hidden, minimize + maximize shown
|
||||
self._layout.replaceWidget(self._min, self._restore)
|
||||
self._restore.show()
|
||||
self._min.hide()
|
||||
elif self.isMinimized():
|
||||
if self.isMinimized():
|
||||
return
|
||||
else:
|
||||
# Maximize hidden, minimize + restore shown
|
||||
self._layout.replaceWidget(self._restore, self._max)
|
||||
self._layout.replaceWidget(self._min, self._restore)
|
||||
self._max.show()
|
||||
|
||||
item1 = self._layout.itemAtPosition(0, self._state1_column)
|
||||
item2 = self._layout.itemAtPosition(0, self._state2_column)
|
||||
self._layout.removeItem(item1)
|
||||
self._layout.removeItem(item2)
|
||||
self._layout.addWidget(self._restore, 0, self._state1_column)
|
||||
self._layout.addWidget(self._max, 0, self._state2_column)
|
||||
self._min.hide()
|
||||
self._restore.show()
|
||||
self._max.show()
|
||||
|
||||
def set_maximized(self):
|
||||
'''Show the minimize and restore icons.'''
|
||||
|
||||
if self.isNormal():
|
||||
# Restore hidden, minimize + maximize shown
|
||||
self._layout.replaceWidget(self._max, self._restore)
|
||||
self._restore.show()
|
||||
if self.isMaximized():
|
||||
return
|
||||
|
||||
item1 = self._layout.itemAtPosition(0, self._state1_column)
|
||||
item2 = self._layout.itemAtPosition(0, self._state2_column)
|
||||
self._layout.removeItem(item1)
|
||||
self._layout.removeItem(item2)
|
||||
self._layout.addWidget(self._min, 0, self._state1_column)
|
||||
self._layout.addWidget(self._restore, 0, self._state2_column)
|
||||
self._max.hide()
|
||||
elif self.isMinimized():
|
||||
# Minimize hidden, restore + maximize shown
|
||||
self._layout.replaceWidget(self._restore, self._min)
|
||||
self._layout.replaceWidget(self._max, self._restore)
|
||||
self._min.show()
|
||||
self._max.hide()
|
||||
self._restore.show()
|
||||
|
||||
def set_restored(self):
|
||||
'''Show the minimize and maximize icons.'''
|
||||
|
||||
if self.isNormal():
|
||||
return
|
||||
elif self.isMinimized():
|
||||
# Minimize hidden, restore + maximize shown
|
||||
self._layout.replaceWidget(self._restore, self._min)
|
||||
|
||||
item1 = self._layout.itemAtPosition(0, self._state1_column)
|
||||
item2 = self._layout.itemAtPosition(0, self._state2_column)
|
||||
self._layout.removeItem(item1)
|
||||
self._layout.removeItem(item2)
|
||||
self._layout.addWidget(self._min, 0, self._state1_column)
|
||||
self._layout.addWidget(self._max, 0, self._state2_column)
|
||||
self._restore.hide()
|
||||
self._min.show()
|
||||
self._restore.hide()
|
||||
else:
|
||||
# Maximize hidden, minimize + restore shown
|
||||
self._layout.replaceWidget(self._restore, self._max)
|
||||
self._max.show()
|
||||
self._restore.hide()
|
||||
|
||||
def set_shaded(self):
|
||||
'''Show the unshade icon (and hide the shade icon).'''
|
||||
|
@ -1113,7 +1165,6 @@ class Titlebar(QtWidgets.QFrame):
|
|||
self._unshade.hide()
|
||||
self._shade.show()
|
||||
|
||||
|
||||
class SizeFrame(QtCore.QObject):
|
||||
'''An invisible frame for resizing events around a window.'''
|
||||
|
||||
|
@ -1232,14 +1283,14 @@ class SizeFrame(QtCore.QObject):
|
|||
|
||||
return WindowEdge.NoEdge
|
||||
|
||||
def top_left(self):
|
||||
def top_left(self, rect):
|
||||
'''Get the top/left position of the window in global coordinates.'''
|
||||
|
||||
# Calculate the top left bounds of our window to get our frame.
|
||||
# We want our frame in global coordinates, but our window
|
||||
# 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())
|
||||
point = rect.topLeft()
|
||||
if self._window.window() != self._window:
|
||||
point = self._window.parent().mapToGlobal(point)
|
||||
|
||||
|
@ -1247,7 +1298,15 @@ class SizeFrame(QtCore.QObject):
|
|||
|
||||
def frame_geometry(self):
|
||||
'''Calculate the frame geometry of our window in global coordinates.'''
|
||||
return QtCore.QRect(self.top_left(), self._window.frameSize())
|
||||
|
||||
rect = self._window.frameGeometry()
|
||||
return QtCore.QRect(self.top_left(rect), self._window.frameSize())
|
||||
|
||||
def geometry(self):
|
||||
'''Calculate the geometry of our window in global coordinates.'''
|
||||
|
||||
rect = self._window.geometry()
|
||||
return QtCore.QRect(self.top_left(rect), self._window.size())
|
||||
|
||||
def update_cursor(self, position):
|
||||
'''Update the cursor shape depending on the cursor position.'''
|
||||
|
@ -1275,35 +1334,10 @@ class SizeFrame(QtCore.QObject):
|
|||
|
||||
self._window.setCursor(self._cursor)
|
||||
|
||||
def unset_cursor(self):
|
||||
'''Unset the custom cursor.'''
|
||||
|
||||
if self._cursor:
|
||||
self._window.unsetCursor()
|
||||
self._cursor = None
|
||||
|
||||
def enter(self, event):
|
||||
'''Handle the enterEvent of the window.'''
|
||||
|
||||
position = shared.single_point_position(args, event)
|
||||
self.update_cursor(self._window.mapToGlobal(position))
|
||||
|
||||
def leave(self, event):
|
||||
'''Handle the leaveEvent of the window.'''
|
||||
|
||||
if not self._pressed:
|
||||
self.unset_cursor()
|
||||
|
||||
def mouse_move(self, event):
|
||||
'''Handle the mouseMoveEvent of the window.'''
|
||||
|
||||
position = shared.single_point_global_position(args, event)
|
||||
if not self._pressed:
|
||||
self.update_cursor(position)
|
||||
return
|
||||
def resize(self, position, rect):
|
||||
'''Resize our window to the adjusted dimensions.'''
|
||||
|
||||
# Get our new frame dimensions.
|
||||
rect = self._band.frameGeometry()
|
||||
if self._press_edge == WindowEdge.NoEdge:
|
||||
return
|
||||
elif self._press_edge == WindowEdge.Top:
|
||||
|
@ -1351,11 +1385,86 @@ class SizeFrame(QtCore.QObject):
|
|||
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)
|
||||
# NOTE: Do not remove this. I have tried everything.
|
||||
# This does not work unless you keep it. There's a weird
|
||||
# bug where the window (only for QMdiSubWindow) now has
|
||||
# a bug where if you click on the title bar, it re-enters
|
||||
# a resize mode, which is independent of this. Shifting
|
||||
# the position by 1 pixel undoes this. Nothing else works,
|
||||
# and I have tried:
|
||||
# - Not due to custom drag/move/resize/frame states.
|
||||
# - Not due to lingering pressed/press_edge/move_edge.
|
||||
# - Not due to a lingering cursor.
|
||||
# - Not due to change event.
|
||||
# - Not due to resize/show event.
|
||||
# - Not due to mouse press/double click/release/move event.
|
||||
# - Not due to the event filter.
|
||||
# - Not due to the QMainWindow-level custom title bar.
|
||||
# - `setFixedSize` on the window on the mouse release
|
||||
# and then undoing on the next resize event eats
|
||||
# the mouse click, but still enters the same mode
|
||||
# (just the window can't be resized).
|
||||
# - Not due to the window-level widgets or margins.
|
||||
# - `setFixedSize` on the title bar just fixes title bar size.
|
||||
# - No previous versions work if we use the local_rect.
|
||||
# - Unsetting the band and use the window directly does nothing.
|
||||
# - Using `setGeometry(rect)` then `setGeometry(local_rect)`.
|
||||
# - Unsetting the band geometry in `mouse_release` event.
|
||||
# - Simulating mouse press+release in `mouse_release`.
|
||||
# - Simulating mouse press+release in `end_frame`.
|
||||
# - Ignoring the subsequent mousePressEvent on the title bar.
|
||||
# - This causes the window to disappear entirely.
|
||||
# - Hide+show inside `mouse_release` causes window to hide.
|
||||
# - Hide+show inside `end_frame` causes window to hide.
|
||||
# - Not due to minimum rect size checks.
|
||||
# - Not due to MDI area limit checks.
|
||||
# - Not related to custom restore/min/max/shade/unshade code.
|
||||
# - Not due to custom hide/setVisible overrides.
|
||||
#
|
||||
# This is almost certain a bug in QMdiArea, but this is a
|
||||
# workaround that produces almost is almost imperceptible,
|
||||
# since the widget is being actively resized.
|
||||
#
|
||||
# I love mess.... but not this.
|
||||
if dx1 == 0 and dy1 == 0 and dx2 == 0 and dy2 == 0:
|
||||
dx1 += 1
|
||||
dy1 += 1
|
||||
dx2 += 1
|
||||
dy2 += 1
|
||||
local_rect.adjust(dx1, dy1, dx2, dy2)
|
||||
|
||||
self._window.setGeometry(local_rect)
|
||||
self._window.set_geometry(local_rect)
|
||||
self._band.setGeometry(rect)
|
||||
|
||||
def unset_cursor(self):
|
||||
'''Unset the custom cursor.'''
|
||||
|
||||
if self._cursor:
|
||||
self._window.unsetCursor()
|
||||
self._cursor = None
|
||||
|
||||
def enter(self, event):
|
||||
'''Handle the enterEvent of the window.'''
|
||||
|
||||
position = shared.single_point_position(args, event)
|
||||
self.update_cursor(self._window.mapToGlobal(position))
|
||||
|
||||
def leave(self, event):
|
||||
'''Handle the leaveEvent of the window.'''
|
||||
|
||||
if not self._pressed:
|
||||
self.unset_cursor()
|
||||
|
||||
def mouse_move(self, event):
|
||||
'''Handle the mouseMoveEvent of the window.'''
|
||||
|
||||
position = shared.single_point_global_position(args, event)
|
||||
if not self._pressed:
|
||||
self.update_cursor(position)
|
||||
return
|
||||
|
||||
self.resize(position, self._band.geometry())
|
||||
|
||||
def mouse_press(self, event):
|
||||
'''Handle the mousePressEvent of the window.'''
|
||||
|
||||
|
@ -1363,16 +1472,16 @@ class SizeFrame(QtCore.QObject):
|
|||
position = shared.single_point_global_position(args, event)
|
||||
rect = self.frame_geometry()
|
||||
self._press_edge = self.cursor_position(position, rect)
|
||||
# We want to separately hand drags, so only
|
||||
# We want to separately handle drags, so only
|
||||
# set this if we are pressing on the edge.
|
||||
if self._press_edge != WindowEdge.NoEdge:
|
||||
self._pressed = True
|
||||
self._band.setGeometry(rect)
|
||||
self._band.setGeometry(self.geometry())
|
||||
|
||||
def mouse_release(self, event):
|
||||
'''Handle the mouseReleaseEvent of the window.'''
|
||||
|
||||
if event.button() == compat.LeftButton:
|
||||
if event.button() == compat.LeftButton and self._pressed:
|
||||
self._pressed = False
|
||||
|
||||
def hover_move(self, event):
|
||||
|
@ -1385,8 +1494,7 @@ class SubWindow(QtWidgets.QMdiSubWindow):
|
|||
'''Base subclass for a QMdiSubwindow.'''
|
||||
|
||||
def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)):
|
||||
super().__init__(parent)
|
||||
self.setWindowFlags(self.windowFlags() | flags)
|
||||
super().__init__(parent, flags=flags)
|
||||
super().setWidget(QtWidgets.QWidget())
|
||||
|
||||
class DefaultSubWindow(SubWindow):
|
||||
|
@ -1414,7 +1522,7 @@ class FramelessSubWindow(SubWindow):
|
|||
# Create our widgets. Sizeframe and sizegrip are mutually exclusive.
|
||||
self._central = QtWidgets.QFrame(super().widget())
|
||||
self._central.setLayout(QtWidgets.QVBoxLayout())
|
||||
self._titlebar = Titlebar(self, self._central, flags)
|
||||
self._titlebar = TitleBar(self, self._central, flags)
|
||||
self._widget = QtWidgets.QWidget(self._central)
|
||||
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
||||
self._sizeframe = None
|
||||
|
@ -1494,6 +1602,10 @@ class FramelessSubWindow(SubWindow):
|
|||
'''Move the window to the desired position'''
|
||||
move_to(self, position)
|
||||
|
||||
def set_geometry(self, rect):
|
||||
'''Set the window geometry.'''
|
||||
set_geometry(self, rect)
|
||||
|
||||
def set_minimum_size(self):
|
||||
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
||||
set_minimum_size(self)
|
||||
|
@ -1519,7 +1631,7 @@ class FramelessSubWindow(SubWindow):
|
|||
if self._sizegrip is not None:
|
||||
self._sizegrip.hide()
|
||||
self.set_larger_minimum_size()
|
||||
self.setGeometry(rect)
|
||||
self.set_geometry(rect)
|
||||
|
||||
def restore(self, rect):
|
||||
'''Restore the window, showing the main widget and size grip.'''
|
||||
|
@ -1528,7 +1640,7 @@ class FramelessSubWindow(SubWindow):
|
|||
if self._sizegrip is not None:
|
||||
self._sizegrip.show()
|
||||
self.set_larger_minimum_size()
|
||||
self.setGeometry(rect)
|
||||
self.set_geometry(rect)
|
||||
|
||||
def shade(self, size):
|
||||
'''Shade the window, hiding the main widget and size grip.'''
|
||||
|
@ -1761,39 +1873,47 @@ class Window(QtWidgets.QMainWindow):
|
|||
# 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.'''
|
||||
def menu_size_to(self, point):
|
||||
'''
|
||||
Size the window so that the position is in the center bottom
|
||||
of the title bar. The position is given in global coordinates.
|
||||
'''
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
app.setOverrideCursor(QtGui.QCursor(cursor))
|
||||
window = getattr(self, '_window_resize', None)
|
||||
if window is None:
|
||||
window = self._subwindow_resize
|
||||
rect = window.geometry()
|
||||
# We add a trivial amount so we avoid a bug where we are
|
||||
# exactly on the sizegrip, which keeps us in resize mode
|
||||
# unless we do another button click, we weirdly doesn't
|
||||
# work well when simulated.
|
||||
point += QtCore.QPoint(2, 2)
|
||||
|
||||
def restore_cursor(self):
|
||||
'''Restore the overridden cursor.'''
|
||||
# If we have a subwindow, need to limit to the MDI area rect.
|
||||
if window.window() != window:
|
||||
point = window.parent().mapFromGlobal(point)
|
||||
area_rect = window.mdiArea().contentsRect()
|
||||
point.setX(min(point.x(), area_rect.right()))
|
||||
point.setY(min(point.y(), area_rect.bottom()))
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
app.restoreOverrideCursor()
|
||||
# Need to ensure we didn't go past the top left.
|
||||
# Don't want to shift to negative values.
|
||||
top_left = rect.topLeft()
|
||||
point.setX(max(top_left.x(), point.x()))
|
||||
point.setY(max(top_left.y(), point.y()))
|
||||
|
||||
# We add a trivial amount to simplify growing the window on Wayland.
|
||||
# Wayland cannot track outside of the application.
|
||||
if IS_TRUE_WAYLAND and window.window() == window:
|
||||
point += QtCore.QPoint(16, 16)
|
||||
rect.setBottomRight(point)
|
||||
window.set_geometry(rect)
|
||||
|
||||
# Ensure we trigger the elide resize timer.
|
||||
titlebar = window._titlebar
|
||||
titlebar._title._timer.start(REPAINT_TIMER)
|
||||
|
||||
def resolve_state(self):
|
||||
'''Handle theoretically possible conflicts in window state.'''
|
||||
|
@ -1806,20 +1926,29 @@ class Window(QtWidgets.QMainWindow):
|
|||
# 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:
|
||||
|
||||
has_state = False
|
||||
if has_state or getattr(self, f'_window_frame', None) is not None:
|
||||
end_resize(self, 'window')
|
||||
if getattr(self, f'_window_resize', None) is not None:
|
||||
has_state = True
|
||||
if has_state or getattr(self, f'_window_resize', None) is not None:
|
||||
end_move(self, 'window')
|
||||
if getattr(self, f'_window_move', None) is not None:
|
||||
has_state = True
|
||||
if has_state or getattr(self, f'_window_move', None) is not None:
|
||||
end_drag(self, 'window')
|
||||
if getattr(self, f'_window_drag', None) is not None:
|
||||
has_state = True
|
||||
if has_state or getattr(self, f'_window_drag', None) is not None:
|
||||
end_frame(self, 'window')
|
||||
if getattr(self, f'_subwindow_frame') is not None:
|
||||
has_state = True
|
||||
if has_state or self._subwindow_frame is not None:
|
||||
end_resize(self, 'subwindow')
|
||||
if getattr(self, f'_subwindow_resize') is not None:
|
||||
has_state = True
|
||||
if has_state or self._subwindow_resize is not None:
|
||||
end_move(self, 'subwindow')
|
||||
if getattr(self, f'_subwindow_move') is not None:
|
||||
has_state = True
|
||||
if has_state or self._subwindow_move is not None:
|
||||
end_drag(self, 'subwindow')
|
||||
has_state = True
|
||||
|
||||
def move_event(self, _, event, window_type):
|
||||
'''Handle window move events.'''
|
||||
|
@ -1830,23 +1959,24 @@ class Window(QtWidgets.QMainWindow):
|
|||
elif event.type() == compat.MouseButtonPress:
|
||||
end_move(self, window_type)
|
||||
|
||||
def resize_event(self, _, event, window_type):
|
||||
def resize_event(self, obj, event, window_type):
|
||||
'''Handle window resize events.'''
|
||||
|
||||
if event.type() == compat.MouseMove:
|
||||
# NOTE: If we're on Wayland, we cant' track hover events outside the
|
||||
# main widget, and we can't guess intermittently since if the mouse
|
||||
# doesn't move, we won't get an `Enter` or `HoverEnter` event, and
|
||||
# `QCursor::pos` will always be the same. What this means is we
|
||||
# can't guess where we left the, and resize until we're back
|
||||
# in the bounds.
|
||||
if event.type() in (compat.MouseMove, compat.HoverMove):
|
||||
position = shared.single_point_global_position(args, event)
|
||||
handle_resize(self, position, window_type)
|
||||
handle_resize(self, position)
|
||||
elif event.type() == compat.MouseButtonPress:
|
||||
end_resize(self, window_type)
|
||||
|
||||
def frame_event(self, window, event, window_type):
|
||||
def frame_event(self, event, frame):
|
||||
'''Handle size adjustments using the window frame.'''
|
||||
|
||||
frame = getattr(window, '_sizeframe', None)
|
||||
# Uses size grips, return early.
|
||||
if frame is None:
|
||||
return
|
||||
|
||||
# No position for the event: we don't use it.
|
||||
if event.type() in (compat.Enter, compat.HoverEnter):
|
||||
frame.enter(event)
|
||||
|
@ -1861,29 +1991,23 @@ class Window(QtWidgets.QMainWindow):
|
|||
elif event.type() == compat.HoverMove:
|
||||
frame.hover_move(event)
|
||||
|
||||
# Store if the frame state is active.
|
||||
if frame.is_active:
|
||||
start_frame(self, frame, window_type)
|
||||
else:
|
||||
end_frame(self, window_type)
|
||||
|
||||
# QT EVENTS
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
'''Custom event filter to handle move and resize events.'''
|
||||
|
||||
self.resolve_state()
|
||||
if getattr(self, 'window_move', None) is not None:
|
||||
if getattr(self, '_window_move', None) is not None:
|
||||
# Cannot occur while the size frame is active.
|
||||
self.move_event(obj, event, 'window')
|
||||
elif getattr(self, 'window_resize', None) is not None:
|
||||
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:
|
||||
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:
|
||||
elif self._subwindow_resize is not None:
|
||||
self.resize_event(obj, event, 'subwindow')
|
||||
elif isinstance(obj, SubWindow) and not obj.isMinimized():
|
||||
handle_frame(self, obj, event, 'subwindow')
|
||||
|
@ -1932,7 +2056,7 @@ class FramelessWindow(Window):
|
|||
self._central = QtWidgets.QFrame(self)
|
||||
self._layout = QtWidgets.QVBoxLayout(self._central)
|
||||
self.setCentralWidget(self._central)
|
||||
self._titlebar = Titlebar(self, self._central, flags)
|
||||
self._titlebar = TitleBar(self, self._central, flags)
|
||||
self._widget = QtWidgets.QWidget(self._central)
|
||||
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
||||
self._sizeframe = None
|
||||
|
@ -1998,26 +2122,6 @@ class FramelessWindow(Window):
|
|||
|
||||
# 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.'''
|
||||
|
@ -2054,6 +2158,10 @@ class FramelessWindow(Window):
|
|||
'''Move the window to the desired position'''
|
||||
move_to(self, position)
|
||||
|
||||
def set_geometry(self, rect):
|
||||
'''Set the window geometry.'''
|
||||
set_geometry(self, rect)
|
||||
|
||||
def set_minimum_size(self):
|
||||
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
||||
set_minimum_size(self)
|
||||
|
@ -2072,9 +2180,11 @@ class FramelessWindow(Window):
|
|||
|
||||
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 showNormal(self):
|
||||
super().showNormal()
|
||||
|
||||
def shade(self, size):
|
||||
'''Shade the window, hiding the main widget and size grip.'''
|
||||
shade(self, size, 'statusbar')
|
||||
|
@ -2093,7 +2203,7 @@ class FramelessWindow(Window):
|
|||
window_resize_event(self, event)
|
||||
|
||||
def showEvent(self, event):
|
||||
'''Call `activateWindow` if we bypass the X11 window manager.'''
|
||||
'''Set the minimum size policies once the widgets are shown.'''
|
||||
window_show_event(self, event, 'statusbar')
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
|
@ -2112,13 +2222,36 @@ class FramelessWindow(Window):
|
|||
'''End the drag event.'''
|
||||
return window_mouse_release_event(self, event, self, 'window')
|
||||
|
||||
def changeEvent(self, event):
|
||||
'''Catch state changes from outside our custom titlebar.'''
|
||||
|
||||
super().changeEvent(event)
|
||||
|
||||
# If we're restoring a top-level widget, need to ensure the
|
||||
# state is properly restored to the correct icons.
|
||||
if event.type() not in (compat.ActivationChange, compat.WindowStateChange):
|
||||
return
|
||||
|
||||
# We have 3 states, and we can have combinations of some of them:
|
||||
# - NoState
|
||||
# - Minimized
|
||||
# - Maximized
|
||||
# - Minimized + Maximized (treat as Minimized).
|
||||
state = self.windowState()
|
||||
if state & compat.WindowMinimized:
|
||||
self._titlebar.minimize()
|
||||
elif state & compat.WindowMaximized:
|
||||
self._titlebar.maximize()
|
||||
else:
|
||||
self._titlebar.restore()
|
||||
|
||||
def main():
|
||||
'Application entry point'
|
||||
|
||||
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:
|
||||
if args.default_window_frame or USE_WAYLAND_FRAME:
|
||||
window_class = DefaultWindow
|
||||
app, window = shared.setup_app(args, unknown, compat, window_class=window_class)
|
||||
app.installEventFilter(window)
|
||||
|
|
Loading…
Reference in New Issue