michis-python-sammlung 0.0.1__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.
- michis_python_sammlung-0.0.1/LICENSE +19 -0
- michis_python_sammlung-0.0.1/PKG-INFO +14 -0
- michis_python_sammlung-0.0.1/README.md +0 -0
- michis_python_sammlung-0.0.1/pyproject.toml +19 -0
- michis_python_sammlung-0.0.1/setup.cfg +4 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/__init__.py +2 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/__init__.py +0 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/clickable_label.py +210 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/expandable_widget.py +494 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/icons/__init__.py +0 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/image_paths.py +130 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung/pyqt/pyqt_util.py +97 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung.egg-info/PKG-INFO +14 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung.egg-info/SOURCES.txt +14 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung.egg-info/dependency_links.txt +1 -0
- michis_python_sammlung-0.0.1/src/michis_python_sammlung.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: michis_python_sammlung
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Collection of mostly QT related stuff
|
|
5
|
+
Author-email: Michael Mischko <mischkom@web.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mischkomichael/Michis_python_sammlung
|
|
8
|
+
Project-URL: Issues, https://github.com/mischkomichael/Michis_python_sammlung/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "michis_python_sammlung"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Michael Mischko", email="mischkom@web.de" },
|
|
6
|
+
]
|
|
7
|
+
description = "Collection of mostly QT related stuff"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICEN[CS]E*"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/mischkomichael/Michis_python_sammlung"
|
|
19
|
+
Issues = "https://github.com/mischkomichael/Michis_python_sammlung/issues"
|
|
File without changes
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PyQt6.QtCore import Qt, QSize, pyqtSignal, Qt, QPoint, QTimer, QRect
|
|
4
|
+
from PyQt6.QtWidgets import QLabel, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLayout, QSizePolicy, QStackedLayout
|
|
5
|
+
from PyQt6.QtGui import QPixmap, QResizeEvent, QColorConstants, QPainter, QRegion, QCursor, QMouseEvent, QColor, QBitmap, QTransform
|
|
6
|
+
|
|
7
|
+
from numbers import Number
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtGui import QImage
|
|
11
|
+
from michis_python_sammlung.pyqt.image_paths import Image_paths
|
|
12
|
+
|
|
13
|
+
def create_alpha_mask(image: QImage | QPixmap) -> QBitmap:
|
|
14
|
+
# Ensure the image has an alpha channel
|
|
15
|
+
if isinstance(image, QPixmap):
|
|
16
|
+
image = image.toImage()
|
|
17
|
+
image_with_alpha = image.convertToFormat(QImage.Format.Format_ARGB32)
|
|
18
|
+
mask_image = image_with_alpha.createAlphaMask()
|
|
19
|
+
return QBitmap.fromImage(mask_image)
|
|
20
|
+
|
|
21
|
+
class Clickable_label(QLabel):
|
|
22
|
+
|
|
23
|
+
sg_clicked = pyqtSignal(QMouseEvent)
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
pixmap: str | QPixmap,
|
|
28
|
+
pixmap_hover: str | QPixmap,
|
|
29
|
+
on_click_action: Callable = lambda: ...,
|
|
30
|
+
cursor: QPixmap | Qt.CursorShape = Qt.CursorShape.ArrowCursor,
|
|
31
|
+
scaled: float = None,
|
|
32
|
+
heuristic_mask: bool = True,
|
|
33
|
+
parent: QWidget = None
|
|
34
|
+
):
|
|
35
|
+
super().__init__(parent)
|
|
36
|
+
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
37
|
+
self._scaled = scaled
|
|
38
|
+
self.configure_pixmaps(pixmap, pixmap_hover, scaled=scaled)
|
|
39
|
+
self._hovering = False
|
|
40
|
+
#somehow, the sizes are too small for the mask to not clip from the top and bottom ...
|
|
41
|
+
self.setMinimumSize(self._pixmap_hover.size())
|
|
42
|
+
cursor = QCursor(cursor) if isinstance(cursor, QPixmap) else cursor
|
|
43
|
+
self.setCursor(cursor)
|
|
44
|
+
self._on_click_action: Callable = on_click_action
|
|
45
|
+
self._heuristic_mask = heuristic_mask
|
|
46
|
+
self._mask_initialized = True
|
|
47
|
+
# ### lazy mask evaluation to make sure Clickable_labels can be instantiated even without properly initialized paintDevice
|
|
48
|
+
#self.set_mask(heuristic_mask=heuristic_mask)
|
|
49
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
50
|
+
self.setAutoFillBackground(False)
|
|
51
|
+
|
|
52
|
+
def sizeHint(self):
|
|
53
|
+
return self._pixmap_hover.size()
|
|
54
|
+
|
|
55
|
+
def showEvent(self, a0):
|
|
56
|
+
if not self._mask_initialized:
|
|
57
|
+
QTimer.singleShot(0, lambda: self._lazy_mask_init(self._heuristic_mask))
|
|
58
|
+
return super().showEvent(a0)
|
|
59
|
+
|
|
60
|
+
def _lazy_mask_init(self, heuristic_mask: bool) -> None:
|
|
61
|
+
self.set_mask(heuristic_mask=heuristic_mask)
|
|
62
|
+
self._mask_initialized
|
|
63
|
+
|
|
64
|
+
def replace_widget(
|
|
65
|
+
self,
|
|
66
|
+
widget_to_replace: QWidget,
|
|
67
|
+
delete_old: bool = True
|
|
68
|
+
) -> Clickable_label | tuple[Clickable_label]:
|
|
69
|
+
"""
|
|
70
|
+
Replace an existing QWidget with self. Return self if delete_old is True or a tuple (self, replaced_widget) if not.
|
|
71
|
+
"""
|
|
72
|
+
layouts_to_search: list[QLayout] = widget_to_replace.parent().findChildren(QVBoxLayout) + widget_to_replace.parent().findChildren(QHBoxLayout)
|
|
73
|
+
found_layout: None | QHBoxLayout | QVBoxLayout = None
|
|
74
|
+
for layout in layouts_to_search:
|
|
75
|
+
for index in range(layout.count()):
|
|
76
|
+
if widget_to_replace is layout.itemAt(index).widget():
|
|
77
|
+
found_layout = layout
|
|
78
|
+
break
|
|
79
|
+
if found_layout is None:
|
|
80
|
+
raise Exception("clickable_label.replace_widget_in_layout: widget has no layout !")
|
|
81
|
+
else:
|
|
82
|
+
found_layout.replaceWidget(widget_to_replace, self)
|
|
83
|
+
if delete_old:
|
|
84
|
+
widget_to_replace.deleteLater()
|
|
85
|
+
return self
|
|
86
|
+
else:
|
|
87
|
+
return (self, widget_to_replace)
|
|
88
|
+
|
|
89
|
+
def resizeEvent(self, a0: QResizeEvent):
|
|
90
|
+
super().resizeEvent(a0)
|
|
91
|
+
self.set_mask(a0.size(), self._heuristic_mask)
|
|
92
|
+
|
|
93
|
+
def enterEvent(self, event):
|
|
94
|
+
self._hovering = True
|
|
95
|
+
self.setPixmap(self._pixmap_hover)
|
|
96
|
+
super().enterEvent(event)
|
|
97
|
+
|
|
98
|
+
def leaveEvent(self, a0):
|
|
99
|
+
self._hovering = False
|
|
100
|
+
self.setPixmap(self._pixmap)
|
|
101
|
+
super().leaveEvent(a0)
|
|
102
|
+
|
|
103
|
+
def mouseReleaseEvent(self, ev):
|
|
104
|
+
self._on_click_action()
|
|
105
|
+
self.sg_clicked.emit(ev)
|
|
106
|
+
return super().mouseReleaseEvent(ev)
|
|
107
|
+
|
|
108
|
+
def set_mask(self, new_size: QSize | None = None, heuristic_mask: bool=True) -> None:
|
|
109
|
+
new_size = new_size if new_size is not None else self._pixmap_hover.size()
|
|
110
|
+
empty_mask = QPixmap(new_size)
|
|
111
|
+
empty_mask.fill(QColorConstants.Transparent)
|
|
112
|
+
painter = QPainter(empty_mask)
|
|
113
|
+
center_anker_x = (new_size.width()-self._pixmap_hover.size().width())//2
|
|
114
|
+
center_anker_y = (new_size.height()-self._pixmap_hover.size().height())//2
|
|
115
|
+
painter.drawPixmap(center_anker_x, center_anker_y, self._pixmap_hover)
|
|
116
|
+
painter.end()
|
|
117
|
+
if not heuristic_mask:
|
|
118
|
+
self._mask = create_alpha_mask(empty_mask)
|
|
119
|
+
else:
|
|
120
|
+
self._mask = empty_mask.createHeuristicMask(clipTight=False)
|
|
121
|
+
self.setMask(self._mask)
|
|
122
|
+
# self.setMask(empty_mask.createMaskFromColor(QColorConstants.Transparent, Qt.MaskMode.MaskInColor))
|
|
123
|
+
|
|
124
|
+
def replace_on_click_action(self, new_action: Callable) -> None:
|
|
125
|
+
if not isinstance(new_action, Callable):
|
|
126
|
+
raise ValueError(f"utility.custom_widgets.clickable_label.Cliclable_label.replace_on_click_action:\
|
|
127
|
+
Action must be of type 'Callable', is {type(new_action)} instead.")
|
|
128
|
+
self._on_click_action = new_action
|
|
129
|
+
|
|
130
|
+
def configure_pixmaps(self,
|
|
131
|
+
pixmap: str | QPixmap,
|
|
132
|
+
pixmap_hover: str | QPixmap,
|
|
133
|
+
scaled: float = None
|
|
134
|
+
) -> None:
|
|
135
|
+
self._pixmap = QPixmap(pixmap) if isinstance(pixmap,str) else pixmap
|
|
136
|
+
self._pixmap_hover = QPixmap(pixmap_hover) if isinstance(pixmap_hover,str) else pixmap_hover
|
|
137
|
+
if self._pixmap.isNull() or self._pixmap_hover.isNull():
|
|
138
|
+
raise ValueError(f"Failed instantiating pixmap from path: {pixmap} / {pixmap_hover}")
|
|
139
|
+
if isinstance(scaled, Number):
|
|
140
|
+
self._scaled = scaled
|
|
141
|
+
self._pixmap = self._pixmap.scaled(self._pixmap.size()*self._scaled,transformMode=Qt.TransformationMode.SmoothTransformation)
|
|
142
|
+
self._pixmap_hover = self._pixmap_hover.scaled(self._pixmap_hover.size()*self._scaled,transformMode=Qt.TransformationMode.SmoothTransformation)
|
|
143
|
+
elif scaled is None:
|
|
144
|
+
self.setScaledContents(False)
|
|
145
|
+
self.setPixmap(self._pixmap)
|
|
146
|
+
|
|
147
|
+
def scale_pixmap(self, scaled: Number) -> None:
|
|
148
|
+
self._scaled = scaled
|
|
149
|
+
self._pixmap = self._pixmap.scaled(self.size()*self._scaled,transformMode=Qt.TransformationMode.SmoothTransformation)
|
|
150
|
+
self._pixmap_hover = self._pixmap_hover.scaled(self.size()*self._scaled,transformMode=Qt.TransformationMode.SmoothTransformation)
|
|
151
|
+
|
|
152
|
+
def reset(self) -> None:
|
|
153
|
+
self.setPixmap(self._pixmap)
|
|
154
|
+
self._hovering = False
|
|
155
|
+
|
|
156
|
+
class Togglable_clickable_label(QWidget):
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
untoggled_label: Clickable_label,
|
|
161
|
+
toggled_label: Clickable_label,
|
|
162
|
+
toggle_on_click: bool = True,
|
|
163
|
+
parent=None,
|
|
164
|
+
allow_expansion=True
|
|
165
|
+
):
|
|
166
|
+
super().__init__(parent=parent)
|
|
167
|
+
if not (isinstance(untoggled_label, Clickable_label) or isinstance(toggled_label, Clickable_label)):
|
|
168
|
+
raise ValueError(f"label_1 and label_2 need to be of type Clickable_label not {type(untoggled_label), type(toggled_label)}")
|
|
169
|
+
self.untoggled_label = untoggled_label
|
|
170
|
+
self.toggled_label = toggled_label
|
|
171
|
+
size_1 = self.untoggled_label._pixmap_hover
|
|
172
|
+
size_2 = self.toggled_label._pixmap_hover
|
|
173
|
+
if allow_expansion:
|
|
174
|
+
self.setMinimumSize(QSize(size_1.width() if size_1.width() > size_2.width() else size_2.width(),
|
|
175
|
+
size_1.height() if size_1.height() > size_2.height() else size_2.height()))
|
|
176
|
+
else:
|
|
177
|
+
self.setFixedSize(QSize(size_1.width() if size_1.width() > size_2.width() else size_2.width(),
|
|
178
|
+
size_1.height() if size_1.height() > size_2.height() else size_2.height()))
|
|
179
|
+
self._layout = QVBoxLayout(self)
|
|
180
|
+
self._layout.setContentsMargins(0,0,0,0)
|
|
181
|
+
self._layout.addWidget(untoggled_label)
|
|
182
|
+
self._layout.addWidget(toggled_label)
|
|
183
|
+
self.toggled = False
|
|
184
|
+
self.toggle(toggle=self.toggled)
|
|
185
|
+
if toggle_on_click:
|
|
186
|
+
print("TOGGLING")
|
|
187
|
+
self.untoggled_label.sg_clicked.connect(self.toggle)
|
|
188
|
+
self.toggled_label.sg_clicked.connect(self.toggle)
|
|
189
|
+
|
|
190
|
+
def toggle(self, mouse_event: QMouseEvent=None, toggle=None) -> None:
|
|
191
|
+
self.toggled = not self.toggled if toggle is None else toggle
|
|
192
|
+
if self.toggled:
|
|
193
|
+
self.untoggled_label.setVisible(False)
|
|
194
|
+
self.toggled_label.setVisible(True)
|
|
195
|
+
else:
|
|
196
|
+
self.toggled_label.setVisible(False)
|
|
197
|
+
self.untoggled_label.setVisible(True)
|
|
198
|
+
|
|
199
|
+
def replace_on_click_actions(self, action_1: Callable, action_2: Callable) -> None:
|
|
200
|
+
self.untoggled_label.replace_on_click_action(action_1)
|
|
201
|
+
self.toggled_label.replace_on_click_action(action_2)
|
|
202
|
+
|
|
203
|
+
def main():
|
|
204
|
+
import sys
|
|
205
|
+
app = QApplication(sys.argv)
|
|
206
|
+
label = Clickable_label(Image_paths.cogwheel, Image_paths.cogwheel_hover, heuristic_mask=False)
|
|
207
|
+
label.show()
|
|
208
|
+
sys.exit(app.exec())
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
from PyQt6.QtCore import Qt, pyqtSignal, QPropertyAnimation, pyqtProperty, QSize, QPoint, QTimer, QCoreApplication, QEvent
|
|
2
|
+
from PyQt6.QtWidgets import QWidget, QApplication, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QListWidget, QSizePolicy, QSpacerItem, QLayout
|
|
3
|
+
from PyQt6.QtGui import QTransform, QPixmap, QPainter, QPen
|
|
4
|
+
import PyQt6.sip as sip
|
|
5
|
+
|
|
6
|
+
from pyqt_util import clear_layout
|
|
7
|
+
from clickable_label import Clickable_label, Togglable_clickable_label, Image_paths
|
|
8
|
+
|
|
9
|
+
import faulthandler
|
|
10
|
+
faulthandler.enable()
|
|
11
|
+
|
|
12
|
+
class Expandable_widget(QWidget):
|
|
13
|
+
|
|
14
|
+
ANIMATION_DURATION = 150
|
|
15
|
+
sg_animation_finished = pyqtSignal()
|
|
16
|
+
sg_animation_started = pyqtSignal()
|
|
17
|
+
|
|
18
|
+
_horizontal_directions = ("left", "right")
|
|
19
|
+
_vertical_directions = ("up", "down")
|
|
20
|
+
_possible_directions = _horizontal_directions + _vertical_directions
|
|
21
|
+
|
|
22
|
+
def __init__(self,
|
|
23
|
+
direction: str = "left",
|
|
24
|
+
animate_button: bool = False,
|
|
25
|
+
parent=None
|
|
26
|
+
):
|
|
27
|
+
super().__init__(parent=parent)
|
|
28
|
+
if not direction in self._possible_directions:
|
|
29
|
+
raise ValueError(f"Invalid directional argument: {direction}. Valid arguments are: {self._possible_directions} !")
|
|
30
|
+
self.direction = direction
|
|
31
|
+
self._content_widget = QWidget(parent=self)
|
|
32
|
+
self.animation = None
|
|
33
|
+
self._expanded = False
|
|
34
|
+
self._animate_button = animate_button
|
|
35
|
+
self._widget_position_at_animation_start = None
|
|
36
|
+
self.toggle_button = self._create_toggle_button()
|
|
37
|
+
self.header_widget = self._create_header_widget()
|
|
38
|
+
# self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
39
|
+
self._setup()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def expanded(self) -> bool:
|
|
43
|
+
return self._expanded
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content_widget(self) -> QWidget:
|
|
47
|
+
return self._content_widget
|
|
48
|
+
|
|
49
|
+
def set_content_widget(self, widget: QWidget) -> None:
|
|
50
|
+
layout = self.layout()
|
|
51
|
+
if self.content_widget is not None:
|
|
52
|
+
removed_widget = layout.replaceWidget(self.content_widget, widget).widget()
|
|
53
|
+
removed_widget.setParent(None)
|
|
54
|
+
removed_widget.deleteLater()
|
|
55
|
+
else:
|
|
56
|
+
layout.addWidget(widget)
|
|
57
|
+
self._content_widget = widget
|
|
58
|
+
self._setup_animation()
|
|
59
|
+
self._execute_toggle()
|
|
60
|
+
|
|
61
|
+
def _create_toggle_button(self) -> Togglable_clickable_label:
|
|
62
|
+
untoggled_pixmap, untoggled_hover_pixmap, toggled_pixmap, toggled_hover_pixmap = self._create_button_pixmaps()
|
|
63
|
+
expand_button = Clickable_label(untoggled_pixmap, untoggled_hover_pixmap, lambda: self.toggle(True, True), scaled=0.6)
|
|
64
|
+
shrink_button = Clickable_label(toggled_pixmap, toggled_hover_pixmap, lambda: self.toggle(False, True), scaled=0.6)
|
|
65
|
+
toggle_button = Togglable_clickable_label(expand_button, shrink_button)
|
|
66
|
+
return toggle_button
|
|
67
|
+
|
|
68
|
+
def _rotate_pixmaps(self,
|
|
69
|
+
base_pixmap: QPixmap,
|
|
70
|
+
base_hover_pixmap: QPixmap,
|
|
71
|
+
rotation_start: QTransform,
|
|
72
|
+
rotation_end: QTransform
|
|
73
|
+
) -> tuple[QPixmap]:
|
|
74
|
+
untoggled_pixmap = base_pixmap.transformed(rotation_start)
|
|
75
|
+
untoggled_hover_pixmap = base_hover_pixmap.transformed(rotation_start)
|
|
76
|
+
toggled_pixmap = base_pixmap.transformed(rotation_end)
|
|
77
|
+
toggled_hover_pixmap = base_hover_pixmap.transformed(rotation_end)
|
|
78
|
+
return untoggled_pixmap, untoggled_hover_pixmap, toggled_pixmap, toggled_hover_pixmap
|
|
79
|
+
|
|
80
|
+
def _create_rotated_pixmaps_left(self,
|
|
81
|
+
base_pixmap: QPixmap,
|
|
82
|
+
base_hover_pixmap: QPixmap
|
|
83
|
+
) -> tuple[QPixmap]:
|
|
84
|
+
if self._animate_button:
|
|
85
|
+
rotation_start = QTransform().rotate(180)
|
|
86
|
+
rotation_end = QTransform().rotate(0)
|
|
87
|
+
else:
|
|
88
|
+
rotation_start = QTransform().rotate(90)
|
|
89
|
+
rotation_end = QTransform().rotate(180)
|
|
90
|
+
return self._rotate_pixmaps(base_pixmap, base_hover_pixmap, rotation_start, rotation_end)
|
|
91
|
+
|
|
92
|
+
def _create_rotated_pixmaps_right(self,
|
|
93
|
+
base_pixmap: QPixmap,
|
|
94
|
+
base_hover_pixmap: QPixmap
|
|
95
|
+
) -> tuple[QPixmap]:
|
|
96
|
+
if self._animate_button:
|
|
97
|
+
rotation_start = QTransform().rotate(0)
|
|
98
|
+
rotation_end = QTransform().rotate(180)
|
|
99
|
+
else:
|
|
100
|
+
rotation_start = QTransform().rotate(90)
|
|
101
|
+
rotation_end = QTransform().rotate(0)
|
|
102
|
+
return self._rotate_pixmaps(base_pixmap, base_hover_pixmap, rotation_start, rotation_end)
|
|
103
|
+
|
|
104
|
+
def _create_rotated_pixmaps_up(self,
|
|
105
|
+
base_pixmap: QPixmap,
|
|
106
|
+
base_hover_pixmap: QPixmap
|
|
107
|
+
) -> tuple[QPixmap]:
|
|
108
|
+
if self._animate_button:
|
|
109
|
+
rotation_start = QTransform().rotate(270)
|
|
110
|
+
rotation_end = QTransform().rotate(90)
|
|
111
|
+
else:
|
|
112
|
+
rotation_start = QTransform().rotate(0)
|
|
113
|
+
rotation_end = QTransform().rotate(270)
|
|
114
|
+
return self._rotate_pixmaps(base_pixmap, base_hover_pixmap, rotation_start, rotation_end)
|
|
115
|
+
|
|
116
|
+
def _create_rotated_pixmaps_down(self,
|
|
117
|
+
base_pixmap: QPixmap,
|
|
118
|
+
base_hover_pixmap: QPixmap
|
|
119
|
+
) -> tuple[QPixmap]:
|
|
120
|
+
if self._animate_button:
|
|
121
|
+
rotation_start = QTransform().rotate(90)
|
|
122
|
+
rotation_end = QTransform().rotate(270)
|
|
123
|
+
else:
|
|
124
|
+
rotation_start = QTransform().rotate(0)
|
|
125
|
+
rotation_end = QTransform().rotate(90)
|
|
126
|
+
return self._rotate_pixmaps(base_pixmap, base_hover_pixmap, rotation_start, rotation_end)
|
|
127
|
+
|
|
128
|
+
def _create_button_pixmaps(self) -> tuple[QPixmap]:
|
|
129
|
+
base_pixmap = QPixmap(Image_paths.play_right)
|
|
130
|
+
base_hover_pixmap = QPixmap(Image_paths.play_right_hover)
|
|
131
|
+
match self.direction:
|
|
132
|
+
case "left":
|
|
133
|
+
return self._create_rotated_pixmaps_left(base_pixmap, base_hover_pixmap)
|
|
134
|
+
case "right":
|
|
135
|
+
return self._create_rotated_pixmaps_right(base_pixmap, base_hover_pixmap)
|
|
136
|
+
case "up":
|
|
137
|
+
return self._create_rotated_pixmaps_up(base_pixmap, base_hover_pixmap)
|
|
138
|
+
case "down":
|
|
139
|
+
return self._create_rotated_pixmaps_down(base_pixmap, base_hover_pixmap)
|
|
140
|
+
|
|
141
|
+
def _setup_animation(self) -> None:
|
|
142
|
+
if self.animation is not None:
|
|
143
|
+
self.animation.setParent(None)
|
|
144
|
+
self.animation.deleteLater()
|
|
145
|
+
if self.direction in self._vertical_directions:
|
|
146
|
+
self.animation = QPropertyAnimation(self, b"_content_maximum_height")
|
|
147
|
+
else:
|
|
148
|
+
self.animation = QPropertyAnimation(self, b"_content_maximum_width")
|
|
149
|
+
self.animation.setDuration(self.ANIMATION_DURATION)
|
|
150
|
+
self.animation.finished.connect(self.sg_animation_finished)
|
|
151
|
+
self.animation.valueChanged.connect(self._animation_value_changed)
|
|
152
|
+
|
|
153
|
+
@pyqtProperty(int)
|
|
154
|
+
def _content_maximum_width(self) -> int:
|
|
155
|
+
return self.content_widget.maximumWidth()
|
|
156
|
+
|
|
157
|
+
def _set_outer_width_anchor_right(self, outer_w: int):
|
|
158
|
+
g = self.geometry()
|
|
159
|
+
right = g.x() + g.width()
|
|
160
|
+
g.setWidth(outer_w)
|
|
161
|
+
g.moveLeft(right)
|
|
162
|
+
self.setGeometry(g)
|
|
163
|
+
|
|
164
|
+
def _set_outer_height_achor_bottom(self, outer_h: int) -> None:
|
|
165
|
+
g = self.geometry()
|
|
166
|
+
bottom = g.y() + g.bottom()
|
|
167
|
+
g.setHeight(outer_h)
|
|
168
|
+
g.moveTop(bottom)
|
|
169
|
+
self.setGeometry(g)
|
|
170
|
+
|
|
171
|
+
@_content_maximum_width.setter
|
|
172
|
+
def _content_maximum_width(self, new_width: int) -> None:
|
|
173
|
+
self.content_widget.setMaximumWidth(new_width)
|
|
174
|
+
|
|
175
|
+
# Outer width = header + content (+ margins if any)
|
|
176
|
+
header_w = self.header_widget.sizeHint().width() # or actual header widget
|
|
177
|
+
outer_w = header_w + new_width
|
|
178
|
+
|
|
179
|
+
if self.direction == "left":
|
|
180
|
+
self._set_outer_width_anchor_right(outer_w)
|
|
181
|
+
else:
|
|
182
|
+
self.resize(outer_w, self.height()) # natural right-grow
|
|
183
|
+
|
|
184
|
+
@pyqtProperty(int)
|
|
185
|
+
def _content_maximum_height(self) -> int:
|
|
186
|
+
return self.content_widget.maximumHeight()
|
|
187
|
+
|
|
188
|
+
@_content_maximum_height.setter
|
|
189
|
+
def _content_maximum_height(self, new_height: int) -> None:
|
|
190
|
+
self.content_widget.setMaximumHeight(new_height)
|
|
191
|
+
|
|
192
|
+
header_h = self.header_widget.sizeHint().height()
|
|
193
|
+
outer_h = header_h + new_height
|
|
194
|
+
|
|
195
|
+
if self.direction == "up":
|
|
196
|
+
self._set_outer_height_achor_bottom(outer_h)
|
|
197
|
+
else:
|
|
198
|
+
self.resize(self.width(), outer_h)
|
|
199
|
+
|
|
200
|
+
def _create_header_widget(self) -> QWidget:
|
|
201
|
+
widget = QWidget(parent=self)
|
|
202
|
+
horizontal_layout = QHBoxLayout()
|
|
203
|
+
horizontal_layout.addWidget(self.toggle_button)
|
|
204
|
+
horizontal_layout.setContentsMargins(0,0,0,0)
|
|
205
|
+
horizontal_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
|
206
|
+
widget.setLayout(horizontal_layout)
|
|
207
|
+
return widget
|
|
208
|
+
|
|
209
|
+
def _setup(self) -> None:
|
|
210
|
+
self._setup_widget()
|
|
211
|
+
self._setup_layout()
|
|
212
|
+
self._setup_animation()
|
|
213
|
+
|
|
214
|
+
def _setup_widget(self) -> None:
|
|
215
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
|
216
|
+
self.setWindowFlags(Qt.WindowType.Widget)
|
|
217
|
+
self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
|
|
218
|
+
|
|
219
|
+
def _execute_toggle(self, animated=False):
|
|
220
|
+
if animated:
|
|
221
|
+
if self.direction in self._vertical_directions:
|
|
222
|
+
start = self.content_widget.maximumHeight()
|
|
223
|
+
end = max(0,self.content_widget.sizeHint().height()) if self._expanded else 0
|
|
224
|
+
else:
|
|
225
|
+
start = self.content_widget.maximumWidth()
|
|
226
|
+
end = max(0,self.content_widget.sizeHint().width()) if self._expanded else 0
|
|
227
|
+
self.animation.stop()
|
|
228
|
+
self.animation.setStartValue(start)
|
|
229
|
+
if self._expanded:
|
|
230
|
+
self._widget_position_at_animation_start = self.pos()
|
|
231
|
+
self.animation.setEndValue(end)
|
|
232
|
+
self.sg_animation_started.emit()
|
|
233
|
+
self.animation.start()
|
|
234
|
+
else:
|
|
235
|
+
if self.direction in self._vertical_directions:
|
|
236
|
+
height = 0 if not self._expanded else max(0,self.content_widget.sizeHint().height())
|
|
237
|
+
self.content_widget.setMaximumHeight(height)
|
|
238
|
+
else:
|
|
239
|
+
width = max(0,self.content_widget.sizeHint().width()) if self._expanded else 0
|
|
240
|
+
self.content_widget.setMaximumWidth(width)
|
|
241
|
+
|
|
242
|
+
def toggle(self,
|
|
243
|
+
expand: bool,
|
|
244
|
+
animated: bool = False,
|
|
245
|
+
button_toggle: bool | None = None
|
|
246
|
+
) -> None:
|
|
247
|
+
self._expanded = expand
|
|
248
|
+
self._execute_toggle(animated)
|
|
249
|
+
if button_toggle is not None:
|
|
250
|
+
self.toggle_button.toggle(button_toggle)
|
|
251
|
+
|
|
252
|
+
def _animation_value_changed(self, new_max_heigt: QSize) -> None: ...
|
|
253
|
+
# self.layout().activate()
|
|
254
|
+
# self.adjustSize()
|
|
255
|
+
|
|
256
|
+
def _create_layout_left(self) -> QLayout:
|
|
257
|
+
layout = QHBoxLayout()
|
|
258
|
+
layout.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
259
|
+
if self._animate_button:
|
|
260
|
+
layout.addWidget(self.header_widget)
|
|
261
|
+
layout.addWidget(self.content_widget)
|
|
262
|
+
else:
|
|
263
|
+
layout.addWidget(self.content_widget)
|
|
264
|
+
layout.addWidget(self.header_widget)
|
|
265
|
+
return layout
|
|
266
|
+
|
|
267
|
+
def _create_layout_right(self) -> QLayout:
|
|
268
|
+
layout = QHBoxLayout()
|
|
269
|
+
layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
|
270
|
+
if self._animate_button:
|
|
271
|
+
layout.addWidget(self.content_widget)
|
|
272
|
+
layout.addWidget(self.header_widget)
|
|
273
|
+
else:
|
|
274
|
+
layout.addWidget(self.header_widget)
|
|
275
|
+
layout.addWidget(self.content_widget)
|
|
276
|
+
return layout
|
|
277
|
+
|
|
278
|
+
def _create_layout_up(self) -> QLayout:
|
|
279
|
+
layout = QVBoxLayout()
|
|
280
|
+
layout.setAlignment(Qt.AlignmentFlag.AlignBottom)
|
|
281
|
+
if self._animate_button:
|
|
282
|
+
layout.addWidget(self.header_widget)
|
|
283
|
+
layout.addWidget(self.content_widget)
|
|
284
|
+
else:
|
|
285
|
+
layout.addWidget(self.content_widget)
|
|
286
|
+
layout.addWidget(self.header_widget)
|
|
287
|
+
return layout
|
|
288
|
+
|
|
289
|
+
def _create_layout_down(self) -> QLayout:
|
|
290
|
+
layout = QVBoxLayout()
|
|
291
|
+
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
292
|
+
if self._animate_button:
|
|
293
|
+
layout.addWidget(self.content_widget)
|
|
294
|
+
layout.addWidget(self.header_widget)
|
|
295
|
+
else:
|
|
296
|
+
layout.addWidget(self.header_widget)
|
|
297
|
+
layout.addWidget(self.content_widget)
|
|
298
|
+
return layout
|
|
299
|
+
|
|
300
|
+
def _setup_layout(self) -> None:
|
|
301
|
+
match self.direction:
|
|
302
|
+
case "left":
|
|
303
|
+
layout = self._create_layout_left()
|
|
304
|
+
case "right":
|
|
305
|
+
layout = self._create_layout_right()
|
|
306
|
+
case "up":
|
|
307
|
+
layout = self._create_layout_up()
|
|
308
|
+
case "down":
|
|
309
|
+
layout = self._create_layout_down()
|
|
310
|
+
layout.setSpacing(0)
|
|
311
|
+
layout.setContentsMargins(0,0,0,0)
|
|
312
|
+
self.setLayout(layout)
|
|
313
|
+
self._execute_toggle()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class Spoiler_content_widget(QWidget):
|
|
317
|
+
|
|
318
|
+
def __init__(self,
|
|
319
|
+
tree_disabled_widgets: list[QHBoxLayout],
|
|
320
|
+
parent=None
|
|
321
|
+
):
|
|
322
|
+
super().__init__(parent=parent)
|
|
323
|
+
self._tree_disabled_widgets = tree_disabled_widgets
|
|
324
|
+
self._content_offset = 0
|
|
325
|
+
self._line_offset = 0
|
|
326
|
+
self._tree_rendering_enabled = True
|
|
327
|
+
self._setup()
|
|
328
|
+
|
|
329
|
+
def _setup(self) -> None:
|
|
330
|
+
vertical_layout = QVBoxLayout()
|
|
331
|
+
vertical_layout.setContentsMargins(0,0,0,0)
|
|
332
|
+
self.setLayout(vertical_layout)
|
|
333
|
+
|
|
334
|
+
def set_content_offset(self, offset: int) -> None:
|
|
335
|
+
self._content_offset = offset
|
|
336
|
+
self.update()
|
|
337
|
+
|
|
338
|
+
def enable_tree_rendering(self, enable: bool) -> None:
|
|
339
|
+
self._tree_rendering_enabled = enable
|
|
340
|
+
self.update()
|
|
341
|
+
|
|
342
|
+
def set_line_offset(self, offset: int) -> None:
|
|
343
|
+
self._line_offset = offset
|
|
344
|
+
self.update()
|
|
345
|
+
|
|
346
|
+
def paintEvent(self, event):
|
|
347
|
+
if self._tree_rendering_enabled:
|
|
348
|
+
pen = QPen(Qt.GlobalColor.black, 2)
|
|
349
|
+
painter = QPainter(self)
|
|
350
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
351
|
+
painter.setPen(pen)
|
|
352
|
+
line_start = QPoint(self._line_offset,0)
|
|
353
|
+
# get all child widgets inside the layout
|
|
354
|
+
for i in range(self.layout().count()):
|
|
355
|
+
l = self.layout().itemAt(i).layout()
|
|
356
|
+
if not l or l in self._tree_disabled_widgets:
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
# get widget geometry relative to container
|
|
360
|
+
rect = l.geometry()
|
|
361
|
+
center_y = rect.top() + rect.height() // 2
|
|
362
|
+
|
|
363
|
+
# draw arrow from margin → widget
|
|
364
|
+
arrow_end_x = line_start.x()+ max(self._content_offset-self._line_offset,0)
|
|
365
|
+
painter.drawLine(line_start.x(), line_start.y(), line_start.x(), center_y)
|
|
366
|
+
painter.drawLine(line_start.x(), center_y, arrow_end_x, center_y)
|
|
367
|
+
else:
|
|
368
|
+
super().paintEvent(event)
|
|
369
|
+
|
|
370
|
+
class Spoiler_widget(Expandable_widget):
|
|
371
|
+
|
|
372
|
+
_content_widget: Spoiler_content_widget
|
|
373
|
+
|
|
374
|
+
def __init__(self,
|
|
375
|
+
tree_rendering: bool = True,
|
|
376
|
+
parent=None
|
|
377
|
+
):
|
|
378
|
+
super().__init__(direction="down", parent=parent)
|
|
379
|
+
self.sg_animation_started.connect(lambda: setattr(self, "_animation_going", True))
|
|
380
|
+
self.sg_animation_finished.connect(lambda: setattr(self, "_animation_going", False))
|
|
381
|
+
self._layout_widget_mapping: dict[QWidget, QHBoxLayout] = {}
|
|
382
|
+
self._tree_disabled_content: list[QHBoxLayout] = []
|
|
383
|
+
self._animation_going = False
|
|
384
|
+
self._content_offset = 0
|
|
385
|
+
|
|
386
|
+
content_widget = Spoiler_content_widget(self._tree_disabled_content)
|
|
387
|
+
content_widget.set_line_offset(self.toggle_button.sizeHint().width()//2)
|
|
388
|
+
self.set_content_widget(content_widget)
|
|
389
|
+
self.enable_tree_rendering(tree_rendering)
|
|
390
|
+
|
|
391
|
+
def set_content_margins(self, left, top, right, bottom) -> None:
|
|
392
|
+
self.content_widget.layout().setContentsMargins(left, top, right, bottom)
|
|
393
|
+
self.update()
|
|
394
|
+
|
|
395
|
+
def enable_tree_rendering(self, enable: bool) -> None:
|
|
396
|
+
self._content_widget.enable_tree_rendering(enable)
|
|
397
|
+
self.set_content_offset(self.toggle_button.sizeHint().width()//2)
|
|
398
|
+
self.update()
|
|
399
|
+
|
|
400
|
+
def set_content_offset(self, offset: int) -> None:
|
|
401
|
+
self._content_offset = offset
|
|
402
|
+
self._content_widget.set_content_offset(offset)
|
|
403
|
+
self.update()
|
|
404
|
+
|
|
405
|
+
def __iter__(self):
|
|
406
|
+
return iter(self._layout_widget_mapping.keys())
|
|
407
|
+
|
|
408
|
+
def count(self) -> int:
|
|
409
|
+
return len(self._layout_widget_mapping)
|
|
410
|
+
|
|
411
|
+
def add_header_widget(self,
|
|
412
|
+
widget: QWidget
|
|
413
|
+
) -> None:
|
|
414
|
+
self.header_widget.layout().addWidget(widget)
|
|
415
|
+
|
|
416
|
+
def _schedule_geo_update(self):
|
|
417
|
+
if not self._expanded:
|
|
418
|
+
return
|
|
419
|
+
QTimer.singleShot(0, self._run_geo_update)
|
|
420
|
+
|
|
421
|
+
def _run_geo_update(self) -> None:
|
|
422
|
+
if self._animation_going:
|
|
423
|
+
return
|
|
424
|
+
self._update_content_geometries()
|
|
425
|
+
|
|
426
|
+
def eventFilter(self, obj, event):
|
|
427
|
+
if event.type() in (QEvent.Type.LayoutRequest, QEvent.Type.Resize):
|
|
428
|
+
self._schedule_geo_update()
|
|
429
|
+
return super().eventFilter(obj, event)
|
|
430
|
+
|
|
431
|
+
def add_widget(self,
|
|
432
|
+
widget: QWidget,
|
|
433
|
+
at_index: int | None = None,
|
|
434
|
+
disable_tree: bool = False
|
|
435
|
+
) -> None:
|
|
436
|
+
layout = QHBoxLayout()
|
|
437
|
+
# layout.insertItem(0, QSpacerItem(self.toggle_button.sizeHint().width()+15 , 0))
|
|
438
|
+
spacer = QSpacerItem(self._content_offset , 0)
|
|
439
|
+
layout.insertItem(0, spacer)
|
|
440
|
+
layout.addWidget(widget)
|
|
441
|
+
widget.installEventFilter(self)
|
|
442
|
+
if at_index is None:
|
|
443
|
+
self.content_widget.layout().addLayout(layout)
|
|
444
|
+
else:
|
|
445
|
+
self.content_widget.layout().insertLayout(at_index, layout)
|
|
446
|
+
if self._expanded:
|
|
447
|
+
QTimer.singleShot(0, self._update_content_geometries)
|
|
448
|
+
self._layout_widget_mapping[widget] = layout
|
|
449
|
+
if disable_tree:
|
|
450
|
+
self._tree_disabled_content.append(layout)
|
|
451
|
+
|
|
452
|
+
def remove_widget(self,
|
|
453
|
+
widget: QWidget
|
|
454
|
+
) -> QWidget:
|
|
455
|
+
layout = self._layout_widget_mapping[widget]
|
|
456
|
+
del self._layout_widget_mapping[widget]
|
|
457
|
+
clear_layout(layout)
|
|
458
|
+
layout.setParent(None)
|
|
459
|
+
layout.deleteLater()
|
|
460
|
+
if layout in self._tree_disabled_content:
|
|
461
|
+
self._tree_disabled_content.remove(layout)
|
|
462
|
+
|
|
463
|
+
def _update_content_geometries(self) -> None:
|
|
464
|
+
#self.content_widget.updateGeometry()
|
|
465
|
+
# self.content_widget.adjustSize()
|
|
466
|
+
self.content_widget.setMaximumHeight(self.content_widget.sizeHint().height())
|
|
467
|
+
self.update()
|
|
468
|
+
|
|
469
|
+
# def sizeHint(self):
|
|
470
|
+
# header_h = self.header_widget.size().height()
|
|
471
|
+
# content_h = self.content_widget.maximumHeight()
|
|
472
|
+
# return QSize(
|
|
473
|
+
# max(self.header_widget.sizeHint().width(), self.content_widget.sizeHint().width()),
|
|
474
|
+
# header_h + content_h
|
|
475
|
+
# )
|
|
476
|
+
|
|
477
|
+
def main():
|
|
478
|
+
import sys
|
|
479
|
+
app = QApplication(sys.argv)
|
|
480
|
+
spoiler = Expandable_widget(direction="down", animate_button=False)
|
|
481
|
+
spoiler_2 = Spoiler_widget()
|
|
482
|
+
spoiler_2.add_widget(QLabel("JOOO"))
|
|
483
|
+
# spoiler = Spoiler_widget()
|
|
484
|
+
# spoiler.add_header_widget(QLabel("I bims 1 spoiler !"))
|
|
485
|
+
# for i in range(5):
|
|
486
|
+
# spoiler.add_content_widget(QLabel(f"Label_{i}"))
|
|
487
|
+
spoiler.set_content_widget(QLabel("I bims 1 Label"))
|
|
488
|
+
spoiler.show()
|
|
489
|
+
#spoiler_2.show()
|
|
490
|
+
sys.exit(app.exec())
|
|
491
|
+
|
|
492
|
+
if __name__ == "__main__":
|
|
493
|
+
main()
|
|
494
|
+
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class Image_paths:
|
|
4
|
+
__relative_source_path__ = os.path.join(os.path.dirname(__file__), "icons\\")
|
|
5
|
+
_green_plus_mask = __relative_source_path__+"green_plus_mask.png"
|
|
6
|
+
apply = __relative_source_path__+"apply_button.png"
|
|
7
|
+
apply_hover = __relative_source_path__+"apply_button_hover.png"
|
|
8
|
+
decline: str = __relative_source_path__+"cancel_button.png"
|
|
9
|
+
decline_hover: str = __relative_source_path__+"cancel_button_hover.png"
|
|
10
|
+
green_lamp: str = __relative_source_path__+"enabled_sending.png"
|
|
11
|
+
green_lamp_hover: str = __relative_source_path__+"endabled_sending_hover.png"
|
|
12
|
+
red_lamp: str = __relative_source_path__+"disabled_sending.png"
|
|
13
|
+
red_lamp_hover: str = __relative_source_path__+"disabled_sending_hover.png"
|
|
14
|
+
grey_lamp: str = __relative_source_path__+"enabled_notSending.png"
|
|
15
|
+
grey_lamp_hover: str = __relative_source_path__+"disabled_notSending.png"
|
|
16
|
+
yellow_lamp: str = __relative_source_path__+"connectionPending.png"
|
|
17
|
+
settings: str = __relative_source_path__+"settings.png"
|
|
18
|
+
load = __relative_source_path__+"load_button.png"
|
|
19
|
+
load_test = __relative_source_path__+"load_button_test.png"
|
|
20
|
+
load_hover = __relative_source_path__+"load_button_hover.png"
|
|
21
|
+
save = __relative_source_path__+"save_button.png"
|
|
22
|
+
save_hover = __relative_source_path__+"save_button_hover.png"
|
|
23
|
+
menu = __relative_source_path__+"menu.png"
|
|
24
|
+
menu_hover = __relative_source_path__+"menu_hover.png"
|
|
25
|
+
power_off = __relative_source_path__+"power_off.png"
|
|
26
|
+
power_off_small = __relative_source_path__+"power_off_small.png"
|
|
27
|
+
power_off_hover = __relative_source_path__+"power_off_hover.png"
|
|
28
|
+
power_off_small_hover = __relative_source_path__+"power_off_small_hover.png"
|
|
29
|
+
power_on_small = __relative_source_path__+"power_on_small.png"
|
|
30
|
+
power_on_hover = __relative_source_path__+"power_on_hover.png"
|
|
31
|
+
power_on_small_hover = __relative_source_path__+"power_on_small_hover.png"
|
|
32
|
+
power_on = __relative_source_path__+"power_on.png"
|
|
33
|
+
power_on_hover = __relative_source_path__+"power_on_hover.png"
|
|
34
|
+
plug_settings = __relative_source_path__+"plug_settings.png"
|
|
35
|
+
plug_settings_hover = __relative_source_path__+"plug_settings_hover.png"
|
|
36
|
+
drag_cursor = __relative_source_path__+"drag_series_device_ui_cursor.png"
|
|
37
|
+
cogwheel = __relative_source_path__+"cogwheel.png"
|
|
38
|
+
cogwheel_hover = __relative_source_path__+"cogwheel_hover.png"
|
|
39
|
+
BAR = __relative_source_path__+"dark_sun.png"
|
|
40
|
+
new_node = __relative_source_path__+"new_node_icon.png"
|
|
41
|
+
new_node_hover = __relative_source_path__+"new_node_icon_hover.png"
|
|
42
|
+
new_action_hover = __relative_source_path__+"new_action_hover.png"
|
|
43
|
+
new_node_plus = __relative_source_path__+"new_node_icon_plus.png"
|
|
44
|
+
new_node_plus_hover = __relative_source_path__+"new_node_icon_plus_hover.png"
|
|
45
|
+
trash = __relative_source_path__+"trash_icon.png"
|
|
46
|
+
trash_hover = __relative_source_path__+"trash_icon_hover.png"
|
|
47
|
+
m_button = __relative_source_path__+"m_button.png"
|
|
48
|
+
m_button_hover = __relative_source_path__+"m_button_hover.png"
|
|
49
|
+
blue_plus = __relative_source_path__+"blue_plus.png"
|
|
50
|
+
blue_plus_hover = __relative_source_path__+"blue_plus_hover.png"
|
|
51
|
+
green_plus = __relative_source_path__+"green_plus.png"
|
|
52
|
+
green_plus_hover = __relative_source_path__+"green_plus_hover.png"
|
|
53
|
+
red_plus = __relative_source_path__+"red_plus.png"
|
|
54
|
+
red_plus_hover = __relative_source_path__+"red_plus_hover.png"
|
|
55
|
+
blue_minus = __relative_source_path__+"blue_minus.png"
|
|
56
|
+
blue_minus_hover = __relative_source_path__+"blue_minus_hover.png"
|
|
57
|
+
green_minus = __relative_source_path__+"green_minus.png"
|
|
58
|
+
green_minus_hover = __relative_source_path__+"green_minus_hover.png"
|
|
59
|
+
red_minus = __relative_source_path__+"red_minus.png"
|
|
60
|
+
red_minus_hover = __relative_source_path__+"red_minus_hover.png"
|
|
61
|
+
question_mark = __relative_source_path__+"question_mark.png"
|
|
62
|
+
question_mark_hover = __relative_source_path__+"question_mark_hover.png"
|
|
63
|
+
eye_open = __relative_source_path__+"eye_open.png"
|
|
64
|
+
eye_open_hover = __relative_source_path__+"eye_open_hover.png"
|
|
65
|
+
eye_filled_open = __relative_source_path__+"filled_eye_open.png"
|
|
66
|
+
eye_filled_open_hover = __relative_source_path__+"filled_eye_open_hover.png"
|
|
67
|
+
eye_filled_crossed = __relative_source_path__+"filled_eye_crossed.png"
|
|
68
|
+
eye_filled_crossed_hover = __relative_source_path__+"filled_eye_crossed_hover.png"
|
|
69
|
+
color_wheel = __relative_source_path__+"color_wheel.png"
|
|
70
|
+
color_wheel_hover = __relative_source_path__+"color_wheel_hover.png"
|
|
71
|
+
m_button = __relative_source_path__+"m_button.png"
|
|
72
|
+
m_button_hover = __relative_source_path__+"m_button_hover.png"
|
|
73
|
+
blue_plus = __relative_source_path__+"blue_plus.png"
|
|
74
|
+
blue_plus_hover = __relative_source_path__+"blue_plus_hover.png"
|
|
75
|
+
question_mark = __relative_source_path__+"question_mark.png"
|
|
76
|
+
question_mark_hover = __relative_source_path__+"question_mark_hover.png"
|
|
77
|
+
eye_open = __relative_source_path__+"eye_open.png"
|
|
78
|
+
eye_open_hover = __relative_source_path__+"eye_open_hover.png"
|
|
79
|
+
eye_filled_open = __relative_source_path__+"filled_eye_open.png"
|
|
80
|
+
eye_filled_open_hover = __relative_source_path__+"filled_eye_open_hover.png"
|
|
81
|
+
eye_filled_crossed = __relative_source_path__+"filled_eye_crossed.png"
|
|
82
|
+
eye_filled_crossed_hover = __relative_source_path__+"filled_eye_crossed_hover.png"
|
|
83
|
+
color_wheel = __relative_source_path__+"color_wheel.png"
|
|
84
|
+
color_wheel_hover = __relative_source_path__+"color_wheel_hover.png"
|
|
85
|
+
thickness = __relative_source_path__+"thickness_icon.png"
|
|
86
|
+
thickness_hover = __relative_source_path__+"thickness_icon_hover.png"
|
|
87
|
+
left_sided_thickness_arrow = __relative_source_path__+"left_sided_thickness_arrow_icon.png"
|
|
88
|
+
right_sided_thickness_arrow = __relative_source_path__+"right_sided_thickness_arrow_icon.png"
|
|
89
|
+
circle_icon = __relative_source_path__+"circle_icon.png"
|
|
90
|
+
circle_icon_hover = __relative_source_path__+"circle_icon_hover.png"
|
|
91
|
+
line_icon = __relative_source_path__+"line_icon.png"
|
|
92
|
+
line_icon_hover = __relative_source_path__+"line_icon_hover.png"
|
|
93
|
+
crosshair_icon = __relative_source_path__+"crosshair_icon.png"
|
|
94
|
+
crosshair_icon_hover = __relative_source_path__+"crosshair_icon_hover.png"
|
|
95
|
+
align_center_icon = __relative_source_path__+"align_center_icon.png"
|
|
96
|
+
align_center_icon_hover = __relative_source_path__+"align_center_icon_hover.png"
|
|
97
|
+
inner_area = __relative_source_path__+"inner_background_2.png"
|
|
98
|
+
outer_ring = __relative_source_path__+"outer_ring_3.png"
|
|
99
|
+
house_icon = __relative_source_path__+"house_icon.png"
|
|
100
|
+
house_icon_hover = __relative_source_path__+"house_icon_hover.png"
|
|
101
|
+
play_icon = __relative_source_path__+"play.png"
|
|
102
|
+
play_icon_hover = __relative_source_path__+"play_hover.png"
|
|
103
|
+
pause_icon = __relative_source_path__+"pause.png"
|
|
104
|
+
pause_icon_hover = __relative_source_path__+"pause_hover.png"
|
|
105
|
+
stop_icon = __relative_source_path__+"stop.png"
|
|
106
|
+
stop_icon_hover = __relative_source_path__+"stop_hover.png"
|
|
107
|
+
stop_icon_2 = __relative_source_path__+"stop_icon.png"
|
|
108
|
+
stop_icon_2_hover = __relative_source_path__+"stop_icon_hover.png"
|
|
109
|
+
lcd_background = __relative_source_path__+"lcd_background.png"
|
|
110
|
+
magnifying_glass = __relative_source_path__+"magnifying_glass.png"
|
|
111
|
+
magnifying_glass_hover = __relative_source_path__+"magnifying_glass_hover.png"
|
|
112
|
+
play_right = __relative_source_path__+"play_right.png"
|
|
113
|
+
play_right_hover = __relative_source_path__+"play_right_hover.png"
|
|
114
|
+
play_left = __relative_source_path__+"play_left.png"
|
|
115
|
+
play_left_hover = __relative_source_path__+"play_left_hover.png"
|
|
116
|
+
starwars_stop = __relative_source_path__+"starwars_stop.png"
|
|
117
|
+
stop_3 = __relative_source_path__+"stop_3.png"
|
|
118
|
+
stop_3_hover = __relative_source_path__+"stop_3_hover.png"
|
|
119
|
+
arrows_outwards = __relative_source_path__+"arrows_outwards_icon.png"
|
|
120
|
+
arrows_outwards_hover = __relative_source_path__+"arrows_outwards_hover_icon.png"
|
|
121
|
+
green_circular_arrow = __relative_source_path__+"green_circular_arrow.png"
|
|
122
|
+
green_circular_arrow_hover = __relative_source_path__+"green_circular_arrow_hover.png"
|
|
123
|
+
grey_grid = __relative_source_path__+"grey_grid.png"
|
|
124
|
+
grey_grid_hover = __relative_source_path__+"grey_grid_hover.png"
|
|
125
|
+
green_grid = __relative_source_path__+"green_grid.png"
|
|
126
|
+
green_grid_hover = __relative_source_path__+"green_grid_hover.png"
|
|
127
|
+
eye_colorful = __relative_source_path__+"eye_colorful.png"
|
|
128
|
+
eye_colorful_hover = __relative_source_path__+"eye_colorful_hover.png"
|
|
129
|
+
eye_crossed = __relative_source_path__+"eye_crossed.png"
|
|
130
|
+
eye_crossed_hover = __relative_source_path__+"eye_crossed_hover.png"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QWidget, QGraphicsOpacityEffect, QLayout, QMainWindow
|
|
2
|
+
from PyQt6.QtGui import QPainter, QColor, QColorConstants, QPen, QGuiApplication
|
|
3
|
+
from PyQt6.QtCore import QRect, pyqtBoundSignal
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Generator, TypeVar
|
|
6
|
+
|
|
7
|
+
def set_font_size(widget: QWidget, font_size: int) -> None:
|
|
8
|
+
font = widget.font()
|
|
9
|
+
font.setPointSize(font_size)
|
|
10
|
+
widget.setFont(font)
|
|
11
|
+
|
|
12
|
+
def center_window(win: QMainWindow):
|
|
13
|
+
screen = QGuiApplication.primaryScreen()
|
|
14
|
+
geo = screen.availableGeometry()
|
|
15
|
+
win_geo = win.frameGeometry()
|
|
16
|
+
win_geo.moveCenter(geo.center())
|
|
17
|
+
win.move(win_geo.topLeft())
|
|
18
|
+
|
|
19
|
+
def disconnect_all(signal: pyqtBoundSignal) -> None:
|
|
20
|
+
signals = True
|
|
21
|
+
while(signals):
|
|
22
|
+
try:
|
|
23
|
+
signal.disconnect()
|
|
24
|
+
except TypeError:
|
|
25
|
+
signals = False
|
|
26
|
+
|
|
27
|
+
def retain_size_when_hidden(widget: QWidget, retain_size: bool) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Determines whether a widget still occupies space when its visibility is set to False.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
widget (QWidget): A QWidget.
|
|
33
|
+
retain_size (bool): Do you want the widget to occupy space when being invisibe ?.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
None
|
|
37
|
+
"""
|
|
38
|
+
policy = widget.sizePolicy()
|
|
39
|
+
policy.setRetainSizeWhenHidden(retain_size)
|
|
40
|
+
widget.setSizePolicy(policy)
|
|
41
|
+
|
|
42
|
+
TWidget = TypeVar("TWidget", bound=QWidget)
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def inactive_signaling(
|
|
46
|
+
widget: TWidget,
|
|
47
|
+
) -> Generator[TWidget, None, None]:
|
|
48
|
+
try:
|
|
49
|
+
widget.blockSignals(True)
|
|
50
|
+
yield widget
|
|
51
|
+
finally:
|
|
52
|
+
widget.blockSignals(False)
|
|
53
|
+
|
|
54
|
+
def set_widget_opacity(
|
|
55
|
+
widget: QWidget,
|
|
56
|
+
value: float
|
|
57
|
+
) -> None:
|
|
58
|
+
"""value between 0.0 (fully transparent) and 1.0 (fully opaque)."""
|
|
59
|
+
effect = widget.graphicsEffect()
|
|
60
|
+
if not isinstance(effect, QGraphicsOpacityEffect):
|
|
61
|
+
effect = QGraphicsOpacityEffect(widget)
|
|
62
|
+
widget.setGraphicsEffect(effect)
|
|
63
|
+
effect.setOpacity(value)
|
|
64
|
+
|
|
65
|
+
def widget_in_layout(widget, layout: QLayout) -> bool:
|
|
66
|
+
"""Check if a widget is in the given layout."""
|
|
67
|
+
for i in range(layout.count()): # Iterate through the layout's items
|
|
68
|
+
item = layout.itemAt(i)
|
|
69
|
+
if item.widget() == widget: # Check if the widget matches
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def add_rect_paint_to_widget(widget: QWidget, thickness: int = 2, color: QColor = QColorConstants.Red) -> None:
|
|
74
|
+
"""
|
|
75
|
+
This does not have the expected effect on Clickable_labels or any other widget with custom masking.
|
|
76
|
+
"""
|
|
77
|
+
paintEvent = widget.paintEvent
|
|
78
|
+
def paintRect(event):
|
|
79
|
+
painter = QPainter(widget)
|
|
80
|
+
pen = QPen()
|
|
81
|
+
pen.setWidth(thickness)
|
|
82
|
+
pen.setColor(color)
|
|
83
|
+
inset = int(pen.widthF() / 2.0)
|
|
84
|
+
painter.setPen(pen)
|
|
85
|
+
painter.drawRect(widget.rect().adjusted(inset, inset, -inset, -inset))
|
|
86
|
+
paintEvent(event)
|
|
87
|
+
widget.paintEvent = paintRect
|
|
88
|
+
|
|
89
|
+
def clear_layout(layout: QLayout) -> None:
|
|
90
|
+
while layout.count() > 0:
|
|
91
|
+
item = layout.takeAt(0)
|
|
92
|
+
if item.widget():
|
|
93
|
+
item.widget().deleteLater()
|
|
94
|
+
elif item.layout():
|
|
95
|
+
clear_layout(item.layout())
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: michis_python_sammlung
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Collection of mostly QT related stuff
|
|
5
|
+
Author-email: Michael Mischko <mischkom@web.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mischkomichael/Michis_python_sammlung
|
|
8
|
+
Project-URL: Issues, https://github.com/mischkomichael/Michis_python_sammlung/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/michis_python_sammlung/__init__.py
|
|
5
|
+
src/michis_python_sammlung.egg-info/PKG-INFO
|
|
6
|
+
src/michis_python_sammlung.egg-info/SOURCES.txt
|
|
7
|
+
src/michis_python_sammlung.egg-info/dependency_links.txt
|
|
8
|
+
src/michis_python_sammlung.egg-info/top_level.txt
|
|
9
|
+
src/michis_python_sammlung/pyqt/__init__.py
|
|
10
|
+
src/michis_python_sammlung/pyqt/clickable_label.py
|
|
11
|
+
src/michis_python_sammlung/pyqt/expandable_widget.py
|
|
12
|
+
src/michis_python_sammlung/pyqt/image_paths.py
|
|
13
|
+
src/michis_python_sammlung/pyqt/pyqt_util.py
|
|
14
|
+
src/michis_python_sammlung/pyqt/icons/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
michis_python_sammlung
|