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/table_list.py ADDED
@@ -0,0 +1,907 @@
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.SingleSelection)
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
+ item = self.currentItem()
124
+ if not item:
125
+ return
126
+
127
+ # Don't start drag if it's a folder and we're in internal move mode
128
+ if self.is_folder_item(item) and self.dragDropMode() == QTreeWidget.DragDropMode.InternalMove:
129
+ super().startDrag(supportedActions)
130
+ return
131
+
132
+ # Extract the table name without the file info in parentheses
133
+ table_name = self.get_table_name_from_item(item)
134
+ if not table_name:
135
+ return
136
+
137
+ # Create mime data with the table name
138
+ mime_data = QMimeData()
139
+ mime_data.setText(table_name)
140
+
141
+ # Add additional information about the item for internal drags
142
+ full_text = item.text(0)
143
+ if ' (' in full_text:
144
+ source = full_text.split(' (')[1][:-1] # Get the source part
145
+ needs_reload = table_name in self.tables_needing_reload
146
+
147
+ # Store additional metadata in mime data
148
+ mime_data.setData('application/x-sqlshell-tablename', table_name.encode())
149
+ mime_data.setData('application/x-sqlshell-source', source.encode())
150
+ mime_data.setData('application/x-sqlshell-needs-reload', str(needs_reload).encode())
151
+
152
+ # Create drag object
153
+ drag = QDrag(self)
154
+ drag.setMimeData(mime_data)
155
+
156
+ # Create a visually appealing drag pixmap
157
+ font = self.font()
158
+ font.setBold(True)
159
+ metrics = self.fontMetrics()
160
+ text_width = metrics.horizontalAdvance(table_name)
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), table_name)
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
208
+ result = drag.exec(supportedActions)
209
+
210
+ # Optional: add a highlight effect after dragging
211
+ if result == Qt.DropAction.CopyAction and item:
212
+ # Briefly highlight the dragged item
213
+ orig_bg = item.background(0)
214
+ item.setBackground(0, QBrush(QColor(26, 188, 156, 100))) # Light green highlight
215
+
216
+ # Reset after a short delay
217
+ QTimer.singleShot(300, lambda: item.setBackground(0, orig_bg))
218
+
219
+ def dropEvent(self, event):
220
+ """Override drop event to handle dropping items into folders"""
221
+ if event.source() == self: # Internal drop
222
+ drop_pos = event.position().toPoint()
223
+ target_item = self.itemAt(drop_pos)
224
+ current_item = self.currentItem()
225
+
226
+ # Only proceed if we have both a current item and a target
227
+ if current_item and not self.is_folder_item(current_item):
228
+ # If dropping onto a folder, move the item to that folder
229
+ if target_item and self.is_folder_item(target_item):
230
+ # Move the item to the target folder
231
+ self.move_item_to_folder(current_item, target_item)
232
+
233
+ # Get table name for status message
234
+ table_name = self.get_table_name_from_item(current_item)
235
+ folder_name = target_item.text(0)
236
+
237
+ # Emit signal for successful drop
238
+ self.itemDropped.emit(table_name, folder_name, True)
239
+
240
+ # Show status message
241
+ if self.parent:
242
+ self.parent.statusBar().showMessage(f'Moved table "{table_name}" to folder "{folder_name}"')
243
+
244
+ # Expand the folder
245
+ target_item.setExpanded(True)
246
+
247
+ # Prevent standard drop behavior as we've handled it
248
+ event.accept()
249
+ return
250
+ elif not target_item:
251
+ # Dropping onto empty space - move to root
252
+ parent = current_item.parent()
253
+ if parent and self.is_folder_item(parent):
254
+ # Get table name for status message
255
+ table_name = self.get_table_name_from_item(current_item)
256
+
257
+ # Get additional information from the item
258
+ full_text = current_item.text(0)
259
+ source = full_text.split(' (')[1][:-1] if ' (' in full_text else ""
260
+ needs_reload = table_name in self.tables_needing_reload
261
+
262
+ # Remove from current folder
263
+ parent.removeChild(current_item)
264
+
265
+ # Add to root
266
+ self.add_table_item(table_name, source, needs_reload)
267
+
268
+ # Emit signal for successful drop to root
269
+ self.itemDropped.emit(table_name, "", True)
270
+
271
+ # Show status message
272
+ if self.parent:
273
+ self.parent.statusBar().showMessage(f'Moved table "{table_name}" to root')
274
+
275
+ # Prevent standard drop behavior
276
+ event.accept()
277
+ return
278
+ # For folders, let the default behavior handle it
279
+ elif current_item and self.is_folder_item(current_item):
280
+ # Use standard behavior for folders
281
+ super().dropEvent(event)
282
+
283
+ # Show feedback
284
+ if target_item and self.is_folder_item(target_item):
285
+ # Expand the folder
286
+ target_item.setExpanded(True)
287
+
288
+ return
289
+
290
+ # Try to extract table information from mime data for external drags
291
+ elif event.mimeData().hasText() and target_item and self.is_folder_item(target_item):
292
+ # This handles drops from other widgets
293
+ mime_data = event.mimeData()
294
+
295
+ # Try to get additional information from custom mime types
296
+ if mime_data.hasFormat('application/x-sqlshell-tablename'):
297
+ # This is a drag from another part of the application with our custom data
298
+ table_name = bytes(mime_data.data('application/x-sqlshell-tablename')).decode()
299
+ source = bytes(mime_data.data('application/x-sqlshell-source')).decode()
300
+ needs_reload_str = bytes(mime_data.data('application/x-sqlshell-needs-reload')).decode()
301
+ needs_reload = needs_reload_str.lower() == 'true'
302
+
303
+ # Create a new item in the target folder
304
+ item = QTreeWidgetItem(target_item)
305
+ item.setText(0, f"{table_name} ({source})")
306
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
307
+
308
+ # Set appropriate icon based on reload status
309
+ if needs_reload:
310
+ self.tables_needing_reload.add(table_name)
311
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
312
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
313
+ else:
314
+ item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
315
+
316
+ # Set item flags
317
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
318
+
319
+ # Expand the folder
320
+ target_item.setExpanded(True)
321
+
322
+ # Emit signal for successful drop
323
+ self.itemDropped.emit(table_name, target_item.text(0), True)
324
+
325
+ # Show status message
326
+ if self.parent:
327
+ self.parent.statusBar().showMessage(f'Added table "{table_name}" to folder "{target_item.text(0)}"')
328
+
329
+ event.accept()
330
+ return
331
+ else:
332
+ # Just a plain text drop - try to use it as a table name
333
+ table_name = mime_data.text()
334
+
335
+ # Find if this table exists in our list
336
+ existing_item = self.find_table_item(table_name)
337
+ if existing_item:
338
+ # Move existing item to the target folder
339
+ self.move_item_to_folder(existing_item, target_item)
340
+
341
+ # Emit signal for successful drop
342
+ self.itemDropped.emit(table_name, target_item.text(0), True)
343
+
344
+ # Show status message
345
+ if self.parent:
346
+ self.parent.statusBar().showMessage(f'Moved table "{table_name}" to folder "{target_item.text(0)}"')
347
+
348
+ event.accept()
349
+ return
350
+
351
+ # Reset folder highlights before default handling
352
+ self._reset_folder_highlights()
353
+
354
+ # For other cases, use the standard behavior
355
+ super().dropEvent(event)
356
+
357
+ def dragEnterEvent(self, event):
358
+ """Handle drag enter events with visual feedback"""
359
+ # Accept the event to allow internal drags
360
+ if event.source() == self:
361
+ event.acceptProposedAction()
362
+ else:
363
+ # Let parent class handle external drags
364
+ super().dragEnterEvent(event)
365
+
366
+ def dragMoveEvent(self, event):
367
+ """Handle drag move with visual feedback for potential drop targets"""
368
+ if event.source() == self:
369
+ # Show visual feedback when hovering over folders
370
+ drop_pos = event.position().toPoint()
371
+ target_item = self.itemAt(drop_pos)
372
+
373
+ # Reset all folder backgrounds
374
+ self._reset_folder_highlights()
375
+
376
+ # Highlight the current target folder if any
377
+ if target_item and self.is_folder_item(target_item):
378
+ target_item.setBackground(0, QBrush(QColor(52, 152, 219, 50))) # Light blue highlight
379
+
380
+ event.acceptProposedAction()
381
+ else:
382
+ # Let parent class handle external drags
383
+ super().dragMoveEvent(event)
384
+
385
+ def _reset_folder_highlights(self):
386
+ """Reset highlights on all folder items"""
387
+ def reset_item(item):
388
+ if not item:
389
+ return
390
+
391
+ if self.is_folder_item(item):
392
+ item.setBackground(0, QBrush()) # Clear background
393
+
394
+ # Process children if this is a folder
395
+ for i in range(item.childCount()):
396
+ reset_item(item.child(i))
397
+
398
+ # Reset all top-level items
399
+ for i in range(self.topLevelItemCount()):
400
+ reset_item(self.topLevelItem(i))
401
+
402
+ def dragLeaveEvent(self, event):
403
+ """Handle drag leave events by resetting visual feedback"""
404
+ self._reset_folder_highlights()
405
+ super().dragLeaveEvent(event)
406
+
407
+ def get_folder_by_name(self, folder_name):
408
+ """Find a folder by name or create it if it doesn't exist"""
409
+ # Look for existing folder
410
+ for i in range(self.topLevelItemCount()):
411
+ item = self.topLevelItem(i)
412
+ if self.is_folder_item(item) and item.text(0) == folder_name:
413
+ return item
414
+
415
+ # Create new folder if not found
416
+ return self.create_folder(folder_name)
417
+
418
+ def create_folder(self, folder_name):
419
+ """Create a new folder in the tree"""
420
+ folder = QTreeWidgetItem(self)
421
+ folder.setText(0, folder_name)
422
+ folder.setIcon(0, QIcon.fromTheme("folder"))
423
+ # Store item type as folder
424
+ folder.setData(0, Qt.ItemDataRole.UserRole, "folder")
425
+ # Make folder text bold
426
+ font = folder.font(0)
427
+ font.setBold(True)
428
+ folder.setFont(0, font)
429
+ # Set folder flags (can drop onto)
430
+ folder.setFlags(folder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
431
+ # Start expanded
432
+ folder.setExpanded(True)
433
+ return folder
434
+
435
+ def add_table_item(self, table_name, source, needs_reload=False, folder_name=None):
436
+ """Add a table item with optional reload icon, optionally in a folder"""
437
+ item_text = f"{table_name} ({source})"
438
+
439
+ # Determine parent (folder or root)
440
+ parent = self
441
+ if folder_name:
442
+ parent = self.get_folder_by_name(folder_name)
443
+
444
+ # Create the item
445
+ item = QTreeWidgetItem(parent)
446
+ item.setText(0, item_text)
447
+ item.setData(0, Qt.ItemDataRole.UserRole, "table")
448
+
449
+ # Set appropriate icon
450
+ if needs_reload:
451
+ # Add to set of tables needing reload
452
+ self.tables_needing_reload.add(table_name)
453
+ # Set an icon for tables that need reloading
454
+ item.setIcon(0, QIcon.fromTheme("view-refresh"))
455
+ # Add tooltip to indicate the table needs to be reloaded
456
+ item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
457
+ else:
458
+ # Regular table icon
459
+ item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
460
+
461
+ # Make item draggable but not a drop target
462
+ item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
463
+
464
+ # If we added to a folder, make sure it's expanded
465
+ if folder_name:
466
+ parent.setExpanded(True)
467
+
468
+ return item
469
+
470
+ def show_context_menu(self, position):
471
+ """Show context menu for the tree widget"""
472
+ item = self.itemAt(position)
473
+
474
+ # Create the menu
475
+ menu = QMenu(self)
476
+
477
+ if not item:
478
+ # Clicked on empty space - show menu for creating a folder
479
+ new_folder_action = menu.addAction(QIcon.fromTheme("folder-new"), "New Folder")
480
+ expand_all_action = menu.addAction(QIcon.fromTheme("view-fullscreen"), "Expand All")
481
+ collapse_all_action = menu.addAction(QIcon.fromTheme("view-restore"), "Collapse All")
482
+
483
+ action = menu.exec(QCursor.pos())
484
+
485
+ if action == new_folder_action:
486
+ self.create_new_folder()
487
+ elif action == expand_all_action:
488
+ self.expandAll()
489
+ elif action == collapse_all_action:
490
+ self.collapseAll()
491
+
492
+ return
493
+
494
+ if self.is_folder_item(item):
495
+ # Folder context menu
496
+ new_subfolder_action = menu.addAction(QIcon.fromTheme("folder-new"), "New Subfolder")
497
+ menu.addSeparator()
498
+ rename_folder_action = menu.addAction("Rename Folder")
499
+ expand_action = None
500
+ collapse_action = None
501
+
502
+ if item.childCount() > 0:
503
+ menu.addSeparator()
504
+ expand_action = menu.addAction("Expand")
505
+ collapse_action = menu.addAction("Collapse")
506
+
507
+ menu.addSeparator()
508
+ delete_folder_action = menu.addAction(QIcon.fromTheme("edit-delete"), "Delete Folder")
509
+
510
+ action = menu.exec(QCursor.pos())
511
+
512
+ if action == new_subfolder_action:
513
+ self.create_new_folder(item)
514
+ elif action == rename_folder_action:
515
+ self.rename_folder(item)
516
+ elif action == delete_folder_action:
517
+ self.delete_folder(item)
518
+ elif expand_action and action == expand_action:
519
+ item.setExpanded(True)
520
+ elif collapse_action and action == collapse_action:
521
+ item.setExpanded(False)
522
+
523
+ else:
524
+ # Table item context menu - defer to parent's context menu handling
525
+ if self.parent and hasattr(self.parent, 'show_tables_context_menu'):
526
+ # Call the main application's context menu handler
527
+ self.parent.show_tables_context_menu(position)
528
+
529
+ def create_new_folder(self, parent_item=None):
530
+ """Create a new folder, optionally as a subfolder"""
531
+ folder_name, ok = QInputDialog.getText(
532
+ self,
533
+ "New Folder",
534
+ "Enter folder name:",
535
+ QLineEdit.EchoMode.Normal
536
+ )
537
+
538
+ if ok and folder_name:
539
+ if parent_item and self.is_folder_item(parent_item):
540
+ # Create subfolder
541
+ subfolder = QTreeWidgetItem(parent_item)
542
+ subfolder.setText(0, folder_name)
543
+ subfolder.setIcon(0, QIcon.fromTheme("folder"))
544
+ subfolder.setData(0, Qt.ItemDataRole.UserRole, "folder")
545
+ # Make folder text bold
546
+ font = subfolder.font(0)
547
+ font.setBold(True)
548
+ subfolder.setFont(0, font)
549
+ # Set folder flags
550
+ subfolder.setFlags(subfolder.flags() | Qt.ItemFlag.ItemIsDropEnabled)
551
+ # Expand parent
552
+ parent_item.setExpanded(True)
553
+ return subfolder
554
+ else:
555
+ # Create top-level folder
556
+ return self.create_folder(folder_name)
557
+
558
+ def rename_folder(self, folder_item):
559
+ """Rename a folder"""
560
+ if not self.is_folder_item(folder_item):
561
+ return
562
+
563
+ current_name = folder_item.text(0)
564
+ new_name, ok = QInputDialog.getText(
565
+ self,
566
+ "Rename Folder",
567
+ "Enter new folder name:",
568
+ QLineEdit.EchoMode.Normal,
569
+ current_name
570
+ )
571
+
572
+ if ok and new_name:
573
+ folder_item.setText(0, new_name)
574
+
575
+ def delete_folder(self, folder_item):
576
+ """Delete a folder and its contents"""
577
+ if not self.is_folder_item(folder_item):
578
+ return
579
+
580
+ # Confirmation dialog
581
+ msg_box = QMessageBox()
582
+ msg_box.setIcon(QMessageBox.Icon.Warning)
583
+ msg_box.setWindowTitle("Delete Folder")
584
+ folder_name = folder_item.text(0)
585
+ msg_box.setText(f"Are you sure you want to delete folder '{folder_name}'?")
586
+
587
+ if folder_item.childCount() > 0:
588
+ msg_box.setInformativeText("The folder contains items that will also be deleted.")
589
+
590
+ msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
591
+ msg_box.setDefaultButton(QMessageBox.StandardButton.No)
592
+
593
+ if msg_box.exec() == QMessageBox.StandardButton.Yes:
594
+ # Get the parent (could be the tree widget or another folder)
595
+ parent = folder_item.parent()
596
+ if parent:
597
+ parent.removeChild(folder_item)
598
+ else:
599
+ # Top-level item
600
+ index = self.indexOfTopLevelItem(folder_item)
601
+ if index >= 0:
602
+ self.takeTopLevelItem(index)
603
+
604
+ def move_item_to_folder(self, item, target_folder):
605
+ """Move an item to a different folder"""
606
+ if not item or not target_folder:
607
+ return
608
+
609
+ # Get table name before moving
610
+ table_name = self.get_table_name_from_item(item)
611
+ folder_name = target_folder.text(0)
612
+
613
+ # Clone the item
614
+ clone = item.clone()
615
+
616
+ # Add to new parent
617
+ target_folder.addChild(clone)
618
+
619
+ # Remove original
620
+ parent = item.parent()
621
+ if parent:
622
+ parent.removeChild(item)
623
+ else:
624
+ # Top-level item
625
+ index = self.indexOfTopLevelItem(item)
626
+ if index >= 0:
627
+ self.takeTopLevelItem(index)
628
+
629
+ # Expand target folder
630
+ target_folder.setExpanded(True)
631
+
632
+ # Select the moved item
633
+ self.setCurrentItem(clone)
634
+
635
+ # Emit signal for successful move, if we were able to get the table name
636
+ if table_name:
637
+ self.itemDropped.emit(table_name, folder_name, True)
638
+
639
+ def clear(self):
640
+ """Override clear to also reset the tables_needing_reload set"""
641
+ super().clear()
642
+ self.tables_needing_reload.clear()
643
+
644
+ def mark_table_reloaded(self, table_name):
645
+ """Mark a table as reloaded by removing its icon"""
646
+ if table_name in self.tables_needing_reload:
647
+ self.tables_needing_reload.remove(table_name)
648
+
649
+ # Find and update the item (across all folders)
650
+ table_item = self.find_table_item(table_name)
651
+ if table_item:
652
+ table_item.setIcon(0, QIcon.fromTheme("x-office-spreadsheet"))
653
+ table_item.setToolTip(0, "")
654
+
655
+ def mark_table_needs_reload(self, table_name):
656
+ """Mark a table as needing reload by adding an icon"""
657
+ self.tables_needing_reload.add(table_name)
658
+
659
+ # Find and update the item (across all folders)
660
+ table_item = self.find_table_item(table_name)
661
+ if table_item:
662
+ table_item.setIcon(0, QIcon.fromTheme("view-refresh"))
663
+ table_item.setToolTip(0, f"Table '{table_name}' needs to be loaded (double-click or use context menu)")
664
+
665
+ def is_table_loaded(self, table_name):
666
+ """Check if a table is loaded (not needing reload)"""
667
+ return table_name not in self.tables_needing_reload
668
+
669
+ def find_table_item(self, table_name):
670
+ """Find a table item by name across all folders"""
671
+ # Helper function to recursively search the tree
672
+ def search_item(parent_item):
673
+ # If parent_item is None, search top-level items
674
+ if parent_item is None:
675
+ for i in range(self.topLevelItemCount()):
676
+ top_item = self.topLevelItem(i)
677
+ result = search_item(top_item)
678
+ if result:
679
+ return result
680
+ return None
681
+
682
+ # Check if current item is the target table
683
+ if not self.is_folder_item(parent_item):
684
+ item_table_name = self.get_table_name_from_item(parent_item)
685
+ if item_table_name == table_name:
686
+ return parent_item
687
+
688
+ # Recursively search children if it's a folder
689
+ for i in range(parent_item.childCount()):
690
+ child = parent_item.child(i)
691
+ result = search_item(child)
692
+ if result:
693
+ return result
694
+
695
+ return None
696
+
697
+ # Start the recursive search
698
+ return search_item(None)
699
+
700
+
701
+ class TestTableListParent(QMainWindow):
702
+ """Test class to serve as parent for the DraggableTablesList during testing"""
703
+
704
+ def __init__(self):
705
+ super().__init__()
706
+ self.setWindowTitle("Table List Test - Drag & Drop Tables to Folders")
707
+ self.setGeometry(100, 100, 400, 600)
708
+
709
+ # Create central widget and layout
710
+ central_widget = QWidget()
711
+ self.setCentralWidget(central_widget)
712
+ main_layout = QVBoxLayout(central_widget)
713
+
714
+ # Add header
715
+ header = QLabel("TABLES")
716
+ header.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")
717
+ main_layout.addWidget(header)
718
+
719
+ # Create and add the tables list
720
+ self.tables_list = DraggableTablesList(self)
721
+ main_layout.addWidget(self.tables_list)
722
+
723
+ # Add status display
724
+ self.status_frame = QFrame()
725
+ self.status_frame.setFrameShape(QFrame.Shape.StyledPanel)
726
+ self.status_frame.setStyleSheet("background-color: rgba(255,255,255,0.1); border-radius: 4px; padding: 8px;")
727
+ status_layout = QVBoxLayout(self.status_frame)
728
+
729
+ self.status_label = QLabel("Try dragging tables between folders!")
730
+ self.status_label.setStyleSheet("color: white;")
731
+ status_layout.addWidget(self.status_label)
732
+
733
+ main_layout.addWidget(self.status_frame)
734
+
735
+ # Create info section
736
+ info_label = QLabel(
737
+ "• Drag tables to folders for organization\n"
738
+ "• Drag tables out of folders to root\n"
739
+ "• Double-click folders to expand/collapse\n"
740
+ "• Right-click for context menu options\n"
741
+ "• Visual feedback shows valid drop targets"
742
+ )
743
+ info_label.setStyleSheet("color: #3498DB; background-color: rgba(255,255,255,0.1); padding: 10px; border-radius: 4px;")
744
+ main_layout.addWidget(info_label)
745
+
746
+ # Apply dark styling to the main window
747
+ self.setStyleSheet("""
748
+ QMainWindow {
749
+ background-color: #2C3E50;
750
+ }
751
+ QLabel {
752
+ color: white;
753
+ }
754
+ """)
755
+
756
+ # Populate with sample data
757
+ self.add_sample_data()
758
+
759
+ # Connect to status updates
760
+ self.tables_list.itemDropped.connect(self.update_drop_status)
761
+
762
+ def add_sample_data(self):
763
+ """Add sample data to the table list"""
764
+ # Create some folders
765
+ sales_folder = self.tables_list.create_folder("Sales Data")
766
+ analytics_folder = self.tables_list.create_folder("Analytics")
767
+ reports_folder = self.tables_list.create_folder("Reports")
768
+
769
+ # Add some tables to root
770
+ self.tables_list.add_table_item("customers", "sample.xlsx")
771
+ self.tables_list.add_table_item("products", "database")
772
+ self.tables_list.add_table_item("employees", "hr.xlsx")
773
+
774
+ # Add tables to folders
775
+ self.tables_list.add_table_item("orders", "orders.csv", folder_name="Sales Data")
776
+ self.tables_list.add_table_item("sales_2023", "sales.parquet", needs_reload=True, folder_name="Sales Data")
777
+
778
+ self.tables_list.add_table_item("analytics_data", "analytics.csv", needs_reload=True, folder_name="Analytics")
779
+ self.tables_list.add_table_item("inventory", "query_result", folder_name="Analytics")
780
+
781
+ # Message to get started
782
+ self.statusBar().showMessage("Try dragging tables between folders and to root area", 5000)
783
+
784
+ def update_drop_status(self, source_item, target_folder, success):
785
+ """Update status label with drag and drop information"""
786
+ if success:
787
+ if source_item and target_folder:
788
+ self.status_label.setText(f"Moved '{source_item}' to folder '{target_folder}'")
789
+ elif source_item:
790
+ self.status_label.setText(f"Moved '{source_item}' to root")
791
+ else:
792
+ self.status_label.setText("Drop operation failed")
793
+
794
+ def reload_selected_table(self, table_name):
795
+ """Mock implementation of reload_selected_table for testing"""
796
+ # Update status
797
+ self.status_label.setText(f"Reloaded table: {table_name}")
798
+
799
+ # Mark the table as reloaded
800
+ self.tables_list.mark_table_reloaded(table_name)
801
+
802
+ # Show confirmation
803
+ QMessageBox.information(
804
+ self,
805
+ "Table Reloaded",
806
+ f"Table '{table_name}' has been reloaded successfully!",
807
+ QMessageBox.StandardButton.Ok
808
+ )
809
+
810
+ def show_table_preview(self, item):
811
+ """Mock implementation of show_table_preview for testing"""
812
+ if not item:
813
+ return
814
+
815
+ # Get table name
816
+ table_name = item.text(0).split(' (')[0]
817
+
818
+ # Update status
819
+ self.status_label.setText(f"Showing preview of: {table_name}")
820
+
821
+ def show_tables_context_menu(self, position):
822
+ """Mock implementation of context menu for table items"""
823
+ item = self.tables_list.itemAt(position)
824
+ if not item or self.tables_list.is_folder_item(item):
825
+ return # Let the tree widget handle folders
826
+
827
+ # Get table name
828
+ table_name = item.text(0).split(' (')[0]
829
+
830
+ # Create context menu
831
+ context_menu = QMenu(self)
832
+ select_action = context_menu.addAction("Select (Test)")
833
+ view_action = context_menu.addAction("View (Test)")
834
+
835
+ # Check if table needs reloading and add appropriate action
836
+ if table_name in self.tables_list.tables_needing_reload:
837
+ reload_action = context_menu.addAction("Reload Table")
838
+ reload_action.setIcon(QIcon.fromTheme("view-refresh"))
839
+ else:
840
+ reload_action = context_menu.addAction("Refresh")
841
+
842
+ # Add move to folder submenu
843
+ move_menu = context_menu.addMenu("Move to Folder")
844
+
845
+ # Add folders to the move menu
846
+ for i in range(self.tables_list.topLevelItemCount()):
847
+ top_item = self.tables_list.topLevelItem(i)
848
+ if self.tables_list.is_folder_item(top_item):
849
+ folder_action = move_menu.addAction(top_item.text(0))
850
+ folder_action.setData(top_item)
851
+
852
+ context_menu.addSeparator()
853
+ delete_action = context_menu.addAction("Delete (Test)")
854
+
855
+ # Show the menu
856
+ action = context_menu.exec(QCursor.pos())
857
+
858
+ # Handle the action
859
+ if action == reload_action:
860
+ self.reload_selected_table(table_name)
861
+ elif action == select_action:
862
+ self.status_label.setText(f"Selected: {table_name}")
863
+ elif action == view_action:
864
+ self.status_label.setText(f"Viewing: {table_name}")
865
+ elif action == delete_action:
866
+ self.status_label.setText(f"Deleted: {table_name}")
867
+ # Actually remove the item as an example
868
+ parent = item.parent()
869
+ if parent:
870
+ parent.removeChild(item)
871
+ else:
872
+ index = self.tables_list.indexOfTopLevelItem(item)
873
+ if index >= 0:
874
+ self.tables_list.takeTopLevelItem(index)
875
+ elif action and action.parent() == move_menu:
876
+ # Get the target folder from action data
877
+ target_folder = action.data()
878
+ if target_folder:
879
+ self.tables_list.move_item_to_folder(item, target_folder)
880
+ self.status_label.setText(f"Moved {table_name} to {target_folder.text(0)}")
881
+
882
+ def statusBar(self):
883
+ """Override statusBar to update our status label"""
884
+ return self
885
+
886
+ def showMessage(self, message, timeout=0):
887
+ """Implement showMessage to work with statusBar() call"""
888
+ self.status_label.setText(message)
889
+
890
+ # If timeout is specified, schedule a reset
891
+ if timeout > 0:
892
+ QTimer.singleShot(timeout, lambda: self.status_label.setText("Try dragging tables between folders!"))
893
+
894
+
895
+ def main():
896
+ """Run the test application"""
897
+ app = QApplication(sys.argv)
898
+
899
+ # Create and show the test window
900
+ test_window = TestTableListParent()
901
+ test_window.show()
902
+
903
+ sys.exit(app.exec())
904
+
905
+
906
+ if __name__ == "__main__":
907
+ main()