Python-FastUI-Widgets 1.0.0__py3-none-any.whl
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.
- fastuiwidgets/__init__.py +12 -0
- fastuiwidgets/_rc/__init__.py +0 -0
- fastuiwidgets/_rc/resource.py +98835 -0
- fastuiwidgets/common/__init__.py +12 -0
- fastuiwidgets/common/animation.py +530 -0
- fastuiwidgets/common/auto_wrap.py +164 -0
- fastuiwidgets/common/color.py +95 -0
- fastuiwidgets/common/config.py +423 -0
- fastuiwidgets/common/exception_handler.py +31 -0
- fastuiwidgets/common/font.py +38 -0
- fastuiwidgets/common/icon.py +703 -0
- fastuiwidgets/common/image_utils.py +198 -0
- fastuiwidgets/common/overload.py +47 -0
- fastuiwidgets/common/router.py +133 -0
- fastuiwidgets/common/screen.py +25 -0
- fastuiwidgets/common/smooth_scroll.py +141 -0
- fastuiwidgets/common/style_sheet.py +512 -0
- fastuiwidgets/common/theme_listener.py +27 -0
- fastuiwidgets/common/translator.py +14 -0
- fastuiwidgets/components/__init__.py +6 -0
- fastuiwidgets/components/date_time/__init__.py +4 -0
- fastuiwidgets/components/date_time/calendar_picker.py +121 -0
- fastuiwidgets/components/date_time/calendar_view.py +671 -0
- fastuiwidgets/components/date_time/date_picker.py +245 -0
- fastuiwidgets/components/date_time/fast_calendar_view.py +487 -0
- fastuiwidgets/components/date_time/picker_base.py +632 -0
- fastuiwidgets/components/date_time/time_picker.py +223 -0
- fastuiwidgets/components/dialog_box/__init__.py +6 -0
- fastuiwidgets/components/dialog_box/color_dialog.py +414 -0
- fastuiwidgets/components/dialog_box/dialog.py +167 -0
- fastuiwidgets/components/dialog_box/folder_list_dialog.py +307 -0
- fastuiwidgets/components/dialog_box/mask_dialog_base.py +120 -0
- fastuiwidgets/components/dialog_box/message_box_base.py +92 -0
- fastuiwidgets/components/dialog_box/message_dialog.py +65 -0
- fastuiwidgets/components/layout/__init__.py +3 -0
- fastuiwidgets/components/layout/expand_layout.py +96 -0
- fastuiwidgets/components/layout/flow_layout.py +236 -0
- fastuiwidgets/components/layout/v_box_layout.py +41 -0
- fastuiwidgets/components/material/__init__.py +6 -0
- fastuiwidgets/components/material/acrylic_combo_box.py +96 -0
- fastuiwidgets/components/material/acrylic_flyout.py +105 -0
- fastuiwidgets/components/material/acrylic_line_edit.py +27 -0
- fastuiwidgets/components/material/acrylic_menu.py +204 -0
- fastuiwidgets/components/material/acrylic_tool_tip.py +39 -0
- fastuiwidgets/components/material/acrylic_widget.py +42 -0
- fastuiwidgets/components/navigation/__init__.py +9 -0
- fastuiwidgets/components/navigation/breadcrumb.py +350 -0
- fastuiwidgets/components/navigation/navigation_bar.py +416 -0
- fastuiwidgets/components/navigation/navigation_interface.py +268 -0
- fastuiwidgets/components/navigation/navigation_panel.py +657 -0
- fastuiwidgets/components/navigation/navigation_widget.py +686 -0
- fastuiwidgets/components/navigation/pivot.py +272 -0
- fastuiwidgets/components/navigation/segmented_widget.py +174 -0
- fastuiwidgets/components/settings/__init__.py +8 -0
- fastuiwidgets/components/settings/custom_color_setting_card.py +139 -0
- fastuiwidgets/components/settings/expand_setting_card.py +390 -0
- fastuiwidgets/components/settings/folder_list_setting_card.py +134 -0
- fastuiwidgets/components/settings/options_setting_card.py +86 -0
- fastuiwidgets/components/settings/setting_card.py +449 -0
- fastuiwidgets/components/settings/setting_card_group.py +48 -0
- fastuiwidgets/components/widgets/__init__.py +41 -0
- fastuiwidgets/components/widgets/acrylic_label.py +261 -0
- fastuiwidgets/components/widgets/button.py +1059 -0
- fastuiwidgets/components/widgets/card_widget.py +369 -0
- fastuiwidgets/components/widgets/check_box.py +203 -0
- fastuiwidgets/components/widgets/combo_box.py +556 -0
- fastuiwidgets/components/widgets/command_bar.py +636 -0
- fastuiwidgets/components/widgets/cycle_list_widget.py +251 -0
- fastuiwidgets/components/widgets/flip_view.py +430 -0
- fastuiwidgets/components/widgets/flyout.py +521 -0
- fastuiwidgets/components/widgets/frameless_window.py +49 -0
- fastuiwidgets/components/widgets/icon_widget.py +53 -0
- fastuiwidgets/components/widgets/info_badge.py +483 -0
- fastuiwidgets/components/widgets/info_bar.py +596 -0
- fastuiwidgets/components/widgets/label.py +553 -0
- fastuiwidgets/components/widgets/line_edit.py +551 -0
- fastuiwidgets/components/widgets/list_view.py +158 -0
- fastuiwidgets/components/widgets/menu.py +1318 -0
- fastuiwidgets/components/widgets/pips_pager.py +331 -0
- fastuiwidgets/components/widgets/progress_bar.py +311 -0
- fastuiwidgets/components/widgets/progress_ring.py +212 -0
- fastuiwidgets/components/widgets/scroll_area.py +125 -0
- fastuiwidgets/components/widgets/scroll_bar.py +673 -0
- fastuiwidgets/components/widgets/separator.py +43 -0
- fastuiwidgets/components/widgets/slider.py +307 -0
- fastuiwidgets/components/widgets/spin_box.py +306 -0
- fastuiwidgets/components/widgets/stacked_widget.py +211 -0
- fastuiwidgets/components/widgets/state_tool_tip.py +188 -0
- fastuiwidgets/components/widgets/switch_button.py +312 -0
- fastuiwidgets/components/widgets/tab_view.py +804 -0
- fastuiwidgets/components/widgets/table_view.py +360 -0
- fastuiwidgets/components/widgets/teaching_tip.py +657 -0
- fastuiwidgets/components/widgets/tool_tip.py +460 -0
- fastuiwidgets/components/widgets/tree_view.py +216 -0
- fastuiwidgets/multimedia/__init__.py +3 -0
- fastuiwidgets/multimedia/media_play_bar.py +319 -0
- fastuiwidgets/multimedia/media_player.py +124 -0
- fastuiwidgets/multimedia/video_widget.py +93 -0
- fastuiwidgets/window/__init__.py +2 -0
- fastuiwidgets/window/fluent_window.py +413 -0
- fastuiwidgets/window/splash_screen.py +92 -0
- fastuiwidgets/window/stacked_widget.py +66 -0
- python_fastui_widgets-1.0.0.dist-info/METADATA +30 -0
- python_fastui_widgets-1.0.0.dist-info/RECORD +107 -0
- python_fastui_widgets-1.0.0.dist-info/WHEEL +5 -0
- python_fastui_widgets-1.0.0.dist-info/licenses/LICENSE +674 -0
- python_fastui_widgets-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from math import floor
|
3
|
+
from io import BytesIO
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
from colorthief import ColorThief
|
8
|
+
from PIL import Image
|
9
|
+
from PySide6.QtGui import QImage, QPixmap
|
10
|
+
from PySide6.QtCore import QIODevice, QBuffer
|
11
|
+
from scipy.ndimage.filters import gaussian_filter
|
12
|
+
|
13
|
+
from .exception_handler import exceptionHandler
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
def gaussianBlur(image, blurRadius=18, brightFactor=1, blurPicSize= None):
|
18
|
+
if isinstance(image, str) and not image.startswith(':'):
|
19
|
+
image = Image.open(image)
|
20
|
+
else:
|
21
|
+
image = fromqpixmap(QPixmap(image))
|
22
|
+
|
23
|
+
if blurPicSize:
|
24
|
+
# adjust image size to reduce computation
|
25
|
+
w, h = image.size
|
26
|
+
ratio = min(blurPicSize[0] / w, blurPicSize[1] / h)
|
27
|
+
w_, h_ = w * ratio, h * ratio
|
28
|
+
|
29
|
+
if w_ < w:
|
30
|
+
image = image.resize((int(w_), int(h_)), Image.ANTIALIAS)
|
31
|
+
|
32
|
+
image = np.array(image)
|
33
|
+
|
34
|
+
# handle gray image
|
35
|
+
if len(image.shape) == 2:
|
36
|
+
image = np.stack([image, image, image], axis=-1)
|
37
|
+
|
38
|
+
# blur each channel
|
39
|
+
for i in range(3):
|
40
|
+
image[:, :, i] = gaussian_filter(
|
41
|
+
image[:, :, i], blurRadius) * brightFactor
|
42
|
+
|
43
|
+
# convert ndarray to QPixmap
|
44
|
+
h, w, c = image.shape
|
45
|
+
if c == 3:
|
46
|
+
format = QImage.Format_RGB888
|
47
|
+
else:
|
48
|
+
format = QImage.Format_RGBA8888
|
49
|
+
|
50
|
+
return QPixmap.fromImage(QImage(image.data, w, h, c*w, format))
|
51
|
+
|
52
|
+
|
53
|
+
# https://github.com/python-pillow/Pillow/blob/main/src/PIL/ImageQt.py
|
54
|
+
def fromqpixmap(im: Union[QImage, QPixmap]):
|
55
|
+
"""
|
56
|
+
:param im: QImage or PIL ImageQt object
|
57
|
+
"""
|
58
|
+
buffer = QBuffer()
|
59
|
+
buffer.open(QIODevice.OpenModeFlag.ReadWrite)
|
60
|
+
|
61
|
+
# preserve alpha channel with png
|
62
|
+
# otherwise ppm is more friendly with Image.open
|
63
|
+
if im.hasAlphaChannel():
|
64
|
+
im.save(buffer, "png")
|
65
|
+
else:
|
66
|
+
im.save(buffer, "ppm")
|
67
|
+
|
68
|
+
b = BytesIO()
|
69
|
+
b.write(buffer.data())
|
70
|
+
buffer.close()
|
71
|
+
b.seek(0)
|
72
|
+
|
73
|
+
return Image.open(b)
|
74
|
+
|
75
|
+
|
76
|
+
class DominantColor:
|
77
|
+
""" Dominant color class """
|
78
|
+
|
79
|
+
@classmethod
|
80
|
+
@exceptionHandler((24, 24, 24))
|
81
|
+
def getDominantColor(cls, imagePath):
|
82
|
+
""" extract dominant color from image
|
83
|
+
|
84
|
+
Parameters
|
85
|
+
----------
|
86
|
+
imagePath: str
|
87
|
+
image path
|
88
|
+
|
89
|
+
Returns
|
90
|
+
-------
|
91
|
+
r, g, b: int
|
92
|
+
gray value of each color channel
|
93
|
+
"""
|
94
|
+
if imagePath.startswith(':'):
|
95
|
+
return (24, 24, 24)
|
96
|
+
|
97
|
+
colorThief = ColorThief(imagePath)
|
98
|
+
|
99
|
+
# scale image to speed up the computation speed
|
100
|
+
if max(colorThief.image.size) > 400:
|
101
|
+
colorThief.image = colorThief.image.resize((400, 400))
|
102
|
+
|
103
|
+
palette = colorThief.get_palette(quality=9)
|
104
|
+
|
105
|
+
# adjust the brightness of palette
|
106
|
+
palette = cls.__adjustPaletteValue(palette)
|
107
|
+
for rgb in palette[:]:
|
108
|
+
h, s, v = cls.rgb2hsv(rgb)
|
109
|
+
if h < 0.02:
|
110
|
+
palette.remove(rgb)
|
111
|
+
if len(palette) <= 2:
|
112
|
+
break
|
113
|
+
|
114
|
+
palette = palette[:5]
|
115
|
+
palette.sort(key=lambda rgb: cls.colorfulness(*rgb), reverse=True)
|
116
|
+
|
117
|
+
return palette[0]
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def __adjustPaletteValue(cls, palette):
|
121
|
+
""" adjust the brightness of palette """
|
122
|
+
newPalette = []
|
123
|
+
for rgb in palette:
|
124
|
+
h, s, v = cls.rgb2hsv(rgb)
|
125
|
+
if v > 0.9:
|
126
|
+
factor = 0.8
|
127
|
+
elif 0.8 < v <= 0.9:
|
128
|
+
factor = 0.9
|
129
|
+
elif 0.7 < v <= 0.8:
|
130
|
+
factor = 0.95
|
131
|
+
else:
|
132
|
+
factor = 1
|
133
|
+
v *= factor
|
134
|
+
newPalette.append(cls.hsv2rgb(h, s, v))
|
135
|
+
|
136
|
+
return newPalette
|
137
|
+
|
138
|
+
@staticmethod
|
139
|
+
def rgb2hsv(rgb):
|
140
|
+
""" convert rgb to hsv """
|
141
|
+
r, g, b = [i / 255 for i in rgb]
|
142
|
+
mx = max(r, g, b)
|
143
|
+
mn = min(r, g, b)
|
144
|
+
df = mx - mn
|
145
|
+
if mx == mn:
|
146
|
+
h = 0
|
147
|
+
elif mx == r:
|
148
|
+
h = (60 * ((g - b) / df) + 360) % 360
|
149
|
+
elif mx == g:
|
150
|
+
h = (60 * ((b - r) / df) + 120) % 360
|
151
|
+
elif mx == b:
|
152
|
+
h = (60 * ((r - g) / df) + 240) % 360
|
153
|
+
s = 0 if mx == 0 else df / mx
|
154
|
+
v = mx
|
155
|
+
return (h, s, v)
|
156
|
+
|
157
|
+
@staticmethod
|
158
|
+
def hsv2rgb(h, s, v):
|
159
|
+
""" convert hsv to rgb """
|
160
|
+
h60 = h / 60.0
|
161
|
+
h60f = floor(h60)
|
162
|
+
hi = int(h60f) % 6
|
163
|
+
f = h60 - h60f
|
164
|
+
p = v * (1 - s)
|
165
|
+
q = v * (1 - f * s)
|
166
|
+
t = v * (1 - (1 - f) * s)
|
167
|
+
r, g, b = 0, 0, 0
|
168
|
+
if hi == 0:
|
169
|
+
r, g, b = v, t, p
|
170
|
+
elif hi == 1:
|
171
|
+
r, g, b = q, v, p
|
172
|
+
elif hi == 2:
|
173
|
+
r, g, b = p, v, t
|
174
|
+
elif hi == 3:
|
175
|
+
r, g, b = p, q, v
|
176
|
+
elif hi == 4:
|
177
|
+
r, g, b = t, p, v
|
178
|
+
elif hi == 5:
|
179
|
+
r, g, b = v, p, q
|
180
|
+
r, g, b = int(r * 255), int(g * 255), int(b * 255)
|
181
|
+
return (r, g, b)
|
182
|
+
|
183
|
+
@staticmethod
|
184
|
+
def colorfulness(r: int, g: int, b: int):
|
185
|
+
rg = np.absolute(r - g)
|
186
|
+
yb = np.absolute(0.5 * (r + g) - b)
|
187
|
+
|
188
|
+
# Compute the mean and standard deviation of both `rg` and `yb`.
|
189
|
+
rg_mean, rg_std = (np.mean(rg), np.std(rg))
|
190
|
+
yb_mean, yb_std = (np.mean(yb), np.std(yb))
|
191
|
+
|
192
|
+
# Combine the mean and standard deviations.
|
193
|
+
std_root = np.sqrt((rg_std ** 2) + (yb_std ** 2))
|
194
|
+
mean_root = np.sqrt((rg_mean ** 2) + (yb_mean ** 2))
|
195
|
+
|
196
|
+
return std_root + (0.3 * mean_root)
|
197
|
+
|
198
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
from functools import singledispatch, update_wrapper
|
3
|
+
|
4
|
+
|
5
|
+
class singledispatchmethod:
|
6
|
+
"""Single-dispatch generic method descriptor.
|
7
|
+
|
8
|
+
Supports wrapping existing descriptors and handles non-descriptor
|
9
|
+
callables as instance methods.
|
10
|
+
"""
|
11
|
+
|
12
|
+
def __init__(self, func):
|
13
|
+
if not callable(func) and not hasattr(func, "__get__"):
|
14
|
+
raise TypeError(f"{func!r} is not callable or a descriptor")
|
15
|
+
|
16
|
+
self.dispatcher = singledispatch(func)
|
17
|
+
self.func = func
|
18
|
+
|
19
|
+
def register(self, cls, method=None):
|
20
|
+
"""generic_method.register(cls, func) -> func
|
21
|
+
|
22
|
+
Registers a new implementation for the given *cls* on a *generic_method*.
|
23
|
+
"""
|
24
|
+
return self.dispatcher.register(cls, func=method)
|
25
|
+
|
26
|
+
def __get__(self, obj, cls=None):
|
27
|
+
def _method(*args, **kwargs):
|
28
|
+
if args:
|
29
|
+
method = self.dispatcher.dispatch(args[0].__class__)
|
30
|
+
else:
|
31
|
+
method = self.func
|
32
|
+
for v in kwargs.values():
|
33
|
+
if v.__class__ in self.dispatcher.registry:
|
34
|
+
method = self.dispatcher.dispatch(v.__class__)
|
35
|
+
if method is not self.func:
|
36
|
+
break
|
37
|
+
|
38
|
+
return method.__get__(obj, cls)(*args, **kwargs)
|
39
|
+
|
40
|
+
_method.__isabstractmethod__ = self.__isabstractmethod__
|
41
|
+
_method.register = self.register
|
42
|
+
update_wrapper(_method, self.func)
|
43
|
+
return _method
|
44
|
+
|
45
|
+
@property
|
46
|
+
def __isabstractmethod__(self):
|
47
|
+
return getattr(self.func, '__isabstractmethod__', False)
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from typing import Dict, List
|
3
|
+
from itertools import groupby
|
4
|
+
|
5
|
+
from PySide6.QtCore import Qt, QObject, Signal
|
6
|
+
from PySide6.QtWidgets import QWidget, QStackedWidget
|
7
|
+
|
8
|
+
|
9
|
+
class RouteItem:
|
10
|
+
""" Route item """
|
11
|
+
|
12
|
+
def __init__(self, stacked: QStackedWidget, routeKey: str):
|
13
|
+
self.stacked = stacked
|
14
|
+
self.routeKey = routeKey
|
15
|
+
|
16
|
+
def __eq__(self, other):
|
17
|
+
if other is None:
|
18
|
+
return False
|
19
|
+
|
20
|
+
return other.stacked is self.stacked and self.routeKey == other.routeKey
|
21
|
+
|
22
|
+
|
23
|
+
class StackedHistory:
|
24
|
+
""" Stacked history """
|
25
|
+
|
26
|
+
def __init__(self, stacked: QStackedWidget):
|
27
|
+
self.stacked = stacked
|
28
|
+
self.defaultRouteKey = None # type: str
|
29
|
+
self.history = [self.defaultRouteKey] # type: List[str]
|
30
|
+
|
31
|
+
def __len__(self):
|
32
|
+
return len(self.history)
|
33
|
+
|
34
|
+
def isEmpty(self):
|
35
|
+
return len(self) <= 1
|
36
|
+
|
37
|
+
def push(self, routeKey: str):
|
38
|
+
if self.history[-1] == routeKey:
|
39
|
+
return False
|
40
|
+
|
41
|
+
self.history.append(routeKey)
|
42
|
+
return True
|
43
|
+
|
44
|
+
def pop(self):
|
45
|
+
if self.isEmpty():
|
46
|
+
return
|
47
|
+
|
48
|
+
self.history.pop()
|
49
|
+
self.goToTop()
|
50
|
+
|
51
|
+
def remove(self, routeKey: str):
|
52
|
+
if routeKey not in self.history:
|
53
|
+
return
|
54
|
+
|
55
|
+
self.history[1:] = [i for i in self.history[1:] if i != routeKey]
|
56
|
+
self.history = [k for k, g in groupby(self.history)]
|
57
|
+
self.goToTop()
|
58
|
+
|
59
|
+
def top(self):
|
60
|
+
return self.history[-1]
|
61
|
+
|
62
|
+
def setDefaultRouteKey(self, routeKey: str):
|
63
|
+
self.defaultRouteKey = routeKey
|
64
|
+
self.history[0] = routeKey
|
65
|
+
|
66
|
+
def goToTop(self):
|
67
|
+
w = self.stacked.findChild(QWidget, self.top())
|
68
|
+
if w:
|
69
|
+
self.stacked.setCurrentWidget(w)
|
70
|
+
|
71
|
+
|
72
|
+
class Router(QObject):
|
73
|
+
""" Router """
|
74
|
+
|
75
|
+
emptyChanged = Signal(bool)
|
76
|
+
|
77
|
+
def __init__(self, parent=None):
|
78
|
+
super().__init__(parent=parent)
|
79
|
+
self.history = [] # type: List[RouteItem]
|
80
|
+
self.stackHistories = {} # type: Dict[QStackedWidget, StackedHistory]
|
81
|
+
|
82
|
+
def setDefaultRouteKey(self, stacked: QStackedWidget, routeKey: str):
|
83
|
+
""" set the default route key of stacked widget """
|
84
|
+
if stacked not in self.stackHistories:
|
85
|
+
self.stackHistories[stacked] = StackedHistory(stacked)
|
86
|
+
|
87
|
+
self.stackHistories[stacked].setDefaultRouteKey(routeKey)
|
88
|
+
|
89
|
+
def push(self, stacked: QStackedWidget, routeKey: str):
|
90
|
+
""" push history
|
91
|
+
|
92
|
+
Parameters
|
93
|
+
----------
|
94
|
+
stacked: QStackedWidget
|
95
|
+
stacked widget
|
96
|
+
|
97
|
+
routeKey: str
|
98
|
+
route key of sub insterface, it should be the object name of sub interface
|
99
|
+
"""
|
100
|
+
item = RouteItem(stacked, routeKey)
|
101
|
+
|
102
|
+
if stacked not in self.stackHistories:
|
103
|
+
self.stackHistories[stacked] = StackedHistory(stacked)
|
104
|
+
|
105
|
+
# don't add duplicated history
|
106
|
+
success = self.stackHistories[stacked].push(routeKey)
|
107
|
+
if success:
|
108
|
+
self.history.append(item)
|
109
|
+
|
110
|
+
self.emptyChanged.emit(not bool(self.history))
|
111
|
+
|
112
|
+
def pop(self):
|
113
|
+
""" pop history """
|
114
|
+
if not self.history:
|
115
|
+
return
|
116
|
+
|
117
|
+
item = self.history.pop()
|
118
|
+
self.emptyChanged.emit(not bool(self.history))
|
119
|
+
self.stackHistories[item.stacked].pop()
|
120
|
+
|
121
|
+
def remove(self, routeKey: str):
|
122
|
+
""" remove history """
|
123
|
+
self.history = [i for i in self.history if i.routeKey != routeKey]
|
124
|
+
self.history = [list(g)[0] for k, g in groupby(self.history, lambda i: i.routeKey)]
|
125
|
+
self.emptyChanged.emit(not bool(self.history))
|
126
|
+
|
127
|
+
for stacked, history in self.stackHistories.items():
|
128
|
+
w = stacked.findChild(QWidget, routeKey)
|
129
|
+
if w:
|
130
|
+
return history.remove(routeKey)
|
131
|
+
|
132
|
+
|
133
|
+
qrouter = Router()
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from PySide6.QtCore import QPoint, QRect
|
2
|
+
from PySide6.QtGui import QCursor
|
3
|
+
from PySide6.QtWidgets import QApplication
|
4
|
+
|
5
|
+
|
6
|
+
def getCurrentScreen():
|
7
|
+
""" get current screen """
|
8
|
+
cursorPos = QCursor.pos()
|
9
|
+
|
10
|
+
for s in QApplication.screens():
|
11
|
+
if s.geometry().contains(cursorPos):
|
12
|
+
return s
|
13
|
+
|
14
|
+
return None
|
15
|
+
|
16
|
+
|
17
|
+
def getCurrentScreenGeometry(avaliable=True):
|
18
|
+
""" get current screen geometry """
|
19
|
+
screen = getCurrentScreen() or QApplication.primaryScreen()
|
20
|
+
|
21
|
+
# this should not happen
|
22
|
+
if not screen:
|
23
|
+
return QRect(0, 0, 1920, 1080)
|
24
|
+
|
25
|
+
return screen.availableGeometry() if avaliable else screen.geometry()
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# coding:utf-8
|
2
|
+
from collections import deque
|
3
|
+
from enum import Enum
|
4
|
+
from math import cos, pi, ceil
|
5
|
+
|
6
|
+
from PySide6.QtCore import QDateTime, Qt, QTimer, QPoint
|
7
|
+
from PySide6.QtGui import QWheelEvent
|
8
|
+
from PySide6.QtWidgets import QApplication, QScrollArea, QAbstractScrollArea
|
9
|
+
|
10
|
+
|
11
|
+
class SmoothScroll:
|
12
|
+
""" Scroll smoothly """
|
13
|
+
|
14
|
+
def __init__(self, widget: QScrollArea, orient=Qt.Vertical):
|
15
|
+
"""
|
16
|
+
Parameters
|
17
|
+
----------
|
18
|
+
widget: QScrollArea
|
19
|
+
scroll area to scroll smoothly
|
20
|
+
|
21
|
+
orient: Orientation
|
22
|
+
scroll orientation
|
23
|
+
"""
|
24
|
+
self.widget = widget
|
25
|
+
self.orient = orient
|
26
|
+
self.fps = 60
|
27
|
+
self.duration = 400
|
28
|
+
self.stepsTotal = 0
|
29
|
+
self.stepRatio = 1.5
|
30
|
+
self.acceleration = 1
|
31
|
+
self.lastWheelEvent = None
|
32
|
+
self.scrollStamps = deque()
|
33
|
+
self.stepsLeftQueue = deque()
|
34
|
+
self.smoothMoveTimer = QTimer(widget)
|
35
|
+
self.smoothMode = SmoothMode(SmoothMode.LINEAR)
|
36
|
+
self.smoothMoveTimer.timeout.connect(self.__smoothMove)
|
37
|
+
|
38
|
+
def setSmoothMode(self, smoothMode):
|
39
|
+
""" set smooth mode """
|
40
|
+
self.smoothMode = smoothMode
|
41
|
+
|
42
|
+
def wheelEvent(self, e):
|
43
|
+
# only process the wheel events triggered by mouse, fixes issue #75
|
44
|
+
delta = e.angleDelta().y() if e.angleDelta().y() != 0 else e.angleDelta().x()
|
45
|
+
if self.smoothMode == SmoothMode.NO_SMOOTH or abs(delta) % 120 != 0:
|
46
|
+
QAbstractScrollArea.wheelEvent(self.widget, e)
|
47
|
+
return
|
48
|
+
|
49
|
+
# push current time to queque
|
50
|
+
now = QDateTime.currentDateTime().toMSecsSinceEpoch()
|
51
|
+
self.scrollStamps.append(now)
|
52
|
+
while now - self.scrollStamps[0] > 500:
|
53
|
+
self.scrollStamps.popleft()
|
54
|
+
|
55
|
+
# adjust the acceration ratio based on unprocessed events
|
56
|
+
accerationRatio = min(len(self.scrollStamps) / 15, 1)
|
57
|
+
self.lastWheelPos = e.position()
|
58
|
+
self.lastWheelGlobalPos = e.globalPosition()
|
59
|
+
|
60
|
+
# get the number of steps
|
61
|
+
self.stepsTotal = self.fps * self.duration / 1000
|
62
|
+
|
63
|
+
# get the moving distance corresponding to each event
|
64
|
+
delta = delta* self.stepRatio
|
65
|
+
if self.acceleration > 0:
|
66
|
+
delta += delta * self.acceleration * accerationRatio
|
67
|
+
|
68
|
+
# form a list of moving distances and steps, and insert it into the queue for processing.
|
69
|
+
self.stepsLeftQueue.append([delta, self.stepsTotal])
|
70
|
+
|
71
|
+
# overflow time of timer: 1000ms/frames
|
72
|
+
self.smoothMoveTimer.start(int(1000 / self.fps))
|
73
|
+
|
74
|
+
def __smoothMove(self):
|
75
|
+
""" scroll smoothly when timer time out """
|
76
|
+
totalDelta = 0
|
77
|
+
|
78
|
+
# Calculate the scrolling distance of all unprocessed events,
|
79
|
+
# the timer will reduce the number of steps by 1 each time it overflows.
|
80
|
+
for i in self.stepsLeftQueue:
|
81
|
+
totalDelta += self.__subDelta(i[0], i[1])
|
82
|
+
i[1] -= 1
|
83
|
+
|
84
|
+
# If the event has been processed, move it out of the queue
|
85
|
+
while self.stepsLeftQueue and self.stepsLeftQueue[0][1] == 0:
|
86
|
+
self.stepsLeftQueue.popleft()
|
87
|
+
|
88
|
+
# construct wheel event
|
89
|
+
if self.orient == Qt.Vertical:
|
90
|
+
pixelDelta = QPoint(round(totalDelta), 0)
|
91
|
+
bar = self.widget.verticalScrollBar()
|
92
|
+
else:
|
93
|
+
pixelDelta = QPoint(0, round(totalDelta))
|
94
|
+
bar = self.widget.horizontalScrollBar()
|
95
|
+
|
96
|
+
e = QWheelEvent(
|
97
|
+
self.lastWheelPos,
|
98
|
+
self.lastWheelGlobalPos,
|
99
|
+
pixelDelta,
|
100
|
+
QPoint(round(totalDelta), 0),
|
101
|
+
Qt.MouseButton.LeftButton,
|
102
|
+
Qt.KeyboardModifier.NoModifier,
|
103
|
+
Qt.ScrollPhase.ScrollBegin,
|
104
|
+
False,
|
105
|
+
)
|
106
|
+
|
107
|
+
# send wheel event to app
|
108
|
+
QApplication.sendEvent(bar, e)
|
109
|
+
|
110
|
+
# stop scrolling if the queque is empty
|
111
|
+
if not self.stepsLeftQueue:
|
112
|
+
self.smoothMoveTimer.stop()
|
113
|
+
|
114
|
+
def __subDelta(self, delta, stepsLeft):
|
115
|
+
""" get the interpolation for each step """
|
116
|
+
m = self.stepsTotal / 2
|
117
|
+
x = abs(self.stepsTotal - stepsLeft - m)
|
118
|
+
|
119
|
+
res = 0
|
120
|
+
if self.smoothMode == SmoothMode.NO_SMOOTH:
|
121
|
+
res = 0
|
122
|
+
elif self.smoothMode == SmoothMode.CONSTANT:
|
123
|
+
res = delta / self.stepsTotal
|
124
|
+
elif self.smoothMode == SmoothMode.LINEAR:
|
125
|
+
res = 2 * delta / self.stepsTotal * (m - x) / m
|
126
|
+
elif self.smoothMode == SmoothMode.QUADRATI:
|
127
|
+
res = 3 / 4 / m * (1 - x * x / m / m) * delta
|
128
|
+
elif self.smoothMode == SmoothMode.COSINE:
|
129
|
+
res = (cos(x * pi / m) + 1) / (2 * m) * delta
|
130
|
+
|
131
|
+
return res
|
132
|
+
|
133
|
+
|
134
|
+
class SmoothMode(Enum):
|
135
|
+
""" Smooth mode """
|
136
|
+
NO_SMOOTH = 0
|
137
|
+
CONSTANT = 1
|
138
|
+
LINEAR = 2
|
139
|
+
QUADRATI = 3
|
140
|
+
COSINE = 4
|
141
|
+
|