aboutsummaryrefslogtreecommitdiffstats
path: root/gui/gui_loading.py
blob: a94e5122911198f204bb6a722a96207f231012ad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
from typing import Any, Callable, List, Optional, Tuple, TypeVar, cast
from queue import Empty, Queue
import threading
import time

from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout

from gui.gui.gui_common import ensure_qt_app


T = TypeVar("T")
StatusCallback = Callable[[str], None]


class _LoadingDialog(QDialog):
    def __init__(self, title: str, initial_message: str) -> None:
        super().__init__()
        self.setWindowTitle(title)
        self.setModal(True)
        self.setFixedWidth(440)

        layout = QVBoxLayout(self)

        self._spinner_frames: List[str] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
        self._spinner_index = 0

        spinner_row = QHBoxLayout()
        self._spinner_label = QLabel(self._spinner_frames[0], self)
        self._spinner_label.setStyleSheet("font-size: 20px; font-weight: 700; color: #8fd18f;")
        spinner_row.addWidget(self._spinner_label)

        self._message_label = QLabel(initial_message, self)
        self._message_label.setWordWrap(True)
        self._message_label.setStyleSheet("font-size: 13px;")
        spinner_row.addWidget(self._message_label, 1)
        layout.addLayout(spinner_row)

        self._progress = QProgressBar(self)
        self._progress.setRange(0, 0)
        self._progress.setTextVisible(False)
        layout.addWidget(self._progress)

        self._hint_label = QLabel("Please wait…", self)
        self._hint_label.setStyleSheet("font-size: 12px; color: #9aa0a6;")
        layout.addWidget(self._hint_label)

        self._spinner_timer = QTimer(self)
        self._spinner_timer.setInterval(90)
        self._spinner_timer.timeout.connect(self._tick_spinner)
        self._spinner_timer.start()

    def _tick_spinner(self) -> None:
        self._spinner_index = (self._spinner_index + 1) % len(self._spinner_frames)
        self._spinner_label.setText(self._spinner_frames[self._spinner_index])

    def set_message(self, message: str) -> None:
        self._message_label.setText(message)


def run_with_loading_popup(
    title: str,
    initial_message: str,
    task: Callable[[StatusCallback], T],
) -> T:
    app = ensure_qt_app()

    dialog = _LoadingDialog(title=title, initial_message=initial_message)
    events: Queue[Tuple[str, Any]] = Queue()

    def publish_status(message: str) -> None:
        events.put(("status", message))

    def worker() -> None:
        try:
            result = task(publish_status)
            events.put(("result", result))
        except Exception as exc:
            events.put(("error", exc))

    thread = threading.Thread(target=worker, daemon=True)
    thread.start()

    dialog.show()
    dialog.raise_()
    dialog.activateWindow()

    done = False
    result_value: Optional[T] = None
    error: Optional[Exception] = None

    while not done:
        app.processEvents()

        while True:
            try:
                event_type, payload = events.get_nowait()
            except Empty:
                break

            if event_type == "status":
                dialog.set_message(str(payload))
            elif event_type == "result":
                result_value = cast(T, payload)
                done = True
            elif event_type == "error":
                error = cast(Exception, payload)
                done = True

        if thread.is_alive() and not done:
            time.sleep(0.03)
            continue

        if not thread.is_alive():
            try:
                event_type, payload = events.get_nowait()
                if event_type == "status":
                    dialog.set_message(str(payload))
                elif event_type == "result":
                    result_value = cast(T, payload)
                elif event_type == "error":
                    error = cast(Exception, payload)
            except Empty:
                pass
            done = True

    dialog.close()
    app.processEvents()

    if error is not None:
        raise error

    return cast(T, result_value)
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage