LCOV - code coverage report
Current view: top level - src/pairinteraction_gui - app.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 27 95 28.4 %
Date: 2026-04-17 09:29:39 Functions: 2 9 22.2 %

          Line data    Source code
       1             : # SPDX-FileCopyrightText: 2025 PairInteraction Developers
       2             : # SPDX-License-Identifier: LGPL-3.0-or-later
       3           1 : from __future__ import annotations
       4             : 
       5           1 : import logging
       6           1 : import os
       7           1 : import signal
       8           1 : from typing import TYPE_CHECKING
       9             : 
      10           1 : from PySide6.QtCore import QObject, QRectF, QSocketNotifier, Qt, QTimer, Signal
      11           1 : from PySide6.QtGui import QColor, QFont, QPainter, QPainterPath, QPen, QPixmap
      12           1 : from PySide6.QtWidgets import QApplication, QSplashScreen
      13             : 
      14           1 : from pairinteraction_gui.worker import MultiThreadWorker
      15             : 
      16             : if TYPE_CHECKING:
      17             :     from types import FrameType
      18             : 
      19           1 : logger = logging.getLogger(__name__)
      20             : 
      21             : 
      22           1 : class MainSignals(QObject):
      23             :     """Signals for the application.
      24             : 
      25             :     We store an instance of this signal class in the Application instance, see app.py.
      26             :     So to access these signals (from anywhere in the application), you can use
      27             :     `Application.instance().signals`.
      28             :     """
      29             : 
      30           1 :     ask_download_database = Signal(str)
      31             : 
      32             : 
      33           1 : class Application(QApplication):
      34             :     """Add some global signals to the QApplication."""
      35             : 
      36           1 :     signals = MainSignals()
      37             : 
      38           1 :     @staticmethod
      39           1 :     def instance() -> Application:
      40             :         """Return the current instance of the application."""
      41           1 :         return QApplication.instance()  # type: ignore [return-value]
      42             : 
      43           1 :     def allow_ctrl_c(self) -> None:
      44             :         # Create a pipe to communicate between the signal handler and the Qt event loop
      45           0 :         pipe_r, pipe_w = os.pipe()
      46             : 
      47           0 :         def signal_handler(signal: int, frame: FrameType | None) -> None:
      48           0 :             os.write(pipe_w, b"x")  # Write a single byte to the pipe
      49             : 
      50           0 :         signal.signal(signal.SIGINT, signal_handler)
      51             : 
      52           0 :         def handle_signal() -> None:
      53           0 :             os.read(pipe_r, 1)  # Read the byte from the pipe to clear it
      54           0 :             logger.info("Ctrl+C detected in terminal. Shutting down gracefully...")
      55           0 :             self.quit()
      56             : 
      57           0 :         sn = QSocketNotifier(pipe_r, QSocketNotifier.Type.Read, parent=self)
      58           0 :         sn.activated.connect(handle_signal)
      59             : 
      60             :         # Create a timer to ensure the event loop processes events regularly
      61             :         # This makes Ctrl+C work even when the application is idle
      62           0 :         timer = QTimer(self)
      63           0 :         timer.timeout.connect(lambda: None)  # Do nothing, just wake up the event loop
      64           0 :         timer.start(200)
      65             : 
      66           1 :     @staticmethod
      67           1 :     def quit() -> None:
      68             :         """Quit the application."""
      69           1 :         logger.debug("Calling Application.quit().")
      70           1 :         MultiThreadWorker.terminate_all()
      71           1 :         QApplication.quit()
      72           1 :         logger.debug("Application.quit() done.")
      73             : 
      74             : 
      75           1 : class SplashScreen(QSplashScreen):
      76             :     """Startup splash screen with the PairInteraction logo."""
      77             : 
      78           1 :     def __init__(self) -> None:
      79           0 :         super().__init__()
      80           0 :         self.setWindowTitle("Launching the PairInteraction GUI")
      81           0 :         border = 16
      82           0 :         logo_size = 250
      83           0 :         full_size = logo_size + 2 * border
      84           0 :         pixmap = QPixmap(full_size, full_size)
      85           0 :         pixmap.fill(QColor("#28354e"))  # "Dark" theme color
      86           0 :         painter = QPainter(pixmap)
      87           0 :         painter.translate(border, border)
      88           0 :         _draw_logo(painter, logo_size)
      89           0 :         painter.end()
      90           0 :         self.setPixmap(pixmap)
      91             : 
      92             : 
      93           1 : def _draw_logo(painter: QPainter, size: int) -> None:
      94             :     """Draw the PairInteraction logo using QPainter, ported from data/icon.tex (TikZ source)."""
      95           0 :     s = size / 19.0
      96             : 
      97           0 :     def tx(x: float) -> float:
      98           0 :         return (x + 2.0) * s
      99             : 
     100           0 :     def ty(y: float) -> float:
     101           0 :         return (16.0 - y) * s
     102             : 
     103           0 :     painter.save()
     104           0 :     painter.setRenderHint(QPainter.RenderHint.Antialiasing)
     105           0 :     painter.setClipRect(QRectF(0.0, 0.0, float(size), float(size)))
     106             : 
     107             :     # White rounded background
     108           0 :     painter.setPen(Qt.PenStyle.NoPen)
     109           0 :     painter.setBrush(QColor("white"))
     110           0 :     painter.drawRoundedRect(QRectF(0.0, 0.0, float(size), float(size)), 4.0 * s, 4.0 * s)
     111             : 
     112             :     # Thick round-capped pen
     113           0 :     pen = QPen()
     114           0 :     pen.setWidthF(0.8 * s)
     115           0 :     pen.setCapStyle(Qt.PenCapStyle.RoundCap)
     116           0 :     pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
     117           0 :     painter.setBrush(Qt.BrushStyle.NoBrush)
     118             : 
     119             :     # Coordinate axes
     120           0 :     pen.setColor(QColor("black"))
     121           0 :     painter.setPen(pen)
     122           0 :     axes = QPainterPath()
     123           0 :     axes.moveTo(tx(0), ty(13))
     124           0 :     axes.lineTo(tx(0), ty(0))
     125           0 :     axes.lineTo(tx(15), ty(0))
     126           0 :     painter.drawPath(axes)
     127             : 
     128             :     # Red curve
     129           0 :     pen.setColor(QColor("red"))
     130           0 :     painter.setPen(pen)
     131           0 :     red = QPainterPath()
     132           0 :     red.moveTo(tx(1), ty(12))
     133           0 :     red.cubicTo(tx(3), ty(-10), tx(6), ty(2), tx(15), ty(1))
     134           0 :     painter.drawPath(red)
     135             : 
     136             :     # Blue curve
     137           0 :     pen.setColor(QColor("blue"))
     138           0 :     painter.setPen(pen)
     139           0 :     blue = QPainterPath()
     140           0 :     blue.moveTo(tx(2), ty(12))
     141           0 :     blue.cubicTo(tx(4), ty(-6), tx(6), ty(5), tx(15), ty(2))
     142           0 :     painter.drawPath(blue)
     143             : 
     144             :     # PI text
     145           0 :     font = QFont("serif")
     146           0 :     font.setPixelSize(round(10.0 * s))
     147           0 :     painter.setFont(font)
     148           0 :     pen.setColor(QColor("black"))
     149           0 :     painter.setPen(pen)
     150           0 :     cx, cy = tx(9), ty(9)
     151           0 :     painter.drawText(
     152             :         QRectF(cx - 7.0 * s, cy - 6.0 * s, 14.0 * s, 12.0 * s),
     153             :         Qt.AlignmentFlag.AlignCenter,
     154             :         "PI",
     155             :     )
     156             : 
     157           0 :     painter.restore()

Generated by: LCOV version 1.16