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:
|
if args.use_x11:
|
||||||
os.environ['XDG_SESSION_TYPE'] = 'x11'
|
os.environ['XDG_SESSION_TYPE'] = 'x11'
|
||||||
|
os.environ['QT_QPA_PLATFORM'] = 'xcb'
|
||||||
|
|
||||||
return args, unknown
|
return args, unknown
|
||||||
|
|
||||||
|
@ -172,6 +173,7 @@ def get_compat_definitions(args):
|
||||||
ns.TextElideMode = QtCore.Qt.TextElideMode
|
ns.TextElideMode = QtCore.Qt.TextElideMode
|
||||||
ns.CursorShape = QtCore.Qt.CursorShape
|
ns.CursorShape = QtCore.Qt.CursorShape
|
||||||
ns.MouseButton = QtCore.Qt.MouseButton
|
ns.MouseButton = QtCore.Qt.MouseButton
|
||||||
|
ns.KeyboardModifier = QtCore.Qt.KeyboardModifier
|
||||||
ns.SizePolicy = QtWidgets.QSizePolicy.Policy
|
ns.SizePolicy = QtWidgets.QSizePolicy.Policy
|
||||||
ns.SizeConstraint = QtWidgets.QLayout.SizeConstraint
|
ns.SizeConstraint = QtWidgets.QLayout.SizeConstraint
|
||||||
|
|
||||||
|
@ -244,6 +246,8 @@ def get_compat_definitions(args):
|
||||||
ns.MouseButtonPress = ns.EventType.MouseButtonPress
|
ns.MouseButtonPress = ns.EventType.MouseButtonPress
|
||||||
ns.MouseButtonRelease = ns.EventType.MouseButtonRelease
|
ns.MouseButtonRelease = ns.EventType.MouseButtonRelease
|
||||||
ns.MouseMove = ns.EventType.MouseMove
|
ns.MouseMove = ns.EventType.MouseMove
|
||||||
|
ns.WindowStateChange = ns.EventType.WindowStateChange
|
||||||
|
ns.ActivationChange = ns.EventType.ActivationChange
|
||||||
ns.WindowPalette = ns.ColorRole.Window
|
ns.WindowPalette = ns.ColorRole.Window
|
||||||
ns.WindowTextPalette = ns.ColorRole.WindowText
|
ns.WindowTextPalette = ns.ColorRole.WindowText
|
||||||
ns.LightPalette = ns.ColorRole.Light
|
ns.LightPalette = ns.ColorRole.Light
|
||||||
|
@ -445,6 +449,7 @@ def get_compat_definitions(args):
|
||||||
ns.WhatsThisCursor = ns.CursorShape.WhatsThisCursor
|
ns.WhatsThisCursor = ns.CursorShape.WhatsThisCursor
|
||||||
ns.LeftButton = ns.MouseButton.LeftButton
|
ns.LeftButton = ns.MouseButton.LeftButton
|
||||||
ns.RightButton = ns.MouseButton.RightButton
|
ns.RightButton = ns.MouseButton.RightButton
|
||||||
|
ns.NoModifier = ns.KeyboardModifier.NoModifier
|
||||||
ns.SizeFixed = ns.SizePolicy.Fixed
|
ns.SizeFixed = ns.SizePolicy.Fixed
|
||||||
ns.SizeMinimum = ns.SizePolicy.Minimum
|
ns.SizeMinimum = ns.SizePolicy.Minimum
|
||||||
ns.SizeMaximum = ns.SizePolicy.Maximum
|
ns.SizeMaximum = ns.SizePolicy.Maximum
|
||||||
|
@ -531,6 +536,8 @@ def get_compat_definitions(args):
|
||||||
ns.MouseButtonPress = QtCore.QEvent.MouseButtonPress
|
ns.MouseButtonPress = QtCore.QEvent.MouseButtonPress
|
||||||
ns.MouseButtonRelease = QtCore.QEvent.MouseButtonRelease
|
ns.MouseButtonRelease = QtCore.QEvent.MouseButtonRelease
|
||||||
ns.MouseMove = QtCore.QEvent.MouseMove
|
ns.MouseMove = QtCore.QEvent.MouseMove
|
||||||
|
ns.WindowStateChange = QtCore.QEvent.WindowStateChange
|
||||||
|
ns.ActivationChange = QtCore.QEvent.ActivationChange
|
||||||
ns.WindowPalette = QtGui.QPalette.Window
|
ns.WindowPalette = QtGui.QPalette.Window
|
||||||
ns.WindowTextPalette = QtGui.QPalette.WindowText
|
ns.WindowTextPalette = QtGui.QPalette.WindowText
|
||||||
ns.LightPalette = QtGui.QPalette.Light
|
ns.LightPalette = QtGui.QPalette.Light
|
||||||
|
@ -726,6 +733,7 @@ def get_compat_definitions(args):
|
||||||
ns.WhatsThisCursor = QtCore.Qt.WhatsThisCursor
|
ns.WhatsThisCursor = QtCore.Qt.WhatsThisCursor
|
||||||
ns.LeftButton = QtCore.Qt.LeftButton
|
ns.LeftButton = QtCore.Qt.LeftButton
|
||||||
ns.RightButton = QtCore.Qt.RightButton
|
ns.RightButton = QtCore.Qt.RightButton
|
||||||
|
ns.NoModifier = QtCore.Qt.NoModifier
|
||||||
ns.SizeFixed = QtWidgets.QSizePolicy.Fixed
|
ns.SizeFixed = QtWidgets.QSizePolicy.Fixed
|
||||||
ns.SizeMinimum = QtWidgets.QSizePolicy.Minimum
|
ns.SizeMinimum = QtWidgets.QSizePolicy.Minimum
|
||||||
ns.SizeMaximum = QtWidgets.QSizePolicy.Maximum
|
ns.SizeMaximum = QtWidgets.QSizePolicy.Maximum
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
|
|
||||||
A full-featured, custom titlebar for a subwindow in an MDI area. This
|
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
|
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:
|
The custom titlebar supports the following:
|
||||||
- Title text
|
- Title text
|
||||||
|
@ -68,7 +69,37 @@
|
||||||
Any other more elaborate style, like a `Panel`, won't be rendered
|
Any other more elaborate style, like a `Panel`, won't be rendered
|
||||||
correctly.
|
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
|
import enum
|
||||||
|
@ -78,19 +109,6 @@ import sys
|
||||||
|
|
||||||
from pathlib import Path
|
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 = shared.create_parser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--minimize-location',
|
'--minimize-location',
|
||||||
|
@ -125,6 +143,11 @@ parser.add_argument(
|
||||||
help='add a top-level shade/unshade button',
|
help='add a top-level shade/unshade button',
|
||||||
action='store_true',
|
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)
|
args, unknown = shared.parse_args(parser)
|
||||||
QtCore, QtGui, QtWidgets = shared.import_qt(args)
|
QtCore, QtGui, QtWidgets = shared.import_qt(args)
|
||||||
compat = shared.get_compat_definitions(args)
|
compat = shared.get_compat_definitions(args)
|
||||||
|
@ -137,9 +160,21 @@ TRACK_TIMER = 20
|
||||||
CLICK_TIMER = 20
|
CLICK_TIMER = 20
|
||||||
# Make the titlebar size too large, so we can get the real value with min.
|
# Make the titlebar size too large, so we can get the real value with min.
|
||||||
TITLEBAR_HEIGHT = 2**16
|
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.
|
# 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('WARNING: Wayland does not support custom title bars.', file=sys.stderr)
|
||||||
print('Applications in Wayland cannot set their own position.', 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)
|
print('Defaulting to the system title bar instead.', file=sys.stderr)
|
||||||
|
@ -370,11 +405,6 @@ class SettingTabs(QtWidgets.QTabWidget):
|
||||||
|
|
||||||
# RESIZE HELPERS
|
# 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):
|
def border_size(self):
|
||||||
'''Get the size of the border, regardless if present.'''
|
'''Get the size of the border, regardless if present.'''
|
||||||
return QtCore.QSize(2 * self._border, 2 * self._border)
|
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.
|
# Also updates the stored previous subwindow position, if applicable.
|
||||||
# This means shading/unshading uses the new position of the window,
|
# This means shading/unshading uses the new position of the window,
|
||||||
# but the old sizes, rather than jump the window back.
|
# 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)
|
self.move(position)
|
||||||
rect = self._titlebar._window_rect
|
rect = self._titlebar._window_rect
|
||||||
if rect is not None:
|
if rect is not None:
|
||||||
rect.moveTo(position)
|
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):
|
def shade(self, size, grip_type):
|
||||||
'''Shade the window, hiding the main widget and size grip.'''
|
'''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:
|
if getattr(self, f'_{grip_type}') is not None:
|
||||||
getattr(self, f'_{grip_type}').show()
|
getattr(self, f'_{grip_type}').show()
|
||||||
self.set_larger_minimum_size()
|
self.set_larger_minimum_size()
|
||||||
self.setGeometry(rect)
|
self.set_geometry(rect)
|
||||||
|
|
||||||
def start_drag(self, event, window_type):
|
def start_drag(self, event, window_type):
|
||||||
'''Start the window drag state.'''
|
'''Start the window drag state.'''
|
||||||
|
@ -484,32 +530,58 @@ def end_move(self, window_type):
|
||||||
'''End the window move state.'''
|
'''End the window move state.'''
|
||||||
setattr(self, f'_{window_type}_move', None)
|
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.'''
|
'''Start the window resize state.'''
|
||||||
|
|
||||||
setattr(self, f'_{window_type}_resize', widget)
|
# NOTE: We can't use a rubber band with mouse tracking,
|
||||||
self.set_cursor(compat.SizeFDiagCursor)
|
# since mouse events only occurs if the user is holding
|
||||||
widget.menu_size_to(QtGui.QCursor.pos())
|
# 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.'''
|
'''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):
|
def end_resize(self, window_type):
|
||||||
'''End the window resize state.'''
|
'''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)
|
setattr(self, f'_{window_type}_resize', None)
|
||||||
self.restore_cursor()
|
window.window().unsetCursor()
|
||||||
self.releaseMouse()
|
if not IS_TRUE_WAYLAND and not sys.platform == 'darwin':
|
||||||
|
self.window().releaseMouse()
|
||||||
|
|
||||||
def start_frame(self, frame, window_type):
|
def start_frame(self, frame, window_type):
|
||||||
'''Start the window frame resize state.'''
|
'''Start the window frame resize state.'''
|
||||||
setattr(self, f'_{window_type}_frame', frame)
|
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.'''
|
'''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):
|
def end_frame(self, window_type):
|
||||||
'''End the window frame resize state.'''
|
'''End the window frame resize state.'''
|
||||||
|
@ -528,6 +600,7 @@ def window_resize_event(self, event):
|
||||||
|
|
||||||
super(type(self), self).resizeEvent(event)
|
super(type(self), self).resizeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
def window_show_event(self, event, grip_type):
|
def window_show_event(self, event, grip_type):
|
||||||
'''Set the minimum size policies once the widgets are shown.'''
|
'''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
|
widget = self._titlebar
|
||||||
if widget.underMouse():
|
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,
|
# the global event filter here. We handle conflicts here,
|
||||||
# so only one of the 4 states can be set. We can't move
|
# so only one of the 4 states can be set. We can't move
|
||||||
# minimized widgets, so don't try.
|
# minimized widgets, so don't try.
|
||||||
is_left = event.button() == compat.LeftButton
|
is_left = event.button() == compat.LeftButton
|
||||||
is_minimized = self.isMinimized() and not widget._is_shaded
|
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:
|
if is_left and not is_minimized and not has_frame:
|
||||||
start_drag(self.window(), event, window_type)
|
start_drag(self.window(), event, window_type)
|
||||||
elif event.button() == compat.RightButton:
|
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):
|
def window_mouse_move_event(self, event, window, window_type):
|
||||||
'''Reposition the window on the move event.'''
|
'''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)
|
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)
|
handle_drag(window, event, self, window_type)
|
||||||
return super(type(self), self).mouseMoveEvent(event)
|
return super(type(self), self).mouseMoveEvent(event)
|
||||||
|
|
||||||
|
@ -659,7 +732,7 @@ class TitleButton(QtWidgets.QToolButton):
|
||||||
self.setIcon(icon)
|
self.setIcon(icon)
|
||||||
self.setAutoRaise(True)
|
self.setAutoRaise(True)
|
||||||
|
|
||||||
class Titlebar(QtWidgets.QFrame):
|
class TitleBar(QtWidgets.QFrame):
|
||||||
'''Custom instance of a QTitlebar'''
|
'''Custom instance of a QTitlebar'''
|
||||||
|
|
||||||
def __init__(self, window, parent=None, flags=None):
|
def __init__(self, window, parent=None, flags=None):
|
||||||
|
@ -746,13 +819,16 @@ class Titlebar(QtWidgets.QFrame):
|
||||||
self._layout.addWidget(self._help, 0, col)
|
self._layout.addWidget(self._help, 0, col)
|
||||||
col += 1
|
col += 1
|
||||||
self._layout.addWidget(self._min, 0, col)
|
self._layout.addWidget(self._min, 0, col)
|
||||||
|
self._state1_column = col
|
||||||
col += 1
|
col += 1
|
||||||
self._layout.addWidget(self._max, 0, col)
|
self._layout.addWidget(self._max, 0, col)
|
||||||
|
self._state2_column = col
|
||||||
col += 1
|
col += 1
|
||||||
if self._has_shade:
|
if self._has_shade:
|
||||||
self._layout.addWidget(self._shade, 0, col)
|
self._layout.addWidget(self._shade, 0, col)
|
||||||
col += 1
|
col += 1
|
||||||
self._layout.addWidget(self._close, 0, col)
|
self._layout.addWidget(self._close, 0, col)
|
||||||
|
self._close_column = col
|
||||||
self._restore.hide()
|
self._restore.hide()
|
||||||
if self._has_shade:
|
if self._has_shade:
|
||||||
self._unshade.hide()
|
self._unshade.hide()
|
||||||
|
@ -877,31 +953,7 @@ class Titlebar(QtWidgets.QFrame):
|
||||||
'''Start a manually triggered resize event.'''
|
'''Start a manually triggered resize event.'''
|
||||||
|
|
||||||
window = self.window()
|
window = self.window()
|
||||||
# Want to intercept all mouse events until the size event finishes.
|
start_resize(window, self._window, self._window_type)
|
||||||
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)
|
|
||||||
|
|
||||||
def minimize(self):
|
def minimize(self):
|
||||||
'''Minimize the current window.'''
|
'''Minimize the current window.'''
|
||||||
|
@ -1041,7 +1093,7 @@ class Titlebar(QtWidgets.QFrame):
|
||||||
if self._window.window() == self._window:
|
if self._window.window() == self._window:
|
||||||
self._window._ignore_hide = False
|
self._window._ignore_hide = False
|
||||||
self.window().show()
|
self.window().show()
|
||||||
self.window().setGeometry(rect)
|
self.window().set_geometry(rect)
|
||||||
|
|
||||||
def help(self):
|
def help(self):
|
||||||
'''Enter what's this mode.'''
|
'''Enter what's this mode.'''
|
||||||
|
@ -1052,50 +1104,50 @@ class Titlebar(QtWidgets.QFrame):
|
||||||
def set_minimized(self):
|
def set_minimized(self):
|
||||||
'''Show the restore and maximize icons.'''
|
'''Show the restore and maximize icons.'''
|
||||||
|
|
||||||
if self.isNormal():
|
if self.isMinimized():
|
||||||
# Restore hidden, minimize + maximize shown
|
|
||||||
self._layout.replaceWidget(self._min, self._restore)
|
|
||||||
self._restore.show()
|
|
||||||
self._min.hide()
|
|
||||||
elif self.isMinimized():
|
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
# Maximize hidden, minimize + restore shown
|
item1 = self._layout.itemAtPosition(0, self._state1_column)
|
||||||
self._layout.replaceWidget(self._restore, self._max)
|
item2 = self._layout.itemAtPosition(0, self._state2_column)
|
||||||
self._layout.replaceWidget(self._min, self._restore)
|
self._layout.removeItem(item1)
|
||||||
self._max.show()
|
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._min.hide()
|
||||||
|
self._restore.show()
|
||||||
|
self._max.show()
|
||||||
|
|
||||||
def set_maximized(self):
|
def set_maximized(self):
|
||||||
'''Show the minimize and restore icons.'''
|
'''Show the minimize and restore icons.'''
|
||||||
|
|
||||||
if self.isNormal():
|
if self.isMaximized():
|
||||||
# Restore hidden, minimize + maximize shown
|
return
|
||||||
self._layout.replaceWidget(self._max, self._restore)
|
|
||||||
self._restore.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._min, 0, self._state1_column)
|
||||||
|
self._layout.addWidget(self._restore, 0, self._state2_column)
|
||||||
self._max.hide()
|
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._min.show()
|
||||||
self._max.hide()
|
self._restore.show()
|
||||||
|
|
||||||
def set_restored(self):
|
def set_restored(self):
|
||||||
'''Show the minimize and maximize icons.'''
|
'''Show the minimize and maximize icons.'''
|
||||||
|
|
||||||
if self.isNormal():
|
if self.isNormal():
|
||||||
return
|
return
|
||||||
elif self.isMinimized():
|
|
||||||
# Minimize hidden, restore + maximize shown
|
item1 = self._layout.itemAtPosition(0, self._state1_column)
|
||||||
self._layout.replaceWidget(self._restore, self._min)
|
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._min.show()
|
||||||
self._restore.hide()
|
|
||||||
else:
|
|
||||||
# Maximize hidden, minimize + restore shown
|
|
||||||
self._layout.replaceWidget(self._restore, self._max)
|
|
||||||
self._max.show()
|
self._max.show()
|
||||||
self._restore.hide()
|
|
||||||
|
|
||||||
def set_shaded(self):
|
def set_shaded(self):
|
||||||
'''Show the unshade icon (and hide the shade icon).'''
|
'''Show the unshade icon (and hide the shade icon).'''
|
||||||
|
@ -1113,7 +1165,6 @@ class Titlebar(QtWidgets.QFrame):
|
||||||
self._unshade.hide()
|
self._unshade.hide()
|
||||||
self._shade.show()
|
self._shade.show()
|
||||||
|
|
||||||
|
|
||||||
class SizeFrame(QtCore.QObject):
|
class SizeFrame(QtCore.QObject):
|
||||||
'''An invisible frame for resizing events around a window.'''
|
'''An invisible frame for resizing events around a window.'''
|
||||||
|
|
||||||
|
@ -1232,14 +1283,14 @@ class SizeFrame(QtCore.QObject):
|
||||||
|
|
||||||
return WindowEdge.NoEdge
|
return WindowEdge.NoEdge
|
||||||
|
|
||||||
def top_left(self):
|
def top_left(self, rect):
|
||||||
'''Get the top/left position of the window in global coordinates.'''
|
'''Get the top/left position of the window in global coordinates.'''
|
||||||
|
|
||||||
# Calculate the top left bounds of our window to get our frame.
|
# Calculate the top left bounds of our window to get our frame.
|
||||||
# We want our frame in global coordinates, but our window
|
# We want our frame in global coordinates, but our window
|
||||||
# might be a subwindow. If it has a parent, then it's a subwindow
|
# might be a subwindow. If it has a parent, then it's a subwindow
|
||||||
# and we need to map our coordinates.
|
# 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:
|
if self._window.window() != self._window:
|
||||||
point = self._window.parent().mapToGlobal(point)
|
point = self._window.parent().mapToGlobal(point)
|
||||||
|
|
||||||
|
@ -1247,7 +1298,15 @@ class SizeFrame(QtCore.QObject):
|
||||||
|
|
||||||
def frame_geometry(self):
|
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())
|
|
||||||
|
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):
|
def update_cursor(self, position):
|
||||||
'''Update the cursor shape depending on the cursor position.'''
|
'''Update the cursor shape depending on the cursor position.'''
|
||||||
|
@ -1275,35 +1334,10 @@ class SizeFrame(QtCore.QObject):
|
||||||
|
|
||||||
self._window.setCursor(self._cursor)
|
self._window.setCursor(self._cursor)
|
||||||
|
|
||||||
def unset_cursor(self):
|
def resize(self, position, rect):
|
||||||
'''Unset the custom cursor.'''
|
'''Resize our window to the adjusted dimensions.'''
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Get our new frame dimensions.
|
# Get our new frame dimensions.
|
||||||
rect = self._band.frameGeometry()
|
|
||||||
if self._press_edge == WindowEdge.NoEdge:
|
if self._press_edge == WindowEdge.NoEdge:
|
||||||
return
|
return
|
||||||
elif self._press_edge == WindowEdge.Top:
|
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()
|
dx2 = min(local_rect.right(), area_rect.right()) - local_rect.right()
|
||||||
dy2 = min(local_rect.bottom(), area_rect.bottom()) - local_rect.bottom()
|
dy2 = min(local_rect.bottom(), area_rect.bottom()) - local_rect.bottom()
|
||||||
rect.adjust(dx1, dy1, dx2, dy2)
|
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)
|
local_rect.adjust(dx1, dy1, dx2, dy2)
|
||||||
|
|
||||||
self._window.setGeometry(local_rect)
|
self._window.set_geometry(local_rect)
|
||||||
self._band.setGeometry(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):
|
def mouse_press(self, event):
|
||||||
'''Handle the mousePressEvent of the window.'''
|
'''Handle the mousePressEvent of the window.'''
|
||||||
|
|
||||||
|
@ -1363,16 +1472,16 @@ class SizeFrame(QtCore.QObject):
|
||||||
position = shared.single_point_global_position(args, event)
|
position = shared.single_point_global_position(args, event)
|
||||||
rect = self.frame_geometry()
|
rect = self.frame_geometry()
|
||||||
self._press_edge = self.cursor_position(position, rect)
|
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.
|
# set this if we are pressing on the edge.
|
||||||
if self._press_edge != WindowEdge.NoEdge:
|
if self._press_edge != WindowEdge.NoEdge:
|
||||||
self._pressed = True
|
self._pressed = True
|
||||||
self._band.setGeometry(rect)
|
self._band.setGeometry(self.geometry())
|
||||||
|
|
||||||
def mouse_release(self, event):
|
def mouse_release(self, event):
|
||||||
'''Handle the mouseReleaseEvent of the window.'''
|
'''Handle the mouseReleaseEvent of the window.'''
|
||||||
|
|
||||||
if event.button() == compat.LeftButton:
|
if event.button() == compat.LeftButton and self._pressed:
|
||||||
self._pressed = False
|
self._pressed = False
|
||||||
|
|
||||||
def hover_move(self, event):
|
def hover_move(self, event):
|
||||||
|
@ -1385,8 +1494,7 @@ class SubWindow(QtWidgets.QMdiSubWindow):
|
||||||
'''Base subclass for a QMdiSubwindow.'''
|
'''Base subclass for a QMdiSubwindow.'''
|
||||||
|
|
||||||
def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)):
|
def __init__(self, parent=None, flags=QtCore.Qt.WindowType(0)):
|
||||||
super().__init__(parent)
|
super().__init__(parent, flags=flags)
|
||||||
self.setWindowFlags(self.windowFlags() | flags)
|
|
||||||
super().setWidget(QtWidgets.QWidget())
|
super().setWidget(QtWidgets.QWidget())
|
||||||
|
|
||||||
class DefaultSubWindow(SubWindow):
|
class DefaultSubWindow(SubWindow):
|
||||||
|
@ -1414,7 +1522,7 @@ class FramelessSubWindow(SubWindow):
|
||||||
# Create our widgets. Sizeframe and sizegrip are mutually exclusive.
|
# Create our widgets. Sizeframe and sizegrip are mutually exclusive.
|
||||||
self._central = QtWidgets.QFrame(super().widget())
|
self._central = QtWidgets.QFrame(super().widget())
|
||||||
self._central.setLayout(QtWidgets.QVBoxLayout())
|
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 = QtWidgets.QWidget(self._central)
|
||||||
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
||||||
self._sizeframe = None
|
self._sizeframe = None
|
||||||
|
@ -1494,6 +1602,10 @@ class FramelessSubWindow(SubWindow):
|
||||||
'''Move the window to the desired position'''
|
'''Move the window to the desired position'''
|
||||||
move_to(self, position)
|
move_to(self, position)
|
||||||
|
|
||||||
|
def set_geometry(self, rect):
|
||||||
|
'''Set the window geometry.'''
|
||||||
|
set_geometry(self, rect)
|
||||||
|
|
||||||
def set_minimum_size(self):
|
def set_minimum_size(self):
|
||||||
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
||||||
set_minimum_size(self)
|
set_minimum_size(self)
|
||||||
|
@ -1519,7 +1631,7 @@ class FramelessSubWindow(SubWindow):
|
||||||
if self._sizegrip is not None:
|
if self._sizegrip is not None:
|
||||||
self._sizegrip.hide()
|
self._sizegrip.hide()
|
||||||
self.set_larger_minimum_size()
|
self.set_larger_minimum_size()
|
||||||
self.setGeometry(rect)
|
self.set_geometry(rect)
|
||||||
|
|
||||||
def restore(self, rect):
|
def restore(self, rect):
|
||||||
'''Restore the window, showing the main widget and size grip.'''
|
'''Restore the window, showing the main widget and size grip.'''
|
||||||
|
@ -1528,7 +1640,7 @@ class FramelessSubWindow(SubWindow):
|
||||||
if self._sizegrip is not None:
|
if self._sizegrip is not None:
|
||||||
self._sizegrip.show()
|
self._sizegrip.show()
|
||||||
self.set_larger_minimum_size()
|
self.set_larger_minimum_size()
|
||||||
self.setGeometry(rect)
|
self.set_geometry(rect)
|
||||||
|
|
||||||
def shade(self, size):
|
def shade(self, size):
|
||||||
'''Shade the window, hiding the main widget and size grip.'''
|
'''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.
|
# Unused since we use the window flags anyway.
|
||||||
return self.maximumSize()
|
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
|
# ACTIONS
|
||||||
|
|
||||||
def set_cursor(self, cursor):
|
def menu_size_to(self, point):
|
||||||
'''Temporarily set the application cursor to the override cursor.'''
|
'''
|
||||||
|
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()
|
window = getattr(self, '_window_resize', None)
|
||||||
app.setOverrideCursor(QtGui.QCursor(cursor))
|
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):
|
# If we have a subwindow, need to limit to the MDI area rect.
|
||||||
'''Restore the overridden cursor.'''
|
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()
|
# Need to ensure we didn't go past the top left.
|
||||||
app.restoreOverrideCursor()
|
# 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):
|
def resolve_state(self):
|
||||||
'''Handle theoretically possible conflicts in window state.'''
|
'''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
|
# 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 next. We use `getattr(obj, attr, None)` for the window-level
|
||||||
# widgets since they might not be present (if using Wayland).
|
# 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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
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')
|
end_drag(self, 'subwindow')
|
||||||
|
has_state = True
|
||||||
|
|
||||||
def move_event(self, _, event, window_type):
|
def move_event(self, _, event, window_type):
|
||||||
'''Handle window move events.'''
|
'''Handle window move events.'''
|
||||||
|
@ -1830,23 +1959,24 @@ class Window(QtWidgets.QMainWindow):
|
||||||
elif event.type() == compat.MouseButtonPress:
|
elif event.type() == compat.MouseButtonPress:
|
||||||
end_move(self, window_type)
|
end_move(self, window_type)
|
||||||
|
|
||||||
def resize_event(self, _, event, window_type):
|
def resize_event(self, obj, event, window_type):
|
||||||
'''Handle window resize events.'''
|
'''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)
|
position = shared.single_point_global_position(args, event)
|
||||||
handle_resize(self, position, window_type)
|
handle_resize(self, position)
|
||||||
elif event.type() == compat.MouseButtonPress:
|
elif event.type() == compat.MouseButtonPress:
|
||||||
end_resize(self, window_type)
|
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.'''
|
'''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.
|
# No position for the event: we don't use it.
|
||||||
if event.type() in (compat.Enter, compat.HoverEnter):
|
if event.type() in (compat.Enter, compat.HoverEnter):
|
||||||
frame.enter(event)
|
frame.enter(event)
|
||||||
|
@ -1861,29 +1991,23 @@ class Window(QtWidgets.QMainWindow):
|
||||||
elif event.type() == compat.HoverMove:
|
elif event.type() == compat.HoverMove:
|
||||||
frame.hover_move(event)
|
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
|
# QT EVENTS
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
'''Custom event filter to handle move and resize events.'''
|
'''Custom event filter to handle move and resize events.'''
|
||||||
|
|
||||||
self.resolve_state()
|
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.
|
# Cannot occur while the size frame is active.
|
||||||
self.move_event(obj, event, 'window')
|
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')
|
self.resize_event(obj, event, 'window')
|
||||||
elif isinstance(obj, Window) and not obj.isMinimized():
|
elif isinstance(obj, Window) and not obj.isMinimized():
|
||||||
handle_frame(self, obj, event, 'window')
|
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.
|
# Cannot occur while the size frame is active.
|
||||||
self.move_event(obj, event, 'subwindow')
|
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')
|
self.resize_event(obj, event, 'subwindow')
|
||||||
elif isinstance(obj, SubWindow) and not obj.isMinimized():
|
elif isinstance(obj, SubWindow) and not obj.isMinimized():
|
||||||
handle_frame(self, obj, event, 'subwindow')
|
handle_frame(self, obj, event, 'subwindow')
|
||||||
|
@ -1932,7 +2056,7 @@ class FramelessWindow(Window):
|
||||||
self._central = QtWidgets.QFrame(self)
|
self._central = QtWidgets.QFrame(self)
|
||||||
self._layout = QtWidgets.QVBoxLayout(self._central)
|
self._layout = QtWidgets.QVBoxLayout(self._central)
|
||||||
self.setCentralWidget(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 = QtWidgets.QWidget(self._central)
|
||||||
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
self._widget.setLayout(QtWidgets.QVBoxLayout())
|
||||||
self._sizeframe = None
|
self._sizeframe = None
|
||||||
|
@ -1998,26 +2122,6 @@ class FramelessWindow(Window):
|
||||||
|
|
||||||
# PROPERTIES
|
# 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
|
@property
|
||||||
def border_size(self):
|
def border_size(self):
|
||||||
'''Get the size of the border, regardless if present.'''
|
'''Get the size of the border, regardless if present.'''
|
||||||
|
@ -2054,6 +2158,10 @@ class FramelessWindow(Window):
|
||||||
'''Move the window to the desired position'''
|
'''Move the window to the desired position'''
|
||||||
move_to(self, position)
|
move_to(self, position)
|
||||||
|
|
||||||
|
def set_geometry(self, rect):
|
||||||
|
'''Set the window geometry.'''
|
||||||
|
set_geometry(self, rect)
|
||||||
|
|
||||||
def set_minimum_size(self):
|
def set_minimum_size(self):
|
||||||
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
'''Sets the minimum size of the window and the titlebar, with clobbering.'''
|
||||||
set_minimum_size(self)
|
set_minimum_size(self)
|
||||||
|
@ -2072,9 +2180,11 @@ class FramelessWindow(Window):
|
||||||
|
|
||||||
def restore(self, _):
|
def restore(self, _):
|
||||||
'''Restore the window, showing the main widget and size grip.'''
|
'''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()
|
self.showNormal()
|
||||||
|
|
||||||
|
def showNormal(self):
|
||||||
|
super().showNormal()
|
||||||
|
|
||||||
def shade(self, size):
|
def shade(self, size):
|
||||||
'''Shade the window, hiding the main widget and size grip.'''
|
'''Shade the window, hiding the main widget and size grip.'''
|
||||||
shade(self, size, 'statusbar')
|
shade(self, size, 'statusbar')
|
||||||
|
@ -2093,7 +2203,7 @@ class FramelessWindow(Window):
|
||||||
window_resize_event(self, event)
|
window_resize_event(self, event)
|
||||||
|
|
||||||
def showEvent(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')
|
window_show_event(self, event, 'statusbar')
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event):
|
def mouseDoubleClickEvent(self, event):
|
||||||
|
@ -2112,13 +2222,36 @@ class FramelessWindow(Window):
|
||||||
'''End the drag event.'''
|
'''End the drag event.'''
|
||||||
return window_mouse_release_event(self, event, self, 'window')
|
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():
|
def main():
|
||||||
'Application entry point'
|
'Application entry point'
|
||||||
|
|
||||||
window_class = FramelessWindow
|
window_class = FramelessWindow
|
||||||
# Wayland does not allow windows to reposition themselves: therefore,
|
# Wayland does not allow windows to reposition themselves: therefore,
|
||||||
# we cannot use the custom titlebar at the application level.
|
# 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
|
window_class = DefaultWindow
|
||||||
app, window = shared.setup_app(args, unknown, compat, window_class=window_class)
|
app, window = shared.setup_app(args, unknown, compat, window_class=window_class)
|
||||||
app.installEventFilter(window)
|
app.installEventFilter(window)
|
||||||
|
|
Loading…
Reference in New Issue