"""Main GUI window.""" from __future__ import annotations from ..version import display_name from .tabs.accounts import AccountsTab from .tabs.apply import ApplyTab from .tabs.collect import CollectTab from .tabs.generate import GenerateTab from .tabs.product_suite import ProductSuiteTab from .tabs.settings import SettingsTab from .widgets import * PREFERRED_WINDOW_SIZE = (1180, 760) MIN_WINDOW_SIZE = (960, 640) WINDOW_SCREEN_MARGIN = 40 def _screen_available_geometry(): app = QApplication.instance() if app is None: return None screen = app.primaryScreen() if screen is None: return None return screen.availableGeometry() def _bounded_dimension(available_size, preferred_size, minimum_size, margin): if available_size <= 0: return preferred_size if available_size > margin * 2: maximum = available_size - margin * 2 else: maximum = available_size if maximum >= minimum_size: return max(minimum_size, min(preferred_size, maximum)) return max(1, min(preferred_size, maximum)) def _fit_and_center_window( window, available_geometry=None, preferred_size=PREFERRED_WINDOW_SIZE, minimum_size=MIN_WINDOW_SIZE, margin=WINDOW_SCREEN_MARGIN, ): available = available_geometry if available_geometry is not None else _screen_available_geometry() if available is None or available.width() <= 0 or available.height() <= 0: window.resize(*preferred_size) return width = _bounded_dimension( available.width(), preferred_size[0], minimum_size[0], margin, ) height = _bounded_dimension( available.height(), preferred_size[1], minimum_size[1], margin, ) window.resize(width, height) x = available.x() + max(0, (available.width() - width) // 2) y = available.y() + max(0, (available.height() - height) // 2) max_x = available.x() + max(0, available.width() - width) max_y = available.y() + max(0, available.height() - height) window.move(min(max(x, available.x()), max_x), min(max(y, available.y()), max_y)) class MainWindow(QMainWindow): """Main application window with the fixed workflow tabs.""" def __init__( self, db_path=None, config=None, config_path=None, ai_models_path=None, startup_status="", ): super().__init__() self._initial_window_fit_applied_after_show = False self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config self.config_path = ( config_path or self.config.get("config_path") or appconfig.CONFIG_PATH ) self.db_path = _database_path(db_path, self.config) self.ai_models_path = ( ai_models_path or self.config.get("ai_models_path") or appconfig.ai_models_config_path(self.config) ) self.setWindowTitle(display_name()) window_icon = app_icon() if window_icon is not None: self.setWindowIcon(window_icon) _fit_and_center_window(self) self.setStyleSheet(BUTTON_BASE_STYLE) self._settings_tab_index = TAB_TITLES.index("设置") self._last_tab_index = 0 self._reverting_tab_change = False self.tabs = QTabWidget() self.tabs.setObjectName("mainTabs") self.tabs.setStyleSheet(TAB_STYLE) self.tabs.currentChanged.connect(self._on_tab_changed) for title in TAB_TITLES: self.tabs.addTab(self._build_tab(title), title) settings_tab = self._settings_tab() if hasattr(settings_tab, "settingsSaved"): settings_tab.settingsSaved.connect(self._on_settings_saved) self.setCentralWidget(self.tabs) self.show_status("就绪", level="muted") if startup_status: self.show_status(startup_status, level="success") def showEvent(self, event): super().showEvent(event) if not self._initial_window_fit_applied_after_show: _fit_and_center_window(self) self._initial_window_fit_applied_after_show = True def show_status(self, message, level="muted"): color = _status_level_color(level) self.statusBar().setStyleSheet(f"QStatusBar {{ color: {color}; }}") self.statusBar().showMessage(str(message)) def _build_tab(self, title): if title == "① 导入采集": return CollectTab( db_path=self.db_path, config=self.config, status_callback=self.show_status, open_accounts_callback=lambda: self.open_accounts_tab(), refresh_workflow_callback=lambda: self.refresh_task_tabs(), ) if title == "② AI生成": return GenerateTab( db_path=self.db_path, config=self.config, config_path=self.config_path, status_callback=self.show_status, title_prompt_path=appconfig.title_prompt_path(self.config), cover_prompts_dir=appconfig.cover_prompts_dir(self.config), open_accounts_callback=lambda: self.open_accounts_tab(), refresh_workflow_callback=lambda: self.refresh_task_tabs(), ) if title == "③ 更新蝦皮": return ApplyTab( db_path=self.db_path, config=self.config, status_callback=self.show_status, open_accounts_callback=lambda: self.open_accounts_tab(), open_settings_callback=lambda: self.open_settings_tab(), refresh_workflow_callback=lambda: self.refresh_task_tabs(), ) if title == "账号管理": return AccountsTab( db_path=self.db_path, config=self.config, status_callback=self.show_status, ) if title == "设置": return SettingsTab( config=self.config, config_path=self.config_path, ai_models_path=self.ai_models_path, status_callback=self.show_status, ) if title == "商品套图": return ProductSuiteTab( db_path=self.db_path, config=self.config, config_path=self.config_path, status_callback=self.show_status, ) raise ValueError(f"未知主界面模块:{title}") def refresh_task_tabs(self): for index in range(self.tabs.count()): widget = self.tabs.widget(index) if hasattr(widget, "refresh_tasks"): widget.refresh_tasks() def _on_settings_saved(self, backend): for index in range(self.tabs.count()): widget = self.tabs.widget(index) if hasattr(widget, "refresh_gateway_state"): widget.refresh_gateway_state() label = "自定义网关" if str(backend) == "direct" else "默认网关" self.show_status("设置已保存,当前使用%s" % label, level="success") def _on_tab_changed(self, index): if self._reverting_tab_change: self._last_tab_index = index self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted") return previous = self._last_tab_index if previous == self._settings_tab_index and index != previous: if not self._confirm_leave_settings_tab(): self._reverting_tab_change = True try: self.tabs.setCurrentIndex(previous) finally: self._reverting_tab_change = False self._last_tab_index = previous self.show_status(f"当前:{self.tabs.tabText(previous)}", level="muted") return self._last_tab_index = index self.show_status(f"当前:{self.tabs.tabText(index)}", level="muted") def _settings_tab(self): return self.tabs.widget(self._settings_tab_index) def _confirm_leave_settings_tab(self): settings_tab = self._settings_tab() if not hasattr(settings_tab, "is_dirty") or not settings_tab.is_dirty(): return True box = QMessageBox(self) box.setWindowTitle("未保存更改") box.setText("设置有未保存更改。要先保存再离开吗?") save_button = box.addButton("保存", QMessageBox.AcceptRole) discard_button = box.addButton("放弃", QMessageBox.DestructiveRole) box.addButton("取消", QMessageBox.RejectRole) box.setDefaultButton(save_button) box.exec() clicked = box.clickedButton() if clicked is save_button: return bool(settings_tab.save_app_settings()) if clicked is discard_button: return bool(settings_tab.discard_unsaved_changes()) return False def closeEvent(self, event): if self._confirm_leave_settings_tab(): event.accept() return event.ignore() def open_accounts_tab(self): self.tabs.setCurrentIndex(TAB_TITLES.index("账号管理")) def open_settings_tab(self): self.tabs.setCurrentIndex(TAB_TITLES.index("设置"))