235 lines
8.4 KiB
Python
235 lines
8.4 KiB
Python
"""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.image_studio import ImageStudioTab
|
|
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())
|
|
_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)
|
|
self.tabs.setTabIcon(TAB_TITLES.index("③ 更新蝦皮"), _warning_dot_icon())
|
|
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(),
|
|
)
|
|
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(),
|
|
)
|
|
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,
|
|
)
|
|
return ImageStudioTab(
|
|
db_path=self.db_path,
|
|
config=self.config,
|
|
config_path=self.config_path,
|
|
status_callback=self.show_status,
|
|
prompts_dir=appconfig.image_studio_prompts_dir(self.config),
|
|
)
|
|
|
|
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_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("⑤ 设置"))
|