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.
Files changed (54) hide show
  1. sqlshell/__init__.py +84 -0
  2. sqlshell/__main__.py +4926 -0
  3. sqlshell/ai_autocomplete.py +392 -0
  4. sqlshell/ai_settings_dialog.py +337 -0
  5. sqlshell/context_suggester.py +768 -0
  6. sqlshell/create_test_data.py +152 -0
  7. sqlshell/data/create_test_data.py +137 -0
  8. sqlshell/db/__init__.py +6 -0
  9. sqlshell/db/database_manager.py +1318 -0
  10. sqlshell/db/export_manager.py +188 -0
  11. sqlshell/editor.py +1166 -0
  12. sqlshell/editor_integration.py +127 -0
  13. sqlshell/execution_handler.py +421 -0
  14. sqlshell/menus.py +262 -0
  15. sqlshell/notification_manager.py +370 -0
  16. sqlshell/query_tab.py +904 -0
  17. sqlshell/resources/__init__.py +1 -0
  18. sqlshell/resources/icon.png +0 -0
  19. sqlshell/resources/logo_large.png +0 -0
  20. sqlshell/resources/logo_medium.png +0 -0
  21. sqlshell/resources/logo_small.png +0 -0
  22. sqlshell/resources/splash_screen.gif +0 -0
  23. sqlshell/space_invaders.py +501 -0
  24. sqlshell/splash_screen.py +405 -0
  25. sqlshell/sqlshell/__init__.py +5 -0
  26. sqlshell/sqlshell/create_test_data.py +118 -0
  27. sqlshell/sqlshell/create_test_databases.py +96 -0
  28. sqlshell/sqlshell_demo.png +0 -0
  29. sqlshell/styles.py +257 -0
  30. sqlshell/suggester_integration.py +330 -0
  31. sqlshell/syntax_highlighter.py +124 -0
  32. sqlshell/table_list.py +996 -0
  33. sqlshell/ui/__init__.py +6 -0
  34. sqlshell/ui/bar_chart_delegate.py +49 -0
  35. sqlshell/ui/filter_header.py +469 -0
  36. sqlshell/utils/__init__.py +16 -0
  37. sqlshell/utils/profile_cn2.py +1661 -0
  38. sqlshell/utils/profile_column.py +2635 -0
  39. sqlshell/utils/profile_distributions.py +616 -0
  40. sqlshell/utils/profile_entropy.py +347 -0
  41. sqlshell/utils/profile_foreign_keys.py +779 -0
  42. sqlshell/utils/profile_keys.py +2834 -0
  43. sqlshell/utils/profile_ohe.py +934 -0
  44. sqlshell/utils/profile_ohe_advanced.py +754 -0
  45. sqlshell/utils/profile_ohe_comparison.py +237 -0
  46. sqlshell/utils/profile_prediction.py +926 -0
  47. sqlshell/utils/profile_similarity.py +876 -0
  48. sqlshell/utils/search_in_df.py +90 -0
  49. sqlshell/widgets.py +400 -0
  50. sqlshell-0.4.4.dist-info/METADATA +441 -0
  51. sqlshell-0.4.4.dist-info/RECORD +54 -0
  52. sqlshell-0.4.4.dist-info/WHEEL +5 -0
  53. sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
  54. 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()