sqlshell 0.1.8__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- sqlshell/LICENSE +21 -0
- sqlshell/MANIFEST.in +6 -0
- sqlshell/README.md +59 -0
- sqlshell/__init__.py +1 -1
- sqlshell/context_suggester.py +765 -0
- sqlshell/create_test_data.py +106 -30
- sqlshell/db/__init__.py +5 -0
- sqlshell/db/database_manager.py +837 -0
- sqlshell/editor.py +610 -52
- sqlshell/main.py +2657 -1164
- sqlshell/menus.py +171 -0
- sqlshell/query_tab.py +201 -0
- sqlshell/resources/create_icon.py +106 -28
- sqlshell/resources/create_splash.py +41 -11
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/splash_screen.py +276 -48
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +275 -0
- sqlshell/table_list.py +907 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +403 -0
- sqlshell/utils/__init__.py +8 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_keys.py +356 -0
- sqlshell-0.2.0.dist-info/METADATA +198 -0
- sqlshell-0.2.0.dist-info/RECORD +41 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/WHEEL +1 -1
- sqlshell/setup.py +0 -42
- sqlshell-0.1.8.dist-info/METADATA +0 -120
- sqlshell-0.1.8.dist-info/RECORD +0 -21
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.2.0.dist-info}/top_level.txt +0 -0
sqlshell/ui/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QStyledItemDelegate
|
|
2
|
+
from PyQt6.QtCore import Qt, QRect
|
|
3
|
+
from PyQt6.QtGui import QColor
|
|
4
|
+
|
|
5
|
+
class BarChartDelegate(QStyledItemDelegate):
|
|
6
|
+
def __init__(self, parent=None):
|
|
7
|
+
super().__init__(parent)
|
|
8
|
+
self.min_val = 0
|
|
9
|
+
self.max_val = 1
|
|
10
|
+
self.bar_color = QColor("#3498DB")
|
|
11
|
+
|
|
12
|
+
def set_range(self, min_val, max_val):
|
|
13
|
+
self.min_val = min_val
|
|
14
|
+
self.max_val = max_val
|
|
15
|
+
|
|
16
|
+
def paint(self, painter, option, index):
|
|
17
|
+
# Draw the default background
|
|
18
|
+
super().paint(painter, option, index)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
text = index.data()
|
|
22
|
+
value = float(text.replace(',', ''))
|
|
23
|
+
|
|
24
|
+
# Calculate normalized value
|
|
25
|
+
range_val = self.max_val - self.min_val if self.max_val != self.min_val else 1
|
|
26
|
+
normalized = (value - self.min_val) / range_val
|
|
27
|
+
|
|
28
|
+
# Define bar dimensions
|
|
29
|
+
bar_height = 16
|
|
30
|
+
max_bar_width = 100
|
|
31
|
+
bar_width = max(5, int(max_bar_width * normalized))
|
|
32
|
+
|
|
33
|
+
# Calculate positions
|
|
34
|
+
text_width = option.fontMetrics.horizontalAdvance(text) + 10
|
|
35
|
+
bar_x = option.rect.left() + text_width + 10
|
|
36
|
+
bar_y = option.rect.center().y() - bar_height // 2
|
|
37
|
+
|
|
38
|
+
# Draw the bar
|
|
39
|
+
bar_rect = QRect(bar_x, bar_y, bar_width, bar_height)
|
|
40
|
+
painter.fillRect(bar_rect, self.bar_color)
|
|
41
|
+
|
|
42
|
+
# Draw the text
|
|
43
|
+
text_rect = QRect(option.rect.left() + 4, option.rect.top(),
|
|
44
|
+
text_width, option.rect.height())
|
|
45
|
+
painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
|
|
46
|
+
|
|
47
|
+
except (ValueError, AttributeError):
|
|
48
|
+
# If not a number, just draw the text
|
|
49
|
+
super().paint(painter, option, index)
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import (QHeaderView, QMenu, QCheckBox, QWidgetAction,
|
|
2
|
+
QWidget, QVBoxLayout, QLineEdit, QHBoxLayout, QPushButton, QTableWidget)
|
|
3
|
+
from PyQt6.QtCore import Qt, QRect, QPoint
|
|
4
|
+
from PyQt6.QtGui import QColor, QFont, QPolygon, QPainterPath, QBrush
|
|
5
|
+
|
|
6
|
+
class FilterHeader(QHeaderView):
|
|
7
|
+
def __init__(self, parent=None):
|
|
8
|
+
super().__init__(Qt.Orientation.Horizontal, parent)
|
|
9
|
+
self.filter_buttons = []
|
|
10
|
+
self.active_filters = {} # Track active filters for each column
|
|
11
|
+
self.columns_with_bars = set() # Track which columns show bar charts
|
|
12
|
+
self.bar_delegates = {} # Store delegates for columns with bars
|
|
13
|
+
self.setSectionsClickable(True)
|
|
14
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
15
|
+
self.customContextMenuRequested.connect(self.show_header_context_menu)
|
|
16
|
+
self.main_window = None # Store reference to main window
|
|
17
|
+
self.filter_icon_color = QColor("#3498DB") # Bright blue color for filter icon
|
|
18
|
+
|
|
19
|
+
def toggle_bar_chart(self, column_index):
|
|
20
|
+
"""Toggle bar chart visualization for a column"""
|
|
21
|
+
table = self.parent()
|
|
22
|
+
if not table:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
if column_index in self.columns_with_bars:
|
|
26
|
+
# Remove bars
|
|
27
|
+
self.columns_with_bars.remove(column_index)
|
|
28
|
+
if column_index in self.bar_delegates:
|
|
29
|
+
table.setItemDelegateForColumn(column_index, None)
|
|
30
|
+
del self.bar_delegates[column_index]
|
|
31
|
+
else:
|
|
32
|
+
# Add bars
|
|
33
|
+
self.columns_with_bars.add(column_index)
|
|
34
|
+
|
|
35
|
+
# Get all values for normalization
|
|
36
|
+
values = []
|
|
37
|
+
for row in range(table.rowCount()):
|
|
38
|
+
item = table.item(row, column_index)
|
|
39
|
+
if item:
|
|
40
|
+
try:
|
|
41
|
+
value = float(item.text().replace(',', ''))
|
|
42
|
+
values.append(value)
|
|
43
|
+
except ValueError:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if not values:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Calculate min and max for normalization
|
|
50
|
+
min_val = min(values)
|
|
51
|
+
max_val = max(values)
|
|
52
|
+
|
|
53
|
+
# Import BarChartDelegate here to avoid circular imports
|
|
54
|
+
from sqlshell.ui.bar_chart_delegate import BarChartDelegate
|
|
55
|
+
|
|
56
|
+
# Create and set up delegate
|
|
57
|
+
delegate = BarChartDelegate(table)
|
|
58
|
+
delegate.set_range(min_val, max_val)
|
|
59
|
+
self.bar_delegates[column_index] = delegate
|
|
60
|
+
table.setItemDelegateForColumn(column_index, delegate)
|
|
61
|
+
|
|
62
|
+
# Update the view
|
|
63
|
+
table.viewport().update()
|
|
64
|
+
|
|
65
|
+
def show_header_context_menu(self, pos):
|
|
66
|
+
"""Show context menu for header section"""
|
|
67
|
+
logical_index = self.logicalIndexAt(pos)
|
|
68
|
+
if logical_index < 0:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Create context menu
|
|
72
|
+
context_menu = QMenu(self)
|
|
73
|
+
context_menu.setStyleSheet("""
|
|
74
|
+
QMenu {
|
|
75
|
+
background-color: white;
|
|
76
|
+
border: 1px solid #BDC3C7;
|
|
77
|
+
padding: 5px;
|
|
78
|
+
}
|
|
79
|
+
QMenu::item {
|
|
80
|
+
padding: 5px 20px;
|
|
81
|
+
}
|
|
82
|
+
QMenu::item:selected {
|
|
83
|
+
background-color: #3498DB;
|
|
84
|
+
color: white;
|
|
85
|
+
}
|
|
86
|
+
""")
|
|
87
|
+
|
|
88
|
+
# Add sort actions
|
|
89
|
+
sort_asc_action = context_menu.addAction("Sort Ascending")
|
|
90
|
+
sort_desc_action = context_menu.addAction("Sort Descending")
|
|
91
|
+
context_menu.addSeparator()
|
|
92
|
+
filter_action = context_menu.addAction("Filter...")
|
|
93
|
+
|
|
94
|
+
# Add bar chart action if column is numeric
|
|
95
|
+
table = self.parent()
|
|
96
|
+
if table and table.rowCount() > 0:
|
|
97
|
+
try:
|
|
98
|
+
# Check if column contains numeric values
|
|
99
|
+
sample_value = table.item(0, logical_index).text()
|
|
100
|
+
float(sample_value.replace(',', '')) # Try converting to float
|
|
101
|
+
|
|
102
|
+
context_menu.addSeparator()
|
|
103
|
+
toggle_bar_action = context_menu.addAction(
|
|
104
|
+
"Remove Bar Chart" if logical_index in self.columns_with_bars
|
|
105
|
+
else "Add Bar Chart"
|
|
106
|
+
)
|
|
107
|
+
except (ValueError, AttributeError):
|
|
108
|
+
toggle_bar_action = None
|
|
109
|
+
else:
|
|
110
|
+
toggle_bar_action = None
|
|
111
|
+
|
|
112
|
+
# Show menu and get selected action
|
|
113
|
+
action = context_menu.exec(self.mapToGlobal(pos))
|
|
114
|
+
|
|
115
|
+
if not action:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
table = self.parent()
|
|
119
|
+
if not table:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if action == sort_asc_action:
|
|
123
|
+
table.sortItems(logical_index, Qt.SortOrder.AscendingOrder)
|
|
124
|
+
elif action == sort_desc_action:
|
|
125
|
+
table.sortItems(logical_index, Qt.SortOrder.DescendingOrder)
|
|
126
|
+
elif action == filter_action:
|
|
127
|
+
self.show_filter_menu(logical_index)
|
|
128
|
+
elif action == toggle_bar_action:
|
|
129
|
+
self.toggle_bar_chart(logical_index)
|
|
130
|
+
|
|
131
|
+
def set_main_window(self, window):
|
|
132
|
+
"""Set the reference to the main window"""
|
|
133
|
+
self.main_window = window
|
|
134
|
+
|
|
135
|
+
def paintSection(self, painter, rect, logical_index):
|
|
136
|
+
"""Override paint section to add filter indicator"""
|
|
137
|
+
super().paintSection(painter, rect, logical_index)
|
|
138
|
+
|
|
139
|
+
if logical_index in self.active_filters:
|
|
140
|
+
# Draw background highlight for filtered columns
|
|
141
|
+
highlight_color = QColor(52, 152, 219, 30) # Light blue background
|
|
142
|
+
painter.fillRect(rect, highlight_color)
|
|
143
|
+
|
|
144
|
+
# Make icon larger and more visible
|
|
145
|
+
icon_size = min(rect.height() - 8, 24) # Larger icon, but not too large
|
|
146
|
+
margin = 6
|
|
147
|
+
icon_rect = QRect(
|
|
148
|
+
rect.right() - icon_size - margin,
|
|
149
|
+
rect.top() + (rect.height() - icon_size) // 2,
|
|
150
|
+
icon_size,
|
|
151
|
+
icon_size
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Draw filter icon with improved visibility
|
|
155
|
+
painter.save()
|
|
156
|
+
|
|
157
|
+
# Set up the pen for better visibility
|
|
158
|
+
pen = painter.pen()
|
|
159
|
+
pen.setWidth(3) # Thicker lines
|
|
160
|
+
pen.setColor(self.filter_icon_color)
|
|
161
|
+
painter.setPen(pen)
|
|
162
|
+
|
|
163
|
+
# Calculate points for larger funnel shape
|
|
164
|
+
points = [
|
|
165
|
+
QPoint(icon_rect.left(), icon_rect.top()),
|
|
166
|
+
QPoint(icon_rect.right(), icon_rect.top()),
|
|
167
|
+
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.center().y()),
|
|
168
|
+
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.bottom()),
|
|
169
|
+
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.bottom()),
|
|
170
|
+
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.center().y()),
|
|
171
|
+
QPoint(icon_rect.left(), icon_rect.top())
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Create and fill path
|
|
175
|
+
path = QPainterPath()
|
|
176
|
+
path.moveTo(float(points[0].x()), float(points[0].y()))
|
|
177
|
+
for point in points[1:]:
|
|
178
|
+
path.lineTo(float(point.x()), float(point.y()))
|
|
179
|
+
|
|
180
|
+
# Fill with semi-transparent blue
|
|
181
|
+
painter.fillPath(path, QBrush(QColor(52, 152, 219, 120))) # More opaque fill
|
|
182
|
+
|
|
183
|
+
# Draw outline
|
|
184
|
+
painter.drawPolyline(QPolygon(points))
|
|
185
|
+
|
|
186
|
+
# If multiple values are filtered, add a number
|
|
187
|
+
if len(self.active_filters[logical_index]) > 1:
|
|
188
|
+
# Draw number with better visibility
|
|
189
|
+
number_rect = QRect(icon_rect.left(), icon_rect.top(),
|
|
190
|
+
icon_rect.width(), icon_rect.height())
|
|
191
|
+
painter.setFont(QFont("Arial", icon_size//2, QFont.Weight.Bold))
|
|
192
|
+
|
|
193
|
+
# Draw text shadow for better contrast
|
|
194
|
+
painter.setPen(QColor("white"))
|
|
195
|
+
painter.drawText(number_rect.adjusted(1, 1, 1, 1),
|
|
196
|
+
Qt.AlignmentFlag.AlignCenter,
|
|
197
|
+
str(len(self.active_filters[logical_index])))
|
|
198
|
+
|
|
199
|
+
# Draw main text
|
|
200
|
+
painter.setPen(self.filter_icon_color)
|
|
201
|
+
painter.drawText(number_rect, Qt.AlignmentFlag.AlignCenter,
|
|
202
|
+
str(len(self.active_filters[logical_index])))
|
|
203
|
+
|
|
204
|
+
painter.restore()
|
|
205
|
+
|
|
206
|
+
# Draw a more visible indicator at the bottom of the header section
|
|
207
|
+
painter.save()
|
|
208
|
+
indicator_height = 3 # Thicker indicator line
|
|
209
|
+
indicator_rect = QRect(rect.left(), rect.bottom() - indicator_height,
|
|
210
|
+
rect.width(), indicator_height)
|
|
211
|
+
painter.fillRect(indicator_rect, self.filter_icon_color)
|
|
212
|
+
painter.restore()
|
|
213
|
+
|
|
214
|
+
def show_filter_menu(self, logical_index):
|
|
215
|
+
if not self.parent() or not isinstance(self.parent(), QTableWidget):
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
table = self.parent()
|
|
219
|
+
unique_values = set()
|
|
220
|
+
|
|
221
|
+
# Collect unique values from the column
|
|
222
|
+
for row in range(table.rowCount()):
|
|
223
|
+
item = table.item(row, logical_index)
|
|
224
|
+
if item and not table.isRowHidden(row):
|
|
225
|
+
unique_values.add(item.text())
|
|
226
|
+
|
|
227
|
+
# Create and show the filter menu
|
|
228
|
+
menu = QMenu(self)
|
|
229
|
+
menu.setStyleSheet("""
|
|
230
|
+
QMenu {
|
|
231
|
+
background-color: white;
|
|
232
|
+
border: 1px solid #BDC3C7;
|
|
233
|
+
padding: 5px;
|
|
234
|
+
}
|
|
235
|
+
QMenu::item {
|
|
236
|
+
padding: 5px 20px;
|
|
237
|
+
}
|
|
238
|
+
QMenu::item:selected {
|
|
239
|
+
background-color: #3498DB;
|
|
240
|
+
color: white;
|
|
241
|
+
}
|
|
242
|
+
QCheckBox {
|
|
243
|
+
padding: 5px;
|
|
244
|
+
}
|
|
245
|
+
QScrollArea {
|
|
246
|
+
border: none;
|
|
247
|
+
}
|
|
248
|
+
""")
|
|
249
|
+
|
|
250
|
+
# Add search box at the top
|
|
251
|
+
search_widget = QWidget(menu)
|
|
252
|
+
search_layout = QVBoxLayout(search_widget)
|
|
253
|
+
search_edit = QLineEdit(search_widget)
|
|
254
|
+
search_edit.setPlaceholderText("Search values...")
|
|
255
|
+
search_layout.addWidget(search_edit)
|
|
256
|
+
|
|
257
|
+
# Add action for search widget
|
|
258
|
+
search_action = QWidgetAction(menu)
|
|
259
|
+
search_action.setDefaultWidget(search_widget)
|
|
260
|
+
menu.addAction(search_action)
|
|
261
|
+
menu.addSeparator()
|
|
262
|
+
|
|
263
|
+
# Add "Select All" checkbox
|
|
264
|
+
select_all = QCheckBox("Select All", menu)
|
|
265
|
+
select_all.setChecked(True)
|
|
266
|
+
select_all_action = QWidgetAction(menu)
|
|
267
|
+
select_all_action.setDefaultWidget(select_all)
|
|
268
|
+
menu.addAction(select_all_action)
|
|
269
|
+
menu.addSeparator()
|
|
270
|
+
|
|
271
|
+
# Create scrollable area for checkboxes
|
|
272
|
+
scroll_widget = QWidget(menu)
|
|
273
|
+
scroll_layout = QVBoxLayout(scroll_widget)
|
|
274
|
+
scroll_layout.setSpacing(2)
|
|
275
|
+
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
|
276
|
+
|
|
277
|
+
# Add checkboxes for unique values
|
|
278
|
+
value_checkboxes = {}
|
|
279
|
+
for value in sorted(unique_values):
|
|
280
|
+
checkbox = QCheckBox(str(value), scroll_widget)
|
|
281
|
+
# Set checked state based on active filters
|
|
282
|
+
checkbox.setChecked(logical_index not in self.active_filters or
|
|
283
|
+
value in self.active_filters[logical_index])
|
|
284
|
+
value_checkboxes[value] = checkbox
|
|
285
|
+
scroll_layout.addWidget(checkbox)
|
|
286
|
+
|
|
287
|
+
# Add scrollable area to menu
|
|
288
|
+
scroll_action = QWidgetAction(menu)
|
|
289
|
+
scroll_action.setDefaultWidget(scroll_widget)
|
|
290
|
+
menu.addAction(scroll_action)
|
|
291
|
+
|
|
292
|
+
# Connect search box to filter checkboxes
|
|
293
|
+
def filter_checkboxes(text):
|
|
294
|
+
for value, checkbox in value_checkboxes.items():
|
|
295
|
+
checkbox.setVisible(text.lower() in str(value).lower())
|
|
296
|
+
|
|
297
|
+
search_edit.textChanged.connect(filter_checkboxes)
|
|
298
|
+
|
|
299
|
+
# Connect select all to other checkboxes
|
|
300
|
+
def toggle_all(state):
|
|
301
|
+
for checkbox in value_checkboxes.values():
|
|
302
|
+
if not checkbox.isHidden(): # Only toggle visible checkboxes
|
|
303
|
+
checkbox.setChecked(state)
|
|
304
|
+
|
|
305
|
+
select_all.stateChanged.connect(toggle_all)
|
|
306
|
+
|
|
307
|
+
# Add Apply and Clear buttons
|
|
308
|
+
menu.addSeparator()
|
|
309
|
+
apply_button = QPushButton("Apply Filter", menu)
|
|
310
|
+
apply_button.setStyleSheet("""
|
|
311
|
+
QPushButton {
|
|
312
|
+
background-color: #2ECC71;
|
|
313
|
+
color: white;
|
|
314
|
+
border: none;
|
|
315
|
+
padding: 5px 15px;
|
|
316
|
+
border-radius: 3px;
|
|
317
|
+
}
|
|
318
|
+
QPushButton:hover {
|
|
319
|
+
background-color: #27AE60;
|
|
320
|
+
}
|
|
321
|
+
""")
|
|
322
|
+
|
|
323
|
+
clear_button = QPushButton("Clear Filter", menu)
|
|
324
|
+
clear_button.setStyleSheet("""
|
|
325
|
+
QPushButton {
|
|
326
|
+
background-color: #E74C3C;
|
|
327
|
+
color: white;
|
|
328
|
+
border: none;
|
|
329
|
+
padding: 5px 15px;
|
|
330
|
+
border-radius: 3px;
|
|
331
|
+
}
|
|
332
|
+
QPushButton:hover {
|
|
333
|
+
background-color: #C0392B;
|
|
334
|
+
}
|
|
335
|
+
""")
|
|
336
|
+
|
|
337
|
+
button_widget = QWidget(menu)
|
|
338
|
+
button_layout = QHBoxLayout(button_widget)
|
|
339
|
+
button_layout.addWidget(apply_button)
|
|
340
|
+
button_layout.addWidget(clear_button)
|
|
341
|
+
|
|
342
|
+
button_action = QWidgetAction(menu)
|
|
343
|
+
button_action.setDefaultWidget(button_widget)
|
|
344
|
+
menu.addAction(button_action)
|
|
345
|
+
|
|
346
|
+
def apply_filter():
|
|
347
|
+
# Get selected values
|
|
348
|
+
selected_values = {value for value, checkbox in value_checkboxes.items()
|
|
349
|
+
if checkbox.isChecked()}
|
|
350
|
+
|
|
351
|
+
if len(selected_values) < len(unique_values):
|
|
352
|
+
# Store active filter only if not all values are selected
|
|
353
|
+
self.active_filters[logical_index] = selected_values
|
|
354
|
+
else:
|
|
355
|
+
# Remove filter if all values are selected
|
|
356
|
+
self.active_filters.pop(logical_index, None)
|
|
357
|
+
|
|
358
|
+
# Apply all active filters
|
|
359
|
+
self.apply_all_filters(table)
|
|
360
|
+
|
|
361
|
+
menu.close()
|
|
362
|
+
self.updateSection(logical_index) # Redraw section to show/hide filter icon
|
|
363
|
+
|
|
364
|
+
def clear_filter():
|
|
365
|
+
# Remove filter for this column
|
|
366
|
+
if logical_index in self.active_filters:
|
|
367
|
+
del self.active_filters[logical_index]
|
|
368
|
+
|
|
369
|
+
# Apply remaining filters
|
|
370
|
+
self.apply_all_filters(table)
|
|
371
|
+
|
|
372
|
+
menu.close()
|
|
373
|
+
self.updateSection(logical_index) # Redraw section to hide filter icon
|
|
374
|
+
|
|
375
|
+
apply_button.clicked.connect(apply_filter)
|
|
376
|
+
clear_button.clicked.connect(clear_filter)
|
|
377
|
+
|
|
378
|
+
# Show menu under the header section
|
|
379
|
+
header_pos = self.mapToGlobal(self.geometry().bottomLeft())
|
|
380
|
+
header_pos.setX(header_pos.x() + self.sectionPosition(logical_index))
|
|
381
|
+
menu.exec(header_pos)
|
|
382
|
+
|
|
383
|
+
def apply_all_filters(self, table):
|
|
384
|
+
"""Apply all active filters to the table"""
|
|
385
|
+
# Show all rows first
|
|
386
|
+
for row in range(table.rowCount()):
|
|
387
|
+
table.setRowHidden(row, False)
|
|
388
|
+
|
|
389
|
+
# Apply each active filter
|
|
390
|
+
for col_idx, allowed_values in self.active_filters.items():
|
|
391
|
+
for row in range(table.rowCount()):
|
|
392
|
+
item = table.item(row, col_idx)
|
|
393
|
+
if item and not table.isRowHidden(row):
|
|
394
|
+
table.setRowHidden(row, item.text() not in allowed_values)
|
|
395
|
+
|
|
396
|
+
# Update status bar with visible row count
|
|
397
|
+
if self.main_window:
|
|
398
|
+
visible_rows = sum(1 for row in range(table.rowCount())
|
|
399
|
+
if not table.isRowHidden(row))
|
|
400
|
+
total_filters = len(self.active_filters)
|
|
401
|
+
filter_text = f" ({total_filters} filter{'s' if total_filters != 1 else ''} active)" if total_filters > 0 else ""
|
|
402
|
+
self.main_window.statusBar().showMessage(
|
|
403
|
+
f"Showing {visible_rows:,} rows{filter_text}")
|