Feature/fsg gamma team ui fix #3
@@ -1013,6 +1013,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.help_agent.apply_theme() # chat body follows theme (header stays fixed)
|
self.help_agent.apply_theme() # chat body follows theme (header stays fixed)
|
||||||
|
|
||||||
# ---- sizing ------------------------------------------------------
|
# ---- sizing ------------------------------------------------------
|
||||||
|
# Share of the available screen the window takes when it has room to. Fixed
|
||||||
|
# pixels do not travel: 1180×760 fills a laptop and looks lost on a 4K
|
||||||
|
# panel. `want_*` stays the floor so a small screen behaves as before.
|
||||||
|
_SCREEN_SHARE_W, _SCREEN_SHARE_H = 0.80, 0.85
|
||||||
|
|
||||||
def _fit_to_screen(self, want_w: int, want_h: int) -> None:
|
def _fit_to_screen(self, want_w: int, want_h: int) -> None:
|
||||||
screen = self.screen() or QGuiApplication.primaryScreen()
|
screen = self.screen() or QGuiApplication.primaryScreen()
|
||||||
avail = screen.availableGeometry() if screen else None
|
avail = screen.availableGeometry() if screen else None
|
||||||
@@ -1020,8 +1025,12 @@ class MainWindow(QMainWindow):
|
|||||||
self.resize(want_w, want_h)
|
self.resize(want_w, want_h)
|
||||||
return
|
return
|
||||||
margin = 60
|
margin = 60
|
||||||
w = min(want_w, avail.width() - margin)
|
# Take a share of the screen, never less than the asked-for size and
|
||||||
h = min(want_h, avail.height() - margin)
|
# never more than the screen can show.
|
||||||
|
w = min(max(want_w, int(avail.width() * self._SCREEN_SHARE_W)),
|
||||||
|
avail.width() - margin)
|
||||||
|
h = min(max(want_h, int(avail.height() * self._SCREEN_SHARE_H)),
|
||||||
|
avail.height() - margin)
|
||||||
# minimum must never exceed what the screen can show
|
# minimum must never exceed what the screen can show
|
||||||
self.setMinimumSize(min(820, avail.width() - margin), min(520, avail.height() - margin))
|
self.setMinimumSize(min(820, avail.width() - margin), min(520, avail.height() - margin))
|
||||||
self.resize(max(w, 1), max(h, 1))
|
self.resize(max(w, 1), max(h, 1))
|
||||||
@@ -1029,6 +1038,25 @@ class MainWindow(QMainWindow):
|
|||||||
frame.moveCenter(avail.center())
|
frame.moveCenter(avail.center())
|
||||||
self.move(frame.topLeft())
|
self.move(frame.topLeft())
|
||||||
|
|
||||||
|
def moveEvent(self, event): # noqa: N802 - Qt override
|
||||||
|
super().moveEvent(event)
|
||||||
|
# Dragged to another monitor: its work area (and scaling) may differ, so
|
||||||
|
# the floating assistant re-pins and the panes re-decide if they fit.
|
||||||
|
self._on_screen_maybe_changed()
|
||||||
|
|
||||||
|
def _on_screen_maybe_changed(self) -> None:
|
||||||
|
screen = self.screen()
|
||||||
|
if screen is getattr(self, "_last_screen", None):
|
||||||
|
return
|
||||||
|
self._last_screen = screen
|
||||||
|
avail = screen.availableGeometry() if screen else None
|
||||||
|
if avail is not None:
|
||||||
|
self.setMinimumSize(min(820, avail.width() - 60),
|
||||||
|
min(520, avail.height() - 60))
|
||||||
|
if getattr(self, "help_agent", None) is not None:
|
||||||
|
self._update_dock_guard()
|
||||||
|
self.help_agent.reposition()
|
||||||
|
|
||||||
# ---- lifecycle ---------------------------------------------------
|
# ---- lifecycle ---------------------------------------------------
|
||||||
def closeEvent(self, event) -> None: # noqa: N802
|
def closeEvent(self, event) -> None: # noqa: N802
|
||||||
keep = (self.tray is not None
|
keep = (self.tray is not None
|
||||||
|
|||||||
+8
-1
@@ -191,4 +191,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -199,4 +199,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -353,4 +353,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -127,4 +127,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -191,4 +191,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Does the layout adapt across screen sizes AND display scalings?
|
||||||
|
|
||||||
|
Two things change between machines, and only one of them is width:
|
||||||
|
|
||||||
|
* the screen is bigger or smaller — more or fewer pixels to lay out in;
|
||||||
|
* the display scale is 100/125/150% — the SAME number of logical pixels
|
||||||
|
holds less, because every label and margin is taller.
|
||||||
|
|
||||||
|
A breakpoint written as a raw pixel number only holds on the machine it was
|
||||||
|
tuned on. This walks a grid of (window size × font scale) and, for each cell,
|
||||||
|
checks that no screen is clipped and that the panes folded when they had to.
|
||||||
|
|
||||||
|
Run: python tools/check_multi_screen.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(REPO.parent))
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # noqa: E402
|
||||||
|
|
||||||
|
# Real-world panels, from a small laptop up to 4K-at-150%-effective.
|
||||||
|
SIZES = [(1280, 720), (1366, 768), (1600, 900), (1920, 1080), (2560, 1440)]
|
||||||
|
# 9pt ≈ 100%, 11pt ≈ 125%, 14pt ≈ 150% of the design baseline.
|
||||||
|
POINTS = [9, 11, 14]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
sandbox = _isolate_home()
|
||||||
|
from PySide6.QtGui import QFont
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
_load_fonts()
|
||||||
|
_freeze_schedulers()
|
||||||
|
|
||||||
|
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||||
|
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||||
|
|
||||||
|
from seed_demo_data import seed
|
||||||
|
seed()
|
||||||
|
|
||||||
|
from cowork_local.app import MainWindow
|
||||||
|
from cowork_local.i18n import set_language
|
||||||
|
from cowork_local.state import AppContext
|
||||||
|
from cowork_local.ui.widgets import ui_scale
|
||||||
|
|
||||||
|
set_language("vi")
|
||||||
|
fails: list[str] = []
|
||||||
|
print(f"{'co chu':>7} {'cua so':>11} {'thang do':>9} {'man bi bo':>10} panel da gap")
|
||||||
|
print("-" * 86)
|
||||||
|
|
||||||
|
for pt in POINTS:
|
||||||
|
f = QFont(app.font())
|
||||||
|
f.setPointSize(pt)
|
||||||
|
app.setFont(f)
|
||||||
|
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
|
||||||
|
win.show()
|
||||||
|
for _ in range(6):
|
||||||
|
app.processEvents()
|
||||||
|
ws = win.workspace
|
||||||
|
dests = [("Project", win._ROW_WORKSPACE, ws._project_tab_idx),
|
||||||
|
("Cowork", win._ROW_WORKSPACE, ws._cowork_tab_idx),
|
||||||
|
("Co4E", win._ROW_WORKSPACE, ws._co4e_tab_idx),
|
||||||
|
("Folder", win._ROW_WORKSPACE, ws._folder_tab_idx),
|
||||||
|
("GraphRAG", win._ROW_WORKSPACE, ws._graphrag_tab_idx),
|
||||||
|
("Schedule", win._ROW_SCHEDULE, None),
|
||||||
|
("Dashboard", win._ROW_DASHBOARD, None),
|
||||||
|
("Monitoring", win._ROW_MONITORING, None)]
|
||||||
|
|
||||||
|
for w, h in SIZES:
|
||||||
|
win.resize(w, h)
|
||||||
|
for _ in range(6):
|
||||||
|
app.processEvents()
|
||||||
|
clipped = []
|
||||||
|
for name, page, sub in dests:
|
||||||
|
win._goto(page, sub)
|
||||||
|
for _ in range(4):
|
||||||
|
app.processEvents()
|
||||||
|
widget = win._page_widgets[page]
|
||||||
|
if widget.minimumSizeHint().width() > widget.width() + 1:
|
||||||
|
clipped.append(name)
|
||||||
|
folded = []
|
||||||
|
if getattr(ws, "_is_narrow", False):
|
||||||
|
folded.append("pane Project/History")
|
||||||
|
import cowork_local.ui.co4e_tab as co4e_mod
|
||||||
|
c4 = win.findChildren(co4e_mod.Co4ETab)[0]
|
||||||
|
if c4._config_collapsed:
|
||||||
|
folded.append("panel cau hinh Co4E")
|
||||||
|
scale = ui_scale(win)
|
||||||
|
print(f"{pt:>5}pt {w:>5}x{h:<5} {scale:>8.2f} "
|
||||||
|
f"{(', '.join(clipped) or 'khong'):>10} {', '.join(folded) or '-'}")
|
||||||
|
if clipped:
|
||||||
|
fails.append(f"{pt}pt {w}x{h}: bi bo — {clipped}")
|
||||||
|
# The window must never demand more than the smallest panel we support.
|
||||||
|
need = win.minimumSizeHint().width()
|
||||||
|
if need > SIZES[0][0]:
|
||||||
|
fails.append(f"{pt}pt: cua so doi toi thieu {need}px, "
|
||||||
|
f"rong hon man nho nhat ({SIZES[0][0]}px)")
|
||||||
|
print(f"{'':>7} {'':>11} {'':>9} cua so doi toi thieu: {need}px")
|
||||||
|
win.close()
|
||||||
|
del win
|
||||||
|
for _ in range(3):
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
print()
|
||||||
|
if fails:
|
||||||
|
print("*** KHONG THICH UNG DUOC ***")
|
||||||
|
for x in fails:
|
||||||
|
print(" " + x)
|
||||||
|
return 1
|
||||||
|
print("KET QUA: bo cuc thich ung o moi co man hinh va muc phong chu da thu")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
+8
-1
@@ -298,4 +298,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
@@ -113,4 +113,11 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
_rc = main()
|
||||||
|
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||||
|
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||||
|
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||||
|
# caught its mutation. Leave immediately with the verdict instead.
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stderr.flush()
|
||||||
|
os._exit(_rc)
|
||||||
|
|||||||
+1
-1
@@ -966,7 +966,7 @@ class Co4ETab(QWidget):
|
|||||||
super().showEvent(e)
|
super().showEvent(e)
|
||||||
self._narrow_guard.attach()
|
self._narrow_guard.attach()
|
||||||
|
|
||||||
def _apply_narrow_layout(self, narrow: bool) -> None:
|
def _apply_narrow_layout(self, narrow: bool) -> None: # noqa: D401
|
||||||
"""Fold the step-config panel on a narrow window, restore it when there
|
"""Fold the step-config panel on a narrow window, restore it when there
|
||||||
is room again.
|
is room again.
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -16,8 +16,8 @@ from PySide6.QtCore import Qt, Signal
|
|||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
||||||
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
|
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
|
||||||
QPlainTextEdit, QPushButton, QScrollArea, QStackedWidget, QTabBar,
|
QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget,
|
||||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
QTabBar, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..core import tasks as taskrepo
|
from ..core import tasks as taskrepo
|
||||||
@@ -89,6 +89,13 @@ class ScheduleTaskTab(QWidget):
|
|||||||
self._title.setStyleSheet("font-weight:700; font-size:15px;")
|
self._title.setStyleSheet("font-weight:700; font-size:15px;")
|
||||||
self.counts_lbl = QLabel("")
|
self.counts_lbl = QLabel("")
|
||||||
self.counts_lbl.setObjectName("hint")
|
self.counts_lbl.setObjectName("hint")
|
||||||
|
# A one-line summary of every lane's count. Left to size itself it
|
||||||
|
# reported a sizeHint wide enough to set the MINIMUM width of the whole
|
||||||
|
# screen — 1285px at 150% scaling, which then became the window's
|
||||||
|
# minimum and stopped the app fitting a 1280px laptop. It is a summary,
|
||||||
|
# and the same numbers are on each lane header, so it gives way first.
|
||||||
|
self.counts_lbl.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
|
||||||
|
self.counts_lbl.setMinimumWidth(0)
|
||||||
self.add_btn = QPushButton()
|
self.add_btn = QPushButton()
|
||||||
self.add_btn.setIcon(icon("plus"))
|
self.add_btn.setIcon(icon("plus"))
|
||||||
self.add_btn.setObjectName("primary")
|
self.add_btn.setObjectName("primary")
|
||||||
@@ -241,8 +248,10 @@ class ScheduleTaskTab(QWidget):
|
|||||||
empty = QListWidgetItem(tr("schedtask.no_tasks"))
|
empty = QListWidgetItem(tr("schedtask.no_tasks"))
|
||||||
empty.setFlags(Qt.NoItemFlags)
|
empty.setFlags(Qt.NoItemFlags)
|
||||||
col.addItem(empty)
|
col.addItem(empty)
|
||||||
self.counts_lbl.setText(" ".join(
|
summary = " ".join(
|
||||||
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s]))
|
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s])
|
||||||
|
self.counts_lbl.setText(summary)
|
||||||
|
self.counts_lbl.setToolTip(summary) # full text stays reachable if clipped
|
||||||
self.calendar.set_tasks(all_tasks)
|
self.calendar.set_tasks(all_tasks)
|
||||||
|
|
||||||
# ---- actions --------------------------------------------------------
|
# ---- actions --------------------------------------------------------
|
||||||
|
|||||||
+23
-1
@@ -159,6 +159,21 @@ def guard_wheel(root: QWidget) -> None:
|
|||||||
w.installEventFilter(_wheel_guard)
|
w.installEventFilter(_wheel_guard)
|
||||||
|
|
||||||
|
|
||||||
|
def ui_scale(widget: QWidget) -> float:
|
||||||
|
"""How much bigger this machine draws things than the design baseline.
|
||||||
|
|
||||||
|
Breakpoints written as raw pixels only hold on the screen they were tuned
|
||||||
|
on. At 125%/150% display scaling Qt still reports logical pixels, but every
|
||||||
|
label, button and margin is taller — so the same layout needs MORE logical
|
||||||
|
width before it stops being cramped. Font height is the honest proxy for
|
||||||
|
that: it moves with the display scale and with a user's font-size choice,
|
||||||
|
both of which change how much fits.
|
||||||
|
|
||||||
|
1.0 at the 15px line height the layouts were measured against.
|
||||||
|
"""
|
||||||
|
return max(0.75, min(2.5, widget.fontMetrics().height() / 15.0))
|
||||||
|
|
||||||
|
|
||||||
class _NarrowGuard(QObject):
|
class _NarrowGuard(QObject):
|
||||||
"""Calls back when the WINDOW crosses a width threshold.
|
"""Calls back when the WINDOW crosses a width threshold.
|
||||||
|
|
||||||
@@ -184,6 +199,11 @@ class _NarrowGuard(QObject):
|
|||||||
if win is not None and win is not self._owner and win is not self._window:
|
if win is not None and win is not self._owner and win is not self._window:
|
||||||
win.installEventFilter(self)
|
win.installEventFilter(self)
|
||||||
self._window = win
|
self._window = win
|
||||||
|
# Dragging the window to a monitor with different scaling changes
|
||||||
|
# how much fits without changing its width, so re-decide then too.
|
||||||
|
handle = win.windowHandle()
|
||||||
|
if handle is not None:
|
||||||
|
handle.screenChanged.connect(lambda *_a: self.check())
|
||||||
self.check()
|
self.check()
|
||||||
|
|
||||||
def eventFilter(self, obj, ev): # noqa: N802 - Qt override
|
def eventFilter(self, obj, ev): # noqa: N802 - Qt override
|
||||||
@@ -194,7 +214,9 @@ class _NarrowGuard(QObject):
|
|||||||
def check(self) -> None:
|
def check(self) -> None:
|
||||||
win = self._owner.window()
|
win = self._owner.window()
|
||||||
width = win.width() if win is not None else self._owner.width()
|
width = win.width() if win is not None else self._owner.width()
|
||||||
narrow = width < self._threshold
|
# The threshold is written for the baseline scale and grows with the
|
||||||
|
# machine's — see ui_scale().
|
||||||
|
narrow = width < self._threshold * ui_scale(self._owner)
|
||||||
if narrow == self._auto:
|
if narrow == self._auto:
|
||||||
return
|
return
|
||||||
self._auto = narrow
|
self._auto = narrow
|
||||||
|
|||||||
Reference in New Issue
Block a user