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)
|
||||
|
||||
# ---- 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:
|
||||
screen = self.screen() or QGuiApplication.primaryScreen()
|
||||
avail = screen.availableGeometry() if screen else None
|
||||
@@ -1020,8 +1025,12 @@ class MainWindow(QMainWindow):
|
||||
self.resize(want_w, want_h)
|
||||
return
|
||||
margin = 60
|
||||
w = min(want_w, avail.width() - margin)
|
||||
h = min(want_h, avail.height() - margin)
|
||||
# Take a share of the screen, never less than the asked-for size and
|
||||
# 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
|
||||
self.setMinimumSize(min(820, avail.width() - margin), min(520, avail.height() - margin))
|
||||
self.resize(max(w, 1), max(h, 1))
|
||||
@@ -1029,6 +1038,25 @@ class MainWindow(QMainWindow):
|
||||
frame.moveCenter(avail.center())
|
||||
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 ---------------------------------------------------
|
||||
def closeEvent(self, event) -> None: # noqa: N802
|
||||
keep = (self.tray is not None
|
||||
|
||||
+8
-1
@@ -191,4 +191,11 @@ def main() -> int:
|
||||
|
||||
|
||||
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__":
|
||||
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__":
|
||||
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__":
|
||||
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__":
|
||||
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__":
|
||||
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__":
|
||||
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)
|
||||
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
|
||||
is room again.
|
||||
|
||||
|
||||
+13
-4
@@ -16,8 +16,8 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
||||
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QStackedWidget, QTabBar,
|
||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget,
|
||||
QTabBar, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core import tasks as taskrepo
|
||||
@@ -89,6 +89,13 @@ class ScheduleTaskTab(QWidget):
|
||||
self._title.setStyleSheet("font-weight:700; font-size:15px;")
|
||||
self.counts_lbl = QLabel("")
|
||||
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.setIcon(icon("plus"))
|
||||
self.add_btn.setObjectName("primary")
|
||||
@@ -241,8 +248,10 @@ class ScheduleTaskTab(QWidget):
|
||||
empty = QListWidgetItem(tr("schedtask.no_tasks"))
|
||||
empty.setFlags(Qt.NoItemFlags)
|
||||
col.addItem(empty)
|
||||
self.counts_lbl.setText(" ".join(
|
||||
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s]))
|
||||
summary = " ".join(
|
||||
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)
|
||||
|
||||
# ---- actions --------------------------------------------------------
|
||||
|
||||
+23
-1
@@ -159,6 +159,21 @@ def guard_wheel(root: QWidget) -> None:
|
||||
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):
|
||||
"""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:
|
||||
win.installEventFilter(self)
|
||||
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()
|
||||
|
||||
def eventFilter(self, obj, ev): # noqa: N802 - Qt override
|
||||
@@ -194,7 +214,9 @@ class _NarrowGuard(QObject):
|
||||
def check(self) -> None:
|
||||
win = self._owner.window()
|
||||
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:
|
||||
return
|
||||
self._auto = narrow
|
||||
|
||||
Reference in New Issue
Block a user