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.
Files changed (107) hide show
  1. fastuiwidgets/__init__.py +12 -0
  2. fastuiwidgets/_rc/__init__.py +0 -0
  3. fastuiwidgets/_rc/resource.py +98835 -0
  4. fastuiwidgets/common/__init__.py +12 -0
  5. fastuiwidgets/common/animation.py +530 -0
  6. fastuiwidgets/common/auto_wrap.py +164 -0
  7. fastuiwidgets/common/color.py +95 -0
  8. fastuiwidgets/common/config.py +423 -0
  9. fastuiwidgets/common/exception_handler.py +31 -0
  10. fastuiwidgets/common/font.py +38 -0
  11. fastuiwidgets/common/icon.py +703 -0
  12. fastuiwidgets/common/image_utils.py +198 -0
  13. fastuiwidgets/common/overload.py +47 -0
  14. fastuiwidgets/common/router.py +133 -0
  15. fastuiwidgets/common/screen.py +25 -0
  16. fastuiwidgets/common/smooth_scroll.py +141 -0
  17. fastuiwidgets/common/style_sheet.py +512 -0
  18. fastuiwidgets/common/theme_listener.py +27 -0
  19. fastuiwidgets/common/translator.py +14 -0
  20. fastuiwidgets/components/__init__.py +6 -0
  21. fastuiwidgets/components/date_time/__init__.py +4 -0
  22. fastuiwidgets/components/date_time/calendar_picker.py +121 -0
  23. fastuiwidgets/components/date_time/calendar_view.py +671 -0
  24. fastuiwidgets/components/date_time/date_picker.py +245 -0
  25. fastuiwidgets/components/date_time/fast_calendar_view.py +487 -0
  26. fastuiwidgets/components/date_time/picker_base.py +632 -0
  27. fastuiwidgets/components/date_time/time_picker.py +223 -0
  28. fastuiwidgets/components/dialog_box/__init__.py +6 -0
  29. fastuiwidgets/components/dialog_box/color_dialog.py +414 -0
  30. fastuiwidgets/components/dialog_box/dialog.py +167 -0
  31. fastuiwidgets/components/dialog_box/folder_list_dialog.py +307 -0
  32. fastuiwidgets/components/dialog_box/mask_dialog_base.py +120 -0
  33. fastuiwidgets/components/dialog_box/message_box_base.py +92 -0
  34. fastuiwidgets/components/dialog_box/message_dialog.py +65 -0
  35. fastuiwidgets/components/layout/__init__.py +3 -0
  36. fastuiwidgets/components/layout/expand_layout.py +96 -0
  37. fastuiwidgets/components/layout/flow_layout.py +236 -0
  38. fastuiwidgets/components/layout/v_box_layout.py +41 -0
  39. fastuiwidgets/components/material/__init__.py +6 -0
  40. fastuiwidgets/components/material/acrylic_combo_box.py +96 -0
  41. fastuiwidgets/components/material/acrylic_flyout.py +105 -0
  42. fastuiwidgets/components/material/acrylic_line_edit.py +27 -0
  43. fastuiwidgets/components/material/acrylic_menu.py +204 -0
  44. fastuiwidgets/components/material/acrylic_tool_tip.py +39 -0
  45. fastuiwidgets/components/material/acrylic_widget.py +42 -0
  46. fastuiwidgets/components/navigation/__init__.py +9 -0
  47. fastuiwidgets/components/navigation/breadcrumb.py +350 -0
  48. fastuiwidgets/components/navigation/navigation_bar.py +416 -0
  49. fastuiwidgets/components/navigation/navigation_interface.py +268 -0
  50. fastuiwidgets/components/navigation/navigation_panel.py +657 -0
  51. fastuiwidgets/components/navigation/navigation_widget.py +686 -0
  52. fastuiwidgets/components/navigation/pivot.py +272 -0
  53. fastuiwidgets/components/navigation/segmented_widget.py +174 -0
  54. fastuiwidgets/components/settings/__init__.py +8 -0
  55. fastuiwidgets/components/settings/custom_color_setting_card.py +139 -0
  56. fastuiwidgets/components/settings/expand_setting_card.py +390 -0
  57. fastuiwidgets/components/settings/folder_list_setting_card.py +134 -0
  58. fastuiwidgets/components/settings/options_setting_card.py +86 -0
  59. fastuiwidgets/components/settings/setting_card.py +449 -0
  60. fastuiwidgets/components/settings/setting_card_group.py +48 -0
  61. fastuiwidgets/components/widgets/__init__.py +41 -0
  62. fastuiwidgets/components/widgets/acrylic_label.py +261 -0
  63. fastuiwidgets/components/widgets/button.py +1059 -0
  64. fastuiwidgets/components/widgets/card_widget.py +369 -0
  65. fastuiwidgets/components/widgets/check_box.py +203 -0
  66. fastuiwidgets/components/widgets/combo_box.py +556 -0
  67. fastuiwidgets/components/widgets/command_bar.py +636 -0
  68. fastuiwidgets/components/widgets/cycle_list_widget.py +251 -0
  69. fastuiwidgets/components/widgets/flip_view.py +430 -0
  70. fastuiwidgets/components/widgets/flyout.py +521 -0
  71. fastuiwidgets/components/widgets/frameless_window.py +49 -0
  72. fastuiwidgets/components/widgets/icon_widget.py +53 -0
  73. fastuiwidgets/components/widgets/info_badge.py +483 -0
  74. fastuiwidgets/components/widgets/info_bar.py +596 -0
  75. fastuiwidgets/components/widgets/label.py +553 -0
  76. fastuiwidgets/components/widgets/line_edit.py +551 -0
  77. fastuiwidgets/components/widgets/list_view.py +158 -0
  78. fastuiwidgets/components/widgets/menu.py +1318 -0
  79. fastuiwidgets/components/widgets/pips_pager.py +331 -0
  80. fastuiwidgets/components/widgets/progress_bar.py +311 -0
  81. fastuiwidgets/components/widgets/progress_ring.py +212 -0
  82. fastuiwidgets/components/widgets/scroll_area.py +125 -0
  83. fastuiwidgets/components/widgets/scroll_bar.py +673 -0
  84. fastuiwidgets/components/widgets/separator.py +43 -0
  85. fastuiwidgets/components/widgets/slider.py +307 -0
  86. fastuiwidgets/components/widgets/spin_box.py +306 -0
  87. fastuiwidgets/components/widgets/stacked_widget.py +211 -0
  88. fastuiwidgets/components/widgets/state_tool_tip.py +188 -0
  89. fastuiwidgets/components/widgets/switch_button.py +312 -0
  90. fastuiwidgets/components/widgets/tab_view.py +804 -0
  91. fastuiwidgets/components/widgets/table_view.py +360 -0
  92. fastuiwidgets/components/widgets/teaching_tip.py +657 -0
  93. fastuiwidgets/components/widgets/tool_tip.py +460 -0
  94. fastuiwidgets/components/widgets/tree_view.py +216 -0
  95. fastuiwidgets/multimedia/__init__.py +3 -0
  96. fastuiwidgets/multimedia/media_play_bar.py +319 -0
  97. fastuiwidgets/multimedia/media_player.py +124 -0
  98. fastuiwidgets/multimedia/video_widget.py +93 -0
  99. fastuiwidgets/window/__init__.py +2 -0
  100. fastuiwidgets/window/fluent_window.py +413 -0
  101. fastuiwidgets/window/splash_screen.py +92 -0
  102. fastuiwidgets/window/stacked_widget.py +66 -0
  103. python_fastui_widgets-1.0.0.dist-info/METADATA +30 -0
  104. python_fastui_widgets-1.0.0.dist-info/RECORD +107 -0
  105. python_fastui_widgets-1.0.0.dist-info/WHEEL +5 -0
  106. python_fastui_widgets-1.0.0.dist-info/licenses/LICENSE +674 -0
  107. 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
+