Skip to content

Commit 22bb784

Browse files
committed
refactor(header): tighten module composition
The header directory had accumulated workarounds with unclear rationale and inline compositions that mixed layout with platform-specific concerns. Extracting the window buttons, toolbar button, and menu-bar item into their own internal components narrows each file's surface and makes the workarounds replaceable with principled bindings rather than carried forward as folklore. Host-integration detection is now toggleable via a constructor flag so the test harness can skip the async side effect that the header view indirectly pulled in.
1 parent 52a99c0 commit 22bb784

18 files changed

Lines changed: 459 additions & 281 deletions

mpvqc/services/host_integration/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

55
from .service import HostIntegrationService as HostIntegrationService
6-
from .service import WindowButtonPreference as WindowButtonPreference
6+
from .window_buttons import WindowButtonPreference as WindowButtonPreference

mpvqc/services/host_integration/service.py

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,47 @@
88
import os
99
import sys
1010
import typing
11-
from dataclasses import dataclass
1211
from functools import cached_property
1312

1413
import inject
15-
from PySide6.QtCore import QEvent, QObject, Signal
14+
from PySide6.QtCore import QEvent, QObject, QRunnable, QThreadPool, Signal
1615

1716
from mpvqc.services.main_window import MainWindowService
1817

19-
logger = logging.getLogger(__name__)
20-
18+
from .window_buttons import DEFAULT_WINDOW_BUTTON_PREFERENCE, WindowButtonPreference, read_window_button_preference
2119

22-
@dataclass(frozen=True)
23-
class WindowButtonPreference:
24-
minimize: bool
25-
maximize: bool
26-
close: bool
20+
logger = logging.getLogger(__name__)
2721

2822

2923
class HostIntegrationService(QObject):
3024
_main_window = inject.attr(MainWindowService)
3125

32-
DEFAULT_WINDOW_BUTTON_PREFERENCE = WindowButtonPreference(minimize=True, maximize=True, close=True)
33-
3426
display_zoom_factor_changed = Signal(float)
27+
window_button_preference_changed = Signal(object)
3528

36-
def __init__(self) -> None:
29+
def __init__(self, detect_configuration: bool = True) -> None:
3730
super().__init__()
38-
self._zoom_factor = self._main_window.display_zoom_factor
39-
self._window = self._main_window.window
40-
self._window.installEventFilter(self)
31+
self._zoom_factor = 1.0
32+
self._window_button_preference = DEFAULT_WINDOW_BUTTON_PREFERENCE
33+
34+
if detect_configuration:
35+
self._zoom_factor = self._main_window.display_zoom_factor
36+
self._window = self._main_window.window
37+
self._window.installEventFilter(self)
38+
self._detect_window_button_preference_async()
39+
40+
def _detect_window_button_preference_async(self) -> None:
41+
def job() -> None:
42+
preference = read_window_button_preference()
43+
if preference != self._window_button_preference:
44+
self._window_button_preference = preference
45+
self.window_button_preference_changed.emit(preference)
46+
47+
QThreadPool.globalInstance().start(QRunnable.create(job))
48+
49+
@property
50+
def window_button_preference(self) -> WindowButtonPreference:
51+
return self._window_button_preference
4152

4253
@typing.override
4354
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
@@ -60,11 +71,6 @@ def is_tiling_window_manager(self) -> bool:
6071
return False
6172
return is_tiling_window_manager()
6273

63-
def get_window_button_preference(self) -> WindowButtonPreference:
64-
if sys.platform == "linux":
65-
return read_linux_window_button_preference()
66-
return self.DEFAULT_WINDOW_BUTTON_PREFERENCE
67-
6874

