LCOV - code coverage report
Current view: top level - src/pairinteraction_gui - app.py (source / functions) Coverage Total Hit
Test: coverage.info Lines: 28.4 % 95 27
Test Date: 2026-11-02 09:44:19 Functions: 22.2 % 9 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 2.0-1