Source code for zooui.windows.mainwindow

## ZooUI - Zooming User Interface
## Copyright (C) 2009 David Roberts <d@vidr.cc>
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License
## as published by the Free Software Foundation; either version 3
## of the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <https://www.gnu.org/licenses/>.

"""ZooUI QMainWindow."""

import math
import os
import subprocess
from logging import Logger
from typing import Any

from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QPushButton, QVBoxLayout

import zooui as ZooUI
from zooui.config import ConfigManager, ValidationError
from zooui.logger import get_logger
from zooui.objects.mediaobjects.pdfmediaobject import PdfMediaObject
from zooui.objects.mediaobjects.stringmediaobject import StringMediaObject
from zooui.objects.mediaobjects.svgmediaobject import SVGMediaObject
from zooui.objects.mediaobjects.tiledmediaobject import TiledMediaObject
from zooui.objects.scene import scene as Scene
from zooui.objects.scene.qzui import QZUI
from zooui.objects.scene.qzui import QZUI as QZUIType
from zooui.utils._packaging import data_dir
from zooui.windows.dialogwindows.dialogwindows import DialogWindows

# Type aliases
ActionKey = str
MenuKey = str
MediaID = str


[docs] class MainWindow(QtWidgets.QMainWindow): """ Constructor : MainWindow(framerate, zoom_sensitivity) Parameters : framerate : int zoom_sensitivity : int MainWindow(framerate, zoom_sensitivity) --> None MainWindow windows are used for displaying the ZooUI interface. This class defines all the interface affordances, menus, widgets, frames etc. Framerate and zoom_sensitivity variables have to be declared the same value as in qzui class """
[docs] def __init__( self, framerate: int = 20, zoom_sensitivity: int = 50, icon: QtGui.QIcon | None = None, config: dict[str, Any] | None = None, autosave_config: dict[str, Any] | None = None, ) -> None: """ Create a new MainWindow. Args: framerate: Frame rate for the QZUI widget zoom_sensitivity: Zoom sensitivity for the QZUI widget icon: Optional QIcon to set as window icon config: Optional full application configuration dictionary autosave_config: Optional autosave configuration dictionary """ QtWidgets.QMainWindow.__init__(self) self.__logger: Logger = get_logger("MainWindow") self.__prev_dir: str = "" self.__action: dict[ActionKey, QtGui.QAction] = {} self.__menu: dict[MenuKey, QtWidgets.QMenu] = {} self.__config: dict[str, Any] = config or {} self.__autosave_config: dict[str, Any] | None = autosave_config self.setWindowTitle("ZooUI") # Set window icon if provided if icon and not icon.isNull(): self.setWindowIcon(icon) self.__logger.debug("Window icon set in MainWindow constructor") # Create QTabWidget as central widget for multi-tab support self.__tab_widget = QtWidgets.QTabWidget() self.__tab_widget.setTabsClosable(True) self.__tab_widget.setMovable(True) self.__tab_widget.setDocumentMode(True) self.setCentralWidget(self.__tab_widget) # Track QZUI instances per tab (parallel to tab indices) self.__zui_tabs: list[QZUIType] = [] self.__framerate = framerate self.__zoom_sensitivity = zoom_sensitivity # Connect tab signals self.__tab_widget.tabCloseRequested.connect(self._close_tab) self.__tab_widget.currentChanged.connect(self._on_tab_changed) self.__create_actions() self.__create_menus() # Create the first tab and load home scene self._add_tab("Home") self.__action_open_scene_home()
[docs] def sizeHint(self) -> QtCore.QSize: """ Method : MainWindow.sizeHint() Parameters : None MainWindow.sizeHint() --> QtCore.QSize Return the recommended size for the widget. """ return QtCore.QSize(1280, 720)
[docs] def minimumSizeHint(self) -> QtCore.QSize: """ Method : MainWindow.minimumSizeHint() Parameters : None MainWindow.minimumSizeHint() --> QtCore.QSize Return the minimum size hint for the widget. """ return QtCore.QSize(160, 120)
@property def current_zui(self) -> "QZUIType": """ Property : MainWindow.current_zui Parameters : None MainWindow.current_zui --> QZUI Return the QZUI instance of the currently active tab. """ index = self.__tab_widget.currentIndex() if index < 0 or index >= len(self.__zui_tabs): raise IndexError("No active tab") return self.__zui_tabs[index] # type: ignore[no-any-return] @property def zui(self) -> "QZUIType": """ Backward-compatible property returning the current tab's QZUI. """ return self.current_zui
[docs] def _add_tab(self, title: str = "Untitled") -> int: """ Method : MainWindow._add_tab(title) Parameters : title : str MainWindow._add_tab(title) --> int Create a new tab with a fresh QZUI and Scene. Returns the tab index. """ zui = QZUI(self, self.__framerate, self.__zoom_sensitivity, self.__config, self.__autosave_config) zui.error.connect(self.__show_error) zui.ocr_region_selected.connect(self.__handle_ocr_region) self.__zui_tabs.append(zui) index = self.__tab_widget.addTab(zui, title) self.__tab_widget.setCurrentIndex(index) zui.setFocus(QtCore.Qt.FocusReason.OtherFocusReason) return index # type: ignore[no-any-return]
[docs] def _close_tab(self, index: int) -> None: """ Method : MainWindow._close_tab(index) Parameters : index : int MainWindow._close_tab(index) --> None Close the tab at the given index. Prevents closing the last remaining tab. Stops autosave and purges tiles for the closed scene. """ if self.__tab_widget.count() <= 1: return zui = self.__zui_tabs[index] scene = zui.scene # Stop autosave on the closing scene if hasattr(scene, "_Scene__autosave_manager"): try: mgr = scene._Scene__autosave_manager if mgr._autosave_active: mgr.disable_autosave() except Exception: pass scene.shutdown_threads() # Purge tiles for the closing scene from zooui.tilesystem import tilemanager as TileManager TileManager.purge() self.__tab_widget.removeTab(index) del self.__zui_tabs[index]
[docs] def _on_tab_changed(self, index: int) -> None: """ Method : MainWindow._on_tab_changed(index) Parameters : index : int MainWindow._on_tab_changed(index) --> None Handle tab change: set focus on the new active QZUI and update window title. """ if index < 0 or index >= len(self.__zui_tabs): return zui = self.__zui_tabs[index] zui.setFocus(QtCore.Qt.FocusReason.OtherFocusReason) # Sync render order menu check state with current scene if "render_order_smaller_top" in self.__action: scene = zui.scene is_smaller_top = scene.render_order == "smaller_on_top" self.__action["render_order_smaller_top"].setChecked(is_smaller_top) self.__update_window_title()
def __update_window_title(self) -> None: """Update the window title to reflect the current tab.""" title = self.__tab_widget.tabText(self.__tab_widget.currentIndex()) self.setWindowTitle(f"ZooUI — {title}") def __show_error(self, text: str, details: Any) -> None: """ Method : MainWindow.__show_error(text, details) Parameters : text : str details : Any MainWindow.__show_error(text, details) --> None Show an error dialog with the given text and details. """ dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("ZooUI - Error") dialog.setText(text) dialog.setDetailedText(str(details)) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.exec() def __create_action( self, key: ActionKey, text: str, callback: Any | None = None, shortcut: str | None = None, checkable: bool = False, ) -> None: """ Method : MainWindow.__create_action(key, text, callback, shortcut, checkable) Parameters : key : str text : str callback : Optional[Any] shortcut : Optional[str] checkable : bool MainWindow.__create_action(key, text, callback, shortcut, checkable) --> None Create a QAction and store it in self.__action[key]. """ self.__action[key] = QtGui.QAction(text, self) if shortcut: self.__action[key].setShortcut(shortcut) if callback: self.__action[key].triggered.connect(callback) self.__action[key].setCheckable(checkable) def __action_new_scene(self) -> None: """ Method : MainWindow.__action_new_scene() Parameters : None MainWindow.__action_new_scene() --> None Create a new scene in the current tab. """ zui = self.current_zui # Stop autosave on current scene before creating new one if hasattr(zui.scene, "_Scene__autosave_manager"): try: logger = get_logger("MainWindow") logger.debug("Stopping autosave on current scene before creating new scene") zui.scene._Scene__autosave_manager.disable_autosave() except Exception as e: logger = get_logger("MainWindow") logger.debug(f"Error stopping autosave on current scene: {e}") zui.scene.shutdown_threads() zui.scene = Scene.new(config=self.__config) current_index = self.__tab_widget.currentIndex() self.__tab_widget.setTabText(current_index, "Untitled") self.__update_window_title() def __action_new_tab(self) -> None: """ Method : MainWindow.__action_new_tab() Parameters : None MainWindow.__action_new_tab() --> None Create a new tab with a fresh scene. """ self._add_tab("Untitled") def __action_close_tab(self) -> None: """ Method : MainWindow.__action_close_tab() Parameters : None MainWindow.__action_close_tab() --> None Close the current tab. """ self._close_tab(self.__tab_widget.currentIndex()) def __action_open_scene(self) -> None: """ Method : MainWindow.__action_open_scene() Parameters : None MainWindow.__action_open_scene() --> None Open a scene from the location chosen by the user in a file selection dialog into the current tab. """ filename = str( QtWidgets.QFileDialog.getOpenFileName(self, "Open scene", self.__prev_dir, "ZooUI Scenes (*.pzs)")[0] ) if filename: self.__prev_dir = os.path.dirname(filename) try: zui = self.current_zui # Stop autosave on current scene before opening new one if hasattr(zui.scene, "_Scene__autosave_manager"): try: logger = get_logger("MainWindow") logger.debug("Stopping autosave on current scene before opening new scene") zui.scene._Scene__autosave_manager.disable_autosave() except Exception as e: logger = get_logger("MainWindow") logger.debug(f"Error stopping autosave on current scene: {e}") zui.scene = Scene.load_scene(filename) current_index = self.__tab_widget.currentIndex() self.__tab_widget.setTabText(current_index, os.path.basename(filename)) self.__update_window_title() except Exception as e: self.__show_error("Unable to open scene ERROR in mainwindow.__action_open_scene \n", e) def __action_open_scene_home(self) -> None: """ Method : MainWindow.__action_open_scene_home() Parameters : None MainWindow.__action_open_scene_home() --> None Open the Home scene in the current tab. """ try: zui = self.current_zui zui.scene = Scene.load_scene(os.path.join(data_dir(), "data", "home.pzs")) current_index = self.__tab_widget.currentIndex() self.__tab_widget.setTabText(current_index, "Home") self.__update_window_title() except Exception as e: self.__show_error("Unable to open the Home scene, ERROR in mainwindow.__action_open_scene_home \n", str(e)) def __action_import_scene(self) -> None: """ Method : MainWindow.__action_import_scene() Parameters : None MainWindow.__action_import_scene() --> None Import a scene from the location chosen by the user in a file selection dialog, merging it into the current tab's scene. """ filename = str( QtWidgets.QFileDialog.getOpenFileName(self, "Import scene", self.__prev_dir, "ZooUI Scenes (*.pzs)")[0] ) if filename: self.__prev_dir = os.path.dirname(filename) try: self.current_zui.scene.import_scene(filename) except Exception as e: self.__show_error("Unable to import scene ERROR in mainwindow.__action_import_scene \n", e) def __action_save_scene(self) -> None: """ Method : MainWindow.__action_save_scene() Parameters : None MainWindow.__action_save_scene() --> None Save the scene or selected mediaobjects to the location chosen by the user in a file selection dialog. If mediaobjects are selected, saves only the selection with "0 0 0" as the first line in the PZS file, preserving relative positions. If no selection, saves the entire scene. """ scene = self.current_zui.scene # Determine if we have a valid selection has_selection = False if scene.selection: if isinstance(scene.selection, list): has_selection = len(scene.selection) > 0 else: has_selection = True # Single object is selected # Choose appropriate dialog title and default filename if has_selection: dialog_title = "Save Selection" default_name = "selection.pzs" else: dialog_title = "Save Scene" default_name = "scene.pzs" filename = str( QtWidgets.QFileDialog.getSaveFileName( self, dialog_title, os.path.join(self.__prev_dir, default_name), "ZooUI Scenes (*.pzs)" )[0] ) if filename: self.__prev_dir = os.path.dirname(filename) try: if has_selection: scene.save_selection(filename) else: scene.save(filename) current_index = self.__tab_widget.currentIndex() self.__tab_widget.setTabText(current_index, os.path.basename(filename)) self.__update_window_title() except Exception as e: self.__show_error("Unable to save scene ERROR in mainwindow.__action_save_scene \n", e) def __action_save_screenshot(self) -> None: """ Method : MainWindow.__action_save_screenshot() Parameters : None MainWindow.__action_save_screenshot() --> None Save a screenshot to the location chosen by the user in a file selection dialog. """ filename = QtWidgets.QFileDialog.getSaveFileName( self, "Save screenshot", os.path.join(self.__prev_dir, "screenshot.png"), "Images (*.bmp *.jpg *.jpeg *.png *.ppm *.tif *.tiff *.xbm *.xpm)", ) if filename: self.__prev_dir = os.path.dirname(filename[0]) try: pixmap = self.current_zui.grab() pixmap.save(filename[0]) except Exception as e: self.__show_error("Unable to save screenshot ERROR in mainwindow.__action_save_screenshot", e) def __action_ocr_screenshot(self) -> None: zui = self.current_zui zui.set_ocr_mode(not zui.is_ocr_mode()) def __handle_ocr_region(self, image: QtGui.QImage) -> None: import io from PIL import Image as PILImage try: import pytesseract except ImportError: self.__show_error( "Tesseract OCR not installed", "Install pytesseract: pip install pytesseract\n" "Then install the Tesseract OCR engine:\n" " Debian/Ubuntu: sudo apt install tesseract-ocr\n" " Fedora: sudo dnf install tesseract\n" " Arch: sudo pacman -S tesseract\n" " macOS: brew install tesseract", ) return buffer = QtCore.QBuffer() buffer.open(QtCore.QBuffer.OpenModeFlag.ReadWrite) image.save(buffer, "PNG") pil_image = PILImage.open(io.BytesIO(buffer.data())) try: text = pytesseract.image_to_string(pil_image).strip() except pytesseract.TesseractNotFoundError: self.__show_error( "Tesseract OCR engine not found", "The pytesseract Python package is installed, but the\n" "Tesseract OCR binary was not found on the system PATH.\n\n" "Install it with:\n" " Debian/Ubuntu: sudo apt install tesseract-ocr\n" " Fedora: sudo dnf install tesseract\n" " Arch: sudo pacman -S tesseract\n" " macOS: brew install tesseract", ) return except Exception as e: self.__show_error("OCR processing failed", e) return if not text: text = "no text detected" dialog = DialogWindows.open_new_string_input_dialog(initial_text=text) try: ok, uri = dialog._run_dialog() except Exception as e: self.__show_error("Error loading Media String", e) ok = False uri = "" if ok and uri: self.__open_media(uri)
[docs] @staticmethod def _get_pdf_page_count(pdf_path: str) -> int | None: """Get page count from a PDF file using pdfinfo. ``pdfinfo`` is part of the Poppler suite and is typically installed alongside ``pdftoppm``. """ try: result = subprocess.run( ["pdfinfo", pdf_path], capture_output=True, text=True, timeout=10, ) for line in result.stdout.splitlines(): if line.startswith("Pages:"): return int(line.split(":", 1)[1].strip()) except Exception: pass return None
def __show_pdf_page_dialog(self, pdf_path: str, page_count: int) -> int | None: """Show a dialog asking which page to open for a large PDF. Returns the 0-indexed page number, or None if cancelled. """ filename = os.path.basename(pdf_path) page_num, ok = QtWidgets.QInputDialog.getInt( self, "Open PDF", f"'{filename}'\nThis PDF contains {page_count} pages.\nWhich page would you like to open?", 1, 1, page_count, ) if ok: return max(0, int(page_num) - 1) return None def __open_media(self, media_id: str, add: bool = True) -> Any | None: # type: ignore[return] """ Method : MainWindow.__open_media(media_id, add) Parameters : media_id : str add : bool MainWindow.__open_media(media_id, add) --> Optional[Any] Open the media with the given media_id. If add is True then the media will be fit to the screen and added to the scene. Otherwise it will be returned. For PDF files larger than 2 MB, a page selection dialog is shown allowing the user to choose which page to open at. """ zui = self.current_zui try: if media_id.startswith("string:"): mediaobject = StringMediaObject(media_id, zui.scene) elif media_id.lower().endswith(".svg") or media_id.startswith("svg_"): mediaobject = SVGMediaObject(media_id, zui.scene) # type: ignore[assignment] elif media_id.lower().endswith(".pdf"): start_page: int | None = 0 pdf_size = os.path.getsize(media_id) if pdf_size > 2 * 1024 * 1024: page_count = self._get_pdf_page_count(media_id) if page_count and page_count > 0: start_page = self.__show_pdf_page_dialog(media_id, page_count) if start_page is None: return None mediaobject = PdfMediaObject(media_id, zui.scene, True, start_page or 0) else: mediaobject = TiledMediaObject(media_id, zui.scene, True) # type: ignore[assignment] except Exception as e: self.__show_error("Unable to open media ERROR in mainwindow.__open_media \n", e) if add: w = zui.width() h = zui.height() try: mediaobject.fit((w // 4, h // 4, w * 3 // 4, h * 3 // 4)) zui.scene.add(mediaobject) except Exception as e: self.__show_error("Error in opening media in __open_media \n", e) # This return actually never engages as add is set to True in the method input arguments else: return mediaobject def __action_open_media_local(self) -> None: """ Method : MainWindow.__action_open_media_local() Parameters : None MainWindow.__action_open_media_local() --> None Open media from the location chosen by the user in a file selection dialog. """ filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open local media", self.__prev_dir) if filename and filename[0]: self.__prev_dir = os.path.dirname(filename[0]) self.__open_media(filename[0]) def __action_open_media_string(self) -> None: """ Method : MainWindow.__action_open_media_string() Parameters : None MainWindow.__action_open_media_string() --> None Render string given by the user in an input dialog. """ dialog = DialogWindows.open_new_string_input_dialog() try: ok, uri = dialog._run_dialog() except Exception as e: self.__show_error("Error loading Media String: \n", e) ok = False uri = "" if ok and uri: self.__open_media(uri) def __action_open_svg_pick(self) -> None: """ Method : MainWindow.__action_open_svg_pick() Parameters : None MainWindow.__action_open_svg_pick() --> None Open SVG file selected by the user in a picker dialog. """ dialog = DialogWindows.open_svg_picker_input_dialog() try: ok, filepath = dialog._run_dialog() self.__logger.debug("SVG filepath: %s", filepath) except Exception as e: self.__show_error("Error loading SVG: \n", e) ok = False filepath = "" if ok and filepath: self.__open_media(filepath) # Supported file extensions for media opening # SVG handled by SVGMediaObject, PDF/PPM/images handled by TiledMediaObject SUPPORTED_EXTENSIONS = { ".svg", # SVGMediaObject ".pdf", # PDFConverter ".ppm", # Direct PPM support ".jpg", ".jpeg", # VipsConverter - JPEG ".png", # VipsConverter - PNG ".gif", # VipsConverter - GIF ".tif", ".tiff", # VipsConverter - TIFF ".webp", # VipsConverter - WebP ".bmp", # VipsConverter - BMP ".heic", ".heif", # VipsConverter - HEIC ".avif", # VipsConverter - AVIF ".jxl", # VipsConverter - JPEG XL } def __action_open_media_dir(self) -> None: """ Method : MainWindow.__action_open_media_dir() Parameters : None MainWindow.__action_open_media_dir() --> None Open media from the directory chosen by the user in a file selection dialog. Only files with supported extensions are opened. """ directory = str(QtWidgets.QFileDialog.getExistingDirectory(self, "Open media directory", self.__prev_dir)) if directory: self.__prev_dir = os.path.dirname(directory) zui = self.current_zui media = [] for filename in os.listdir(directory): filename = os.path.join(directory, filename) if not os.path.isdir(filename): # Check if file has a supported extension ext = os.path.splitext(filename)[1].lower() if ext not in self.SUPPORTED_EXTENSIONS: continue mediaobject = self.__open_media(filename, False) if mediaobject: media.append(mediaobject) cells_per_side = math.ceil(math.sqrt(len(media))) cellsize = float(min(zui.width(), zui.height())) // cells_per_side innersize = 0.9 * cellsize centre = (zui.width() // 2, zui.height() // 2) bbox = ( centre[0] - innersize // 2, centre[1] - innersize // 2, centre[0] + innersize // 2, centre[1] + innersize // 2, ) grid_centre = 0.5 * cells_per_side for y in range(cells_per_side): for x in range(cells_per_side): if not media: break mediaobject = media.pop(0) ## resize and centre object mediaobject.fit(bbox) mediaobject.centre = centre ## aim object towards grid cell mediaobject.aim("x", (x + 0.5 - grid_centre) * cellsize) mediaobject.aim("y", (y + 0.5 - grid_centre) * cellsize) zui.scene.add(mediaobject) def __action_set_fps(self, act: QtGui.QAction) -> None: """ Method : MainWindow.__action_set_fps(act) Parameters : act : QtGui.QAction MainWindow.__action_set_fps(act) --> None Set the framerate to the value specified in act. """ self.current_zui.framerate = int(act.fps // 2) def __action_fullscreen(self) -> None: """ Method : MainWindow.__action_fullscreen() Parameters : None MainWindow.__action_fullscreen() --> None Toggles fullscreen mode. """ self.setWindowState(self.windowState() ^ QtCore.Qt.WindowState.WindowFullScreen) def __action_toggle_render_order(self) -> None: """ Method : MainWindow.__action_toggle_render_order() Parameters : None MainWindow.__action_toggle_render_order() --> None Toggle render order between smaller_on_top and larger_on_top. """ action = self.__action["render_order_smaller_top"] new_mode = "smaller_on_top" if action.isChecked() else "larger_on_top" scene = self.current_zui.scene scene.set_render_order(new_mode) # Persist to config try: config_manager = ConfigManager() full_config = config_manager.load() full_config.setdefault("render", {})["order"] = new_mode config_manager.save(full_config) self.__logger.debug(f"Render order set to '{new_mode}'") except ValidationError as e: self.__logger.error(f"Cannot save render order: {e}") except Exception as e: self.__show_error(f"Failed to save render order: {e!s}", e) def __action_about(self) -> None: """ Method : MainWindow.__action_about() Parameters : None MainWindow.__action_about() --> None Display the ZooUI about dialog. """ QtWidgets.QMessageBox.about( self, f"ZooUI {ZooUI.__version__}", ZooUI.__doc__ + "\n" + ZooUI.__copyright__ + "\n" + ZooUI.__copyright_notice__, ) def __action_about_qt(self) -> None: """ Method : MainWindow.__action_about_qt() Parameters : None MainWindow.__action_about_qt() --> None Display the Qt about dialog. """ QtWidgets.QMessageBox.aboutQt(self) def __action_usage(self) -> None: """ Method : MainWindow.__action_usage() Parameters : None MainWindow.__action_usage() --> None Display the Usage Instructions dialog from the RST documentation. """ DialogWindows.usage_dialog(self).exec() def __action_save_and_quit(self) -> None: """ Method : MainWindow.__action_save_and_quit() Parameters : None MainWindow.__action_save_and_quit() --> None Calls the __action_save_scene and then quit. """ self.__action_save_scene() QtWidgets.QApplication.closeAllWindows() def __action_confirm_quit(self) -> None: """ Method : MainWindow.__action_confirm_quit() Parameters : None MainWindow.__action_confirm_quit() --> None Ask user if it really wants to quit. """ # Create the dialog window dialog = QDialog() dialog.setWindowTitle("Confirm Quit") # Add label to the layout label = QLabel("Are you sure you want to quit?") buttons = QDialogButtonBox(QDialogButtonBox.Yes | QDialogButtonBox.No, dialog) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) save_button = QPushButton("Save and Quit", dialog) save_button.setIcon(QtGui.QIcon.fromTheme("document-save")) save_button.clicked.connect(self.__action_save_and_quit) buttons.addButton(save_button, QDialogButtonBox.ActionRole) layout = QVBoxLayout(dialog) layout.addWidget(label) layout.addWidget(buttons) # Make dialog wrap content instead of having fixed large size dialog.adjustSize() dialog.setFixedSize(dialog.sizeHint()) response = dialog.exec() if response == QDialog.Accepted: for zui in self.__zui_tabs: zui.scene.shutdown_threads() QtWidgets.QApplication.closeAllWindows() elif response == QDialog.Rejected: dialog.close() def __action_set_zoom_sensitivity(self) -> None: """ Method : MainWindow.__action_set_zoom_sensitivity() Parameters : None MainWindow.__action_set_zoom_sensitivity() --> None Set the zoom sensitivity for the ZUI. """ ok_pressed, text_input = DialogWindows.open_zoom_sensitivity_input_dialog(self.current_zui.zoom_sensitivity) """ dialog = QInputDialog() dialog.setWindowTitle("Set zoom sensitivity") dialog.resize(300, 80) # Set the size here ok_pressed = dialog.exec() text_input = dialog.textValue() """ if ok_pressed and text_input: try: value = int(text_input) except ValueError: self.__show_error("Sensitivity input must be an integer", "") return current_sens = ( int(1000 / self.current_zui.zoom_sensitivity) if self.current_zui.zoom_sensitivity != 0 else 0 ) if value < 0 or value > 100: self.__show_error(f"Sensitivity input must range from 0 to 100, current: {current_sens}", "") elif value == 0: self.current_zui.zoom_sensitivity = 1000 else: self.current_zui.zoom_sensitivity = int(1000 // value) def __action_copy(self) -> None: """ Method : MainWindow.__action_copy() Parameters : None MainWindow.__action_copy() --> None Handle Copy menu action. """ if self.current_zui.scene.selection: self.current_zui.scene.copy_selection() def __action_paste(self) -> None: """ Method : MainWindow.__action_paste() Parameters : None MainWindow.__action_paste() --> None Handle Paste menu action. """ zui = self.current_zui # Paste at viewport centre (center of visible area) # Calculate viewport centre in scene coordinates # Viewport centre is at (width/2, height/2) in screen coordinates viewport_centre_x = zui.width() / 2 viewport_centre_y = zui.height() / 2 # Convert screen coordinates to scene coordinates # Formula: scene_x = (screen_x - scene.origin[0]) * (2 ** -scene.zoomlevel) scene = zui.scene scene_origin = scene.origin scene_zoomlevel = scene.zoomlevel scale = 2**-scene_zoomlevel scene_x = (viewport_centre_x - scene_origin[0]) * scale scene_y = (viewport_centre_y - scene_origin[1]) * scale zui.scene.paste((scene_x, scene_y)) def __action_set_home_point(self) -> None: """ Method : MainWindow.__action_set_home_point() Parameters : None MainWindow.__action_set_home_point() --> None Handle Set Home Point menu action. """ self.current_zui.set_home_point() def __action_go_to_home_point(self) -> None: """ Method : MainWindow.__action_go_to_home_point() Parameters : None MainWindow.__action_go_to_home_point() --> None Handle Go to Home Point menu action. """ self.current_zui.go_to_home_point() def __action_autosave_settings(self) -> None: """ Method : MainWindow.__action_autosave_settings() Parameters : None MainWindow.__action_autosave_settings() --> None Handle Autosave Settings menu action. """ # Get current autosave configuration from scene scene = self.current_zui.scene current_config = scene.get_autosave_config() if hasattr(scene, "get_autosave_config") else None # Show dialog to get new configuration from zooui.windows.dialogwindows.autosavesettingsdialog import AutosaveSettingsDialog new_config = AutosaveSettingsDialog.get_autosave_settings(self, current_config) if new_config is not None: # Update scene configuration if hasattr(scene, "set_autosave_config"): scene.set_autosave_config(new_config) # Save to user config using ConfigManager try: config_manager = ConfigManager() # Load current config to preserve other sections full_config = config_manager.load() # Update autosave section, preserving keys not in the dialog # (e.g. backup_dir which is only configurable via JSON) full_config["autosave"].update(new_config) # Save back config_manager.save(full_config) self.__logger.info("Autosave settings saved to user configuration") except ValidationError as e: self.__logger.error(f"Cannot save autosave settings: {e}") QtWidgets.QMessageBox.warning(self, "Configuration Error", f"Cannot save settings: {e}") except Exception as e: self.__show_error(f"Failed to save autosave settings: {e!s}", e) def __action_zoom_settings(self) -> None: """ Method : MainWindow.__action_zoom_settings() Parameters : None MainWindow.__action_zoom_settings() --> None Handle Zoom Settings menu action. """ # Get current zoom configuration config_manager = ConfigManager() current_config = config_manager.load() zoom_config = current_config.get("zoom", {}) # Show dialog to get new configuration from zooui.windows.dialogwindows.zoomsettingsdialog import ZoomSettingsDialog new_config = ZoomSettingsDialog.get_zoom_settings(self, zoom_config) if new_config is not None: # Update config current_config["zoom"] = new_config try: config_manager.save(current_config) self.__logger.info("Zoom settings saved to user configuration") except ValidationError as e: self.__logger.error(f"Cannot save zoom settings: {e}") QtWidgets.QMessageBox.warning(self, "Configuration Error", f"Cannot save settings: {e}") except Exception as e: self.__show_error(f"Failed to save zoom settings: {e!s}", e) def __create_actions(self) -> None: """ Method : MainWindow.__create_actions() Parameters : None MainWindow.__create_actions() --> None Create the QActions required for the interface. """ self.__action = {} self.__create_action("new_tab", "&New Tab", self.__action_new_tab, "Ctrl+T") self.__create_action("close_tab", "&Close Tab", self.__action_close_tab, "Ctrl+W") self.__create_action("new_scene", "&New Scene", self.__action_new_scene, "Ctrl+N") self.__create_action("open_scene", "&Open Scene", self.__action_open_scene, "Ctrl+O") self.__create_action("import_scene", "&Import Scene", self.__action_import_scene, "Ctrl+I") self.__create_action("open_scene_home", "Open &Home Scene", self.__action_open_scene_home, "Ctrl+Home") self.__create_action("save_scene", "&Save Scene", self.__action_save_scene, "Ctrl+S") self.__create_action("save_screenshot", "Save Screens&hot", self.__action_save_screenshot, "Ctrl+H") self.__create_action("open_media_local", "Open &Local Media", self.__action_open_media_local, "Ctrl+L") self.__create_action("open_media_string", "Open new &String", self.__action_open_media_string, "Ctrl+U") self.__create_action("open_svg_pick", "Open new &SVG", self.__action_open_svg_pick, "Ctrl+G") self.__create_action("open_media_dir", "Open Media &Directory", self.__action_open_media_dir, "Ctrl+D") self.__create_action("quit", "&Quit", self.__action_confirm_quit, "Ctrl+Q") self.__create_action("set_zoom_sensitivity", "Adjust &Sensitivity", self.__action_set_zoom_sensitivity) self.__action["group_set_fps"] = QtGui.QActionGroup(self) for i in range(10, 41, 10): key = "set_fps_%d" % i self.__create_action(key, "%d FPS" % i, checkable=True) self.__action[key].fps = i self.__action["group_set_fps"].addAction(self.__action[key]) self.__action["group_set_fps"].triggered[QtGui.QAction].connect(self.__action_set_fps) self.__action["set_fps_%d" % self.__framerate].setChecked(True) self.__create_action("fullscreen", "&Fullscreen", self.__action_fullscreen, "Ctrl+F") self.__create_action( "render_order_smaller_top", "Render Order: &Smaller on Top", self.__action_toggle_render_order, "Ctrl+R", checkable=True, ) self.__action["render_order_smaller_top"].setChecked(True) self.__create_action("copy", "Copy &SVG", self.__action_copy, "Ctrl+C") self.__create_action("paste", "Paste &SVG", self.__action_paste, "Ctrl+V") self.__create_action("set_home_point", "Set &Home Point", self.__action_set_home_point, "Ctrl+Alt+H") self.__create_action("go_to_home_point", "&Go to Home Point", self.__action_go_to_home_point, "Ctrl+J") self.__create_action("ocr_screenshot", "OCR Screens&hot", self.__action_ocr_screenshot, "Ctrl+Alt+O") self.__create_action("autosave_settings", "&Autosave Settings", self.__action_autosave_settings) self.__create_action("zoom_settings", "&Zoom Settings", self.__action_zoom_settings) self.__create_action("usage", "&Usage", self.__action_usage) self.__create_action("about", "&About", self.__action_about) self.__create_action("about_qt", "About &Qt", self.__action_about_qt) def __create_menus(self) -> None: """ Method : MainWindow.__create_menus() Parameters : None MainWindow.__create_menus() --> None Create the menus. """ self.__menu = {} self.__menu["file"] = self.menuBar().addMenu("&File") self.__menu["file"].addAction(self.__action["new_tab"]) self.__menu["file"].addAction(self.__action["close_tab"]) self.__menu["file"].addSeparator() self.__menu["file"].addAction(self.__action["new_scene"]) self.__menu["file"].addAction(self.__action["open_scene"]) self.__menu["file"].addAction(self.__action["import_scene"]) self.__menu["file"].addAction(self.__action["open_scene_home"]) self.__menu["file"].addAction(self.__action["save_scene"]) self.__menu["file"].addAction(self.__action["save_screenshot"]) self.__menu["file"].addAction(self.__action["open_media_local"]) self.__menu["file"].addAction(self.__action["open_media_string"]) self.__menu["file"].addAction(self.__action["open_svg_pick"]) self.__menu["file"].addAction(self.__action["open_media_dir"]) self.__menu["file"].addAction(self.__action["quit"]) self.__menu["view"] = self.menuBar().addMenu("&View") self.__menu["set_fps"] = self.__menu["view"].addMenu("Set &Framerate") self.__menu["set_fps"].addActions(self.__action["group_set_fps"].actions()) self.__menu["view"].addAction(self.__action["set_zoom_sensitivity"]) self.__menu["view"].addAction(self.__action["fullscreen"]) self.__menu["view"].addSeparator() self.__menu["view"].addAction(self.__action["render_order_smaller_top"]) self.__menu["actions"] = self.menuBar().addMenu("&Actions") self.__menu["actions"].addAction(self.__action["copy"]) self.__menu["actions"].addAction(self.__action["paste"]) self.__menu["actions"].addSeparator() self.__menu["actions"].addAction(self.__action["set_home_point"]) self.__menu["actions"].addAction(self.__action["go_to_home_point"]) self.__menu["actions"].addSeparator() self.__menu["actions"].addAction(self.__action["ocr_screenshot"]) self.__menu["settings"] = self.menuBar().addMenu("&Settings") self.__menu["settings"].addAction(self.__action["autosave_settings"]) self.__menu["settings"].addAction(self.__action["zoom_settings"]) self.__menu["help"] = self.menuBar().addMenu("&Help") self.__menu["help"].addAction(self.__action["usage"]) self.__menu["help"].addAction(self.__action["about"]) self.__menu["help"].addAction(self.__action["about_qt"])
[docs] def showEvent(self, event: QtGui.QShowEvent) -> None: """ Method : MainWindow.showEvent(event) Parameters : event : QtGui.QShowEvent MainWindow.showEvent(event) --> None Handle show event by focusing the current tab's QZUI widget. """ ## focus the current tab's QZUI widget whenever this window is shown self.current_zui.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)