Skip to content

Commit ac7994c

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 ac7994c

21 files changed

Lines changed: 584 additions & 388 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

mpvqc/viewmodels/header/toolbar.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,13 @@
1717
class MpvqcToolBarViewModel(QObject):
1818
_player = inject.attr(PlayerService)
1919

20-
frameStepBackwardVisibleChanged = Signal(bool)
21-
frameStepForwardVisibleChanged = Signal(bool)
20+
frameStepVisibleChanged = Signal(bool)
2221
cycleSubtitleTrackVisibleChanged = Signal(bool)
2322
cycleAudioTrackVisibleChanged = Signal(bool)
2423

2524
def __init__(self, parent: QObject | None = None) -> None:
2625
super().__init__(parent)
27-
self._frame_step_backward_visible = self._player.video_loaded
28-
self._frame_step_forward_visible = self._player.video_loaded
26+
self._frame_step_visible = self._player.video_loaded
2927
self._cycle_subtitle_track_visible = self._should_show_cycle_subtitle()
3028
self._cycle_audio_track_visible = self._should_show_cycle_audio()
3129

@@ -44,9 +42,7 @@ def _should_show_cycle_subtitle(self) -> bool:
4442
@Slot(bool)
4543
def _on_video_loaded_changed(self, video_loaded: bool) -> None:
4644
# pyrefly: ignore [bad-assignment]
47-
self.frameStepBackwardVisible = video_loaded
48-
# pyrefly: ignore [bad-assignment]
49-
self.frameStepForwardVisible = video_loaded
45+
self.frameStepVisible = video_loaded
5046
# pyrefly: ignore [bad-assignment]
5147
self.cycleSubtitleTrackVisible = self._should_show_cycle_subtitle()
5248
# pyrefly: ignore [bad-assignment]
@@ -78,25 +74,15 @@ def requestCycleSubtitleTrack(self) -> None:
7874
def requestCycleAudioTrack(self) -> None:
7975
self._player.cycle_audio_track()
8076

81-
@Property(bool, notify=frameStepBackwardVisibleChanged)
82-
def frameStepBackwardVisible(self) -> bool:
83-
return self._frame_step_backward_visible
84-
85-
@frameStepBackwardVisible.setter
86-
def frameStepBackwardVisible(self, value: bool) -> None:
87-
if self._frame_step_backward_visible != value:
88-
self._frame_step_backward_visible = value
89-
self.frameStepBackwardVisibleChanged.emit(value)
90-
91-
@Property(bool, notify=frameStepForwardVisibleChanged)
92-
def frameStepForwardVisible(self) -> bool:
93-
return self._frame_step_forward_visible
94-
95-
@frameStepForwardVisible.setter
96-
def frameStepForwardVisible(self, value: bool) -> None:
97-
if self._frame_step_forward_visible != value:
98-
self._frame_step_forward_visible = value
99-
self.frameStepForwardVisibleChanged.emit(value)
77+
@Property(bool, notify=frameStepVisibleChanged)
78+
def frameStepVisible(self) -> bool:
79+
return self._frame_step_visible
80+
81+
@frameStepVisible.setter
82+
def frameStepVisible(self, value: bool) -> None:
83+
if self._frame_step_visible != value:
84+
self._frame_step_visible = value
85+
self.frameStepVisibleChanged.emit(value)
10086

10187
@Property(bool, notify=cycleSubtitleTrackVisibleChanged)
10288
def cycleSubtitleTrackVisible(self) -> bool:

pyproject.toml

Lines changed: 9 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,17 @@ 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/MpvqcLanguageSubMenu.qml",
294+
"qt/qml/views/header/MpvqcMenuBar.qml",
295+
"qt/qml/views/header/MpvqcMenuBarItem.qml",
296+
"qt/qml/views/header/MpvqcMenuBarMenu.qml",
297+
"qt/qml/views/header/MpvqcRadioMenu.qml",
298+
"qt/qml/views/header/MpvqcToolBarButton.qml",
293299
"qt/qml/views/header/MpvqcToolBarView.qml",
294300
"qt/qml/views/header/qmldir",
301+
"qt/qml/views/header/tst_MpvqcHeaderWindowButtons.qml",
295302
"qt/qml/views/main/MpvqcContentKeyHandler.qml",
296303
"qt/qml/views/main/MpvqcContentView.qml",
297304
"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

0 commit comments

Comments
 (0)