6975
def is_tiling_window_manager() -> bool:
7076
tiling_wms = {
@@ -90,20 +96,3 @@ def is_tiling_window_manager() -> bool:
9096
logger.debug("Running on tiling window manager")
9197

9298
return is_tiling_wm
93-
94-
95-
def read_linux_window_button_preference() -> WindowButtonPreference:
96-
from mpvqc.services.host_integration.portals import SettingsPortal
97-
98-
with SettingsPortal() as portal:
99-
layout = portal.read_one("org.gnome.desktop.wm.preferences", "button-layout")
100-
101-
if layout is None:
102-
return WindowButtonPreference(minimize=True, maximize=True, close=True)
103-
104-
buttons = layout.lower()
105-
return WindowButtonPreference(
106-
minimize="minimize" in buttons,
107-
maximize="maximize" in buttons,
108-
close="close" in buttons,
109-
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SPDX-FileCopyrightText: mpvQC developers
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
from __future__ import annotations
6+
7+
import sys
8+
from dataclasses import dataclass
9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from typing import Final
13+
14+
15+
@dataclass(frozen=True)
16+
class WindowButtonPreference:
17+
minimize: bool
18+
maximize: bool
19+
close: bool
20+
21+
22+
DEFAULT_WINDOW_BUTTON_PREFERENCE: Final = WindowButtonPreference(minimize=True, maximize=True, close=True)
23+
24+
25+
def read_window_button_preference() -> WindowButtonPreference:
26+
if sys.platform == "linux":
27+
return read_linux_window_button_preference()
28+
return DEFAULT_WINDOW_BUTTON_PREFERENCE
29+
30+
31+
def read_linux_window_button_preference() -> WindowButtonPreference:
32+
from mpvqc.services.host_integration.portals import SettingsPortal
33+
34+
with SettingsPortal() as portal:
35+
layout = portal.read_one("org.gnome.desktop.wm.preferences", "button-layout")
36+
37+
if layout is None:
38+
return DEFAULT_WINDOW_BUTTON_PREFERENCE
39+
40+
buttons = layout.lower()
41+
return WindowButtonPreference(
42+
minimize="minimize" in buttons,
43+
maximize="maximize" in buttons,
44+
close="close" in buttons,
45+
)

mpvqc/utility/window_buttons.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

55
import inject
6-
from PySide6.QtCore import Property, QObject, QRunnable, QThreadPool, Signal
6+
from PySide6.QtCore import Property, QObject, Signal, Slot
77
from PySide6.QtQml import QmlElement
88

99
from mpvqc.services import HostIntegrationService, WindowButtonPreference
@@ -23,14 +23,8 @@ class MpvqcWindowButtons(QObject):
2323

2424
def __init__(self, parent: QObject | None = None) -> None:
2525
super().__init__(parent)
26-
self._preference: WindowButtonPreference = self._host_integration.DEFAULT_WINDOW_BUTTON_PREFERENCE
27-
28-
def _detection_job() -> None:
29-
preferences = self._host_integration.get_window_button_preference()
30-
self._on_detection_complete(preferences)
31-
32-
job = QRunnable.create(_detection_job)
33-
QThreadPool.globalInstance().start(job)
26+
self._preference = self._host_integration.window_button_preference
27+
self._host_integration.window_button_preference_changed.connect(self._on_preference_changed)
3428

3529
@Property(bool, notify=showMinimizeButtonChanged)
3630
def showMinimizeButton(self) -> bool:
@@ -44,15 +38,14 @@ def showMaximizeButton(self) -> bool:
4438
def showCloseButton(self) -> bool:
4539
return self._preference.close
4640

47-
def _on_detection_complete(self, new_preference: WindowButtonPreference) -> None:
48-
old_preference = self._preference
49-
self._preference = new_preference
50-
51-
if old_preference.minimize != new_preference.minimize:
52-
self.showMinimizeButtonChanged.emit(new_preference.minimize)
53-
54-
if old_preference.maximize != new_preference.maximize:
55-
self.showMaximizeButtonChanged.emit(new_preference.maximize)
56-
57-
if old_preference.close != new_preference.close:
58-
self.showCloseButtonChanged.emit(new_preference.close)
41+
@Slot(object)
42+
def _on_preference_changed(self, preference: WindowButtonPreference) -> None:
43+
old = self._preference
44+
self._preference = preference
45+
46+
if old.minimize != preference.minimize:
47+
self.showMinimizeButtonChanged.emit(preference.minimize)
48+
if old.maximize != preference.maximize:
49+
self.showMaximizeButtonChanged.emit(preference.maximize)
50+
if old.close != preference.close:
51+
self.showCloseButtonChanged.emit(preference.close)

mpvqc/viewmodels/header/header.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
#
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

5-
import sys
6-
75
import inject
86
from PySide6.QtCore import Property, QCoreApplication, QObject, Signal, Slot
97
from PySide6.QtQml import QmlElement
@@ -39,10 +37,6 @@ def __init__(self, parent: QObject | None = None) -> None:
3937
self._settings.languageChanged.connect(lambda _: self.windowTitleChanged.emit(self.windowTitle))
4038
self._state.saved_changed.connect(lambda _: self.windowTitleChanged.emit(self.windowTitle))
4139

42-
@Property(bool, constant=True, final=True)
43-
def isWindows(self) -> bool:
44-
return sys.platform == "win32"
45-
4640
@Property(str, notify=windowTitleChanged)
4741
def windowTitle(self) -> str:
4842
window_title_format = self._settings.window_title_format

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ files = [
155155
"mpvqc/services/host_integration/__init__.py",
156156
"mpvqc/services/host_integration/portals.py",
157157
"mpvqc/services/host_integration/service.py",
158+
"mpvqc/services/host_integration/window_buttons.py",
158159
"mpvqc/services/i18n.py",
159160
"mpvqc/services/importer.py",
160161
"mpvqc/services/key_command.py",
@@ -228,7 +229,6 @@ files = [
228229
"qt/qml/components/MpvqcKeyboardFocusableButtonBox.qml",
229230
"qt/qml/components/MpvqcLabelWithToolTip.qml",
230231
"qt/qml/components/MpvqcMenu.qml",
231-
"qt/qml/components/MpvqcMenuBarMenu.qml",
232232
"qt/qml/components/MpvqcMessageBox.qml",
233233
"qt/qml/components/MpvqcPositionedMenu.qml",
234234
"qt/qml/components/MpvqcSpinBoxRow.qml",
@@ -288,10 +288,15 @@ files = [
288288
"qt/qml/views/footer/MpvqcFooterView.qml",
289289
"qt/qml/views/footer/qmldir",
290290
"qt/qml/views/footer/tst_MpvqcFooterView.qml",
291-
"qt/qml/views/header/MpvqcHeaderMenuBar.qml",
292291
"qt/qml/views/header/MpvqcHeaderView.qml",
292+
"qt/qml/views/header/MpvqcHeaderWindowButtons.qml",
293+
"qt/qml/views/header/MpvqcMenuBar.qml",
294+
"qt/qml/views/header/MpvqcMenuBarItem.qml",
295+
"qt/qml/views/header/MpvqcMenuBarMenu.qml",
296+
"qt/qml/views/header/MpvqcToolBarButton.qml",
293297
"qt/qml/views/header/MpvqcToolBarView.qml",
294298
"qt/qml/views/header/qmldir",
299+
"qt/qml/views/header/tst_MpvqcHeaderWindowButtons.qml",
295300
"qt/qml/views/main/MpvqcContentKeyHandler.qml",
296301
"qt/qml/views/main/MpvqcContentView.qml",
297302
"qt/qml/views/main/MpvqcFileDropArea.qml",

qt/qml/components/qmldir

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ MpvqcDialog MpvqcDialog.qml
99
MpvqcHeader MpvqcHeader.qml
1010
MpvqcIconLabel MpvqcIconLabel.qml
1111
MpvqcMenu MpvqcMenu.qml
12-
MpvqcMenuBarMenu MpvqcMenuBarMenu.qml
1312
MpvqcMessageBox MpvqcMessageBox.qml
1413
MpvqcPositionedMenu MpvqcPositionedMenu.qml
1514
MpvqcSpinBoxRow MpvqcSpinBoxRow.qml

qt/qml/views/header/MpvqcHeaderView.qml

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ Item {
1818
required property MpvqcHeaderViewModel viewModel
1919
required property MpvqcMenuBarViewModel menuBarViewModel
2020

21-
readonly property MpvqcWindowButtons windowButtons: MpvqcWindowButtons {}
22-
2321
readonly property alias menuBarWidth: _menuBar.width
2422
readonly property alias menuBarHeight: _menuBar.height
2523

@@ -51,7 +49,7 @@ Item {
5149
width: root.width
5250
spacing: 0
5351

54-
MpvqcHeaderMenuBar {
52+
MpvqcMenuBar {
5553
id: _menuBar
5654

5755
viewModel: root.menuBarViewModel
@@ -75,7 +73,7 @@ Item {
7573
Item {
7674
id: _leftTitleSpacer
7775

78-
Layout.preferredWidth: Math.max(root.minTitleSpacing + 16, root.width / 2 - root.leftContentWidth - _title.implicitWidth / 2)
76+
Layout.preferredWidth: Math.max(root.minTitleSpacing + root.separatorMargin * 2, root.width / 2 - root.leftContentWidth - _title.implicitWidth / 2)
7977
Layout.preferredHeight: root.menuBarHeight
8078
}
8179

@@ -91,78 +89,12 @@ Item {
9189
verticalAlignment: Text.AlignVCenter
9290
}
9391

94-
Row {
95-
id: _windowButtons
96-
92+
MpvqcHeaderWindowButtons {
9793
Layout.preferredHeight: root.menuBarHeight
9894

99-
ToolButton {
100-
id: _minimizeButton
101-
102-
visible: root.windowButtons.showMinimizeButton
103-
height: root.menuBarHeight
104-
focusPolicy: Qt.NoFocus
105-
icon.width: 20
106-
icon.height: 20
107-
icon.source: "qrc:/data/icons/minimize_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg"
108-
109-
onClicked: {
110-
root.viewModel.requestMinimize();
111-
}
112-
}
113-
114-
ToolButton {
115-
id: _maximizeButton
116-
117-
readonly property url iconMaximize: "qrc:/data/icons/open_in_full_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg"
118-
readonly property url iconNormalize: "qrc:/data/icons/close_fullscreen_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg"
119-
120-
visible: root.windowButtons.showMaximizeButton
121-
height: root.menuBarHeight
122-
focusPolicy: Qt.NoFocus
123-
icon.width: 18
124-
icon.height: 18
125-
icon.source: MpvqcWindowUtility.isMaximized ? iconNormalize : iconMaximize
126-
127-
onClicked: {
128-
root.viewModel.requestToggleMaximize();
129-
}
130-
}
131-
132-
ToolButton {
133-
id: _closeButton
134-
135-
visible: root.windowButtons.showCloseButton
136-
height: root.menuBarHeight
137-
focusPolicy: Qt.NoFocus
138-
139-
icon {
140-
width: 18
141-
height: 18
142-
source: "qrc:/data/icons/close_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg"
143-
color: {
144-
if (root.viewModel.isWindows && _closeButton.hovered) {
145-
return "#FFFFFD";
146-
} else if (_closeButton.hovered) {
147-
return MpvqcTheme.background;
148-
} else {
149-
return MpvqcTheme.foreground;
150-
}
151-
}
152-
}
153-
154-
onClicked: {
155-
root.viewModel.requestClose();
156-
}
157-
158-
Binding {
159-
when: true
160-
target: _closeButton.background
161-
property: "color"
162-
value: root.viewModel.isWindows ? "#C42C1E" : MpvqcTheme.control
163-
restoreMode: Binding.RestoreNone
164-
}
165-
}
95+
onMinimizeRequested: root.viewModel.requestMinimize()
96+
onToggleMaximizeRequested: root.viewModel.requestToggleMaximize()
97+
onCloseRequested: root.viewModel.requestClose()
16698
}
16799
}
168100
}

0 commit comments

Comments
 (0)