batoolset-gui 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- batoolset_gui-0.2.0/LICENSE +24 -0
- batoolset_gui-0.2.0/PKG-INFO +54 -0
- batoolset_gui-0.2.0/README.md +22 -0
- batoolset_gui-0.2.0/batoolset/GUI/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/GUI/customdialog.py +136 -0
- batoolset_gui-0.2.0/batoolset/GUI/exception_handler.py +74 -0
- batoolset_gui-0.2.0/batoolset/GUI/list_gui.py +514 -0
- batoolset_gui-0.2.0/batoolset/GUI/multi_image_display.py +1038 -0
- batoolset_gui-0.2.0/batoolset/GUI/open.py +325 -0
- batoolset_gui-0.2.0/batoolset/GUI/overlay_hints.py +45 -0
- batoolset_gui-0.2.0/batoolset/GUI/paint2.py +1369 -0
- batoolset_gui-0.2.0/batoolset/GUI/scrollablepaint.py +1165 -0
- batoolset_gui-0.2.0/batoolset/dialogs/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/dialogs/opensave.py +228 -0
- batoolset_gui-0.2.0/batoolset/drawings/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/drawings/math_text.py +294 -0
- batoolset_gui-0.2.0/batoolset/drawings/pdf_svg_renderer.py +376 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/Position.py +183 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/circle2d.py +99 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/ellipse2d.py +336 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/freehand2d.py +235 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/group.py +1985 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/image2d.py +3157 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/line2d.py +579 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/point2d.py +257 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/polygon2d.py +357 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/polyline2d.py +235 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/rect2d.py +388 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/rectangle2d.py +601 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/scalebar.py +310 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/serializablefont.py +133 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/serializableguiparams.py +14 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/square2d.py +102 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/test_publication_style.py +220 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/textorientation.py +45 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/tools.py +467 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/transform2D.py +350 -0
- batoolset_gui-0.2.0/batoolset/drawings/shapes/txt2d.py +615 -0
- batoolset_gui-0.2.0/batoolset/drawings/svg_clip_renderer.py +247 -0
- batoolset_gui-0.2.0/batoolset/drawings/svg_embed.py +165 -0
- batoolset_gui-0.2.0/batoolset/drawings/svg_text_renderer.py +183 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/col.py +343 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/image_group.py +248 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/image_organizer.py +96 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/paint.py +184 -0
- batoolset_gui-0.2.0/batoolset/drawings/widgets/vectorial.py +256 -0
- batoolset_gui-0.2.0/batoolset/figure/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/figure/alignment.py +822 -0
- batoolset_gui-0.2.0/batoolset/figure/column.py +1117 -0
- batoolset_gui-0.2.0/batoolset/figure/fig_tools.py +285 -0
- batoolset_gui-0.2.0/batoolset/figure/minimal_debugger_scale_etc.py +139 -0
- batoolset_gui-0.2.0/batoolset/figure/panel.py +697 -0
- batoolset_gui-0.2.0/batoolset/figure/row.py +852 -0
- batoolset_gui-0.2.0/batoolset/figure/user_selection.py +287 -0
- batoolset_gui-0.2.0/batoolset/icons/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/icons/tools.py +248 -0
- batoolset_gui-0.2.0/batoolset/luts/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/luts/lut_minimal_test.py +1308 -0
- batoolset_gui-0.2.0/batoolset/luts/lutcreatorfromformula.py +209 -0
- batoolset_gui-0.2.0/batoolset/luts/luteditor.py +243 -0
- batoolset_gui-0.2.0/batoolset/pyqts/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/pyqts/pleasewaitdialog.py +14 -0
- batoolset_gui-0.2.0/batoolset/pyqts/tools.py +760 -0
- batoolset_gui-0.2.0/batoolset/serializations/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/serializations/tools.py +1338 -0
- batoolset_gui-0.2.0/batoolset/theme.py +647 -0
- batoolset_gui-0.2.0/batoolset/uis/__init__.py +0 -0
- batoolset_gui-0.2.0/batoolset/uis/blinker.py +56 -0
- batoolset_gui-0.2.0/batoolset_gui.egg-info/PKG-INFO +54 -0
- batoolset_gui-0.2.0/batoolset_gui.egg-info/SOURCES.txt +77 -0
- batoolset_gui-0.2.0/batoolset_gui.egg-info/dependency_links.txt +1 -0
- batoolset_gui-0.2.0/batoolset_gui.egg-info/requires.txt +6 -0
- batoolset_gui-0.2.0/batoolset_gui.egg-info/top_level.txt +1 -0
- batoolset_gui-0.2.0/setup.cfg +4 -0
- batoolset_gui-0.2.0/setup.py +33 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, baigouy
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: batoolset-gui
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Qt/GUI layer for batoolset (drawings, figure, luts, widgets)
|
|
5
|
+
Home-page: https://github.com/baigouy/BAtoolset-gui
|
|
6
|
+
Author: Benoit Aigouy
|
|
7
|
+
Author-email: benoit.aigouy@gmail.com
|
|
8
|
+
License: BSD
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7, <3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: batoolset>=0.2.0
|
|
16
|
+
Requires-Dist: PyQt6
|
|
17
|
+
Requires-Dist: QtPy>=2.1.0
|
|
18
|
+
Requires-Dist: qtawesome
|
|
19
|
+
Requires-Dist: pyqt6-plugins
|
|
20
|
+
Requires-Dist: matplotlib>=3.5.2
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# batoolset-gui
|
|
34
|
+
|
|
35
|
+
The Qt/GUI layer of [batoolset](https://github.com/baigouy/BAtoolset): vector
|
|
36
|
+
shapes and images for figures (`batoolset.drawings`), figure layout
|
|
37
|
+
(`batoolset.figure`), LUTs with an editor and inverted LUTs (`batoolset.luts`),
|
|
38
|
+
painting widgets and dialogs (`batoolset.GUI`, `batoolset.dialogs`,
|
|
39
|
+
`batoolset.pyqts`, `batoolset.uis`) and object serialization
|
|
40
|
+
(`batoolset.serializations`).
|
|
41
|
+
|
|
42
|
+
It installs into the same `batoolset` package as the core library, which needs
|
|
43
|
+
no Qt. Since 0.2.0 these modules are no longer part of `batoolset` itself.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install batoolset-gui
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This installs `batoolset` (0.2.0 or later), PyQt6 and QtPy. It is used by
|
|
50
|
+
[PyFigures](https://github.com/baigouy/PyFigures).
|
|
51
|
+
|
|
52
|
+
## Licence
|
|
53
|
+
|
|
54
|
+
BSD 2-Clause, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# batoolset-gui
|
|
2
|
+
|
|
3
|
+
The Qt/GUI layer of [batoolset](https://github.com/baigouy/BAtoolset): vector
|
|
4
|
+
shapes and images for figures (`batoolset.drawings`), figure layout
|
|
5
|
+
(`batoolset.figure`), LUTs with an editor and inverted LUTs (`batoolset.luts`),
|
|
6
|
+
painting widgets and dialogs (`batoolset.GUI`, `batoolset.dialogs`,
|
|
7
|
+
`batoolset.pyqts`, `batoolset.uis`) and object serialization
|
|
8
|
+
(`batoolset.serializations`).
|
|
9
|
+
|
|
10
|
+
It installs into the same `batoolset` package as the core library, which needs
|
|
11
|
+
no Qt. Since 0.2.0 these modules are no longer part of `batoolset` itself.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install batoolset-gui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This installs `batoolset` (0.2.0 or later), PyQt6 and QtPy. It is used by
|
|
18
|
+
[PyFigures](https://github.com/baigouy/PyFigures).
|
|
19
|
+
|
|
20
|
+
## Licence
|
|
21
|
+
|
|
22
|
+
BSD 2-Clause, see [LICENSE](LICENSE).
|
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from qtpy import QtWidgets, QtCore
|
|
2
|
+
from qtpy.QtWidgets import (QDialog, QVBoxLayout, QLabel, QPushButton, QApplication, QWidget,QFrame)
|
|
3
|
+
from qtpy.QtCore import Qt,Signal
|
|
4
|
+
|
|
5
|
+
from batoolset.pyqts.tools import disable_wheel_for_combo_and_spin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# from batoolset.pyqts.tools import disable_wheel_for_widget_and_children
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomDialog(QDialog):
|
|
12
|
+
option_selected = Signal(str)
|
|
13
|
+
accepting_keywords = ["yes", "ok", "done", "validate", "y"]
|
|
14
|
+
rejecting_keywords = ["no", "cancel", "quit", "abandon", "reject", "stop", "n", "q"]
|
|
15
|
+
|
|
16
|
+
def __init__(self, title="Custom Dialog", message="Do you want to proceed?", main_widget=None, parent=None, options=None, add_separator=True, auto_adjust=False, reset_style_sheet=True):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
|
|
19
|
+
if reset_style_sheet:
|
|
20
|
+
# self.setStyleSheet("") # make sure to reset style sheet # this doesn't work
|
|
21
|
+
self.setStyleSheet("background-color: none;") # make sure to reset style sheet
|
|
22
|
+
# main_widget.setStyleSheet("")
|
|
23
|
+
main_widget.setStyleSheet("background-color: none;")
|
|
24
|
+
|
|
25
|
+
# self.setStyleSheet("background-color: lightblue;")
|
|
26
|
+
|
|
27
|
+
# Set the object name for the widget
|
|
28
|
+
# self.setObjectName("VectorialDrawPane2")
|
|
29
|
+
|
|
30
|
+
# Set the background color
|
|
31
|
+
# if background is not None:
|
|
32
|
+
# self.setStyleSheet("") # prevent it from getting the style sheet of the parent
|
|
33
|
+
|
|
34
|
+
# Set the window flags to stay on top
|
|
35
|
+
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
36
|
+
|
|
37
|
+
self.setWindowTitle(title)
|
|
38
|
+
|
|
39
|
+
self.main_widget = main_widget
|
|
40
|
+
|
|
41
|
+
layout = QVBoxLayout()
|
|
42
|
+
if main_widget:
|
|
43
|
+
# main_widget.setStyleSheet("") # also reset style sheet of content
|
|
44
|
+
layout.addWidget(main_widget)
|
|
45
|
+
|
|
46
|
+
# Add a separator
|
|
47
|
+
separator = QFrame()
|
|
48
|
+
separator.setFrameShape(QFrame.HLine)
|
|
49
|
+
separator.setFrameShadow(QFrame.Sunken)
|
|
50
|
+
layout.addWidget(separator)
|
|
51
|
+
|
|
52
|
+
if message:
|
|
53
|
+
label = QLabel(message)
|
|
54
|
+
layout.addWidget(label)
|
|
55
|
+
|
|
56
|
+
if options is None:
|
|
57
|
+
options = ["Yes", "No"]
|
|
58
|
+
|
|
59
|
+
if isinstance(options, str):
|
|
60
|
+
options=[options]
|
|
61
|
+
|
|
62
|
+
for option in options:
|
|
63
|
+
button = QPushButton(option)
|
|
64
|
+
button.setFocusPolicy(Qt.NoFocus)# prevent auto selection from buttons
|
|
65
|
+
layout.addWidget(button)
|
|
66
|
+
button.clicked.connect(lambda _, opt=option: self.handle_option_clicked(opt))
|
|
67
|
+
|
|
68
|
+
self.setLayout(layout)
|
|
69
|
+
|
|
70
|
+
# disable_wheel_for_widget_and_children(self)
|
|
71
|
+
|
|
72
|
+
# disable_wheel_for_combo_and_spin(self.main_widget)
|
|
73
|
+
# disable_wheel_for_combo_and_spin(self.main_widget)
|
|
74
|
+
|
|
75
|
+
if not auto_adjust:
|
|
76
|
+
try:
|
|
77
|
+
try:
|
|
78
|
+
# Get the screen where the application is currently displayed
|
|
79
|
+
screen = self.screen()
|
|
80
|
+
if screen is not None:
|
|
81
|
+
screen_geometry = screen.geometry()
|
|
82
|
+
else:
|
|
83
|
+
# If the application is not displayed on any screen, use the primary screen
|
|
84
|
+
screen_geometry = QtWidgets.QApplication.primaryScreen().geometry()
|
|
85
|
+
except:
|
|
86
|
+
try:
|
|
87
|
+
from qtpy.QtGui import QGuiApplication
|
|
88
|
+
screen_geometry = QGuiApplication.primaryScreen().geometry()
|
|
89
|
+
except:
|
|
90
|
+
pass # no big deal if all fails...
|
|
91
|
+
# Set width and height to 75% of the screen size
|
|
92
|
+
width = int(screen_geometry.width() * 0.85)
|
|
93
|
+
height = int(screen_geometry.height() * 0.85)
|
|
94
|
+
self.resize(width, height)
|
|
95
|
+
except:
|
|
96
|
+
pass # no big deal if it fails...
|
|
97
|
+
else:
|
|
98
|
+
# force the window to assume the smallest minimum size can be very useful when min size is set
|
|
99
|
+
self.adjustSize()
|
|
100
|
+
|
|
101
|
+
def handle_option_clicked(self, option):
|
|
102
|
+
self.selected_option = option
|
|
103
|
+
if option.lower() in CustomDialog.accepting_keywords:
|
|
104
|
+
self.accept() # Close the dialog with QDialog.Accepted status
|
|
105
|
+
elif option.lower() in CustomDialog.rejecting_keywords:
|
|
106
|
+
self.reject() # Close the dialog with QDialog.Rejected status
|
|
107
|
+
else:
|
|
108
|
+
self.accept() # Close the dialog with QDialog.Accepted status
|
|
109
|
+
|
|
110
|
+
# def exec_(self):
|
|
111
|
+
# result = super().exec_()
|
|
112
|
+
# if result == QDialog.Accepted:
|
|
113
|
+
# return self.selected_option
|
|
114
|
+
# else:
|
|
115
|
+
# return None
|
|
116
|
+
|
|
117
|
+
def get_selected_answer(self):
|
|
118
|
+
return self.selected_option
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
import sys
|
|
122
|
+
app = QApplication(sys.argv)
|
|
123
|
+
|
|
124
|
+
options = ["Option 1", "Option 2", "Option 3", "No"] # None
|
|
125
|
+
options = "Done"
|
|
126
|
+
message = "Do you want to proceed?" # "Do you want to proceed?" # None
|
|
127
|
+
auto_adjust = True # force the window to take minimal size if used (calls adjustsize)
|
|
128
|
+
custom_dialog = CustomDialog(title="Custom Dialog", message=message , main_widget=QWidget(), options=options, auto_adjust=auto_adjust)
|
|
129
|
+
custom_dialog.option_selected.connect(lambda option: print(f"Selected option: {option}"))
|
|
130
|
+
selected_option = custom_dialog.exec_()
|
|
131
|
+
print(selected_option)
|
|
132
|
+
if selected_option:
|
|
133
|
+
print(f"Selected option: {selected_option}")
|
|
134
|
+
print(custom_dialog.selected_option)
|
|
135
|
+
else:
|
|
136
|
+
print("Dialog was closed without selecting an option.")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Qt-safe global exception handler.
|
|
2
|
+
|
|
3
|
+
Under PyQt6, an unhandled Python exception that propagates out of a slot, timer
|
|
4
|
+
callback or event handler makes Qt call ``qFatal()``/``abort()``. On Windows that
|
|
5
|
+
surfaces as a hard crash with exit code ``0xC0000409``
|
|
6
|
+
(STATUS_STACK_BUFFER_OVERRUN) — the whole application dies with no message and no
|
|
7
|
+
traceback, so it looks like a native stack overflow even though it is just a
|
|
8
|
+
Python exception (e.g. a UnicodeEncodeError while autosaving).
|
|
9
|
+
|
|
10
|
+
The important detail: PyQt only calls ``qFatal()`` when the *default*
|
|
11
|
+
``sys.excepthook`` is installed. Installing a non-default hook both suppresses the
|
|
12
|
+
abort (the app keeps running) and lets us log the error and warn the user.
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
import traceback
|
|
16
|
+
|
|
17
|
+
from batoolset.tools.logger import TA_logger
|
|
18
|
+
|
|
19
|
+
logger = TA_logger()
|
|
20
|
+
|
|
21
|
+
_original_excepthook = sys.excepthook
|
|
22
|
+
_reporting = False # guard against re-entrancy while a dialog is being shown
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _show_dialog(short, details):
|
|
26
|
+
"""Best-effort warning dialog; never let the reporter itself raise."""
|
|
27
|
+
try:
|
|
28
|
+
from qtpy.QtWidgets import QApplication, QMessageBox
|
|
29
|
+
if QApplication.instance() is None:
|
|
30
|
+
return
|
|
31
|
+
box = QMessageBox()
|
|
32
|
+
box.setIcon(QMessageBox.Warning)
|
|
33
|
+
box.setWindowTitle('An error occurred')
|
|
34
|
+
box.setText(short)
|
|
35
|
+
box.setInformativeText(
|
|
36
|
+
'The application will keep running, but the last action may not have '
|
|
37
|
+
'completed. Details are in the log.')
|
|
38
|
+
box.setDetailedText(details)
|
|
39
|
+
box.setStandardButtons(QMessageBox.Ok)
|
|
40
|
+
box.exec_()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def global_excepthook(exctype, value, tb):
|
|
46
|
+
"""Log the exception and warn the user instead of aborting the process."""
|
|
47
|
+
# keep Ctrl+C / normal interpreter-exit behaviour
|
|
48
|
+
if issubclass(exctype, (KeyboardInterrupt, SystemExit)):
|
|
49
|
+
_original_excepthook(exctype, value, tb)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
details = ''.join(traceback.format_exception(exctype, value, tb))
|
|
53
|
+
try:
|
|
54
|
+
logger.error('Unhandled exception (suppressed to keep the app alive):\n' + details)
|
|
55
|
+
except Exception:
|
|
56
|
+
sys.stderr.write(details)
|
|
57
|
+
|
|
58
|
+
global _reporting
|
|
59
|
+
if _reporting:
|
|
60
|
+
# already showing a dialog (or we faulted while reporting) — don't stack them
|
|
61
|
+
return
|
|
62
|
+
_reporting = True
|
|
63
|
+
try:
|
|
64
|
+
_show_dialog(f'{exctype.__name__}: {value}', details)
|
|
65
|
+
finally:
|
|
66
|
+
_reporting = False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def install_global_exception_handler():
|
|
70
|
+
"""Install the Qt-safe global exception hook. Idempotent."""
|
|
71
|
+
if getattr(sys, '_batoolset_excepthook_installed', False):
|
|
72
|
+
return
|
|
73
|
+
sys.excepthook = global_excepthook
|
|
74
|
+
sys._batoolset_excepthook_installed = True
|