jbqt 0.1.12__tar.gz → 0.1.13__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.
Potentially problematic release.
This version of jbqt might be problematic. Click here for more details.
- {jbqt-0.1.12 → jbqt-0.1.13}/PKG-INFO +3 -2
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/__init__.py +1 -4
- jbqt-0.1.13/jbqt/common/__init__.py +9 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/common/qt_utils.py +3 -3
- jbqt-0.1.12/jbqt/common/consts.py → jbqt-0.1.13/jbqt/consts/__init__.py +12 -8
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/dialogs/input_form.py +1 -1
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/models/chip_button.py +1 -4
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/models/chips.py +2 -4
- jbqt-0.1.13/jbqt/types/__init__.py +14 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/chip_button.py +22 -15
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/chips.py +46 -27
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/multiselect.py +33 -21
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/simple.py +12 -8
- jbqt-0.1.13/jbqt/widgets/widget_utils.py +88 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/pyproject.toml +3 -2
- jbqt-0.1.12/jbqt/common/__init__.py +0 -16
- jbqt-0.1.12/jbqt/types/__init__.py +0 -8
- jbqt-0.1.12/jbqt/widgets/widget_utils.py +0 -77
- {jbqt-0.1.12 → jbqt-0.1.13}/README.md +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/dialogs/__init__.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/dialogs/file_dialog.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/dialogs/text_preview.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/models/__init__.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/view_icons.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/__init__.py +0 -0
- {jbqt-0.1.12 → jbqt-0.1.13}/jbqt/widgets/toast.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: jbqt
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Joseph Bochinski
|
|
6
6
|
Author-email: stirgejr@gmail.com
|
|
@@ -8,7 +8,8 @@ Requires-Python: >=3.12,<4.0
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
10
10
|
Requires-Dist: fuzzywuzzy (>=0.18.0,<0.19.0)
|
|
11
|
-
Requires-Dist:
|
|
11
|
+
Requires-Dist: jbconsts (>=0.1.1,<0.2.0)
|
|
12
|
+
Requires-Dist: jbutils (>=0.1.23,<0.2.0)
|
|
12
13
|
Requires-Dist: pyqt6 (>=6.9.0,<7.0.0)
|
|
13
14
|
Requires-Dist: python-levenshtein (>=0.27.1,<0.28.0)
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"""Common exports for jbqt"""
|
|
2
2
|
|
|
3
|
-
from jbqt.common import
|
|
4
|
-
from jbqt.common.consts import GlobalRefs
|
|
3
|
+
from jbqt.common import qt_utils
|
|
5
4
|
from jbqt.common.qt_utils import register_app
|
|
6
5
|
|
|
7
6
|
__all__ = [
|
|
8
|
-
"GlobalRefs",
|
|
9
|
-
"consts",
|
|
10
7
|
"qt_utils",
|
|
11
8
|
"register_app",
|
|
12
9
|
]
|
|
@@ -14,7 +14,7 @@ from PyQt6.QtWidgets import (
|
|
|
14
14
|
QDoubleSpinBox,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import jbqt.consts as consts
|
|
18
18
|
from jbqt.models import IChipsWidget
|
|
19
19
|
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ def get_item_value(item: QListWidgetItem) -> str:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def register_app(app: QApplication, icon_dir: str = "") -> None:
|
|
27
|
-
consts.
|
|
27
|
+
consts.QtGlobalRefs.app = app
|
|
28
28
|
consts.set_icon_dir(icon_dir)
|
|
29
29
|
|
|
30
30
|
|
|
@@ -78,4 +78,4 @@ def set_widget_value(widget: QWidget, value: Any) -> None:
|
|
|
78
78
|
|
|
79
79
|
elif isinstance(widget, QCalendarWidget):
|
|
80
80
|
if isinstance(value, (date, QDate)):
|
|
81
|
-
widget.
|
|
81
|
+
widget.setSelectedDate(value)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
"""Collection of constants and global references for the jbqt package."""
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import re
|
|
3
5
|
|
|
4
6
|
from typing import Optional
|
|
5
7
|
|
|
6
|
-
import jbutils
|
|
7
8
|
|
|
8
9
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QScrollArea, QWidget
|
|
9
10
|
from PyQt6.QtCore import Qt, QSize
|
|
@@ -14,11 +15,8 @@ from jbqt.types import IconGetter
|
|
|
14
15
|
TAG_RE = re.compile(r"^\((.+)\)(\d+\.\d+)$")
|
|
15
16
|
LIST_ITEM_ROLE = Qt.ItemDataRole.UserRole - 1
|
|
16
17
|
|
|
17
|
-
STYLES = jbutils.STYLES
|
|
18
|
-
COLORS = jbutils.COLORS
|
|
19
|
-
|
|
20
18
|
|
|
21
|
-
class
|
|
19
|
+
class QtGlobalRefs:
|
|
22
20
|
app: QApplication | None = None
|
|
23
21
|
main_window: QMainWindow | None = None
|
|
24
22
|
scroll_area: QScrollArea | None = None
|
|
@@ -79,7 +77,7 @@ def set_icon_dir(path: str) -> None:
|
|
|
79
77
|
QtPaths.icon_dir = path
|
|
80
78
|
|
|
81
79
|
|
|
82
|
-
class
|
|
80
|
+
class QtIconSizes:
|
|
83
81
|
ICON_XS = QSize(16, 16)
|
|
84
82
|
ICON_SM = QSize(20, 20)
|
|
85
83
|
ICON_MD = QSize(22, 22)
|
|
@@ -109,14 +107,14 @@ def recolor_icon(
|
|
|
109
107
|
color = QColor(color)
|
|
110
108
|
|
|
111
109
|
if scale is None:
|
|
112
|
-
scale =
|
|
110
|
+
scale = QtIconSizes.ICON_MD
|
|
113
111
|
pixmap = QPixmap(image_path)
|
|
114
112
|
icon_key = os.path.splitext(os.path.basename(image_path))[0]
|
|
115
113
|
|
|
116
114
|
if pixmap.isNull():
|
|
117
115
|
fallback = THEME_ICONS.get(icon_key, STD_ICONS.get(icon_key))
|
|
118
116
|
if fallback:
|
|
119
|
-
pixmap = fallback.pixmap()
|
|
117
|
+
pixmap = fallback.pixmap(scale)
|
|
120
118
|
|
|
121
119
|
recolored_pixmap = QPixmap(pixmap.size())
|
|
122
120
|
|
|
@@ -177,3 +175,9 @@ class ICONS:
|
|
|
177
175
|
return getattr(cls, name)
|
|
178
176
|
else:
|
|
179
177
|
return default
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = [
|
|
181
|
+
"ICONS",
|
|
182
|
+
"QtIconSizes",
|
|
183
|
+
]
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"""Model for chips widget"""
|
|
2
2
|
|
|
3
|
-
from abc import abstractmethod, ABC
|
|
4
|
-
|
|
5
|
-
from PyQt6.QtCore import QObject
|
|
6
3
|
from PyQt6.QtWidgets import QWidget
|
|
7
4
|
|
|
8
5
|
|
|
9
6
|
class IChipButton(QWidget):
|
|
10
7
|
"""ChipButton model"""
|
|
11
8
|
|
|
12
|
-
def __init__(self, parent:
|
|
9
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
13
10
|
super().__init__(parent)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Model for chips widget"""
|
|
2
2
|
|
|
3
|
-
from abc import abstractmethod, ABC
|
|
4
|
-
|
|
5
3
|
from PyQt6.QtCore import QObject
|
|
6
4
|
from PyQt6.QtWidgets import QWidget
|
|
7
5
|
|
|
@@ -11,9 +9,9 @@ from jbqt.models.chip_button import IChipButton
|
|
|
11
9
|
class IChipsWidget(QWidget):
|
|
12
10
|
"""ChipsWidget model"""
|
|
13
11
|
|
|
14
|
-
def __init__(self, parent:
|
|
12
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
15
13
|
super().__init__(parent)
|
|
16
|
-
self.values: list =
|
|
14
|
+
self.values: list[str] = []
|
|
17
15
|
|
|
18
16
|
def add_chip(self):
|
|
19
17
|
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Common type definitions for the jbqt package"""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, Optional, runtime_checkable
|
|
4
|
+
from PyQt6.QtGui import QIcon, QColor
|
|
5
|
+
from PyQt6.QtCore import QSize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class IconGetter(Protocol):
|
|
10
|
+
"""Factory function that assembles a QIcon instance"""
|
|
11
|
+
|
|
12
|
+
def __call__(
|
|
13
|
+
self, color: Optional[str | QColor] = ..., size: QSize | int | None = ...
|
|
14
|
+
) -> QIcon: ...
|
|
@@ -11,13 +11,12 @@ from PyQt6.QtWidgets import (
|
|
|
11
11
|
QWidget,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
from
|
|
14
|
+
import jbqt.consts as consts
|
|
15
|
+
from jbconsts import STYLES, COLORS
|
|
16
16
|
from jbqt.models import IChipButton
|
|
17
17
|
from jbqt.widgets.simple import ClickableLabel
|
|
18
18
|
from jbqt.dialogs import InputDialog
|
|
19
19
|
from jbqt.widgets.widget_utils import (
|
|
20
|
-
debug_scroll_pos,
|
|
21
20
|
preserve_scroll,
|
|
22
21
|
)
|
|
23
22
|
|
|
@@ -29,12 +28,11 @@ class ChipButton(IChipButton):
|
|
|
29
28
|
on_update: Callable,
|
|
30
29
|
on_remove: Callable,
|
|
31
30
|
use_weight: bool = False,
|
|
32
|
-
weight_re: Pattern = None,
|
|
33
|
-
debug: bool = False,
|
|
31
|
+
weight_re: Pattern | None = None,
|
|
34
32
|
):
|
|
35
33
|
super().__init__()
|
|
36
34
|
|
|
37
|
-
self._tag = tag
|
|
35
|
+
self._tag: str = tag
|
|
38
36
|
self.weight: float = 1.0
|
|
39
37
|
self.use_weight: bool = use_weight
|
|
40
38
|
|
|
@@ -64,7 +62,7 @@ class ChipButton(IChipButton):
|
|
|
64
62
|
self.tag_label.setFixedHeight(24)
|
|
65
63
|
self.tag_label.clicked.connect(self.toggle_edit_mode)
|
|
66
64
|
|
|
67
|
-
self.weight_button: QPushButton = None
|
|
65
|
+
self.weight_button: QPushButton | None = None
|
|
68
66
|
|
|
69
67
|
if use_weight:
|
|
70
68
|
self.weight_button = QPushButton(consts.ICONS.CODE(), "")
|
|
@@ -137,17 +135,24 @@ class ChipButton(IChipButton):
|
|
|
137
135
|
def text(self, value: str) -> None:
|
|
138
136
|
self._tag = value
|
|
139
137
|
|
|
140
|
-
def eventFilter(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
def eventFilter(
|
|
139
|
+
self, a0: QObject | None, a1: QKeyEvent | QEvent | None
|
|
140
|
+
) -> bool:
|
|
141
|
+
focus: QWidget | None = None
|
|
142
|
+
|
|
143
|
+
if consts.QtGlobalRefs.app:
|
|
144
|
+
focus = consts.QtGlobalRefs.app.focusWidget()
|
|
145
|
+
|
|
146
|
+
if not a1 or not isinstance(a1, QKeyEvent):
|
|
147
|
+
return super().eventFilter(a0, a1)
|
|
148
|
+
|
|
149
|
+
if a1.type() == QEvent.Type.KeyPress and focus == self.edit_line:
|
|
150
|
+
match a1.key():
|
|
146
151
|
case Qt.Key.Key_Return | Qt.Key.Key_Enter:
|
|
147
152
|
self.edit_tag()
|
|
148
153
|
case Qt.Key.Key_Escape:
|
|
149
154
|
self.cancel()
|
|
150
|
-
return super().eventFilter(
|
|
155
|
+
return super().eventFilter(a0, a1)
|
|
151
156
|
|
|
152
157
|
def toggle_hidden(self, hide: bool = True) -> None:
|
|
153
158
|
for widget in self.widgets:
|
|
@@ -167,7 +172,9 @@ class ChipButton(IChipButton):
|
|
|
167
172
|
self.edit_line.setMinimumWidth(self.tag_label.width() + 10)
|
|
168
173
|
self.toggle_hidden(False)
|
|
169
174
|
|
|
170
|
-
def emit_update(
|
|
175
|
+
def emit_update(
|
|
176
|
+
self, text: str | None = None, weight: float | None = None
|
|
177
|
+
) -> None:
|
|
171
178
|
text = text or self._tag
|
|
172
179
|
if weight is None:
|
|
173
180
|
weight = self.weight
|
|
@@ -14,11 +14,10 @@ from PyQt6.QtWidgets import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
import jbutils as utils
|
|
17
|
-
|
|
17
|
+
import jbqt.consts as consts
|
|
18
18
|
from jbqt.models import IChipsWidget
|
|
19
19
|
from jbqt.widgets.chip_button import ChipButton
|
|
20
20
|
from jbqt.widgets.multiselect import MultiSelectComboBox
|
|
21
|
-
from jbqt.widgets.widget_utils import debug_scroll_pos
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
class ChipsWidget(IChipsWidget):
|
|
@@ -26,16 +25,16 @@ class ChipsWidget(IChipsWidget):
|
|
|
26
25
|
|
|
27
26
|
def __init__(
|
|
28
27
|
self,
|
|
29
|
-
chips=None,
|
|
30
|
-
data: dict = None,
|
|
28
|
+
chips: list[str] | None = None,
|
|
29
|
+
data: dict | list | None = None,
|
|
31
30
|
path: str = "",
|
|
32
31
|
label: str = "",
|
|
33
|
-
weight_re: Pattern = None,
|
|
32
|
+
weight_re: Pattern | None = None,
|
|
34
33
|
use_weight: bool = False,
|
|
35
34
|
debug: bool = False,
|
|
36
35
|
):
|
|
37
36
|
super().__init__()
|
|
38
|
-
self.values = chips or []
|
|
37
|
+
self.values: list[str] = chips or []
|
|
39
38
|
# Main layout
|
|
40
39
|
main_layout = QVBoxLayout()
|
|
41
40
|
self.debug = debug
|
|
@@ -48,10 +47,10 @@ class ChipsWidget(IChipsWidget):
|
|
|
48
47
|
weight_re = weight_re or consts.TAG_RE
|
|
49
48
|
|
|
50
49
|
self.weight_re = weight_re
|
|
51
|
-
self.options = data
|
|
50
|
+
self.options = data or {}
|
|
52
51
|
self.data = data or {}
|
|
53
52
|
self.data_path = path
|
|
54
|
-
self.list_widget: QComboBox | MultiSelectComboBox = None
|
|
53
|
+
self.list_widget: QComboBox | MultiSelectComboBox | None = None
|
|
55
54
|
|
|
56
55
|
if data is not None:
|
|
57
56
|
if isinstance(data, dict):
|
|
@@ -112,21 +111,32 @@ class ChipsWidget(IChipsWidget):
|
|
|
112
111
|
|
|
113
112
|
self.setLayout(main_layout)
|
|
114
113
|
|
|
115
|
-
def eventFilter(
|
|
116
|
-
|
|
114
|
+
def eventFilter(
|
|
115
|
+
self, a0: QObject | None, a1: QKeyEvent | QEvent | None
|
|
116
|
+
) -> bool:
|
|
117
|
+
focus: QWidget | None = None
|
|
118
|
+
if consts.QtGlobalRefs.app:
|
|
119
|
+
focus = consts.QtGlobalRefs.app.focusWidget()
|
|
117
120
|
|
|
118
|
-
if
|
|
119
|
-
|
|
121
|
+
if not a1 or not isinstance(a1, QKeyEvent):
|
|
122
|
+
return super().eventFilter(a0, a1)
|
|
123
|
+
|
|
124
|
+
if a1.type() == QEvent.Type.KeyPress and focus == self.input_field:
|
|
125
|
+
match a1.key():
|
|
120
126
|
case Qt.Key.Key_Return | Qt.Key.Key_Enter:
|
|
121
127
|
self.add_chip()
|
|
122
|
-
return super().eventFilter(
|
|
128
|
+
return super().eventFilter(a0, a1)
|
|
123
129
|
|
|
124
130
|
def get_unweight_chip(self, tag: str) -> str:
|
|
125
|
-
if self.weight_re
|
|
126
|
-
|
|
131
|
+
if self.weight_re:
|
|
132
|
+
search_re = self.weight_re.search(tag)
|
|
133
|
+
if search_re:
|
|
134
|
+
return search_re.groups()[0]
|
|
135
|
+
|
|
127
136
|
return tag
|
|
128
137
|
|
|
129
|
-
def get_chip_list(self, tags: list[str]) -> list[str]:
|
|
138
|
+
def get_chip_list(self, tags: list[str] | None = None) -> list[str]:
|
|
139
|
+
tags = tags or []
|
|
130
140
|
return [self.get_unweight_chip(tag) for tag in tags]
|
|
131
141
|
|
|
132
142
|
def same_values(self, values: list[str]) -> bool:
|
|
@@ -146,6 +156,9 @@ class ChipsWidget(IChipsWidget):
|
|
|
146
156
|
self.set_items(selections, debug=True)
|
|
147
157
|
|
|
148
158
|
def update_multiselect(self, values: list[str]) -> None:
|
|
159
|
+
if not self.list_widget:
|
|
160
|
+
return
|
|
161
|
+
|
|
149
162
|
if isinstance(self.list_widget, MultiSelectComboBox):
|
|
150
163
|
self.list_widget.set_selected(values, emit=False)
|
|
151
164
|
else:
|
|
@@ -154,17 +167,27 @@ class ChipsWidget(IChipsWidget):
|
|
|
154
167
|
|
|
155
168
|
def clear_widgets(self) -> None:
|
|
156
169
|
while self.chip_layout.count():
|
|
157
|
-
|
|
170
|
+
layout = self.chip_layout.takeAt(0)
|
|
171
|
+
if not layout:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
widget = layout.widget()
|
|
175
|
+
|
|
158
176
|
if widget is not None:
|
|
159
177
|
widget.deleteLater()
|
|
160
178
|
|
|
161
179
|
def set_items(
|
|
162
|
-
self,
|
|
180
|
+
self,
|
|
181
|
+
values: list[str] | None = None,
|
|
182
|
+
emit: bool = True,
|
|
183
|
+
debug: bool = False,
|
|
163
184
|
) -> None:
|
|
164
185
|
self.clear_widgets()
|
|
186
|
+
if self.values is None:
|
|
187
|
+
self.values = []
|
|
165
188
|
# TODO: Temp fix for weighted/custom tags and incoming multiselect values
|
|
166
189
|
if values:
|
|
167
|
-
tags = self.
|
|
190
|
+
tags = self.get_chip_list(self.values)
|
|
168
191
|
for value in values:
|
|
169
192
|
if value not in tags:
|
|
170
193
|
self.values.append(value)
|
|
@@ -172,22 +195,18 @@ class ChipsWidget(IChipsWidget):
|
|
|
172
195
|
self.values = utils.dedupe_list(self.values)
|
|
173
196
|
self.values = [value for value in self.values if value]
|
|
174
197
|
for item in self.values:
|
|
175
|
-
if not consts.
|
|
198
|
+
if not consts.QtGlobalRefs.debug_set:
|
|
176
199
|
debug = True
|
|
177
|
-
consts.
|
|
200
|
+
consts.QtGlobalRefs.debug_set = True
|
|
178
201
|
else:
|
|
179
202
|
debug = False
|
|
180
203
|
chip_button = ChipButton(
|
|
181
204
|
item,
|
|
182
205
|
self.update_chip,
|
|
183
206
|
self.remove_chip,
|
|
184
|
-
debug=debug,
|
|
185
207
|
weight_re=self.weight_re,
|
|
186
208
|
use_weight=self.use_weight,
|
|
187
209
|
)
|
|
188
|
-
""" chip_button.setToolTip(
|
|
189
|
-
f"Tag Count: {consts.TAG_COUNTS.get(item, "N/A")}"
|
|
190
|
-
) """
|
|
191
210
|
|
|
192
211
|
self.chip_layout.addWidget(chip_button)
|
|
193
212
|
|
|
@@ -212,7 +231,7 @@ class ChipsWidget(IChipsWidget):
|
|
|
212
231
|
self.emit_changes()
|
|
213
232
|
self.input_field.setText("")
|
|
214
233
|
|
|
215
|
-
def remove_chip(self, button: ChipButton):
|
|
234
|
+
def remove_chip(self, button: ChipButton): # type: ignore
|
|
216
235
|
self.chip_layout.removeWidget(button)
|
|
217
236
|
button.deleteLater() # Schedule button widget for deletion
|
|
218
237
|
self.values.remove(button.text)
|
|
@@ -227,6 +246,6 @@ class ChipsWidget(IChipsWidget):
|
|
|
227
246
|
self.set_items()
|
|
228
247
|
|
|
229
248
|
def remove_all(self, *_) -> None:
|
|
230
|
-
self.values
|
|
249
|
+
self.values.clear()
|
|
231
250
|
self.clear_widgets()
|
|
232
251
|
self.emit_changes()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from re import Pattern
|
|
2
2
|
|
|
3
|
-
from PyQt6.QtCore import QObject, pyqtSignal, Qt
|
|
3
|
+
from PyQt6.QtCore import QObject, pyqtSignal, Qt, QEvent
|
|
4
4
|
from PyQt6.QtGui import QCursor, QMouseEvent, QWheelEvent
|
|
5
|
-
from PyQt6.QtWidgets import QComboBox, QListWidget, QListWidgetItem
|
|
5
|
+
from PyQt6.QtWidgets import QComboBox, QListWidget, QListWidgetItem, QLineEdit
|
|
6
6
|
|
|
7
7
|
import jbutils as utils
|
|
8
|
-
|
|
8
|
+
import jbqt.consts as consts
|
|
9
|
+
from jbqt.common import qt_utils
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class MultiSelectComboBox(QComboBox):
|
|
@@ -13,11 +14,11 @@ class MultiSelectComboBox(QComboBox):
|
|
|
13
14
|
|
|
14
15
|
def __init__(
|
|
15
16
|
self,
|
|
16
|
-
data: list | dict = None,
|
|
17
|
-
selected: list[str] = None,
|
|
17
|
+
data: list | dict | None = None,
|
|
18
|
+
selected: list[str] | None = None,
|
|
18
19
|
multi_enabled: bool = True,
|
|
19
20
|
use_weight: bool = False,
|
|
20
|
-
weight_re: Pattern = None,
|
|
21
|
+
weight_re: Pattern | None = None,
|
|
21
22
|
):
|
|
22
23
|
super().__init__()
|
|
23
24
|
|
|
@@ -30,11 +31,12 @@ class MultiSelectComboBox(QComboBox):
|
|
|
30
31
|
self._multi_select_enabled: bool = multi_enabled
|
|
31
32
|
# Make the combo box read-only and use a custom view
|
|
32
33
|
self.setEditable(True)
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
self.line_edit().setReadOnly(True)
|
|
34
36
|
# Override the mouse press event of the QLineEdit
|
|
35
|
-
self.
|
|
37
|
+
self.line_edit().installEventFilter(self)
|
|
36
38
|
|
|
37
|
-
self.view: QListWidget = QListWidget()
|
|
39
|
+
self.view: QListWidget = QListWidget() # type: ignore
|
|
38
40
|
self._selected = selected or []
|
|
39
41
|
|
|
40
42
|
# Set the custom view
|
|
@@ -52,26 +54,32 @@ class MultiSelectComboBox(QComboBox):
|
|
|
52
54
|
self.view.itemClicked.connect(self.toggle_check_state)
|
|
53
55
|
self.update_display_text()
|
|
54
56
|
|
|
55
|
-
def
|
|
57
|
+
def line_edit(self) -> QLineEdit:
|
|
58
|
+
return self.lineEdit() or QLineEdit(parent=self)
|
|
59
|
+
|
|
60
|
+
def eventFilter(
|
|
61
|
+
self, a0: QObject | None, a1: QMouseEvent | QEvent | None
|
|
62
|
+
) -> bool:
|
|
56
63
|
|
|
57
64
|
if (
|
|
58
|
-
|
|
59
|
-
and
|
|
65
|
+
a0 == self.lineEdit()
|
|
66
|
+
and isinstance(a1, QMouseEvent)
|
|
67
|
+
and a1.type() == QMouseEvent.Type.MouseButtonRelease
|
|
60
68
|
):
|
|
61
69
|
self.showPopup()
|
|
62
70
|
|
|
63
71
|
return True
|
|
64
|
-
return super().eventFilter(
|
|
72
|
+
return super().eventFilter(a0, a1)
|
|
65
73
|
|
|
66
|
-
def wheelEvent(self,
|
|
74
|
+
def wheelEvent(self, e: QWheelEvent | None):
|
|
67
75
|
# Check if the mouse cursor is within the bounds of the line edit
|
|
68
76
|
pos = self.mapFromGlobal(QCursor.pos())
|
|
69
|
-
if self.
|
|
77
|
+
if self.line_edit().rect().contains(pos):
|
|
70
78
|
# Consume the wheel event to prevent scrolling
|
|
71
79
|
return
|
|
72
80
|
|
|
73
81
|
# If not over the line edit, let the base class handle it (scrolling)
|
|
74
|
-
super().wheelEvent(
|
|
82
|
+
super().wheelEvent(e)
|
|
75
83
|
|
|
76
84
|
def toggle_check_state(self, item: QListWidgetItem):
|
|
77
85
|
# Toggle check state if the item is selectable
|
|
@@ -94,7 +102,7 @@ class MultiSelectComboBox(QComboBox):
|
|
|
94
102
|
|
|
95
103
|
for i in range(self.view.count()):
|
|
96
104
|
item = self.view.item(i)
|
|
97
|
-
if item != selected:
|
|
105
|
+
if item and item != selected:
|
|
98
106
|
item.setCheckState(Qt.CheckState.Unchecked)
|
|
99
107
|
self.update_display_text()
|
|
100
108
|
|
|
@@ -143,11 +151,11 @@ class MultiSelectComboBox(QComboBox):
|
|
|
143
151
|
selected_items = []
|
|
144
152
|
for i in range(self.view.count()):
|
|
145
153
|
item = self.view.item(i)
|
|
146
|
-
if item.checkState() == Qt.CheckState.Checked:
|
|
154
|
+
if item and item.checkState() == Qt.CheckState.Checked:
|
|
147
155
|
selected_items.append(item.text().strip())
|
|
148
156
|
|
|
149
157
|
text = ", ".join(selected_items)
|
|
150
|
-
self.
|
|
158
|
+
self.line_edit().setText(text)
|
|
151
159
|
|
|
152
160
|
def showPopup(self):
|
|
153
161
|
# Override showPopup to update text when the dropdown is shown
|
|
@@ -173,8 +181,10 @@ class MultiSelectComboBox(QComboBox):
|
|
|
173
181
|
self.selectedChanged.emit(self.get_selected())
|
|
174
182
|
|
|
175
183
|
def get_unweight_chip(self, tag: str) -> str:
|
|
176
|
-
if self.weight_re
|
|
177
|
-
|
|
184
|
+
if self.weight_re:
|
|
185
|
+
search_re = self.weight_re.search(tag)
|
|
186
|
+
if search_re:
|
|
187
|
+
return search_re.groups()[0]
|
|
178
188
|
return tag
|
|
179
189
|
|
|
180
190
|
def get_chip_list(self, tags: list[str]) -> list[str]:
|
|
@@ -190,6 +200,8 @@ class MultiSelectComboBox(QComboBox):
|
|
|
190
200
|
# Update the checked state of items based on the new selection
|
|
191
201
|
for i in range(self.view.count()):
|
|
192
202
|
item = self.view.item(i)
|
|
203
|
+
if not item:
|
|
204
|
+
continue
|
|
193
205
|
|
|
194
206
|
item.setCheckState(
|
|
195
207
|
Qt.CheckState.Checked
|
|
@@ -14,9 +14,9 @@ class ClickableLabel(QLabel):
|
|
|
14
14
|
super().__init__(text, parent)
|
|
15
15
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
16
16
|
|
|
17
|
-
def mousePressEvent(self,
|
|
17
|
+
def mousePressEvent(self, ev):
|
|
18
18
|
self.clicked.emit()
|
|
19
|
-
super().mousePressEvent(
|
|
19
|
+
super().mousePressEvent(ev)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def is_valid_key(event: QKeyEvent) -> bool:
|
|
@@ -37,6 +37,7 @@ def is_valid_key(event: QKeyEvent) -> bool:
|
|
|
37
37
|
|
|
38
38
|
if event.keyCombination().keyboardModifiers().name == "ControlModifier":
|
|
39
39
|
return True
|
|
40
|
+
return False
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class LongIntSpinBox(QLineEdit):
|
|
@@ -55,13 +56,16 @@ class LongIntSpinBox(QLineEdit):
|
|
|
55
56
|
if value != self.value():
|
|
56
57
|
self.setText(str(value))
|
|
57
58
|
|
|
58
|
-
def value(self) -> int:
|
|
59
|
+
def value(self) -> int | None:
|
|
59
60
|
text = self.text()
|
|
60
61
|
if NUM_RE.match(text):
|
|
61
62
|
return int(text)
|
|
62
63
|
|
|
63
|
-
def eventFilter(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
def eventFilter(
|
|
65
|
+
self, a0: QObject | None, a1: QKeyEvent | QEvent | None
|
|
66
|
+
) -> bool:
|
|
67
|
+
if isinstance(a1, QKeyEvent):
|
|
68
|
+
if a1.type() == QEvent.Type.KeyPress:
|
|
69
|
+
if not is_valid_key(a1):
|
|
70
|
+
return True
|
|
71
|
+
return super().eventFilter(a0, a1)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from jbqt.consts import QtGlobalRefs
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def debug_scroll_pos(func: Callable):
|
|
7
|
+
def wrapper(self, *args, **kwargs):
|
|
8
|
+
if QtGlobalRefs.scroll_area is None:
|
|
9
|
+
return func(self, *args, **kwargs)
|
|
10
|
+
focus = None
|
|
11
|
+
if QtGlobalRefs.main_window:
|
|
12
|
+
focus = QtGlobalRefs.main_window.focusWidget()
|
|
13
|
+
print(f"scroll pos {func.__name__}")
|
|
14
|
+
print("before", QtGlobalRefs.scroll_area.verticalScrollBar().value(), focus)
|
|
15
|
+
result = func(self, *args, **kwargs)
|
|
16
|
+
|
|
17
|
+
if QtGlobalRefs.main_window:
|
|
18
|
+
focus = QtGlobalRefs.main_window.focusWidget()
|
|
19
|
+
print(
|
|
20
|
+
f"after {QtGlobalRefs.scroll_area.verticalScrollBar().value()} {focus}\n"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
return wrapper
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def debug_scroll_pos_no_args(func: Callable):
|
|
29
|
+
def wrapper(self):
|
|
30
|
+
if QtGlobalRefs.scroll_area is None:
|
|
31
|
+
return func(self)
|
|
32
|
+
focus = None
|
|
33
|
+
if QtGlobalRefs.main_window:
|
|
34
|
+
focus = QtGlobalRefs.main_window.focusWidget()
|
|
35
|
+
print(f"scroll pos {func.__name__}")
|
|
36
|
+
print("before", QtGlobalRefs.scroll_area.verticalScrollBar().value(), focus)
|
|
37
|
+
result = func(self)
|
|
38
|
+
if QtGlobalRefs.main_window:
|
|
39
|
+
focus = QtGlobalRefs.main_window.focusWidget()
|
|
40
|
+
print(
|
|
41
|
+
f"after {QtGlobalRefs.scroll_area.verticalScrollBar().value()} {focus}\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
return wrapper
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _call_func(func: Callable, *args, **kwargs):
|
|
50
|
+
if args and kwargs:
|
|
51
|
+
return func(*args, **kwargs)
|
|
52
|
+
elif args:
|
|
53
|
+
return func(*args)
|
|
54
|
+
elif kwargs:
|
|
55
|
+
return func(**kwargs)
|
|
56
|
+
else:
|
|
57
|
+
return func()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def preserve_scroll(func: Callable):
|
|
61
|
+
def wrapper(self, *args, **kwargs):
|
|
62
|
+
if QtGlobalRefs.scroll_area is None:
|
|
63
|
+
return _call_func(func, self, *args, **kwargs)
|
|
64
|
+
|
|
65
|
+
scrollbar = QtGlobalRefs.scroll_area.verticalScrollBar()
|
|
66
|
+
if not scrollbar:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
scroll_pos = scrollbar.value()
|
|
70
|
+
|
|
71
|
+
if not QtGlobalRefs.app:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
focus = QtGlobalRefs.app.focusWidget()
|
|
75
|
+
if not focus:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
result = _call_func(
|
|
79
|
+
func,
|
|
80
|
+
self,
|
|
81
|
+
*args,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
scrollbar.setValue(scroll_pos)
|
|
85
|
+
focus.setFocus()
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "jbqt"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.13"
|
|
8
8
|
description = ""
|
|
9
9
|
authors = [ "Joseph Bochinski <stirgejr@gmail.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -14,4 +14,5 @@ python = "^3.12"
|
|
|
14
14
|
pyqt6 = "^6.9.0"
|
|
15
15
|
fuzzywuzzy = "^0.18.0"
|
|
16
16
|
python-levenshtein = "^0.27.1"
|
|
17
|
-
|
|
17
|
+
jbconsts = "^0.1.1"
|
|
18
|
+
jbutils = "^0.1.23"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"""Const exports"""
|
|
2
|
-
|
|
3
|
-
from jbqt.common import consts, qt_utils
|
|
4
|
-
from jbqt.common.consts import COLORS, STYLES, SIZES, GlobalRefs, ICONS
|
|
5
|
-
from jbqt.common.qt_utils import register_app
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"COLORS",
|
|
9
|
-
"consts",
|
|
10
|
-
"GlobalRefs",
|
|
11
|
-
"ICONS",
|
|
12
|
-
"qt_utils",
|
|
13
|
-
"register_app",
|
|
14
|
-
"SIZES",
|
|
15
|
-
"STYLES",
|
|
16
|
-
]
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
|
|
3
|
-
from jbqt.common.consts import GlobalRefs
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def debug_scroll_pos(func: Callable):
|
|
7
|
-
def wrapper(self, *args, **kwargs):
|
|
8
|
-
if GlobalRefs.scroll_area is None:
|
|
9
|
-
return func(self, *args, **kwargs)
|
|
10
|
-
focus = None
|
|
11
|
-
if GlobalRefs.main_window:
|
|
12
|
-
focus = GlobalRefs.main_window.focusWidget()
|
|
13
|
-
print(f"scroll pos {func.__name__}")
|
|
14
|
-
print("before", GlobalRefs.scroll_area.verticalScrollBar().value(), focus)
|
|
15
|
-
result = func(self, *args, **kwargs)
|
|
16
|
-
|
|
17
|
-
if GlobalRefs.main_window:
|
|
18
|
-
focus = GlobalRefs.main_window.focusWidget()
|
|
19
|
-
print(
|
|
20
|
-
f"after {GlobalRefs.scroll_area.verticalScrollBar().value()} {focus}\n"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
return result
|
|
24
|
-
|
|
25
|
-
return wrapper
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def debug_scroll_pos_no_args(func: Callable):
|
|
29
|
-
def wrapper(self):
|
|
30
|
-
if GlobalRefs.scroll_area is None:
|
|
31
|
-
return func(self)
|
|
32
|
-
focus = None
|
|
33
|
-
if GlobalRefs.main_window:
|
|
34
|
-
focus = GlobalRefs.main_window.focusWidget()
|
|
35
|
-
print(f"scroll pos {func.__name__}")
|
|
36
|
-
print("before", GlobalRefs.scroll_area.verticalScrollBar().value(), focus)
|
|
37
|
-
result = func(self)
|
|
38
|
-
if GlobalRefs.main_window:
|
|
39
|
-
focus = GlobalRefs.main_window.focusWidget()
|
|
40
|
-
print(
|
|
41
|
-
f"after {GlobalRefs.scroll_area.verticalScrollBar().value()} {focus}\n"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
return result
|
|
45
|
-
|
|
46
|
-
return wrapper
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _call_func(func: Callable, *args, **kwargs):
|
|
50
|
-
if args and kwargs:
|
|
51
|
-
return func(*args, **kwargs)
|
|
52
|
-
elif args:
|
|
53
|
-
return func(*args)
|
|
54
|
-
elif kwargs:
|
|
55
|
-
return func(**kwargs)
|
|
56
|
-
else:
|
|
57
|
-
return func()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def preserve_scroll(func: Callable):
|
|
61
|
-
def wrapper(self, *args, **kwargs):
|
|
62
|
-
if GlobalRefs.scroll_area is None:
|
|
63
|
-
return _call_func(func, self, *args, **kwargs)
|
|
64
|
-
|
|
65
|
-
scroll_pos = GlobalRefs.scroll_area.verticalScrollBar().value()
|
|
66
|
-
focus = GlobalRefs.app.focusWidget()
|
|
67
|
-
result = _call_func(
|
|
68
|
-
func,
|
|
69
|
-
self,
|
|
70
|
-
*args,
|
|
71
|
-
**kwargs,
|
|
72
|
-
)
|
|
73
|
-
GlobalRefs.scroll_area.verticalScrollBar().setValue(scroll_pos)
|
|
74
|
-
focus.setFocus()
|
|
75
|
-
return result
|
|
76
|
-
|
|
77
|
-
return wrapper
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|