diff options
Diffstat (limited to 'sb_builder/mainwindow.py')
| -rw-r--r-- | sb_builder/mainwindow.py | 174 |
1 files changed, 98 insertions, 76 deletions
diff --git a/sb_builder/mainwindow.py b/sb_builder/mainwindow.py index eeac61a..c6ea02e 100644 --- a/sb_builder/mainwindow.py +++ b/sb_builder/mainwindow.py @@ -21,26 +21,24 @@ import time from PyQt6.QtCore import QMetaType, QObject, QSize, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt6.QtDBus import QDBusArgument, QDBusConnection, QDBusInterface, QDBusMessage from PyQt6.QtGui import QIcon, QKeySequence, QShortcut -from PyQt6.QtWidgets import (QLabel, QMainWindow, QMessageBox, QPushButton, QStyle, QTabBar, - QTabWidget, QToolBar) +from PyQt6.QtWidgets import (QLabel, QMainWindow, QMessageBox, QPushButton, QStyle, QTabWidget, + QToolBar, QToolButton) from . import VERSION, config from .dashboard import Dashboard from .fmt import job_title from .hints_tab import HINT_JOBS, HintResultDialog, HintsTab -from .jobs import JobsTab +from .jobs import JobsTab, NextStep, is_preview from .packages import PackagesTab from .results import ResultsTab from .remote import DOWN, MISSING NOTIFY = "org.freedesktop.Notifications" NOTIFY_PATH = "/org/freedesktop/Notifications" -# ribbon entries: label, freedesktop icon name, Qt fallback icon -RIBBON = (("Dashboard", "go-home", QStyle.StandardPixmap.SP_DirHomeIcon), - ("Packages", "package-x-generic", QStyle.StandardPixmap.SP_FileDialogDetailedView), - ("Hints", "document-edit", QStyle.StandardPixmap.SP_FileDialogContentsView), - ("Results", "view-list-text", QStyle.StandardPixmap.SP_MessageBoxWarning), - ("Jobs", "system-run", QStyle.StandardPixmap.SP_MediaPlay)) +# Jobs tab icon: running, then the end status until the tab is shown +BADGE = {"running": ("system-run", QStyle.StandardPixmap.SP_MediaPlay), + "ok": ("emblem-ok", QStyle.StandardPixmap.SP_DialogApplyButton), + "bad": ("dialog-error", QStyle.StandardPixmap.SP_MessageBoxCritical)} # tabs as a distinct, centered row; colors from the palette so the theme applies TAB_STYLE = """ QTabWidget::tab-bar { alignment: center; } @@ -86,11 +84,10 @@ class MainWindow(QMainWindow): self.cfg, self.remote = cfg, remote self.notifier = notifier or Notifier(self) self.notifier.clicked.connect(self.open_job) - self.jobs_tab = None - self.packages_tab = None - self.results_tab = None - self.hints_tab = None self.hint_dialogs = [] # open HintResultDialogs, kept alive while shown + self.next_steps = {} # job id -> its open NextStep dialog + self.mine = set() # ids of the jobs this session started: only they get next steps + self.badge = None # Jobs tab icon state: None, "running", "ok", "bad" self.seen = None # job id -> status at the last poll; None before the first self.online = None # None until the first answer self.overlays = [] @@ -99,17 +96,29 @@ class MainWindow(QMainWindow): self.running_id = None self.setWindowTitle("sb-builder") - self.tabs = QTabWidget(tabsClosable=True) + self.tabs = QTabWidget() self.tabs.setStyleSheet(TAB_STYLE) - self.tabs.tabCloseRequested.connect(self._close_tab) - self.tabs.currentChanged.connect(lambda _i: self.poll_stats()) self.dashboard = Dashboard() - self.dashboard.refresh_btn.clicked.connect(self.poll_stats) + self.dashboard.refresh.triggered.connect(self.poll_stats) self.dashboard.tiles["Packages"].link(self.open_packages) self.dashboard.jobs.link(self.open_jobs) self.dashboard.result_clicked.connect(self.open_results) - self.tabs.addTab(self.dashboard, "Dashboard") - self.tabs.tabBar().setTabButton(0, QTabBar.ButtonPosition.RightSide, None) + self.jobs_tab = JobsTab(self.remote) + self.jobs_tab.refresh_requested.connect(self.poll_jobs) + self.jobs_tab.started.connect(self.mine.add) + self.jobs_tab.preview_requested.connect(self.show_next_step) + self.packages_tab = PackagesTab(self.remote) + self.packages_tab.start_requested.connect(lambda args: self.jobs_tab.start(*args)) + self.packages_tab.hints_requested.connect(self.open_hints) + self.hints_tab = HintsTab(self.remote, self.cfg) + self.hints_tab.start_requested.connect(lambda args: self.jobs_tab.start(*args)) + self.hints_tab.result_ready.connect(self.show_hint_result) + self.results_tab = ResultsTab(self.remote) + # permanent tabs (UI guidelines); each loads on first show + for tab, label in ((self.dashboard, "Dashboard"), (self.packages_tab, "Packages"), + (self.hints_tab, "Hints"), (self.results_tab, "Results"), + (self.jobs_tab, "Jobs")): + self.tabs.addTab(tab, label) self.setCentralWidget(self.tabs) file_menu = self.menuBar().addMenu("&File") @@ -118,17 +127,18 @@ class MainWindow(QMainWindow): # window-wide: QTabWidget's own Ctrl+PgUp/PgDn needs focus on the tabs self.tab_next = QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: self._cycle_tab(1)) self.tab_prev = QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: self._cycle_tab(-1)) - self.ribbon = QToolBar("Screens", movable=False, floatable=False) + # main ribbon: the current tab's actions; small ribbon under it: its view controls + self.ribbon = QToolBar("Actions", movable=False, floatable=False) self.ribbon.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon) self.ribbon.setIconSize(QSize(32, 32)) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.ribbon) - slots = {"Dashboard": lambda: self.tabs.setCurrentWidget(self.dashboard), - "Packages": self.open_packages, "Hints": lambda: self.open_hints(), - "Results": lambda: self.open_results(), - "Jobs": self.open_jobs} - for label, name, fallback in RIBBON: - icon = QIcon.fromTheme(name, self.style().standardIcon(fallback)) - self.ribbon.addAction(icon, label, slots[label]) + self.addToolBarBreak() + self.view_bar = QToolBar("View", movable=False, floatable=False) + self.view_bar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.view_bar.setIconSize(QSize(16, 16)) + self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.view_bar) + self.tabs.currentChanged.connect(self._tab_changed) + self._tab_changed() self.conn = QLabel("● connecting") self.key = QLabel() @@ -179,9 +189,12 @@ class MainWindow(QMainWindow): jobs = data["jobs"] self._notify_ends(jobs) self.dashboard.show_jobs(jobs) - if self.jobs_tab: - self.jobs_tab.set_jobs(jobs) + self.jobs_tab.set_jobs(jobs) run = next((j for j in jobs if j["status"] == "running"), None) + if run: + self._badge("running") + elif self.badge == "running": + self._badge(None) # ended unseen (first poll, reconnect): nothing to report self.running_id = run and run["id"] self.job_btn.setVisible(run is not None) if run: @@ -196,8 +209,7 @@ class MainWindow(QMainWindow): if err: return self._failed(err) self.overlays = data.get("overlays", []) - if self.jobs_tab: - self.jobs_tab.set_overlays(self.overlays) + self.jobs_tab.set_overlays(self.overlays) if data["version"] != VERSION: self.conn.setText(f"● helper {data['version']}, GUI {VERSION}: run ./install.sh --remote") @@ -285,24 +297,57 @@ class MainWindow(QMainWindow): event = END_EVENT.get(j["status"]) if event and prev.get(j["id"], "running") != j["status"]: self.notify(event, job_title(j), j["id"]) + if self.tabs.currentWidget() is not self.jobs_tab: + self._badge("ok" if j["status"] == "ok" else "bad") if j["kind"] in ("slackrepo", "publish"): # build state changed self._reload(self.packages_tab, self.results_tab) if j["kind"] in HINT_JOBS and j["status"] in ("ok", "failed"): self._reload(self.packages_tab, self.hints_tab) + if j["id"] not in self.mine: + continue # started elsewhere or before this session: news, not a next step + if j["kind"] in HINT_JOBS and j["status"] in ("ok", "failed"): self.remote.call(lambda d, err: d and self.show_hint_result(d), "job-result", j["id"]) + elif is_preview(j): + self.show_next_step(j, auto=True) @staticmethod def _reload(*tabs): for tab in tabs: - if tab: + if tab.loaded: # a tab not shown yet loads fresh when it is tab.reload() + def show_next_step(self, job, auto=False): + """The preview's next step as a dialog; auto: a shadowing preview without a plan shows none.""" + if job["id"] in self.next_steps: + self.next_steps[job["id"]].raise_() + return + d = NextStep(self.remote, job, self.cfg["builds"]["publish"], self) + d.run.connect(lambda args: (self.jobs_tab.start_signed if args[0] == "slackrepo" + else self.jobs_tab.start)(*args)) + d.finished.connect(lambda _r: self.next_steps.pop(job["id"], None)) + self.next_steps[job["id"]] = d + if d.shadow and auto: + d.loaded.connect(lambda plan: d.show() if plan else d.reject()) + else: + d.show() + + def _badge(self, state): + self.badge = state + i = self.tabs.indexOf(self.jobs_tab) + # in the text, not a color: TAB_STYLE's tab color would override setTabTextColor + self.tabs.setTabText(i, "Jobs (running)" if state == "running" else "Jobs") + if state is None: + self.tabs.setTabIcon(i, QIcon()) + return + name, fallback = BADGE[state] + self.tabs.setTabIcon(i, QIcon.fromTheme(name, self.style().standardIcon(fallback))) + def show_hint_result(self, result): if not result["ok"] and not result["failed"]: return # nothing changed, nothing failed: no news d = HintResultDialog(self.remote, result, self) d.build_requested.connect( - lambda pkgs: self.open_jobs().start("slackrepo", "update", "--preview", *pkgs)) + lambda pkgs: self.jobs_tab.start("slackrepo", "update", "--preview", *pkgs)) d.changed.connect(lambda: self._reload(self.packages_tab, self.hints_tab)) d.finished.connect(lambda _r: self.hint_dialogs.remove(d)) self.hint_dialogs.append(d) @@ -310,49 +355,42 @@ class MainWindow(QMainWindow): # ── tabs ──────────────────────────────────────────────────────────────── - def open_jobs(self): - if self.jobs_tab is None: - self.jobs_tab = JobsTab(self.remote, self.cfg["builds"]["publish"]) - self.jobs_tab.refresh_requested.connect(self.poll_jobs) - self.jobs_tab.set_overlays(self.overlays) - self.tabs.addTab(self.jobs_tab, "Jobs") - self.poll_jobs() - self.tabs.setCurrentWidget(self.jobs_tab) - return self.jobs_tab + def _tab_changed(self, _i=None): + tab = self.tabs.currentWidget() + for bar, acts in ((self.ribbon, tab.actions), (self.view_bar, tab.view_actions)): + bar.clear() + bar.addActions(acts) + for a in acts: + if a.menu(): # a menu action opens its menu on a plain click + bar.widgetForAction(a).setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) + self.view_bar.setVisible(bool(tab.view_actions)) + if not getattr(tab, "loaded", True): + tab.reload() + if tab is self.jobs_tab and self.badge != "running": + self._badge(None) + self.poll_stats() def _cycle_tab(self, step): self.tabs.setCurrentIndex((self.tabs.currentIndex() + step) % self.tabs.count()) + def open_jobs(self): + self.tabs.setCurrentWidget(self.jobs_tab) + return self.jobs_tab + def open_packages(self): - if self.packages_tab is None: - self.packages_tab = PackagesTab(self.remote) - self.packages_tab.start_requested.connect(lambda args: self.open_jobs().start(*args)) - self.packages_tab.hints_requested.connect(self.open_hints) - self.tabs.addTab(self.packages_tab, "Packages") - self.packages_tab.reload() self.tabs.setCurrentWidget(self.packages_tab) return self.packages_tab def open_hints(self, name=None): - if self.hints_tab is None: - self.hints_tab = HintsTab(self.remote, self.cfg) - self.hints_tab.start_requested.connect(lambda args: self.open_jobs().start(*args)) - self.hints_tab.result_ready.connect(self.show_hint_result) - self.tabs.addTab(self.hints_tab, "Hints") - self.hints_tab.reload() + self.tabs.setCurrentWidget(self.hints_tab) if name: self.hints_tab.select(name) - self.tabs.setCurrentWidget(self.hints_tab) return self.hints_tab def open_results(self, result=None): - if self.results_tab is None: - self.results_tab = ResultsTab(self.remote) - self.tabs.addTab(self.results_tab, "Results") - self.results_tab.reload() + self.tabs.setCurrentWidget(self.results_tab) if result: self.results_tab.filter_to(result) - self.tabs.setCurrentWidget(self.results_tab) return self.results_tab def open_job(self, jid): @@ -362,22 +400,6 @@ class MainWindow(QMainWindow): if jid: self.open_jobs().select(jid) - def _close_tab(self, i): - w = self.tabs.widget(i) - if w is self.dashboard: - return - self.tabs.removeTab(i) - if w is self.jobs_tab: - self.jobs_tab = None - w.stop() - if w is self.packages_tab: - self.packages_tab = None - if w is self.results_tab: - self.results_tab = None - if w is self.hints_tab: - self.hints_tab = None - w.deleteLater() - def reload_config(self): cfg, err = config.load() if err: |
