sqlshell 0.4.4__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.
- sqlshell/__init__.py +84 -0
- sqlshell/__main__.py +4926 -0
- sqlshell/ai_autocomplete.py +392 -0
- sqlshell/ai_settings_dialog.py +337 -0
- sqlshell/context_suggester.py +768 -0
- sqlshell/create_test_data.py +152 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/db/__init__.py +6 -0
- sqlshell/db/database_manager.py +1318 -0
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor.py +1166 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/menus.py +262 -0
- sqlshell/notification_manager.py +370 -0
- sqlshell/query_tab.py +904 -0
- sqlshell/resources/__init__.py +1 -0
- 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/resources/splash_screen.gif +0 -0
- sqlshell/space_invaders.py +501 -0
- sqlshell/splash_screen.py +405 -0
- sqlshell/sqlshell/__init__.py +5 -0
- sqlshell/sqlshell/create_test_data.py +118 -0
- sqlshell/sqlshell/create_test_databases.py +96 -0
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +330 -0
- sqlshell/syntax_highlighter.py +124 -0
- sqlshell/table_list.py +996 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +469 -0
- sqlshell/utils/__init__.py +16 -0
- sqlshell/utils/profile_cn2.py +1661 -0
- sqlshell/utils/profile_column.py +2635 -0
- sqlshell/utils/profile_distributions.py +616 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_foreign_keys.py +779 -0
- sqlshell/utils/profile_keys.py +2834 -0
- sqlshell/utils/profile_ohe.py +934 -0
- sqlshell/utils/profile_ohe_advanced.py +754 -0
- sqlshell/utils/profile_ohe_comparison.py +237 -0
- sqlshell/utils/profile_prediction.py +926 -0
- sqlshell/utils/profile_similarity.py +876 -0
- sqlshell/utils/search_in_df.py +90 -0
- sqlshell/widgets.py +400 -0
- sqlshell-0.4.4.dist-info/METADATA +441 -0
- sqlshell-0.4.4.dist-info/RECORD +54 -0
- sqlshell-0.4.4.dist-info/WHEEL +5 -0
- sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
- sqlshell-0.4.4.dist-info/top_level.txt +1 -0
sqlshell/table_list.py
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from PyQt6.QtWidgets import (QApplication, QListWidget, QListWidgetItem,
|
|
5
|
+
QMessageBox, QMainWindow, QVBoxLayout, QLabel,
|
|
6
|
+
QWidget, QHBoxLayout, QFrame, QTreeWidget, QTreeWidgetItem,
|
|
7
|
+
QMenu, QInputDialog, QLineEdit)
|
|
8
|
+
from PyQt6.QtCore import Qt, QPoint, QMimeData, QTimer, QSize
|
|
9
|
+
from PyQt6.QtGui import QIcon, QDrag, QPainter, QColor, QBrush, QPixmap, QFont, QCursor, QAction
|
|
10
|
+
from PyQt6.QtCore import pyqtSignal
|
|
11
|
+
|
|
12
|
+
class DraggableTablesList(QTreeWidget):
|
|
13
|
+
"""Custom QTreeWidget that provides folders and drag-and-drop functionality for table names.
|
|
14
|
+
|
|
15
|
+
Features:
|
|
16
|
+
- Hierarchical display of tables in folders
|
|
17
|
+
- Drag and drop tables between folders
|
|
18
|
+
- Visual feedback when dragging tables over folders
|
|
19
|
+
- Double-click to expand/collapse folders
|
|
20
|
+
- Tables can be dragged into query editor for SQL generation
|
|
21
|
+
- Context menu for folder management and table operations
|
|
22
|
+
- Tables needing reload are marked with special icons
|
|
23
|
+
- Tables can be dragged from root to folders and vice versa
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Define signals
|
|
27
|
+
itemDropped = pyqtSignal(str, str, bool) # source_item, target_folder, success
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent=None):
|
|
30
|
+
super().__init__(parent)
|
|
31
|
+
self.parent = parent
|
|
32
|
+
self.setDragEnabled(True)
|
|
33
|
+
self.setAcceptDrops(True)
|
|
34
|
+
self.setDragDropMode(QTreeWidget.DragDropMode.InternalMove)
|
|
35
|
+
|
|
36
|
+
# Configure tree widget
|
|
37
|
+
self.setHeaderHidden(True)
|
|
38
|
+
self.setColumnCount(1)
|
|
39
|
+
self.setIndentation(15) # Smaller indentation for a cleaner look
|
|
40
|
+
self.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
|
|
41
|
+
self.setExpandsOnDoubleClick(False) # Handle double-clicks manually
|
|
42
|
+
|
|
43
|
+
# Apply custom styling
|
|
44
|
+
self.setStyleSheet(self.get_stylesheet())
|
|
45
|
+
|
|
46
|
+
# Store tables that need reloading
|
|
47
|
+
self.tables_needing_reload = set()
|
|
48
|
+
|
|
49
|
+
# Connect signals
|
|
50
|
+
self.itemDoubleClicked.connect(self.handle_item_double_click)
|
|
51
|
+
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
52
|
+
self.customContextMenuRequested.connect(self.show_context_menu)
|
|
53
|
+
|
|
54
|
+
def get_stylesheet(self):
|
|
55
|
+
"""Get the stylesheet for the draggable tables list"""
|
|
56
|
+
return """
|
|
57
|
+
QTreeWidget {
|
|
58
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
59
|
+
border: none;
|
|
60
|
+
border-radius: 4px;
|
|
61
|
+
color: white;
|
|
62
|
+
}
|
|
63
|
+
QTreeWidget::item:selected {
|
|
64
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
65
|
+
}
|
|
66
|
+
QTreeWidget::item:hover:!selected {
|
|
67
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
68
|
+
}
|
|
69
|
+
QTreeWidget::branch {
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
}
|
|
72
|
+
QTreeWidget::branch:has-children:!has-siblings:closed,
|
|
73
|
+
QTreeWidget::branch:closed:has-children:has-siblings {
|
|
74
|
+
border-image: none;
|
|
75
|
+
image: url(:/images/branch-closed);
|
|
76
|
+
}
|
|
77
|
+
QTreeWidget::branch:open:has-children:!has-siblings,
|
|
78
|
+
QTreeWidget::branch:open:has-children:has-siblings {
|
|
79
|
+
border-image: none;
|
|
80
|
+
image: url(:/images/branch-open);
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def handle_item_double_click(self, item, column):
|
|
85
|
+
"""Handle double-clicking on a tree item"""
|
|
86
|
+
if not item:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Check if it's a folder - toggle expand/collapse
|
|
90
|
+
if self.is_folder_item(item):
|
|
91
|
+
if item.isExpanded():
|
|
92
|
+
item.setExpanded(False)
|
|
93
|
+
else:
|
|
94
|
+
item.setExpanded(True)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# For table items, get the table name
|
|
98
|
+
table_name = self.get_table_name_from_item(item)
|
|
99
|
+
|
|
100
|
+
# Check if this table needs reloading
|
|
101
|
+
if table_name in self.tables_needing_reload:
|
|
102
|
+
# Reload the table immediately without prompting
|
|
103
|
+
if self.parent and hasattr(self.parent, 'reload_selected_table'):
|
|
104
|
+
self.parent.reload_selected_table(table_name)
|
|
105
|
+
|
|
106
|
+
# For non-folder items, handle showing the table preview
|
|
107
|
+
if self.parent and hasattr(self.parent, 'show_table_preview'):
|
|
108
|
+
self.parent.show_table_preview(item)
|
|
109
|
+
|
|
110
|
+
def is_folder_item(self, item):
|
|
111
|
+
"""Check if an item is a folder"""
|
|
112
|
+
return item.data(0, Qt.ItemDataRole.UserRole) == "folder"
|
|
113
|
+
|
|
114
|
+
def get_table_name_from_item(self, item):
|
|
115
|
+
"""Extract the table name from an item (without the source info)"""
|
|
116
|
+
if self.is_folder_item(item):
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return item.text(0).split(' (')[0]
|
|
120
|
+
|
|
121
|
+
def startDrag(self, supportedActions):
|
|
122
|
+
"""Override startDrag to customize the drag data."""
|
|
123
|
+
# Check for multiple selected items
|
|
124
|
+
selected_items = self.selectedItems()
|
|
125
|
+
if len(selected_items) > 1:
|
|
126
|
+
# Only support dragging multiple items to the editor (not for folder management)
|
|
127
|
+
# Filter out folder items
|
|
128
|
+
table_items = [item for item in selected_items if not self.is_folder_item(item)]
|
|
129
|
+
|
|
130
|
+
if not table_items:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Extract table names
|
|
134
|
+
table_names = [self.get_table_name_from_item(item) for item in table_items]
|
|
135
|
+
table_names = [name for name in table_names if name] # Remove None values
|
|
136
|
+
|
|
137
|
+
if not table_names:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Create mime data with comma-separated table names
|
|
141
|
+
mime_data = QMimeData()
|
|
142
|
+
mime_data.setText(", ".join(table_names))
|
|
143
|
+
|
|
144
|
+
# Create drag object
|
|
145
|
+
drag = QDrag(self)
|
|
146
|
+
drag.setMimeData(mime_data)
|
|
147
|
+
|
|
148
|
+
# Create a visually appealing drag pixmap
|
|
149
|
+
font = self.font()
|
|
150
|
+
font.setBold(True)
|
|
151
|
+
metrics = self.fontMetrics()
|
|
152
|
+
|
|
153
|
+
# Build a preview label with limited number of tables
|
|
154
|
+
display_names = table_names[:3]
|
|
155
|
+
if len(table_names) > 3:
|
|
156
|
+
display_text = f"{', '.join(display_names)} (+{len(table_names) - 3} more)"
|
|
157
|
+
else:
|
|
158
|
+
display_text = ", ".join(display_names)
|
|
159
|
+
|
|
160
|
+
text_width = metrics.horizontalAdvance(display_text)
|
|
161
|
+
text_height = metrics.height()
|
|
162
|
+
|
|
163
|
+
# Make the pixmap large enough for the text plus padding and a small icon
|
|
164
|
+
padding = 10
|
|
165
|
+
pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
|
|
166
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
167
|
+
|
|
168
|
+
# Begin painting
|
|
169
|
+
painter = QPainter(pixmap)
|
|
170
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
171
|
+
|
|
172
|
+
# Draw a nice rounded rectangle background
|
|
173
|
+
bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
|
|
174
|
+
painter.setBrush(QBrush(bg_color))
|
|
175
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
176
|
+
painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
|
|
177
|
+
|
|
178
|
+
# Draw text
|
|
179
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
180
|
+
painter.setFont(font)
|
|
181
|
+
painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), display_text)
|
|
182
|
+
|
|
183
|
+
# Draw a small database icon (simulated)
|
|
184
|
+
icon_x = padding / 2
|
|
185
|
+
icon_y = (pixmap.height() - 12) / 2
|
|
186
|
+
|
|
187
|
+
# Draw a simple database icon as a blue circle with lines
|
|
188
|
+
table_icon_color = QColor("#3498DB")
|
|
189
|
+
painter.setBrush(QBrush(table_icon_color))
|
|
190
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
191
|
+
painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
|
|
192
|
+
|
|
193
|
+
# Draw "table" lines inside the circle
|
|
194
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
195
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
|
|
196
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
|
|
197
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
|
|
198
|
+
|
|
199
|
+
painter.end()
|
|
200
|
+
|
|
201
|
+
# Set the drag pixmap
|
|
202
|
+
drag.setPixmap(pixmap)
|
|
203
|
+
|
|
204
|
+
# Set hotspot to be at the top-left corner of the text
|
|
205
|
+
drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
|
|
206
|
+
|
|
207
|
+
# Execute drag operation - only allow copy action for multiple tables
|
|
208
|
+
drag.exec(Qt.DropAction.CopyAction)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Single item drag (original functionality)
|
|
212
|
+
item = self.currentItem()
|
|
213
|
+
if not item:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# Don't start drag if it's a folder and we're in internal move mode
|
|
217
|
+
if self.is_folder_item(item) and self.dragDropMode() == QTreeWidget.DragDropMode.InternalMove:
|
|
218
|
+
super().startDrag(supportedActions)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Extract the table name without the file info in parentheses
|
|
222
|
+
table_name = self.get_table_name_from_item(item)
|
|
223
|
+
if not table_name:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Create mime data with the table name
|
|
227
|
+
mime_data = QMimeData()
|
|
228
|
+
mime_data.setText(table_name)
|
|
229
|
+
|
|
230
|
+
# Add additional information about the item for internal drags
|
|
231
|
+
full_text = item.text(0)
|
|
232
|
+
if ' (' in full_text:
|
|
233
|
+
source = full_text.split(' (')[1][:-1] # Get the source part
|
|
234
|
+
needs_reload = table_name in self.tables_needing_reload
|
|
235
|
+
|
|
236
|
+
# Store additional metadata in mime data
|
|
237
|
+
mime_data.setData('application/x-sqlshell-tablename', table_name.encode())
|
|
238
|
+
mime_data.setData('application/x-sqlshell-source', source.encode())
|
|
239
|
+
mime_data.setData('application/x-sqlshell-needs-reload', str(needs_reload).encode())
|
|
240
|
+
|
|
241
|
+
# Create drag object
|
|
242
|
+
drag = QDrag(self)
|
|
243
|
+
drag.setMimeData(mime_data)
|
|
244
|
+
|
|
245
|
+
# Create a visually appealing drag pixmap
|
|
246
|
+
font = self.font()
|
|
247
|
+
font.setBold(True)
|
|
248
|
+
metrics = self.fontMetrics()
|
|
249
|
+
text_width = metrics.horizontalAdvance(table_name)
|
|
250
|
+
text_height = metrics.height()
|
|
251
|
+
|
|
252
|
+
# Make the pixmap large enough for the text plus padding and a small icon
|
|
253
|
+
padding = 10
|
|
254
|
+
pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
|
|
255
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
256
|
+
|
|
257
|
+
# Begin painting
|
|
258
|
+
painter = QPainter(pixmap)
|
|
259
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
260
|
+
|
|
261
|
+
# Draw a nice rounded rectangle background
|
|
262
|
+
bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
|
|
263
|
+
painter.setBrush(QBrush(bg_color))
|
|
264
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
265
|
+
painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
|
|
266
|
+
|
|
267
|
+
# Draw text
|
|
268
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
269
|
+
painter.setFont(font)
|
|
270
|
+
painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), table_name)
|
|
271
|
+
|
|
272
|
+
# Draw a small database icon (simulated)
|
|
273
|
+
icon_x = padding / 2
|
|
274
|
+
icon_y = (pixmap.height() - 12) / 2
|
|
275
|
+
|
|
276
|
+
# Draw a simple database icon as a blue circle with lines
|
|
277
|
+
table_icon_color = QColor("#3498DB")
|
|
278
|
+
painter.setBrush(QBrush(table_icon_color))
|
|
279
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
280
|
+
painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
|
|
281
|
+
|
|
282
|
+
# Draw "table" lines inside the circle
|
|
283
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
284
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
|
|
285
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
|
|
286
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
|
|
287
|
+
|
|
288
|
+
painter.end()
|
|
289
|
+
|
|
290
|
+
# Set the drag pixmap
|
|
291
|
+
drag.setPixmap(pixmap)
|
|
292
|
+
|
|
293
|
+
# Set hotspot to be at the top-left corner of the text
|
|
294
|
+
drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
|
|
295
|
+
|
|
296
|
+
# Execute drag operation
|
|
297
|
+
result = drag.exec(supportedActions)
|
|
298
|
+
|
|
299
|
+
# Optional: add a highlight effect after dragging
|
|
300
|
+
if result == Qt.DropAction.CopyAction and item:
|
|
301
|
+
# Briefly highlight the dragged item
|
|
302
|
+
orig_bg = item.background(0)
|
|
303
|
+
item.setBackground(0, QBrush(QColor(26, 188, 156, 100))) # Light green highlight
|
|
304
|
+
|
|
305
|
+
# Reset after a short delay
|
|
306
|
+
QTimer.singleShot(300, lambda: item.setBackground(0, orig_bg))
|
|
307
|
+
|
|
308
|
+
def dropEvent(self, event):
|
|
309
|
+
"""Override drop event to handle dropping items into folders"""
|
|
310
|
+
if event.source() == self: # Internal drop
|
|
311
|
+
drop_pos = event.position().toPoint()
|
|
312
|
+
target_item = self.itemAt(drop_pos)
|
|
313
|
+
current_item = self.currentItem()
|
|
314
|
+
|
|
315
|
+
# Only proceed if we have both a current item and a target
|
|
316
|
+
if current_item and not self.is_folder_item(current_item):
|
|
317
|
+
# If dropping onto a folder, move the item to that folder
|
|
318
|
+
if target_item and self.is_folder_item(target_item):
|
|
319
|
+
# Move the item to the target folder
|
|
320
|
+
self.move_item_to_folder(current_item, target_item)
|
|
321
|
+
|
|
322
|
+
# Get table name for status message
|
|
323
|
+
table_name = self.get_table_name_from_item(current_item)
|
|
324
|
+
folder_name = target_item.text(0)
|
|
325
|
+
|
|
326
|
+
# Emit signal for successful drop
|
|
327
|
+
self.itemDropped.emit(table_name, folder_name, True)
|
|
328
|
+
|
|
329
|
+
# Show status message
|
|
330
|
+
if self.parent:
|
|
331
|
+
self.parent.statusBar().showMessage(f'Moved table "{table_name}" to folder "{folder_name}"')
|
|
332
|
+
|
|
333
|
+
# Expand the folder
|
|
334
|
+
target_item.setExpanded(True)
|
|
335
|
+
|
|
336
|
+
# Prevent standard drop behavior as we've handled it
|
|
337
|
+
event.accept()
|
|
338
|
+
return
|
|
339
|
+
elif not target_item:
|
|
340
|
+
# Dropping onto empty space - move to root
|
|
341
|
+
parent = current_item.parent()
|
|
342
|
+
if parent and self.is_folder_item(parent):
|
|
343
|
+
# Get table name for status message
|
|
344
|
+
table_name = self.get_table_name_from_item(current_item)
|
|
345
|
+
|
|
346
|
+
# Get additional information from the item
|
|
347
|
+
full_text = current_item.text(0)
|
|
348
|
+
source = full_text.split(' (')[1][:-1] if ' (' in full_text else ""
|
|
349
|
+
needs_reload = table_name in self.tables_needing_reload
|
|
350
|
+
|
|
351
|
+
# Remove from current folder
|
|
352
|
+
parent.removeChild(current_item)
|
|
353
|
+
|
|
354
|
+
# Add to root
|
|
355
|
+
self.add_table_item(table_name, source, needs_reload)
|
|
356
|
+
|
|
357
|
+
# Emit signal for successful drop to root
|
|
358
|
+
self.itemDropped.emit(table_name, "", True)
|
|
359
|
+
|
|
360
|
+
# Show status message
|
|
361
|
+
if self.parent:
|
|
362
|
+
self.parent.statusBar().showMessage(f'Moved table "{table_name}" to root')
|
|
363
|
+
|
|
364
|
+
# Prevent standard drop behavior
|
|
365
|
+
event.accept()
|
|
366
|
+
return
|
|
367
|
+
# For folders, let the default behavior handle it
|
|
368
|
+
elif current_item and self.is_folder_item(current_item):
|
|
369
|
+
# Use standard behavior for folders
|
|
370
|
+
super().dropEvent(event)
|
|
371
|
+
|
|
372
|
+
# Show feedback
|
|
373
|
+
if target_item and self.is_folder_item(target_item):
|
|
374
|
+
# Expand the folder
|
|
375
|
+
target_item.setExpanded(True)
|
|
376
|
+
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# Try to extract table information from mime data for external drags
|
|
380
|
+
elif event.mimeData().hasText() and target_item and self.is_folder_item(target_item):
|
|
381
|
+
# This handles drops from other widgets
|
|
382
|
+
mime_data = event.mimeData()
|
|
383
|
+
|
|
384
|
+
# Try to get additional information from custom mime types
|
|
385
|
+
if mime_data.hasFormat('application/x-sqlshell-tablename'):
|
|
386
|
+
# This is a drag from another part of the application with our custom data
|
|
387
|
+
table_name = bytes(mime_data.data('application/x-sqlshell-tablename')).decode()
|
|
388
|
+
source = bytes(mime_data.data('application/x-sqlshell-source')).decode()
|
|
389
|
+
needs_reload_str = bytes(mime_data.data('application/x-sqlshell-needs-reload')).decode()
|
|
390
|
+
needs_reload = needs_reload_str.lower() == 'true'
|
|
391
|
+
|
|
392
|
+
# Create a new item in the target folder
|
|
393
|
+
item = QTreeWidgetItem(target_item)
|
|
394
|
+
item.setText(0, f"{table_name} ({source})")
|
|
395
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
396
|
+
|
|
397
|
+
# Set appropriate icon based on reload status
|
|
398
|
+
if needs_reload:
|
|
399
|
+
self.tables_needing_reload.add(table_name)
|
|
400
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
401
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
402
|
+
else:
|
|
403
|
+
item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
|
|
404
|
+
|
|
405
|
+
# Set item flags
|
|
406
|
+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
|
|
407
|
+
|
|
408
|
+
# Expand the folder
|
|
409
|
+
target_item.setExpanded(True)
|
|
410
|
+
|
|
411
|
+
# Emit signal for successful drop
|
|
412
|
+
self.itemDropped.emit(table_name, target_item.text(0), True)
|
|
413
|
+
|
|
414
|
+
# Show status message
|
|
415
|
+
if self.parent:
|
|
416
|
+
self.parent.statusBar().showMessage(f'Added table "{table_name}" to folder "{target_item.text(0)}"')
|
|
417
|
+
|
|
418
|
+
event.accept()
|
|
419
|
+
return
|
|
420
|
+
else:
|
|
421
|
+
# Just a plain text drop - try to use it as a table name
|
|
422
|
+
table_name = mime_data.text()
|
|
423
|
+
|
|
424
|
+
# Find if this table exists in our list
|
|
425
|
+
existing_item = self.find_table_item(table_name)
|
|
426
|
+
if existing_item:
|
|
427
|
+
# Move existing item to the target folder
|
|
428
|
+
self.move_item_to_folder(existing_item, target_item)
|
|
429
|
+
|
|
430
|
+
# Emit signal for successful drop
|
|
431
|
+
self.itemDropped.emit(table_name, target_item.text(0), True)
|
|
432
|
+
|
|
433
|
+
# Show status message
|
|
434
|
+
if self.parent:
|
|
435
|
+
self.parent.statusBar().showMessage(f'Moved table "{table_name}" to folder "{target_item.text(0)}"')
|
|
436
|
+
|
|
437
|
+
event.accept()
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
# Reset folder highlights before default handling
|
|
441
|
+
self._reset_folder_highlights()
|
|
442
|
+
|
|
443
|
+
# For other cases, use the standard behavior
|
|
444
|
+
super().dropEvent(event)
|
|
445
|
+
|
|
446
|
+
def dragEnterEvent(self, event):
|
|
447
|
+
"""Handle drag enter events with visual feedback"""
|
|
448
|
+
# Accept the event to allow internal drags
|
|
449
|
+
if event.source() == self:
|
|
450
|
+
event.acceptProposedAction()
|
|
451
|
+
else:
|
|
452
|
+
# Let parent class handle external drags
|
|
453
|
+
super().dragEnterEvent(event)
|
|
454
|
+
|
|
455
|
+
def dragMoveEvent(self, event):
|
|
456
|
+
"""Handle drag move with visual feedback for potential drop targets"""
|
|
457
|
+
if event.source() == self:
|
|
458
|
+
# Show visual feedback when hovering over folders
|
|
459
|
+
drop_pos = event.position().toPoint()
|
|
460
|
+
target_item = self.itemAt(drop_pos)
|
|
461
|
+
|
|
462
|
+
# Reset all folder backgrounds
|
|
463
|
+
self._reset_folder_highlights()
|
|
464
|
+
|
|
465
|
+
# Highlight the current target folder if any
|
|
466
|
+
if target_item and self.is_folder_item(target_item):
|
|
467
|
+
target_item.setBackground(0, QBrush(QColor(52, 152, 219, 50))) # Light blue highlight
|
|
468
|
+
|
|
469
|
+
event.acceptProposedAction()
|
|
470
|
+
else:
|
|
471
|
+
# Let parent class handle external drags
|
|
472
|
+
super().dragMoveEvent(event)
|
|
473
|
+
|
|
474
|
+
def _reset_folder_highlights(self):
|
|
475
|
+
"""Reset highlights on all folder items"""
|
|
476
|
+
def reset_item(item):
|
|
477
|
+
if not item:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
if self.is_folder_item(item):
|
|
481
|
+
item.setBackground(0, QBrush()) # Clear background
|
|
482
|
+
|
|
483
|
+
# Process children if this is a folder
|
|
484
|
+
for i in range(item.childCount()):
|
|
485
|
+
reset_item(item.child(i))
|
|
486
|
+
|
|
487
|
+
# Reset all top-level items
|
|
488
|
+
for i in range(self.topLevelItemCount()):
|
|
489
|
+
reset_item(self.topLevelItem(i))
|
|
490
|
+
|
|
491
|
+
def dragLeaveEvent(self, event):
|
|
492
|
+
"""Handle drag leave events by resetting visual feedback"""
|
|
493
|
+
self._reset_folder_highlights()
|
|
494
|
+
super().dragLeaveEvent(event)
|
|
495
|
+
|
|
496
|
+
def get_folder_by_name(self, folder_name):
|
|
497
|
+
"""Find a folder by name or create it if it doesn't exist"""
|
|
498
|
+
# Look for existing folder
|
|
499
|
+
for i in range(self.topLevelItemCount()):
|
|
500
|
+
item = self.topLevelItem(i)
|
|
501
|
+
if self.is_folder_item(item) and item.text(0) == folder_name:
|
|
502
|
+
return item
|
|
503
|
+
|
|
504
|
+
# Create new folder if not found
|
|
505
|
+
return self.create_folder(folder_name)
|
|
506
|
+
|
|
507
|
+
def create_folder(self, folder_name):
|
|
508
|
+
"""Create a new folder in the tree"""
|
|
509
|
+
folder = QTreeWidgetItem(self)
|
|
510
|
+
folder.setText(0, folder_name)
|
|
511
|
+
folder.setIcon(0, QIcon.fromTheme("folder"))
|
|
512
|
+
# Store item type as folder
|
|
513
|
+
folder.setData(0, Qt.ItemDataRole.UserRole, "folder")
|
|
514
|
+
# Make folder text bold
|
|
515
|
+
font = folder.font(0)
|
|
516
|
+
font.setBold(True)
|
|
517
|
+
folder.setFont(0, font)
|
|
518
|
+
# Set folder flags (can drop onto)
|
|
519
|
+
folder.setFlags(folder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
|
|
520
|
+
# Start expanded
|
|
521
|
+
folder.setExpanded(True)
|
|
522
|
+
return folder
|
|
523
|
+
|
|
524
|
+
def add_table_item(self, table_name, source, needs_reload=False, folder_name=None):
|
|
525
|
+
"""Add a table item with optional reload icon, optionally in a folder"""
|
|
526
|
+
item_text = f"{table_name} ({source})"
|
|
527
|
+
|
|
528
|
+
# Determine parent (folder or root)
|
|
529
|
+
parent = self
|
|
530
|
+
if folder_name:
|
|
531
|
+
parent = self.get_folder_by_name(folder_name)
|
|
532
|
+
|
|
533
|
+
# Create the item
|
|
534
|
+
item = QTreeWidgetItem(parent)
|
|
535
|
+
item.setText(0, item_text)
|
|
536
|
+
item.setData(0, Qt.ItemDataRole.UserRole, "table")
|
|
537
|
+
|
|
538
|
+
# Set appropriate icon
|
|
539
|
+
if needs_reload:
|
|
540
|
+
# Add to set of tables needing reload
|
|
541
|
+
self.tables_needing_reload.add(table_name)
|
|
542
|
+
# Set an icon for tables that need reloading
|
|
543
|
+
item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
544
|
+
# Add tooltip to indicate the table needs to be reloaded
|
|
545
|
+
item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
546
|
+
else:
|
|
547
|
+
# Regular table icon
|
|
548
|
+
item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
|
|
549
|
+
|
|
550
|
+
# Make item draggable but not a drop target
|
|
551
|
+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
|
|
552
|
+
|
|
553
|
+
# If we added to a folder, make sure it's expanded
|
|
554
|
+
if folder_name:
|
|
555
|
+
parent.setExpanded(True)
|
|
556
|
+
|
|
557
|
+
return item
|
|
558
|
+
|
|
559
|
+
def show_context_menu(self, position):
|
|
560
|
+
"""Show context menu for the tree widget"""
|
|
561
|
+
item = self.itemAt(position)
|
|
562
|
+
|
|
563
|
+
# Create the menu
|
|
564
|
+
menu = QMenu(self)
|
|
565
|
+
|
|
566
|
+
if not item:
|
|
567
|
+
# Clicked on empty space - show menu for creating a folder
|
|
568
|
+
new_folder_action = menu.addAction(QIcon.fromTheme("folder-new"), "New Folder")
|
|
569
|
+
expand_all_action = menu.addAction(QIcon.fromTheme("view-fullscreen"), "Expand All")
|
|
570
|
+
collapse_all_action = menu.addAction(QIcon.fromTheme("view-restore"), "Collapse All")
|
|
571
|
+
|
|
572
|
+
action = menu.exec(QCursor.pos())
|
|
573
|
+
|
|
574
|
+
if action == new_folder_action:
|
|
575
|
+
self.create_new_folder()
|
|
576
|
+
elif action == expand_all_action:
|
|
577
|
+
self.expandAll()
|
|
578
|
+
elif action == collapse_all_action:
|
|
579
|
+
self.collapseAll()
|
|
580
|
+
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
if self.is_folder_item(item):
|
|
584
|
+
# Folder context menu
|
|
585
|
+
new_subfolder_action = menu.addAction(QIcon.fromTheme("folder-new"), "New Subfolder")
|
|
586
|
+
menu.addSeparator()
|
|
587
|
+
rename_folder_action = menu.addAction("Rename Folder")
|
|
588
|
+
expand_action = None
|
|
589
|
+
collapse_action = None
|
|
590
|
+
|
|
591
|
+
if item.childCount() > 0:
|
|
592
|
+
menu.addSeparator()
|
|
593
|
+
expand_action = menu.addAction("Expand")
|
|
594
|
+
collapse_action = menu.addAction("Collapse")
|
|
595
|
+
|
|
596
|
+
menu.addSeparator()
|
|
597
|
+
delete_folder_action = menu.addAction(QIcon.fromTheme("edit-delete"), "Delete Folder")
|
|
598
|
+
|
|
599
|
+
action = menu.exec(QCursor.pos())
|
|
600
|
+
|
|
601
|
+
if action == new_subfolder_action:
|
|
602
|
+
self.create_new_folder(item)
|
|
603
|
+
elif action == rename_folder_action:
|
|
604
|
+
self.rename_folder(item)
|
|
605
|
+
elif action == delete_folder_action:
|
|
606
|
+
self.delete_folder(item)
|
|
607
|
+
elif expand_action and action == expand_action:
|
|
608
|
+
item.setExpanded(True)
|
|
609
|
+
elif collapse_action and action == collapse_action:
|
|
610
|
+
item.setExpanded(False)
|
|
611
|
+
|
|
612
|
+
else:
|
|
613
|
+
# Table item context menu - defer to parent's context menu handling
|
|
614
|
+
if self.parent and hasattr(self.parent, 'show_tables_context_menu'):
|
|
615
|
+
# Call the main application's context menu handler
|
|
616
|
+
self.parent.show_tables_context_menu(position)
|
|
617
|
+
|
|
618
|
+
def create_new_folder(self, parent_item=None):
|
|
619
|
+
"""Create a new folder, optionally as a subfolder"""
|
|
620
|
+
folder_name, ok = QInputDialog.getText(
|
|
621
|
+
self,
|
|
622
|
+
"New Folder",
|
|
623
|
+
"Enter folder name:",
|
|
624
|
+
QLineEdit.EchoMode.Normal
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if ok and folder_name:
|
|
628
|
+
if parent_item and self.is_folder_item(parent_item):
|
|
629
|
+
# Create subfolder
|
|
630
|
+
subfolder = QTreeWidgetItem(parent_item)
|
|
631
|
+
subfolder.setText(0, folder_name)
|
|
632
|
+
subfolder.setIcon(0, QIcon.fromTheme("folder"))
|
|
633
|
+
subfolder.setData(0, Qt.ItemDataRole.UserRole, "folder")
|
|
634
|
+
# Make folder text bold
|
|
635
|
+
font = subfolder.font(0)
|
|
636
|
+
font.setBold(True)
|
|
637
|
+
subfolder.setFont(0, font)
|
|
638
|
+
# Set folder flags
|
|
639
|
+
subfolder.setFlags(subfolder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
|
|
640
|
+
# Expand parent
|
|
641
|
+
parent_item.setExpanded(True)
|
|
642
|
+
return subfolder
|
|
643
|
+
else:
|
|
644
|
+
# Create top-level folder
|
|
645
|
+
return self.create_folder(folder_name)
|
|
646
|
+
|
|
647
|
+
def rename_folder(self, folder_item):
|
|
648
|
+
"""Rename a folder"""
|
|
649
|
+
if not self.is_folder_item(folder_item):
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
current_name = folder_item.text(0)
|
|
653
|
+
new_name, ok = QInputDialog.getText(
|
|
654
|
+
self,
|
|
655
|
+
"Rename Folder",
|
|
656
|
+
"Enter new folder name:",
|
|
657
|
+
QLineEdit.EchoMode.Normal,
|
|
658
|
+
current_name
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
if ok and new_name:
|
|
662
|
+
folder_item.setText(0, new_name)
|
|
663
|
+
|
|
664
|
+
def delete_folder(self, folder_item):
|
|
665
|
+
"""Delete a folder and its contents"""
|
|
666
|
+
if not self.is_folder_item(folder_item):
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
# Confirmation dialog
|
|
670
|
+
msg_box = QMessageBox()
|
|
671
|
+
msg_box.setIcon(QMessageBox.Icon.Warning)
|
|
672
|
+
msg_box.setWindowTitle("Delete Folder")
|
|
673
|
+
folder_name = folder_item.text(0)
|
|
674
|
+
msg_box.setText(f"Are you sure you want to delete folder '{folder_name}'?")
|
|
675
|
+
|
|
676
|
+
if folder_item.childCount() > 0:
|
|
677
|
+
msg_box.setInformativeText("The folder contains items that will also be deleted.")
|
|
678
|
+
|
|
679
|
+
msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
680
|
+
msg_box.setDefaultButton(QMessageBox.StandardButton.No)
|
|
681
|
+
|
|
682
|
+
if msg_box.exec() == QMessageBox.StandardButton.Yes:
|
|
683
|
+
# Get the parent (could be the tree widget or another folder)
|
|
684
|
+
parent = folder_item.parent()
|
|
685
|
+
if parent:
|
|
686
|
+
parent.removeChild(folder_item)
|
|
687
|
+
else:
|
|
688
|
+
# Top-level item
|
|
689
|
+
index = self.indexOfTopLevelItem(folder_item)
|
|
690
|
+
if index >= 0:
|
|
691
|
+
self.takeTopLevelItem(index)
|
|
692
|
+
|
|
693
|
+
def move_item_to_folder(self, item, target_folder):
|
|
694
|
+
"""Move an item to a different folder"""
|
|
695
|
+
if not item or not target_folder:
|
|
696
|
+
return
|
|
697
|
+
|
|
698
|
+
# Get table name before moving
|
|
699
|
+
table_name = self.get_table_name_from_item(item)
|
|
700
|
+
folder_name = target_folder.text(0)
|
|
701
|
+
|
|
702
|
+
# Clone the item
|
|
703
|
+
clone = item.clone()
|
|
704
|
+
|
|
705
|
+
# Add to new parent
|
|
706
|
+
target_folder.addChild(clone)
|
|
707
|
+
|
|
708
|
+
# Remove original
|
|
709
|
+
parent = item.parent()
|
|
710
|
+
if parent:
|
|
711
|
+
parent.removeChild(item)
|
|
712
|
+
else:
|
|
713
|
+
# Top-level item
|
|
714
|
+
index = self.indexOfTopLevelItem(item)
|
|
715
|
+
if index >= 0:
|
|
716
|
+
self.takeTopLevelItem(index)
|
|
717
|
+
|
|
718
|
+
# Expand target folder
|
|
719
|
+
target_folder.setExpanded(True)
|
|
720
|
+
|
|
721
|
+
# Select the moved item
|
|
722
|
+
self.setCurrentItem(clone)
|
|
723
|
+
|
|
724
|
+
# Emit signal for successful move, if we were able to get the table name
|
|
725
|
+
if table_name:
|
|
726
|
+
self.itemDropped.emit(table_name, folder_name, True)
|
|
727
|
+
|
|
728
|
+
def clear(self):
|
|
729
|
+
"""Override clear to also reset the tables_needing_reload set"""
|
|
730
|
+
super().clear()
|
|
731
|
+
self.tables_needing_reload.clear()
|
|
732
|
+
|
|
733
|
+
def mark_table_reloaded(self, table_name):
|
|
734
|
+
"""Mark a table as reloaded by removing its icon"""
|
|
735
|
+
if table_name in self.tables_needing_reload:
|
|
736
|
+
self.tables_needing_reload.remove(table_name)
|
|
737
|
+
|
|
738
|
+
# Find and update the item (across all folders)
|
|
739
|
+
table_item = self.find_table_item(table_name)
|
|
740
|
+
if table_item:
|
|
741
|
+
table_item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
|
|
742
|
+
table_item.setToolTip(0, "")
|
|
743
|
+
|
|
744
|
+
def mark_table_needs_reload(self, table_name):
|
|
745
|
+
"""Mark a table as needing reload by adding an icon"""
|
|
746
|
+
self.tables_needing_reload.add(table_name)
|
|
747
|
+
|
|
748
|
+
# Find and update the item (across all folders)
|
|
749
|
+
table_item = self.find_table_item(table_name)
|
|
750
|
+
if table_item:
|
|
751
|
+
table_item.setIcon(0, QIcon.fromTheme("view-refresh"))
|
|
752
|
+
table_item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
|
|
753
|
+
|
|
754
|
+
def is_table_loaded(self, table_name):
|
|
755
|
+
"""Check if a table is loaded (not needing reload)"""
|
|
756
|
+
return table_name not in self.tables_needing_reload
|
|
757
|
+
|
|
758
|
+
def find_table_item(self, table_name):
|
|
759
|
+
"""Find a table item by name across all folders"""
|
|
760
|
+
# Helper function to recursively search the tree
|
|
761
|
+
def search_item(parent_item):
|
|
762
|
+
# If parent_item is None, search top-level items
|
|
763
|
+
if parent_item is None:
|
|
764
|
+
for i in range(self.topLevelItemCount()):
|
|
765
|
+
top_item = self.topLevelItem(i)
|
|
766
|
+
result = search_item(top_item)
|
|
767
|
+
if result:
|
|
768
|
+
return result
|
|
769
|
+
return None
|
|
770
|
+
|
|
771
|
+
# Check if current item is the target table
|
|
772
|
+
if not self.is_folder_item(parent_item):
|
|
773
|
+
item_table_name = self.get_table_name_from_item(parent_item)
|
|
774
|
+
if item_table_name == table_name:
|
|
775
|
+
return parent_item
|
|
776
|
+
|
|
777
|
+
# Recursively search children if it's a folder
|
|
778
|
+
for i in range(parent_item.childCount()):
|
|
779
|
+
child = parent_item.child(i)
|
|
780
|
+
result = search_item(child)
|
|
781
|
+
if result:
|
|
782
|
+
return result
|
|
783
|
+
|
|
784
|
+
return None
|
|
785
|
+
|
|
786
|
+
# Start the recursive search
|
|
787
|
+
return search_item(None)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class TestTableListParent(QMainWindow):
|
|
791
|
+
"""Test class to serve as parent for the DraggableTablesList during testing"""
|
|
792
|
+
|
|
793
|
+
def __init__(self):
|
|
794
|
+
super().__init__()
|
|
795
|
+
self.setWindowTitle("Table List Test - Drag & Drop Tables to Folders")
|
|
796
|
+
self.setGeometry(100, 100, 400, 600)
|
|
797
|
+
|
|
798
|
+
# Create central widget and layout
|
|
799
|
+
central_widget = QWidget()
|
|
800
|
+
self.setCentralWidget(central_widget)
|
|
801
|
+
main_layout = QVBoxLayout(central_widget)
|
|
802
|
+
|
|
803
|
+
# Add header
|
|
804
|
+
header = QLabel("TABLES")
|
|
805
|
+
header.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")
|
|
806
|
+
main_layout.addWidget(header)
|
|
807
|
+
|
|
808
|
+
# Create and add the tables list
|
|
809
|
+
self.tables_list = DraggableTablesList(self)
|
|
810
|
+
main_layout.addWidget(self.tables_list)
|
|
811
|
+
|
|
812
|
+
# Add status display
|
|
813
|
+
self.status_frame = QFrame()
|
|
814
|
+
self.status_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
|
815
|
+
self.status_frame.setStyleSheet("background-color: rgba(255,255,255,0.1); border-radius: 4px; padding: 8px;")
|
|
816
|
+
status_layout = QVBoxLayout(self.status_frame)
|
|
817
|
+
|
|
818
|
+
self.status_label = QLabel("Try dragging tables between folders!")
|
|
819
|
+
self.status_label.setStyleSheet("color: white;")
|
|
820
|
+
status_layout.addWidget(self.status_label)
|
|
821
|
+
|
|
822
|
+
main_layout.addWidget(self.status_frame)
|
|
823
|
+
|
|
824
|
+
# Create info section
|
|
825
|
+
info_label = QLabel(
|
|
826
|
+
"• Drag tables to folders for organization\n"
|
|
827
|
+
"• Drag tables out of folders to root\n"
|
|
828
|
+
"• Double-click folders to expand/collapse\n"
|
|
829
|
+
"• Right-click for context menu options\n"
|
|
830
|
+
"• Visual feedback shows valid drop targets"
|
|
831
|
+
)
|
|
832
|
+
info_label.setStyleSheet("color: #3498DB; background-color: rgba(255,255,255,0.1); padding: 10px; border-radius: 4px;")
|
|
833
|
+
main_layout.addWidget(info_label)
|
|
834
|
+
|
|
835
|
+
# Apply dark styling to the main window
|
|
836
|
+
self.setStyleSheet("""
|
|
837
|
+
QMainWindow {
|
|
838
|
+
background-color: #2C3E50;
|
|
839
|
+
}
|
|
840
|
+
QLabel {
|
|
841
|
+
color: white;
|
|
842
|
+
}
|
|
843
|
+
""")
|
|
844
|
+
|
|
845
|
+
# Populate with sample data
|
|
846
|
+
self.add_sample_data()
|
|
847
|
+
|
|
848
|
+
# Connect to status updates
|
|
849
|
+
self.tables_list.itemDropped.connect(self.update_drop_status)
|
|
850
|
+
|
|
851
|
+
def add_sample_data(self):
|
|
852
|
+
"""Add sample data to the table list"""
|
|
853
|
+
# Create some folders
|
|
854
|
+
sales_folder = self.tables_list.create_folder("Sales Data")
|
|
855
|
+
analytics_folder = self.tables_list.create_folder("Analytics")
|
|
856
|
+
reports_folder = self.tables_list.create_folder("Reports")
|
|
857
|
+
|
|
858
|
+
# Add some tables to root
|
|
859
|
+
self.tables_list.add_table_item("customers", "sample.xlsx")
|
|
860
|
+
self.tables_list.add_table_item("products", "database")
|
|
861
|
+
self.tables_list.add_table_item("employees", "hr.xlsx")
|
|
862
|
+
|
|
863
|
+
# Add tables to folders
|
|
864
|
+
self.tables_list.add_table_item("orders", "orders.csv", folder_name="Sales Data")
|
|
865
|
+
self.tables_list.add_table_item("sales_2023", "sales.parquet", needs_reload=True, folder_name="Sales Data")
|
|
866
|
+
|
|
867
|
+
self.tables_list.add_table_item("analytics_data", "analytics.csv", needs_reload=True, folder_name="Analytics")
|
|
868
|
+
self.tables_list.add_table_item("inventory", "query_result", folder_name="Analytics")
|
|
869
|
+
|
|
870
|
+
# Message to get started
|
|
871
|
+
self.statusBar().showMessage("Try dragging tables between folders and to root area", 5000)
|
|
872
|
+
|
|
873
|
+
def update_drop_status(self, source_item, target_folder, success):
|
|
874
|
+
"""Update status label with drag and drop information"""
|
|
875
|
+
if success:
|
|
876
|
+
if source_item and target_folder:
|
|
877
|
+
self.status_label.setText(f"Moved '{source_item}' to folder '{target_folder}'")
|
|
878
|
+
elif source_item:
|
|
879
|
+
self.status_label.setText(f"Moved '{source_item}' to root")
|
|
880
|
+
else:
|
|
881
|
+
self.status_label.setText("Drop operation failed")
|
|
882
|
+
|
|
883
|
+
def reload_selected_table(self, table_name):
|
|
884
|
+
"""Mock implementation of reload_selected_table for testing"""
|
|
885
|
+
# Update status
|
|
886
|
+
self.status_label.setText(f"Reloaded table: {table_name}")
|
|
887
|
+
|
|
888
|
+
# Mark the table as reloaded
|
|
889
|
+
self.tables_list.mark_table_reloaded(table_name)
|
|
890
|
+
|
|
891
|
+
# Show confirmation
|
|
892
|
+
QMessageBox.information(
|
|
893
|
+
self,
|
|
894
|
+
"Table Reloaded",
|
|
895
|
+
f"Table '{table_name}' has been reloaded successfully!",
|
|
896
|
+
QMessageBox.StandardButton.Ok
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
def show_table_preview(self, item):
|
|
900
|
+
"""Mock implementation of show_table_preview for testing"""
|
|
901
|
+
if not item:
|
|
902
|
+
return
|
|
903
|
+
|
|
904
|
+
# Get table name
|
|
905
|
+
table_name = item.text(0).split(' (')[0]
|
|
906
|
+
|
|
907
|
+
# Update status
|
|
908
|
+
self.status_label.setText(f"Showing preview of: {table_name}")
|
|
909
|
+
|
|
910
|
+
def show_tables_context_menu(self, position):
|
|
911
|
+
"""Mock implementation of context menu for table items"""
|
|
912
|
+
item = self.tables_list.itemAt(position)
|
|
913
|
+
if not item or self.tables_list.is_folder_item(item):
|
|
914
|
+
return # Let the tree widget handle folders
|
|
915
|
+
|
|
916
|
+
# Get table name
|
|
917
|
+
table_name = item.text(0).split(' (')[0]
|
|
918
|
+
|
|
919
|
+
# Create context menu
|
|
920
|
+
context_menu = QMenu(self)
|
|
921
|
+
select_action = context_menu.addAction("Select (Test)")
|
|
922
|
+
view_action = context_menu.addAction("View (Test)")
|
|
923
|
+
|
|
924
|
+
# Check if table needs reloading and add appropriate action
|
|
925
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
926
|
+
reload_action = context_menu.addAction("Reload Table")
|
|
927
|
+
reload_action.setIcon(QIcon.fromTheme("view-refresh"))
|
|
928
|
+
else:
|
|
929
|
+
reload_action = context_menu.addAction("Refresh")
|
|
930
|
+
|
|
931
|
+
# Add move to folder submenu
|
|
932
|
+
move_menu = context_menu.addMenu("Move to Folder")
|
|
933
|
+
|
|
934
|
+
# Add folders to the move menu
|
|
935
|
+
for i in range(self.tables_list.topLevelItemCount()):
|
|
936
|
+
top_item = self.tables_list.topLevelItem(i)
|
|
937
|
+
if self.tables_list.is_folder_item(top_item):
|
|
938
|
+
folder_action = move_menu.addAction(top_item.text(0))
|
|
939
|
+
folder_action.setData(top_item)
|
|
940
|
+
|
|
941
|
+
context_menu.addSeparator()
|
|
942
|
+
delete_action = context_menu.addAction("Delete (Test)")
|
|
943
|
+
|
|
944
|
+
# Show the menu
|
|
945
|
+
action = context_menu.exec(QCursor.pos())
|
|
946
|
+
|
|
947
|
+
# Handle the action
|
|
948
|
+
if action == reload_action:
|
|
949
|
+
self.reload_selected_table(table_name)
|
|
950
|
+
elif action == select_action:
|
|
951
|
+
self.status_label.setText(f"Selected: {table_name}")
|
|
952
|
+
elif action == view_action:
|
|
953
|
+
self.status_label.setText(f"Viewing: {table_name}")
|
|
954
|
+
elif action == delete_action:
|
|
955
|
+
self.status_label.setText(f"Deleted: {table_name}")
|
|
956
|
+
# Actually remove the item as an example
|
|
957
|
+
parent = item.parent()
|
|
958
|
+
if parent:
|
|
959
|
+
parent.removeChild(item)
|
|
960
|
+
else:
|
|
961
|
+
index = self.tables_list.indexOfTopLevelItem(item)
|
|
962
|
+
if index >= 0:
|
|
963
|
+
self.tables_list.takeTopLevelItem(index)
|
|
964
|
+
elif action and action.parent() == move_menu:
|
|
965
|
+
# Get the target folder from action data
|
|
966
|
+
target_folder = action.data()
|
|
967
|
+
if target_folder:
|
|
968
|
+
self.tables_list.move_item_to_folder(item, target_folder)
|
|
969
|
+
self.status_label.setText(f"Moved {table_name} to {target_folder.text(0)}")
|
|
970
|
+
|
|
971
|
+
def statusBar(self):
|
|
972
|
+
"""Override statusBar to update our status label"""
|
|
973
|
+
return self
|
|
974
|
+
|
|
975
|
+
def showMessage(self, message, timeout=0):
|
|
976
|
+
"""Implement showMessage to work with statusBar() call"""
|
|
977
|
+
self.status_label.setText(message)
|
|
978
|
+
|
|
979
|
+
# If timeout is specified, schedule a reset
|
|
980
|
+
if timeout > 0:
|
|
981
|
+
QTimer.singleShot(timeout, lambda: self.status_label.setText("Try dragging tables between folders!"))
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def main():
|
|
985
|
+
"""Run the test application"""
|
|
986
|
+
app = QApplication(sys.argv)
|
|
987
|
+
|
|
988
|
+
# Create and show the test window
|
|
989
|
+
test_window = TestTableListParent()
|
|
990
|
+
test_window.show()
|
|
991
|
+
|
|
992
|
+
sys.exit(app.exec())
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
if __name__ == "__main__":
|
|
996
|
+
main()
|