sqlshell 0.1.8__py3-none-any.whl → 0.1.9__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/__init__.py +1 -1
- sqlshell/db/__init__.py +5 -0
- sqlshell/db/database_manager.py +691 -0
- sqlshell/editor.py +546 -45
- sqlshell/main.py +1472 -889
- sqlshell/query_tab.py +172 -0
- sqlshell/resources/create_icon.py +106 -28
- sqlshell/resources/create_splash.py +41 -11
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/setup.py +1 -1
- sqlshell/splash_screen.py +276 -48
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +403 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.1.9.dist-info}/METADATA +8 -6
- sqlshell-0.1.9.dist-info/RECORD +31 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.1.9.dist-info}/WHEEL +1 -1
- sqlshell-0.1.8.dist-info/RECORD +0 -21
- {sqlshell-0.1.8.dist-info → sqlshell-0.1.9.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.8.dist-info → sqlshell-0.1.9.dist-info}/top_level.txt +0 -0
sqlshell/main.py
CHANGED
|
@@ -7,8 +7,6 @@ if __name__ == "__main__":
|
|
|
7
7
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
8
8
|
sys.path.insert(0, project_root)
|
|
9
9
|
|
|
10
|
-
import duckdb
|
|
11
|
-
import sqlite3
|
|
12
10
|
import pandas as pd
|
|
13
11
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
14
12
|
QHBoxLayout, QTextEdit, QPushButton, QFileDialog,
|
|
@@ -16,10 +14,10 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
16
14
|
QTableWidgetItem, QHeaderView, QMessageBox, QPlainTextEdit,
|
|
17
15
|
QCompleter, QFrame, QToolButton, QSizePolicy, QTabWidget,
|
|
18
16
|
QStyleFactory, QToolBar, QStatusBar, QLineEdit, QMenu,
|
|
19
|
-
QCheckBox, QWidgetAction, QMenuBar, QInputDialog,
|
|
20
|
-
|
|
21
|
-
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint
|
|
22
|
-
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath
|
|
17
|
+
QCheckBox, QWidgetAction, QMenuBar, QInputDialog, QProgressDialog,
|
|
18
|
+
QListWidgetItem)
|
|
19
|
+
from PyQt6.QtCore import Qt, QAbstractTableModel, QRegularExpression, QRect, QSize, QStringListModel, QPropertyAnimation, QEasingCurve, QTimer, QPoint, QMimeData
|
|
20
|
+
from PyQt6.QtGui import QFont, QColor, QSyntaxHighlighter, QTextCharFormat, QPainter, QTextFormat, QTextCursor, QIcon, QPalette, QLinearGradient, QBrush, QPixmap, QPolygon, QPainterPath, QDrag
|
|
23
21
|
import numpy as np
|
|
24
22
|
from datetime import datetime
|
|
25
23
|
|
|
@@ -27,459 +25,128 @@ from sqlshell import create_test_data
|
|
|
27
25
|
from sqlshell.splash_screen import AnimatedSplashScreen
|
|
28
26
|
from sqlshell.syntax_highlighter import SQLSyntaxHighlighter
|
|
29
27
|
from sqlshell.editor import LineNumberArea, SQLEditor
|
|
28
|
+
from sqlshell.ui import FilterHeader, BarChartDelegate
|
|
29
|
+
from sqlshell.db import DatabaseManager
|
|
30
|
+
from sqlshell.query_tab import QueryTab
|
|
30
31
|
|
|
31
|
-
class
|
|
32
|
+
class DraggableTablesList(QListWidget):
|
|
33
|
+
"""Custom QListWidget that provides better drag functionality for table names."""
|
|
34
|
+
|
|
32
35
|
def __init__(self, parent=None):
|
|
33
36
|
super().__init__(parent)
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
def set_range(self, min_val, max_val):
|
|
39
|
-
self.min_val = min_val
|
|
40
|
-
self.max_val = max_val
|
|
41
|
-
|
|
42
|
-
def paint(self, painter, option, index):
|
|
43
|
-
# Draw the default background
|
|
44
|
-
super().paint(painter, option, index)
|
|
37
|
+
self.parent = parent
|
|
38
|
+
self.setDragEnabled(True)
|
|
39
|
+
self.setDragDropMode(QListWidget.DragDropMode.DragOnly)
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# Calculate normalized value
|
|
51
|
-
range_val = self.max_val - self.min_val if self.max_val != self.min_val else 1
|
|
52
|
-
normalized = (value - self.min_val) / range_val
|
|
53
|
-
|
|
54
|
-
# Define bar dimensions
|
|
55
|
-
bar_height = 16
|
|
56
|
-
max_bar_width = 100
|
|
57
|
-
bar_width = max(5, int(max_bar_width * normalized))
|
|
58
|
-
|
|
59
|
-
# Calculate positions
|
|
60
|
-
text_width = option.fontMetrics.horizontalAdvance(text) + 10
|
|
61
|
-
bar_x = option.rect.left() + text_width + 10
|
|
62
|
-
bar_y = option.rect.center().y() - bar_height // 2
|
|
63
|
-
|
|
64
|
-
# Draw the bar
|
|
65
|
-
bar_rect = QRect(bar_x, bar_y, bar_width, bar_height)
|
|
66
|
-
painter.fillRect(bar_rect, self.bar_color)
|
|
67
|
-
|
|
68
|
-
# Draw the text
|
|
69
|
-
text_rect = QRect(option.rect.left() + 4, option.rect.top(),
|
|
70
|
-
text_width, option.rect.height())
|
|
71
|
-
painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
|
|
72
|
-
|
|
73
|
-
except (ValueError, AttributeError):
|
|
74
|
-
# If not a number, just draw the text
|
|
75
|
-
super().paint(painter, option, index)
|
|
76
|
-
|
|
77
|
-
class FilterHeader(QHeaderView):
|
|
78
|
-
def __init__(self, parent=None):
|
|
79
|
-
super().__init__(Qt.Orientation.Horizontal, parent)
|
|
80
|
-
self.filter_buttons = []
|
|
81
|
-
self.active_filters = {} # Track active filters for each column
|
|
82
|
-
self.columns_with_bars = set() # Track which columns show bar charts
|
|
83
|
-
self.bar_delegates = {} # Store delegates for columns with bars
|
|
84
|
-
self.setSectionsClickable(True)
|
|
85
|
-
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
86
|
-
self.customContextMenuRequested.connect(self.show_header_context_menu)
|
|
87
|
-
self.main_window = None # Store reference to main window
|
|
88
|
-
self.filter_icon_color = QColor("#3498DB") # Bright blue color for filter icon
|
|
89
|
-
|
|
90
|
-
def toggle_bar_chart(self, column_index):
|
|
91
|
-
"""Toggle bar chart visualization for a column"""
|
|
92
|
-
table = self.parent()
|
|
93
|
-
if not table:
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
if column_index in self.columns_with_bars:
|
|
97
|
-
# Remove bars
|
|
98
|
-
self.columns_with_bars.remove(column_index)
|
|
99
|
-
if column_index in self.bar_delegates:
|
|
100
|
-
table.setItemDelegateForColumn(column_index, None)
|
|
101
|
-
del self.bar_delegates[column_index]
|
|
102
|
-
else:
|
|
103
|
-
# Add bars
|
|
104
|
-
self.columns_with_bars.add(column_index)
|
|
105
|
-
|
|
106
|
-
# Get all values for normalization
|
|
107
|
-
values = []
|
|
108
|
-
for row in range(table.rowCount()):
|
|
109
|
-
item = table.item(row, column_index)
|
|
110
|
-
if item:
|
|
111
|
-
try:
|
|
112
|
-
value = float(item.text().replace(',', ''))
|
|
113
|
-
values.append(value)
|
|
114
|
-
except ValueError:
|
|
115
|
-
continue
|
|
116
|
-
|
|
117
|
-
if not values:
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
# Calculate min and max for normalization
|
|
121
|
-
min_val = min(values)
|
|
122
|
-
max_val = max(values)
|
|
123
|
-
|
|
124
|
-
# Create and set up delegate
|
|
125
|
-
delegate = BarChartDelegate(table)
|
|
126
|
-
delegate.set_range(min_val, max_val)
|
|
127
|
-
self.bar_delegates[column_index] = delegate
|
|
128
|
-
table.setItemDelegateForColumn(column_index, delegate)
|
|
129
|
-
|
|
130
|
-
# Update the view
|
|
131
|
-
table.viewport().update()
|
|
132
|
-
|
|
133
|
-
def show_header_context_menu(self, pos):
|
|
134
|
-
"""Show context menu for header section"""
|
|
135
|
-
logical_index = self.logicalIndexAt(pos)
|
|
136
|
-
if logical_index < 0:
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
# Create context menu
|
|
140
|
-
context_menu = QMenu(self)
|
|
141
|
-
context_menu.setStyleSheet("""
|
|
142
|
-
QMenu {
|
|
143
|
-
background-color: white;
|
|
144
|
-
border: 1px solid #BDC3C7;
|
|
145
|
-
padding: 5px;
|
|
146
|
-
}
|
|
147
|
-
QMenu::item {
|
|
148
|
-
padding: 5px 20px;
|
|
149
|
-
}
|
|
150
|
-
QMenu::item:selected {
|
|
151
|
-
background-color: #3498DB;
|
|
152
|
-
color: white;
|
|
153
|
-
}
|
|
154
|
-
""")
|
|
155
|
-
|
|
156
|
-
# Add sort actions
|
|
157
|
-
sort_asc_action = context_menu.addAction("Sort Ascending")
|
|
158
|
-
sort_desc_action = context_menu.addAction("Sort Descending")
|
|
159
|
-
context_menu.addSeparator()
|
|
160
|
-
filter_action = context_menu.addAction("Filter...")
|
|
161
|
-
|
|
162
|
-
# Add bar chart action if column is numeric
|
|
163
|
-
table = self.parent()
|
|
164
|
-
if table and table.rowCount() > 0:
|
|
165
|
-
try:
|
|
166
|
-
# Check if column contains numeric values
|
|
167
|
-
sample_value = table.item(0, logical_index).text()
|
|
168
|
-
float(sample_value.replace(',', '')) # Try converting to float
|
|
169
|
-
|
|
170
|
-
context_menu.addSeparator()
|
|
171
|
-
toggle_bar_action = context_menu.addAction(
|
|
172
|
-
"Remove Bar Chart" if logical_index in self.columns_with_bars
|
|
173
|
-
else "Add Bar Chart"
|
|
174
|
-
)
|
|
175
|
-
except (ValueError, AttributeError):
|
|
176
|
-
toggle_bar_action = None
|
|
177
|
-
else:
|
|
178
|
-
toggle_bar_action = None
|
|
179
|
-
|
|
180
|
-
# Show menu and get selected action
|
|
181
|
-
action = context_menu.exec(self.mapToGlobal(pos))
|
|
182
|
-
|
|
183
|
-
if not action:
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
table = self.parent()
|
|
187
|
-
if not table:
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
if action == sort_asc_action:
|
|
191
|
-
table.sortItems(logical_index, Qt.SortOrder.AscendingOrder)
|
|
192
|
-
elif action == sort_desc_action:
|
|
193
|
-
table.sortItems(logical_index, Qt.SortOrder.DescendingOrder)
|
|
194
|
-
elif action == filter_action:
|
|
195
|
-
self.show_filter_menu(logical_index)
|
|
196
|
-
elif action == toggle_bar_action:
|
|
197
|
-
self.toggle_bar_chart(logical_index)
|
|
198
|
-
|
|
199
|
-
def set_main_window(self, window):
|
|
200
|
-
"""Set the reference to the main window"""
|
|
201
|
-
self.main_window = window
|
|
202
|
-
|
|
203
|
-
def paintSection(self, painter, rect, logical_index):
|
|
204
|
-
"""Override paint section to add filter indicator"""
|
|
205
|
-
super().paintSection(painter, rect, logical_index)
|
|
206
|
-
|
|
207
|
-
if logical_index in self.active_filters:
|
|
208
|
-
# Draw background highlight for filtered columns
|
|
209
|
-
highlight_color = QColor(52, 152, 219, 30) # Light blue background
|
|
210
|
-
painter.fillRect(rect, highlight_color)
|
|
211
|
-
|
|
212
|
-
# Make icon larger and more visible
|
|
213
|
-
icon_size = min(rect.height() - 8, 24) # Larger icon, but not too large
|
|
214
|
-
margin = 6
|
|
215
|
-
icon_rect = QRect(
|
|
216
|
-
rect.right() - icon_size - margin,
|
|
217
|
-
rect.top() + (rect.height() - icon_size) // 2,
|
|
218
|
-
icon_size,
|
|
219
|
-
icon_size
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Draw filter icon with improved visibility
|
|
223
|
-
painter.save()
|
|
224
|
-
|
|
225
|
-
# Set up the pen for better visibility
|
|
226
|
-
pen = painter.pen()
|
|
227
|
-
pen.setWidth(3) # Thicker lines
|
|
228
|
-
pen.setColor(self.filter_icon_color)
|
|
229
|
-
painter.setPen(pen)
|
|
230
|
-
|
|
231
|
-
# Calculate points for larger funnel shape
|
|
232
|
-
points = [
|
|
233
|
-
QPoint(icon_rect.left(), icon_rect.top()),
|
|
234
|
-
QPoint(icon_rect.right(), icon_rect.top()),
|
|
235
|
-
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.center().y()),
|
|
236
|
-
QPoint(icon_rect.center().x() + icon_size//3, icon_rect.bottom()),
|
|
237
|
-
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.bottom()),
|
|
238
|
-
QPoint(icon_rect.center().x() - icon_size//3, icon_rect.center().y()),
|
|
239
|
-
QPoint(icon_rect.left(), icon_rect.top())
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
# Create and fill path
|
|
243
|
-
path = QPainterPath()
|
|
244
|
-
path.moveTo(float(points[0].x()), float(points[0].y()))
|
|
245
|
-
for point in points[1:]:
|
|
246
|
-
path.lineTo(float(point.x()), float(point.y()))
|
|
247
|
-
|
|
248
|
-
# Fill with semi-transparent blue
|
|
249
|
-
painter.fillPath(path, QBrush(QColor(52, 152, 219, 120))) # More opaque fill
|
|
250
|
-
|
|
251
|
-
# Draw outline
|
|
252
|
-
painter.drawPolyline(QPolygon(points))
|
|
253
|
-
|
|
254
|
-
# If multiple values are filtered, add a number
|
|
255
|
-
if len(self.active_filters[logical_index]) > 1:
|
|
256
|
-
# Draw number with better visibility
|
|
257
|
-
number_rect = QRect(icon_rect.left(), icon_rect.top(),
|
|
258
|
-
icon_rect.width(), icon_rect.height())
|
|
259
|
-
painter.setFont(QFont("Arial", icon_size//2, QFont.Weight.Bold))
|
|
260
|
-
|
|
261
|
-
# Draw text shadow for better contrast
|
|
262
|
-
painter.setPen(QColor("white"))
|
|
263
|
-
painter.drawText(number_rect.adjusted(1, 1, 1, 1),
|
|
264
|
-
Qt.AlignmentFlag.AlignCenter,
|
|
265
|
-
str(len(self.active_filters[logical_index])))
|
|
266
|
-
|
|
267
|
-
# Draw main text
|
|
268
|
-
painter.setPen(self.filter_icon_color)
|
|
269
|
-
painter.drawText(number_rect, Qt.AlignmentFlag.AlignCenter,
|
|
270
|
-
str(len(self.active_filters[logical_index])))
|
|
271
|
-
|
|
272
|
-
painter.restore()
|
|
273
|
-
|
|
274
|
-
# Draw a more visible indicator at the bottom of the header section
|
|
275
|
-
painter.save()
|
|
276
|
-
indicator_height = 3 # Thicker indicator line
|
|
277
|
-
indicator_rect = QRect(rect.left(), rect.bottom() - indicator_height,
|
|
278
|
-
rect.width(), indicator_height)
|
|
279
|
-
painter.fillRect(indicator_rect, self.filter_icon_color)
|
|
280
|
-
painter.restore()
|
|
281
|
-
|
|
282
|
-
def show_filter_menu(self, logical_index):
|
|
283
|
-
if not self.parent() or not isinstance(self.parent(), QTableWidget):
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
table = self.parent()
|
|
287
|
-
unique_values = set()
|
|
288
|
-
|
|
289
|
-
# Collect unique values from the column
|
|
290
|
-
for row in range(table.rowCount()):
|
|
291
|
-
item = table.item(row, logical_index)
|
|
292
|
-
if item and not table.isRowHidden(row):
|
|
293
|
-
unique_values.add(item.text())
|
|
294
|
-
|
|
295
|
-
# Create and show the filter menu
|
|
296
|
-
menu = QMenu(self)
|
|
297
|
-
menu.setStyleSheet("""
|
|
298
|
-
QMenu {
|
|
299
|
-
background-color: white;
|
|
300
|
-
border: 1px solid #BDC3C7;
|
|
301
|
-
padding: 5px;
|
|
302
|
-
}
|
|
303
|
-
QMenu::item {
|
|
304
|
-
padding: 5px 20px;
|
|
305
|
-
}
|
|
306
|
-
QMenu::item:selected {
|
|
307
|
-
background-color: #3498DB;
|
|
308
|
-
color: white;
|
|
309
|
-
}
|
|
310
|
-
QCheckBox {
|
|
311
|
-
padding: 5px;
|
|
312
|
-
}
|
|
313
|
-
QScrollArea {
|
|
41
|
+
# Apply custom styling
|
|
42
|
+
self.setStyleSheet("""
|
|
43
|
+
QListWidget {
|
|
44
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
314
45
|
border: none;
|
|
315
|
-
|
|
316
|
-
""")
|
|
317
|
-
|
|
318
|
-
# Add search box at the top
|
|
319
|
-
search_widget = QWidget(menu)
|
|
320
|
-
search_layout = QVBoxLayout(search_widget)
|
|
321
|
-
search_edit = QLineEdit(search_widget)
|
|
322
|
-
search_edit.setPlaceholderText("Search values...")
|
|
323
|
-
search_layout.addWidget(search_edit)
|
|
324
|
-
|
|
325
|
-
# Add action for search widget
|
|
326
|
-
search_action = QWidgetAction(menu)
|
|
327
|
-
search_action.setDefaultWidget(search_widget)
|
|
328
|
-
menu.addAction(search_action)
|
|
329
|
-
menu.addSeparator()
|
|
330
|
-
|
|
331
|
-
# Add "Select All" checkbox
|
|
332
|
-
select_all = QCheckBox("Select All", menu)
|
|
333
|
-
select_all.setChecked(True)
|
|
334
|
-
select_all_action = QWidgetAction(menu)
|
|
335
|
-
select_all_action.setDefaultWidget(select_all)
|
|
336
|
-
menu.addAction(select_all_action)
|
|
337
|
-
menu.addSeparator()
|
|
338
|
-
|
|
339
|
-
# Create scrollable area for checkboxes
|
|
340
|
-
scroll_widget = QWidget(menu)
|
|
341
|
-
scroll_layout = QVBoxLayout(scroll_widget)
|
|
342
|
-
scroll_layout.setSpacing(2)
|
|
343
|
-
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
|
344
|
-
|
|
345
|
-
# Add checkboxes for unique values
|
|
346
|
-
value_checkboxes = {}
|
|
347
|
-
for value in sorted(unique_values):
|
|
348
|
-
checkbox = QCheckBox(str(value), scroll_widget)
|
|
349
|
-
# Set checked state based on active filters
|
|
350
|
-
checkbox.setChecked(logical_index not in self.active_filters or
|
|
351
|
-
value in self.active_filters[logical_index])
|
|
352
|
-
value_checkboxes[value] = checkbox
|
|
353
|
-
scroll_layout.addWidget(checkbox)
|
|
354
|
-
|
|
355
|
-
# Add scrollable area to menu
|
|
356
|
-
scroll_action = QWidgetAction(menu)
|
|
357
|
-
scroll_action.setDefaultWidget(scroll_widget)
|
|
358
|
-
menu.addAction(scroll_action)
|
|
359
|
-
|
|
360
|
-
# Connect search box to filter checkboxes
|
|
361
|
-
def filter_checkboxes(text):
|
|
362
|
-
for value, checkbox in value_checkboxes.items():
|
|
363
|
-
checkbox.setVisible(text.lower() in str(value).lower())
|
|
364
|
-
|
|
365
|
-
search_edit.textChanged.connect(filter_checkboxes)
|
|
366
|
-
|
|
367
|
-
# Connect select all to other checkboxes
|
|
368
|
-
def toggle_all(state):
|
|
369
|
-
for checkbox in value_checkboxes.values():
|
|
370
|
-
if not checkbox.isHidden(): # Only toggle visible checkboxes
|
|
371
|
-
checkbox.setChecked(state)
|
|
372
|
-
|
|
373
|
-
select_all.stateChanged.connect(toggle_all)
|
|
374
|
-
|
|
375
|
-
# Add Apply and Clear buttons
|
|
376
|
-
menu.addSeparator()
|
|
377
|
-
apply_button = QPushButton("Apply Filter", menu)
|
|
378
|
-
apply_button.setStyleSheet("""
|
|
379
|
-
QPushButton {
|
|
380
|
-
background-color: #2ECC71;
|
|
46
|
+
border-radius: 4px;
|
|
381
47
|
color: white;
|
|
382
|
-
border: none;
|
|
383
|
-
padding: 5px 15px;
|
|
384
|
-
border-radius: 3px;
|
|
385
48
|
}
|
|
386
|
-
|
|
387
|
-
background-color:
|
|
388
|
-
}
|
|
389
|
-
""")
|
|
390
|
-
|
|
391
|
-
clear_button = QPushButton("Clear Filter", menu)
|
|
392
|
-
clear_button.setStyleSheet("""
|
|
393
|
-
QPushButton {
|
|
394
|
-
background-color: #E74C3C;
|
|
395
|
-
color: white;
|
|
396
|
-
border: none;
|
|
397
|
-
padding: 5px 15px;
|
|
398
|
-
border-radius: 3px;
|
|
49
|
+
QListWidget::item:selected {
|
|
50
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
399
51
|
}
|
|
400
|
-
|
|
401
|
-
background-color:
|
|
52
|
+
QListWidget::item:hover:!selected {
|
|
53
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
402
54
|
}
|
|
403
55
|
""")
|
|
404
56
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
button_action = QWidgetAction(menu)
|
|
411
|
-
button_action.setDefaultWidget(button_widget)
|
|
412
|
-
menu.addAction(button_action)
|
|
413
|
-
|
|
414
|
-
def apply_filter():
|
|
415
|
-
# Get selected values
|
|
416
|
-
selected_values = {value for value, checkbox in value_checkboxes.items()
|
|
417
|
-
if checkbox.isChecked()}
|
|
57
|
+
def startDrag(self, supportedActions):
|
|
58
|
+
"""Override startDrag to customize the drag data."""
|
|
59
|
+
item = self.currentItem()
|
|
60
|
+
if not item:
|
|
61
|
+
return
|
|
418
62
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
#
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
63
|
+
# Extract the table name without the file info in parentheses
|
|
64
|
+
table_name = item.text().split(' (')[0]
|
|
65
|
+
|
|
66
|
+
# Create mime data with the table name
|
|
67
|
+
mime_data = QMimeData()
|
|
68
|
+
mime_data.setText(table_name)
|
|
69
|
+
|
|
70
|
+
# Create drag object
|
|
71
|
+
drag = QDrag(self)
|
|
72
|
+
drag.setMimeData(mime_data)
|
|
73
|
+
|
|
74
|
+
# Create a visually appealing drag pixmap
|
|
75
|
+
font = self.font()
|
|
76
|
+
font.setBold(True)
|
|
77
|
+
metrics = self.fontMetrics()
|
|
78
|
+
text_width = metrics.horizontalAdvance(table_name)
|
|
79
|
+
text_height = metrics.height()
|
|
80
|
+
|
|
81
|
+
# Make the pixmap large enough for the text plus padding and a small icon
|
|
82
|
+
padding = 10
|
|
83
|
+
pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
|
|
84
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
85
|
+
|
|
86
|
+
# Begin painting
|
|
87
|
+
painter = QPainter(pixmap)
|
|
88
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
89
|
+
|
|
90
|
+
# Draw a nice rounded rectangle background
|
|
91
|
+
bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
|
|
92
|
+
painter.setBrush(QBrush(bg_color))
|
|
93
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
94
|
+
painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
|
|
95
|
+
|
|
96
|
+
# Draw text
|
|
97
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
98
|
+
painter.setFont(font)
|
|
99
|
+
painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), table_name)
|
|
100
|
+
|
|
101
|
+
# Draw a small database icon (simulated)
|
|
102
|
+
icon_x = padding / 2
|
|
103
|
+
icon_y = (pixmap.height() - 12) / 2
|
|
104
|
+
|
|
105
|
+
# Draw a simple database icon as a blue circle with lines
|
|
106
|
+
table_icon_color = QColor("#3498DB")
|
|
107
|
+
painter.setBrush(QBrush(table_icon_color))
|
|
108
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
109
|
+
painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
|
|
110
|
+
|
|
111
|
+
# Draw "table" lines inside the circle
|
|
112
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
113
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
|
|
114
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
|
|
115
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
|
|
116
|
+
|
|
117
|
+
painter.end()
|
|
118
|
+
|
|
119
|
+
# Set the drag pixmap
|
|
120
|
+
drag.setPixmap(pixmap)
|
|
121
|
+
|
|
122
|
+
# Set hotspot to be at the top-left corner of the text
|
|
123
|
+
drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
|
|
124
|
+
|
|
125
|
+
# Execute drag operation
|
|
126
|
+
result = drag.exec(supportedActions)
|
|
127
|
+
|
|
128
|
+
# Optional: add a highlight effect after dragging
|
|
129
|
+
if result == Qt.DropAction.CopyAction and item:
|
|
130
|
+
# Briefly highlight the dragged item
|
|
131
|
+
orig_bg = item.background()
|
|
132
|
+
item.setBackground(QBrush(QColor(26, 188, 156, 100))) # Light green highlight
|
|
133
|
+
|
|
134
|
+
# Reset after a short delay
|
|
135
|
+
QTimer.singleShot(300, lambda: item.setBackground(orig_bg))
|
|
472
136
|
|
|
473
137
|
class SQLShell(QMainWindow):
|
|
474
138
|
def __init__(self):
|
|
475
139
|
super().__init__()
|
|
476
|
-
self.
|
|
477
|
-
self.current_connection_type = None
|
|
478
|
-
self.loaded_tables = {} # Keep track of loaded tables
|
|
479
|
-
self.table_columns = {} # Keep track of table columns
|
|
140
|
+
self.db_manager = DatabaseManager()
|
|
480
141
|
self.current_df = None # Store the current DataFrame for filtering
|
|
481
142
|
self.filter_widgets = [] # Store filter line edits
|
|
482
143
|
self.current_project_file = None # Store the current project file path
|
|
144
|
+
self.recent_projects = [] # Store list of recent projects
|
|
145
|
+
self.max_recent_projects = 10 # Maximum number of recent projects to track
|
|
146
|
+
self.tabs = [] # Store list of all tabs
|
|
147
|
+
|
|
148
|
+
# Load recent projects from settings
|
|
149
|
+
self.load_recent_projects()
|
|
483
150
|
|
|
484
151
|
# Define color scheme
|
|
485
152
|
self.colors = {
|
|
@@ -499,6 +166,9 @@ class SQLShell(QMainWindow):
|
|
|
499
166
|
|
|
500
167
|
self.init_ui()
|
|
501
168
|
self.apply_stylesheet()
|
|
169
|
+
|
|
170
|
+
# Create initial tab
|
|
171
|
+
self.add_tab()
|
|
502
172
|
|
|
503
173
|
def apply_stylesheet(self):
|
|
504
174
|
"""Apply custom stylesheet to the application"""
|
|
@@ -643,6 +313,44 @@ class SQLShell(QMainWindow):
|
|
|
643
313
|
padding: 8px;
|
|
644
314
|
}}
|
|
645
315
|
|
|
316
|
+
QTabWidget::pane {{
|
|
317
|
+
border: 1px solid {self.colors['border']};
|
|
318
|
+
border-radius: 4px;
|
|
319
|
+
top: -1px;
|
|
320
|
+
background-color: white;
|
|
321
|
+
}}
|
|
322
|
+
|
|
323
|
+
QTabBar::tab {{
|
|
324
|
+
background-color: {self.colors['light_bg']};
|
|
325
|
+
color: {self.colors['text']};
|
|
326
|
+
border: 1px solid {self.colors['border']};
|
|
327
|
+
border-bottom: none;
|
|
328
|
+
border-top-left-radius: 4px;
|
|
329
|
+
border-top-right-radius: 4px;
|
|
330
|
+
padding: 8px 12px;
|
|
331
|
+
margin-right: 2px;
|
|
332
|
+
min-width: 80px;
|
|
333
|
+
}}
|
|
334
|
+
|
|
335
|
+
QTabBar::tab:selected {{
|
|
336
|
+
background-color: white;
|
|
337
|
+
border-bottom: 1px solid white;
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
QTabBar::tab:hover:!selected {{
|
|
341
|
+
background-color: #E3F2FD;
|
|
342
|
+
}}
|
|
343
|
+
|
|
344
|
+
QTabBar::close-button {{
|
|
345
|
+
image: url(close.png);
|
|
346
|
+
subcontrol-position: right;
|
|
347
|
+
}}
|
|
348
|
+
|
|
349
|
+
QTabBar::close-button:hover {{
|
|
350
|
+
background-color: rgba(255, 0, 0, 0.2);
|
|
351
|
+
border-radius: 2px;
|
|
352
|
+
}}
|
|
353
|
+
|
|
646
354
|
QPlainTextEdit, QTextEdit {{
|
|
647
355
|
background-color: white;
|
|
648
356
|
border-radius: 4px;
|
|
@@ -657,7 +365,39 @@ class SQLShell(QMainWindow):
|
|
|
657
365
|
|
|
658
366
|
def init_ui(self):
|
|
659
367
|
self.setWindowTitle('SQL Shell')
|
|
660
|
-
|
|
368
|
+
|
|
369
|
+
# Get screen geometry for smart sizing
|
|
370
|
+
screen = QApplication.primaryScreen()
|
|
371
|
+
screen_geometry = screen.availableGeometry()
|
|
372
|
+
screen_width = screen_geometry.width()
|
|
373
|
+
screen_height = screen_geometry.height()
|
|
374
|
+
|
|
375
|
+
# Calculate adaptive window size based on screen size
|
|
376
|
+
# Use 85% of screen size for larger screens, fixed size for smaller screens
|
|
377
|
+
if screen_width >= 1920 and screen_height >= 1080: # Larger screens
|
|
378
|
+
window_width = int(screen_width * 0.85)
|
|
379
|
+
window_height = int(screen_height * 0.85)
|
|
380
|
+
self.setGeometry(
|
|
381
|
+
(screen_width - window_width) // 2, # Center horizontally
|
|
382
|
+
(screen_height - window_height) // 2, # Center vertically
|
|
383
|
+
window_width,
|
|
384
|
+
window_height
|
|
385
|
+
)
|
|
386
|
+
else: # Default for smaller screens
|
|
387
|
+
self.setGeometry(100, 100, 1400, 800)
|
|
388
|
+
|
|
389
|
+
# Remember if the window was maximized
|
|
390
|
+
self.was_maximized = False
|
|
391
|
+
|
|
392
|
+
# Set application icon
|
|
393
|
+
icon_path = os.path.join(os.path.dirname(__file__), "resources", "icon.png")
|
|
394
|
+
if os.path.exists(icon_path):
|
|
395
|
+
self.setWindowIcon(QIcon(icon_path))
|
|
396
|
+
else:
|
|
397
|
+
# Fallback to the main logo if the icon isn't found
|
|
398
|
+
main_logo_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "sqlshell_logo.png")
|
|
399
|
+
if os.path.exists(main_logo_path):
|
|
400
|
+
self.setWindowIcon(QIcon(main_logo_path))
|
|
661
401
|
|
|
662
402
|
# Create menu bar
|
|
663
403
|
menubar = self.menuBar()
|
|
@@ -672,6 +412,10 @@ class SQLShell(QMainWindow):
|
|
|
672
412
|
open_project_action.setShortcut('Ctrl+O')
|
|
673
413
|
open_project_action.triggered.connect(self.open_project)
|
|
674
414
|
|
|
415
|
+
# Add Recent Projects submenu
|
|
416
|
+
self.recent_projects_menu = file_menu.addMenu('Recent Projects')
|
|
417
|
+
self.update_recent_projects_menu()
|
|
418
|
+
|
|
675
419
|
save_project_action = file_menu.addAction('Save Project')
|
|
676
420
|
save_project_action.setShortcut('Ctrl+S')
|
|
677
421
|
save_project_action.triggered.connect(self.save_project)
|
|
@@ -686,6 +430,48 @@ class SQLShell(QMainWindow):
|
|
|
686
430
|
exit_action.setShortcut('Ctrl+Q')
|
|
687
431
|
exit_action.triggered.connect(self.close)
|
|
688
432
|
|
|
433
|
+
# Add View menu for window management
|
|
434
|
+
view_menu = menubar.addMenu('&View')
|
|
435
|
+
|
|
436
|
+
# Maximized window option
|
|
437
|
+
maximize_action = view_menu.addAction('Maximize Window')
|
|
438
|
+
maximize_action.setShortcut('F11')
|
|
439
|
+
maximize_action.triggered.connect(self.toggle_maximize_window)
|
|
440
|
+
|
|
441
|
+
# Zoom submenu
|
|
442
|
+
zoom_menu = view_menu.addMenu('Zoom')
|
|
443
|
+
|
|
444
|
+
zoom_in_action = zoom_menu.addAction('Zoom In')
|
|
445
|
+
zoom_in_action.setShortcut('Ctrl++')
|
|
446
|
+
zoom_in_action.triggered.connect(lambda: self.change_zoom(1.1))
|
|
447
|
+
|
|
448
|
+
zoom_out_action = zoom_menu.addAction('Zoom Out')
|
|
449
|
+
zoom_out_action.setShortcut('Ctrl+-')
|
|
450
|
+
zoom_out_action.triggered.connect(lambda: self.change_zoom(0.9))
|
|
451
|
+
|
|
452
|
+
reset_zoom_action = zoom_menu.addAction('Reset Zoom')
|
|
453
|
+
reset_zoom_action.setShortcut('Ctrl+0')
|
|
454
|
+
reset_zoom_action.triggered.connect(lambda: self.reset_zoom())
|
|
455
|
+
|
|
456
|
+
# Add Tab menu
|
|
457
|
+
tab_menu = menubar.addMenu('&Tab')
|
|
458
|
+
|
|
459
|
+
new_tab_action = tab_menu.addAction('New Tab')
|
|
460
|
+
new_tab_action.setShortcut('Ctrl+T')
|
|
461
|
+
new_tab_action.triggered.connect(self.add_tab)
|
|
462
|
+
|
|
463
|
+
duplicate_tab_action = tab_menu.addAction('Duplicate Current Tab')
|
|
464
|
+
duplicate_tab_action.setShortcut('Ctrl+D')
|
|
465
|
+
duplicate_tab_action.triggered.connect(self.duplicate_current_tab)
|
|
466
|
+
|
|
467
|
+
rename_tab_action = tab_menu.addAction('Rename Current Tab')
|
|
468
|
+
rename_tab_action.setShortcut('Ctrl+R')
|
|
469
|
+
rename_tab_action.triggered.connect(self.rename_current_tab)
|
|
470
|
+
|
|
471
|
+
close_tab_action = tab_menu.addAction('Close Current Tab')
|
|
472
|
+
close_tab_action.setShortcut('Ctrl+W')
|
|
473
|
+
close_tab_action.triggered.connect(self.close_current_tab)
|
|
474
|
+
|
|
689
475
|
# Create custom status bar
|
|
690
476
|
status_bar = QStatusBar()
|
|
691
477
|
self.setStatusBar(status_bar)
|
|
@@ -755,21 +541,7 @@ class SQLShell(QMainWindow):
|
|
|
755
541
|
left_layout.addLayout(table_actions_layout)
|
|
756
542
|
|
|
757
543
|
# Tables list with custom styling
|
|
758
|
-
self.tables_list =
|
|
759
|
-
self.tables_list.setStyleSheet("""
|
|
760
|
-
QListWidget {
|
|
761
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
762
|
-
border: none;
|
|
763
|
-
border-radius: 4px;
|
|
764
|
-
color: white;
|
|
765
|
-
}
|
|
766
|
-
QListWidget::item:selected {
|
|
767
|
-
background-color: rgba(255, 255, 255, 0.2);
|
|
768
|
-
}
|
|
769
|
-
QListWidget::item:hover:!selected {
|
|
770
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
771
|
-
}
|
|
772
|
-
""")
|
|
544
|
+
self.tables_list = DraggableTablesList()
|
|
773
545
|
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
774
546
|
self.tables_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
775
547
|
self.tables_list.customContextMenuRequested.connect(self.show_tables_context_menu)
|
|
@@ -778,7 +550,7 @@ class SQLShell(QMainWindow):
|
|
|
778
550
|
# Add spacer at the bottom
|
|
779
551
|
left_layout.addStretch()
|
|
780
552
|
|
|
781
|
-
# Right panel for query and results
|
|
553
|
+
# Right panel for query tabs and results
|
|
782
554
|
right_panel = QFrame()
|
|
783
555
|
right_panel.setObjectName("content_panel")
|
|
784
556
|
right_layout = QVBoxLayout(right_panel)
|
|
@@ -790,143 +562,82 @@ class SQLShell(QMainWindow):
|
|
|
790
562
|
query_header.setObjectName("header_label")
|
|
791
563
|
right_layout.addWidget(query_header)
|
|
792
564
|
|
|
793
|
-
# Create
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
query_layout = QVBoxLayout(query_widget)
|
|
802
|
-
query_layout.setContentsMargins(16, 16, 16, 16)
|
|
803
|
-
query_layout.setSpacing(12)
|
|
804
|
-
|
|
805
|
-
# Query input
|
|
806
|
-
self.query_edit = SQLEditor()
|
|
807
|
-
# Apply syntax highlighting to the query editor
|
|
808
|
-
self.sql_highlighter = SQLSyntaxHighlighter(self.query_edit.document())
|
|
809
|
-
query_layout.addWidget(self.query_edit)
|
|
810
|
-
|
|
811
|
-
# Button row
|
|
812
|
-
button_layout = QHBoxLayout()
|
|
813
|
-
button_layout.setSpacing(8)
|
|
814
|
-
|
|
815
|
-
self.execute_btn = QPushButton('Execute Query')
|
|
816
|
-
self.execute_btn.setObjectName("primary_button")
|
|
817
|
-
self.execute_btn.setIcon(QIcon.fromTheme("media-playback-start"))
|
|
818
|
-
self.execute_btn.clicked.connect(self.execute_query)
|
|
819
|
-
self.execute_btn.setToolTip("Execute Query (Ctrl+Enter)")
|
|
820
|
-
|
|
821
|
-
self.clear_btn = QPushButton('Clear')
|
|
822
|
-
self.clear_btn.setIcon(QIcon.fromTheme("edit-clear"))
|
|
823
|
-
self.clear_btn.clicked.connect(self.clear_query)
|
|
824
|
-
|
|
825
|
-
button_layout.addWidget(self.execute_btn)
|
|
826
|
-
button_layout.addWidget(self.clear_btn)
|
|
827
|
-
button_layout.addStretch()
|
|
828
|
-
|
|
829
|
-
query_layout.addLayout(button_layout)
|
|
830
|
-
|
|
831
|
-
# Bottom part - Results section
|
|
832
|
-
results_widget = QFrame()
|
|
833
|
-
results_widget.setObjectName("content_panel")
|
|
834
|
-
results_layout = QVBoxLayout(results_widget)
|
|
835
|
-
results_layout.setContentsMargins(16, 16, 16, 16)
|
|
836
|
-
results_layout.setSpacing(12)
|
|
837
|
-
|
|
838
|
-
# Results header with row count and export options
|
|
839
|
-
results_header_layout = QHBoxLayout()
|
|
840
|
-
|
|
841
|
-
results_title = QLabel("RESULTS")
|
|
842
|
-
results_title.setObjectName("header_label")
|
|
843
|
-
|
|
844
|
-
self.row_count_label = QLabel("")
|
|
845
|
-
self.row_count_label.setStyleSheet(f"color: {self.colors['text_light']}; font-style: italic;")
|
|
846
|
-
|
|
847
|
-
results_header_layout.addWidget(results_title)
|
|
848
|
-
results_header_layout.addWidget(self.row_count_label)
|
|
849
|
-
results_header_layout.addStretch()
|
|
850
|
-
|
|
851
|
-
# Export buttons
|
|
852
|
-
export_layout = QHBoxLayout()
|
|
853
|
-
export_layout.setSpacing(8)
|
|
854
|
-
|
|
855
|
-
self.export_excel_btn = QPushButton('Export to Excel')
|
|
856
|
-
self.export_excel_btn.setIcon(QIcon.fromTheme("x-office-spreadsheet"))
|
|
857
|
-
self.export_excel_btn.clicked.connect(self.export_to_excel)
|
|
858
|
-
|
|
859
|
-
self.export_parquet_btn = QPushButton('Export to Parquet')
|
|
860
|
-
self.export_parquet_btn.setIcon(QIcon.fromTheme("document-save"))
|
|
861
|
-
self.export_parquet_btn.clicked.connect(self.export_to_parquet)
|
|
862
|
-
|
|
863
|
-
export_layout.addWidget(self.export_excel_btn)
|
|
864
|
-
export_layout.addWidget(self.export_parquet_btn)
|
|
865
|
-
|
|
866
|
-
results_header_layout.addLayout(export_layout)
|
|
867
|
-
results_layout.addLayout(results_header_layout)
|
|
868
|
-
|
|
869
|
-
# Table widget for results with modern styling
|
|
870
|
-
self.results_table = QTableWidget()
|
|
871
|
-
self.results_table.setSortingEnabled(True)
|
|
872
|
-
self.results_table.setAlternatingRowColors(True)
|
|
873
|
-
|
|
874
|
-
# Set custom header for filtering
|
|
875
|
-
header = FilterHeader(self.results_table)
|
|
876
|
-
header.set_main_window(self) # Set reference to main window
|
|
877
|
-
self.results_table.setHorizontalHeader(header)
|
|
878
|
-
header.setStretchLastSection(True)
|
|
879
|
-
header.setSectionsMovable(True)
|
|
880
|
-
|
|
881
|
-
self.results_table.verticalHeader().setVisible(False)
|
|
882
|
-
self.results_table.setShowGrid(True)
|
|
883
|
-
self.results_table.setGridStyle(Qt.PenStyle.SolidLine)
|
|
884
|
-
self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
885
|
-
|
|
886
|
-
results_layout.addWidget(self.results_table)
|
|
887
|
-
|
|
888
|
-
# Add widgets to splitter
|
|
889
|
-
splitter.addWidget(query_widget)
|
|
890
|
-
splitter.addWidget(results_widget)
|
|
565
|
+
# Create tab widget for multiple queries
|
|
566
|
+
self.tab_widget = QTabWidget()
|
|
567
|
+
self.tab_widget.setTabsClosable(True)
|
|
568
|
+
self.tab_widget.setMovable(True)
|
|
569
|
+
self.tab_widget.tabCloseRequested.connect(self.close_tab)
|
|
570
|
+
|
|
571
|
+
# Connect double-click signal for direct tab renaming
|
|
572
|
+
self.tab_widget.tabBarDoubleClicked.connect(self.handle_tab_double_click)
|
|
891
573
|
|
|
892
|
-
#
|
|
893
|
-
|
|
574
|
+
# Add a "+" button to the tab bar
|
|
575
|
+
self.tab_widget.setCornerWidget(self.create_tab_corner_widget())
|
|
894
576
|
|
|
895
|
-
right_layout.addWidget(
|
|
577
|
+
right_layout.addWidget(self.tab_widget)
|
|
896
578
|
|
|
897
579
|
# Add panels to main layout
|
|
898
580
|
main_layout.addWidget(left_panel, 1)
|
|
899
581
|
main_layout.addWidget(right_panel, 4)
|
|
900
582
|
|
|
901
583
|
# Status bar
|
|
902
|
-
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment')
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
)
|
|
584
|
+
self.statusBar().showMessage('Ready | Ctrl+Enter: Execute Query | Ctrl+K: Toggle Comment | Ctrl+T: New Tab')
|
|
585
|
+
|
|
586
|
+
def create_tab_corner_widget(self):
|
|
587
|
+
"""Create a corner widget with a + button to add new tabs"""
|
|
588
|
+
corner_widget = QWidget()
|
|
589
|
+
layout = QHBoxLayout(corner_widget)
|
|
590
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
591
|
+
layout.setSpacing(0)
|
|
592
|
+
|
|
593
|
+
add_tab_btn = QToolButton()
|
|
594
|
+
add_tab_btn.setText("+")
|
|
595
|
+
add_tab_btn.setToolTip("Add new tab (Ctrl+T)")
|
|
596
|
+
add_tab_btn.setStyleSheet("""
|
|
597
|
+
QToolButton {
|
|
598
|
+
background-color: transparent;
|
|
599
|
+
border: none;
|
|
600
|
+
border-radius: 4px;
|
|
601
|
+
padding: 4px;
|
|
602
|
+
font-weight: bold;
|
|
603
|
+
font-size: 16px;
|
|
604
|
+
color: #3498DB;
|
|
605
|
+
}
|
|
606
|
+
QToolButton:hover {
|
|
607
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
608
|
+
}
|
|
609
|
+
QToolButton:pressed {
|
|
610
|
+
background-color: rgba(52, 152, 219, 0.4);
|
|
611
|
+
}
|
|
612
|
+
""")
|
|
613
|
+
add_tab_btn.clicked.connect(self.add_tab)
|
|
614
|
+
|
|
615
|
+
layout.addWidget(add_tab_btn)
|
|
616
|
+
return corner_widget
|
|
912
617
|
|
|
913
618
|
def populate_table(self, df):
|
|
914
619
|
"""Populate the results table with DataFrame data using memory-efficient chunking"""
|
|
915
620
|
try:
|
|
621
|
+
# Get the current tab
|
|
622
|
+
current_tab = self.get_current_tab()
|
|
623
|
+
if not current_tab:
|
|
624
|
+
return
|
|
625
|
+
|
|
916
626
|
# Store the current DataFrame for filtering
|
|
917
|
-
|
|
627
|
+
current_tab.current_df = df.copy()
|
|
628
|
+
self.current_df = df.copy() # Keep this for compatibility with existing code
|
|
918
629
|
|
|
919
630
|
# Remember which columns had bar charts
|
|
920
|
-
header =
|
|
631
|
+
header = current_tab.results_table.horizontalHeader()
|
|
921
632
|
if isinstance(header, FilterHeader):
|
|
922
633
|
columns_with_bars = header.columns_with_bars.copy()
|
|
923
634
|
else:
|
|
924
635
|
columns_with_bars = set()
|
|
925
636
|
|
|
926
637
|
# Clear existing data
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
638
|
+
current_tab.results_table.clearContents()
|
|
639
|
+
current_tab.results_table.setRowCount(0)
|
|
640
|
+
current_tab.results_table.setColumnCount(0)
|
|
930
641
|
|
|
931
642
|
if df.empty:
|
|
932
643
|
self.statusBar().showMessage("Query returned no results")
|
|
@@ -935,11 +646,11 @@ class SQLShell(QMainWindow):
|
|
|
935
646
|
# Set up the table dimensions
|
|
936
647
|
row_count = len(df)
|
|
937
648
|
col_count = len(df.columns)
|
|
938
|
-
|
|
649
|
+
current_tab.results_table.setColumnCount(col_count)
|
|
939
650
|
|
|
940
651
|
# Set column headers
|
|
941
652
|
headers = [str(col) for col in df.columns]
|
|
942
|
-
|
|
653
|
+
current_tab.results_table.setHorizontalHeaderLabels(headers)
|
|
943
654
|
|
|
944
655
|
# Calculate chunk size (adjust based on available memory)
|
|
945
656
|
CHUNK_SIZE = 1000
|
|
@@ -950,27 +661,30 @@ class SQLShell(QMainWindow):
|
|
|
950
661
|
chunk = df.iloc[chunk_start:chunk_end]
|
|
951
662
|
|
|
952
663
|
# Add rows for this chunk
|
|
953
|
-
|
|
664
|
+
current_tab.results_table.setRowCount(chunk_end)
|
|
954
665
|
|
|
955
666
|
for row_idx, (_, row_data) in enumerate(chunk.iterrows(), start=chunk_start):
|
|
956
667
|
for col_idx, value in enumerate(row_data):
|
|
957
668
|
formatted_value = self.format_value(value)
|
|
958
669
|
item = QTableWidgetItem(formatted_value)
|
|
959
|
-
|
|
670
|
+
current_tab.results_table.setItem(row_idx, col_idx, item)
|
|
960
671
|
|
|
961
672
|
# Process events to keep UI responsive
|
|
962
673
|
QApplication.processEvents()
|
|
963
674
|
|
|
964
675
|
# Optimize column widths
|
|
965
|
-
|
|
676
|
+
current_tab.results_table.resizeColumnsToContents()
|
|
966
677
|
|
|
967
678
|
# Restore bar charts for columns that previously had them
|
|
968
|
-
header =
|
|
679
|
+
header = current_tab.results_table.horizontalHeader()
|
|
969
680
|
if isinstance(header, FilterHeader):
|
|
970
681
|
for col_idx in columns_with_bars:
|
|
971
682
|
if col_idx < col_count: # Only if column still exists
|
|
972
683
|
header.toggle_bar_chart(col_idx)
|
|
973
684
|
|
|
685
|
+
# Update row count label
|
|
686
|
+
current_tab.row_count_label.setText(f"{row_count:,} rows")
|
|
687
|
+
|
|
974
688
|
# Update status
|
|
975
689
|
memory_usage = df.memory_usage(deep=True).sum() / (1024 * 1024) # Convert to MB
|
|
976
690
|
self.statusBar().showMessage(
|
|
@@ -1041,11 +755,10 @@ class SQLShell(QMainWindow):
|
|
|
1041
755
|
return str(value)
|
|
1042
756
|
|
|
1043
757
|
def browse_files(self):
|
|
1044
|
-
if not self.
|
|
758
|
+
if not self.db_manager.is_connected():
|
|
1045
759
|
# Create a default in-memory DuckDB connection if none exists
|
|
1046
|
-
|
|
1047
|
-
self.
|
|
1048
|
-
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
760
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
761
|
+
self.db_info_label.setText(connection_info)
|
|
1049
762
|
|
|
1050
763
|
file_names, _ = QFileDialog.getOpenFileNames(
|
|
1051
764
|
self,
|
|
@@ -1056,38 +769,8 @@ class SQLShell(QMainWindow):
|
|
|
1056
769
|
|
|
1057
770
|
for file_name in file_names:
|
|
1058
771
|
try:
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
elif file_name.endswith('.csv'):
|
|
1062
|
-
df = pd.read_csv(file_name)
|
|
1063
|
-
elif file_name.endswith('.parquet'):
|
|
1064
|
-
df = pd.read_parquet(file_name)
|
|
1065
|
-
else:
|
|
1066
|
-
raise ValueError("Unsupported file format")
|
|
1067
|
-
|
|
1068
|
-
# Generate table name from file name
|
|
1069
|
-
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1070
|
-
table_name = self.sanitize_table_name(base_name)
|
|
1071
|
-
|
|
1072
|
-
# Ensure unique table name
|
|
1073
|
-
original_name = table_name
|
|
1074
|
-
counter = 1
|
|
1075
|
-
while table_name in self.loaded_tables:
|
|
1076
|
-
table_name = f"{original_name}_{counter}"
|
|
1077
|
-
counter += 1
|
|
1078
|
-
|
|
1079
|
-
# Handle table creation based on database type
|
|
1080
|
-
if self.current_connection_type == 'sqlite':
|
|
1081
|
-
# For SQLite, create a table from the DataFrame
|
|
1082
|
-
df.to_sql(table_name, self.conn, index=False, if_exists='replace')
|
|
1083
|
-
else:
|
|
1084
|
-
# For DuckDB, register the DataFrame as a view
|
|
1085
|
-
self.conn.register(table_name, df)
|
|
1086
|
-
|
|
1087
|
-
self.loaded_tables[table_name] = file_name
|
|
1088
|
-
|
|
1089
|
-
# Store column names
|
|
1090
|
-
self.table_columns[table_name] = df.columns.tolist()
|
|
772
|
+
# Use the database manager to load the file
|
|
773
|
+
table_name, df = self.db_manager.load_file(file_name)
|
|
1091
774
|
|
|
1092
775
|
# Update UI
|
|
1093
776
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
@@ -1113,26 +796,11 @@ class SQLShell(QMainWindow):
|
|
|
1113
796
|
self.results_table.setColumnCount(0)
|
|
1114
797
|
self.row_count_label.setText("")
|
|
1115
798
|
|
|
1116
|
-
def sanitize_table_name(self, name):
|
|
1117
|
-
# Replace invalid characters with underscores
|
|
1118
|
-
import re
|
|
1119
|
-
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
1120
|
-
# Ensure it starts with a letter
|
|
1121
|
-
if not name[0].isalpha():
|
|
1122
|
-
name = 'table_' + name
|
|
1123
|
-
return name.lower()
|
|
1124
|
-
|
|
1125
799
|
def remove_selected_table(self):
|
|
1126
800
|
current_item = self.tables_list.currentItem()
|
|
1127
801
|
if current_item:
|
|
1128
802
|
table_name = current_item.text().split(' (')[0]
|
|
1129
|
-
if
|
|
1130
|
-
# Remove from DuckDB
|
|
1131
|
-
self.conn.execute(f'DROP VIEW IF EXISTS {table_name}')
|
|
1132
|
-
# Remove from our tracking
|
|
1133
|
-
del self.loaded_tables[table_name]
|
|
1134
|
-
if table_name in self.table_columns:
|
|
1135
|
-
del self.table_columns[table_name]
|
|
803
|
+
if self.db_manager.remove_table(table_name):
|
|
1136
804
|
# Remove from list widget
|
|
1137
805
|
self.tables_list.takeItem(self.tables_list.row(current_item))
|
|
1138
806
|
self.statusBar().showMessage(f'Removed table "{table_name}"')
|
|
@@ -1146,15 +814,6 @@ class SQLShell(QMainWindow):
|
|
|
1146
814
|
def open_database(self):
|
|
1147
815
|
"""Open a database connection with proper error handling and resource management"""
|
|
1148
816
|
try:
|
|
1149
|
-
if self.conn:
|
|
1150
|
-
# Close existing connection before opening new one
|
|
1151
|
-
if self.current_connection_type == "duckdb":
|
|
1152
|
-
self.conn.close()
|
|
1153
|
-
else: # sqlite
|
|
1154
|
-
self.conn.close()
|
|
1155
|
-
self.conn = None
|
|
1156
|
-
self.current_connection_type = None
|
|
1157
|
-
|
|
1158
817
|
filename, _ = QFileDialog.getOpenFileName(
|
|
1159
818
|
self,
|
|
1160
819
|
"Open Database",
|
|
@@ -1163,86 +822,214 @@ class SQLShell(QMainWindow):
|
|
|
1163
822
|
)
|
|
1164
823
|
|
|
1165
824
|
if filename:
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
self.
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
self.load_database_tables()
|
|
1174
|
-
self.statusBar().showMessage(f"Connected to database: {filename}")
|
|
1175
|
-
|
|
1176
|
-
except (sqlite3.Error, duckdb.Error) as e:
|
|
1177
|
-
QMessageBox.critical(self, "Database Connection Error",
|
|
1178
|
-
f"Failed to open database:\n\n{str(e)}")
|
|
1179
|
-
self.statusBar().showMessage("Failed to open database")
|
|
1180
|
-
self.conn = None
|
|
1181
|
-
self.current_connection_type = None
|
|
1182
|
-
|
|
1183
|
-
def is_sqlite_db(self, filename):
|
|
1184
|
-
"""Check if the file is a SQLite database"""
|
|
1185
|
-
try:
|
|
1186
|
-
with open(filename, 'rb') as f:
|
|
1187
|
-
header = f.read(16)
|
|
1188
|
-
return header[:16] == b'SQLite format 3\x00'
|
|
1189
|
-
except:
|
|
1190
|
-
return False
|
|
1191
|
-
|
|
1192
|
-
def load_database_tables(self):
|
|
1193
|
-
"""Load all tables from the current database"""
|
|
1194
|
-
try:
|
|
1195
|
-
if self.current_connection_type == 'sqlite':
|
|
1196
|
-
query = "SELECT name FROM sqlite_master WHERE type='table'"
|
|
1197
|
-
cursor = self.conn.cursor()
|
|
1198
|
-
tables = cursor.execute(query).fetchall()
|
|
1199
|
-
for (table_name,) in tables:
|
|
1200
|
-
self.loaded_tables[table_name] = 'database'
|
|
1201
|
-
self.tables_list.addItem(f"{table_name} (database)")
|
|
825
|
+
try:
|
|
826
|
+
# Clear existing database tables from the list widget
|
|
827
|
+
for i in range(self.tables_list.count() - 1, -1, -1):
|
|
828
|
+
item = self.tables_list.item(i)
|
|
829
|
+
if item and item.text().endswith('(database)'):
|
|
830
|
+
self.tables_list.takeItem(i)
|
|
1202
831
|
|
|
1203
|
-
#
|
|
1204
|
-
|
|
1205
|
-
column_query = f"PRAGMA table_info({table_name})"
|
|
1206
|
-
columns = cursor.execute(column_query).fetchall()
|
|
1207
|
-
self.table_columns[table_name] = [col[1] for col in columns] # Column name is at index 1
|
|
1208
|
-
except Exception:
|
|
1209
|
-
self.table_columns[table_name] = []
|
|
1210
|
-
else: # duckdb
|
|
1211
|
-
query = "SELECT table_name FROM information_schema.tables WHERE table_schema='main'"
|
|
1212
|
-
result = self.conn.execute(query).fetchdf()
|
|
1213
|
-
for table_name in result['table_name']:
|
|
1214
|
-
self.loaded_tables[table_name] = 'database'
|
|
1215
|
-
self.tables_list.addItem(f"{table_name} (database)")
|
|
832
|
+
# Use the database manager to open the database
|
|
833
|
+
self.db_manager.open_database(filename)
|
|
1216
834
|
|
|
1217
|
-
#
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
835
|
+
# Update UI with tables from the database
|
|
836
|
+
for table_name, source in self.db_manager.loaded_tables.items():
|
|
837
|
+
if source == 'database':
|
|
838
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
839
|
+
|
|
840
|
+
# Update the completer with table and column names
|
|
841
|
+
self.update_completer()
|
|
842
|
+
|
|
843
|
+
# Update status bar
|
|
844
|
+
self.statusBar().showMessage(f"Connected to database: {filename}")
|
|
845
|
+
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
846
|
+
|
|
847
|
+
except Exception as e:
|
|
848
|
+
QMessageBox.critical(self, "Database Connection Error",
|
|
849
|
+
f"Failed to open database:\n\n{str(e)}")
|
|
850
|
+
self.statusBar().showMessage("Failed to open database")
|
|
851
|
+
|
|
1227
852
|
except Exception as e:
|
|
1228
|
-
|
|
853
|
+
QMessageBox.critical(self, "Error",
|
|
854
|
+
f"Unexpected error:\n\n{str(e)}")
|
|
855
|
+
self.statusBar().showMessage("Error opening database")
|
|
1229
856
|
|
|
1230
857
|
def update_completer(self):
|
|
1231
|
-
"""Update the completer with table and column names"""
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
858
|
+
"""Update the completer with table and column names in a non-blocking way"""
|
|
859
|
+
try:
|
|
860
|
+
# Check if any tabs exist
|
|
861
|
+
if self.tab_widget.count() == 0:
|
|
862
|
+
return
|
|
863
|
+
|
|
864
|
+
# Start a background update with a timer
|
|
865
|
+
self.statusBar().showMessage("Updating auto-completion...", 2000)
|
|
866
|
+
|
|
867
|
+
# Track query history and frequently used terms
|
|
868
|
+
if not hasattr(self, 'query_history'):
|
|
869
|
+
self.query_history = []
|
|
870
|
+
self.completion_usage = {} # Track usage frequency
|
|
871
|
+
|
|
872
|
+
# Get completion words from the database manager
|
|
873
|
+
try:
|
|
874
|
+
completion_words = self.db_manager.get_all_table_columns()
|
|
875
|
+
except Exception as e:
|
|
876
|
+
self.statusBar().showMessage(f"Error getting completions: {str(e)}", 2000)
|
|
877
|
+
completion_words = []
|
|
878
|
+
|
|
879
|
+
# Add frequently used terms from query history with higher priority
|
|
880
|
+
if hasattr(self, 'completion_usage') and self.completion_usage:
|
|
881
|
+
# Get the most frequently used terms (top 100)
|
|
882
|
+
frequent_terms = sorted(
|
|
883
|
+
self.completion_usage.items(),
|
|
884
|
+
key=lambda x: x[1],
|
|
885
|
+
reverse=True
|
|
886
|
+
)[:100]
|
|
887
|
+
|
|
888
|
+
# Add these to our completion words
|
|
889
|
+
for term, _ in frequent_terms:
|
|
890
|
+
if term not in completion_words:
|
|
891
|
+
completion_words.append(term)
|
|
892
|
+
|
|
893
|
+
# Limit to a reasonable number of items to prevent performance issues
|
|
894
|
+
MAX_COMPLETION_ITEMS = 2000 # Increased from 1000 to accommodate more smart suggestions
|
|
895
|
+
if len(completion_words) > MAX_COMPLETION_ITEMS:
|
|
896
|
+
# Create a more advanced prioritization strategy
|
|
897
|
+
prioritized_words = []
|
|
898
|
+
|
|
899
|
+
# First, include all table names
|
|
900
|
+
tables = list(self.db_manager.loaded_tables.keys())
|
|
901
|
+
prioritized_words.extend(tables)
|
|
902
|
+
|
|
903
|
+
# Then add most common SQL keywords and patterns
|
|
904
|
+
sql_keywords = [w for w in completion_words if w.isupper() and len(w) > 1]
|
|
905
|
+
prioritized_words.extend(sql_keywords[:200]) # Cap at 200 keywords
|
|
906
|
+
|
|
907
|
+
# Add frequently used items
|
|
908
|
+
if hasattr(self, 'completion_usage'):
|
|
909
|
+
frequent_items = [
|
|
910
|
+
item for item, _ in sorted(
|
|
911
|
+
self.completion_usage.items(),
|
|
912
|
+
key=lambda x: x[1],
|
|
913
|
+
reverse=True
|
|
914
|
+
)[:100] # Top 100 most used
|
|
915
|
+
]
|
|
916
|
+
prioritized_words.extend(frequent_items)
|
|
917
|
+
|
|
918
|
+
# Add table.column patterns which are very useful
|
|
919
|
+
qualified_columns = [w for w in completion_words if '.' in w and w.split('.')[0] in tables]
|
|
920
|
+
prioritized_words.extend(qualified_columns[:300]) # Cap at 300 qualified columns
|
|
921
|
+
|
|
922
|
+
# Add common completion patterns
|
|
923
|
+
patterns = [w for w in completion_words if ' ' in w] # Spaces indicate phrases/patterns
|
|
924
|
+
prioritized_words.extend(patterns[:200]) # Cap at 200 patterns
|
|
925
|
+
|
|
926
|
+
# Finally add other columns
|
|
927
|
+
remaining_slots = MAX_COMPLETION_ITEMS - len(prioritized_words)
|
|
928
|
+
remaining_words = [
|
|
929
|
+
w for w in completion_words
|
|
930
|
+
if w not in prioritized_words
|
|
931
|
+
and not w.isupper()
|
|
932
|
+
and '.' not in w
|
|
933
|
+
and ' ' not in w
|
|
934
|
+
]
|
|
935
|
+
prioritized_words.extend(remaining_words[:remaining_slots])
|
|
936
|
+
|
|
937
|
+
# Remove duplicates while preserving order
|
|
938
|
+
seen = set()
|
|
939
|
+
completion_words = []
|
|
940
|
+
for item in prioritized_words:
|
|
941
|
+
if item not in seen:
|
|
942
|
+
seen.add(item)
|
|
943
|
+
completion_words.append(item)
|
|
944
|
+
|
|
945
|
+
# Ensure we don't exceed the maximum
|
|
946
|
+
completion_words = completion_words[:MAX_COMPLETION_ITEMS]
|
|
947
|
+
|
|
948
|
+
# Use a single model for all tabs to save memory and improve performance
|
|
949
|
+
model = QStringListModel(completion_words)
|
|
1239
950
|
|
|
1240
|
-
|
|
1241
|
-
|
|
951
|
+
# Keep a reference to the model to prevent garbage collection
|
|
952
|
+
self._current_completer_model = model
|
|
953
|
+
|
|
954
|
+
# Only update the current tab immediately
|
|
955
|
+
current_index = self.tab_widget.currentIndex()
|
|
956
|
+
if current_index >= 0:
|
|
957
|
+
current_tab = self.tab_widget.widget(current_index)
|
|
958
|
+
if current_tab and hasattr(current_tab, 'query_edit'):
|
|
959
|
+
try:
|
|
960
|
+
current_tab.query_edit.update_completer_model(model)
|
|
961
|
+
except Exception as e:
|
|
962
|
+
self.statusBar().showMessage(f"Error updating current tab completer: {str(e)}", 2000)
|
|
963
|
+
|
|
964
|
+
# Only schedule updates for additional tabs if we have more than 3 tabs
|
|
965
|
+
# This reduces overhead for common usage patterns
|
|
966
|
+
if self.tab_widget.count() > 1:
|
|
967
|
+
# Calculate a reasonable maximum delay (ms)
|
|
968
|
+
max_delay = min(500, 50 * self.tab_widget.count())
|
|
969
|
+
|
|
970
|
+
# Store timers to prevent garbage collection
|
|
971
|
+
if not hasattr(self, '_completer_timers'):
|
|
972
|
+
self._completer_timers = []
|
|
973
|
+
|
|
974
|
+
# Clear old timers
|
|
975
|
+
for timer in self._completer_timers:
|
|
976
|
+
if timer.isActive():
|
|
977
|
+
timer.stop()
|
|
978
|
+
self._completer_timers = []
|
|
979
|
+
|
|
980
|
+
# Schedule updates for other tabs with increasing delays
|
|
981
|
+
for i in range(self.tab_widget.count()):
|
|
982
|
+
if i != current_index:
|
|
983
|
+
tab = self.tab_widget.widget(i)
|
|
984
|
+
if tab and not tab.isHidden() and hasattr(tab, 'query_edit'):
|
|
985
|
+
delay = int((i + 1) / self.tab_widget.count() * max_delay)
|
|
986
|
+
|
|
987
|
+
timer = QTimer()
|
|
988
|
+
timer.setSingleShot(True)
|
|
989
|
+
# Store tab and model as local variables for the lambda
|
|
990
|
+
# to avoid closure issues
|
|
991
|
+
tab_ref = tab
|
|
992
|
+
model_ref = model
|
|
993
|
+
timer.timeout.connect(
|
|
994
|
+
lambda t=tab_ref, m=model_ref: self._update_tab_completer(t, m))
|
|
995
|
+
self._completer_timers.append(timer)
|
|
996
|
+
timer.start(delay)
|
|
997
|
+
|
|
998
|
+
# Process events to keep UI responsive
|
|
999
|
+
QApplication.processEvents()
|
|
1000
|
+
|
|
1001
|
+
# Return True to indicate success
|
|
1002
|
+
return True
|
|
1003
|
+
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
# Catch any errors to prevent hanging
|
|
1006
|
+
self.statusBar().showMessage(f"Auto-completion update error: {str(e)}", 2000)
|
|
1007
|
+
return False
|
|
1008
|
+
|
|
1009
|
+
def _update_tab_completer(self, tab, model):
|
|
1010
|
+
"""Helper method to update a tab's completer with the given model"""
|
|
1011
|
+
if tab and not tab.isHidden() and hasattr(tab, 'query_edit'): # Only update visible tabs with query editors
|
|
1012
|
+
try:
|
|
1013
|
+
tab.query_edit.update_completer_model(model)
|
|
1014
|
+
QApplication.processEvents() # Keep UI responsive
|
|
1015
|
+
except Exception as e:
|
|
1016
|
+
print(f"Error updating tab completer: {e}")
|
|
1017
|
+
# Try a simpler approach as fallback
|
|
1018
|
+
try:
|
|
1019
|
+
if hasattr(tab.query_edit, 'all_sql_keywords'):
|
|
1020
|
+
fallback_model = QStringListModel(tab.query_edit.all_sql_keywords)
|
|
1021
|
+
tab.query_edit.completer.setModel(fallback_model)
|
|
1022
|
+
except Exception:
|
|
1023
|
+
pass # Last resort: ignore errors to prevent crashes
|
|
1242
1024
|
|
|
1243
1025
|
def execute_query(self):
|
|
1244
1026
|
try:
|
|
1245
|
-
|
|
1027
|
+
# Get the current tab
|
|
1028
|
+
current_tab = self.get_current_tab()
|
|
1029
|
+
if not current_tab:
|
|
1030
|
+
return
|
|
1031
|
+
|
|
1032
|
+
query = current_tab.get_query_text().strip()
|
|
1246
1033
|
if not query:
|
|
1247
1034
|
QMessageBox.warning(self, "Empty Query", "Please enter a SQL query to execute.")
|
|
1248
1035
|
return
|
|
@@ -1250,29 +1037,24 @@ class SQLShell(QMainWindow):
|
|
|
1250
1037
|
start_time = datetime.now()
|
|
1251
1038
|
|
|
1252
1039
|
try:
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
else: # sqlite
|
|
1256
|
-
result = pd.read_sql_query(query, self.conn)
|
|
1040
|
+
# Use the database manager to execute the query
|
|
1041
|
+
result = self.db_manager.execute_query(query)
|
|
1257
1042
|
|
|
1258
1043
|
execution_time = (datetime.now() - start_time).total_seconds()
|
|
1259
1044
|
self.populate_table(result)
|
|
1260
1045
|
self.statusBar().showMessage(f"Query executed successfully. Time: {execution_time:.2f}s. Rows: {len(result)}")
|
|
1261
1046
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
else:
|
|
1274
|
-
QMessageBox.critical(self, "Database Error",
|
|
1275
|
-
f"An error occurred while executing the query:\n\n{error_msg}")
|
|
1047
|
+
# Record query in history and update completion usage
|
|
1048
|
+
self._update_query_history(query)
|
|
1049
|
+
|
|
1050
|
+
except SyntaxError as e:
|
|
1051
|
+
QMessageBox.critical(self, "SQL Syntax Error", str(e))
|
|
1052
|
+
self.statusBar().showMessage("Query execution failed: syntax error")
|
|
1053
|
+
except ValueError as e:
|
|
1054
|
+
QMessageBox.critical(self, "Query Error", str(e))
|
|
1055
|
+
self.statusBar().showMessage("Query execution failed")
|
|
1056
|
+
except Exception as e:
|
|
1057
|
+
QMessageBox.critical(self, "Database Error", str(e))
|
|
1276
1058
|
self.statusBar().showMessage("Query execution failed")
|
|
1277
1059
|
|
|
1278
1060
|
except Exception as e:
|
|
@@ -1280,59 +1062,129 @@ class SQLShell(QMainWindow):
|
|
|
1280
1062
|
f"An unexpected error occurred:\n\n{str(e)}")
|
|
1281
1063
|
self.statusBar().showMessage("Query execution failed")
|
|
1282
1064
|
|
|
1065
|
+
def _update_query_history(self, query):
|
|
1066
|
+
"""Update query history and track term usage for improved autocompletion"""
|
|
1067
|
+
import re
|
|
1068
|
+
|
|
1069
|
+
# Initialize history if it doesn't exist
|
|
1070
|
+
if not hasattr(self, 'query_history'):
|
|
1071
|
+
self.query_history = []
|
|
1072
|
+
self.completion_usage = {}
|
|
1073
|
+
|
|
1074
|
+
# Add query to history (limit to 100 queries)
|
|
1075
|
+
self.query_history.append(query)
|
|
1076
|
+
if len(self.query_history) > 100:
|
|
1077
|
+
self.query_history.pop(0)
|
|
1078
|
+
|
|
1079
|
+
# Extract terms and patterns from the query to update usage frequency
|
|
1080
|
+
|
|
1081
|
+
# Extract table and column names
|
|
1082
|
+
table_pattern = r'\b([a-zA-Z0-9_]+)\b\.([a-zA-Z0-9_]+)\b'
|
|
1083
|
+
qualified_columns = re.findall(table_pattern, query)
|
|
1084
|
+
for table, column in qualified_columns:
|
|
1085
|
+
qualified_name = f"{table}.{column}"
|
|
1086
|
+
self.completion_usage[qualified_name] = self.completion_usage.get(qualified_name, 0) + 1
|
|
1087
|
+
|
|
1088
|
+
# Also count the table and column separately
|
|
1089
|
+
self.completion_usage[table] = self.completion_usage.get(table, 0) + 1
|
|
1090
|
+
self.completion_usage[column] = self.completion_usage.get(column, 0) + 1
|
|
1091
|
+
|
|
1092
|
+
# Extract SQL keywords
|
|
1093
|
+
keyword_pattern = r'\b([A-Z_]{2,})\b'
|
|
1094
|
+
keywords = re.findall(keyword_pattern, query.upper())
|
|
1095
|
+
for keyword in keywords:
|
|
1096
|
+
self.completion_usage[keyword] = self.completion_usage.get(keyword, 0) + 1
|
|
1097
|
+
|
|
1098
|
+
# Extract common SQL patterns
|
|
1099
|
+
patterns = [
|
|
1100
|
+
r'(SELECT\s+.*?\s+FROM)',
|
|
1101
|
+
r'(GROUP\s+BY\s+.*?(?:HAVING|ORDER|LIMIT|$))',
|
|
1102
|
+
r'(ORDER\s+BY\s+.*?(?:LIMIT|$))',
|
|
1103
|
+
r'(INNER\s+JOIN|LEFT\s+JOIN|RIGHT\s+JOIN|FULL\s+JOIN).*?ON\s+.*?=\s+.*?(?:WHERE|JOIN|GROUP|ORDER|LIMIT|$)',
|
|
1104
|
+
r'(INSERT\s+INTO\s+.*?\s+VALUES)',
|
|
1105
|
+
r'(UPDATE\s+.*?\s+SET\s+.*?\s+WHERE)',
|
|
1106
|
+
r'(DELETE\s+FROM\s+.*?\s+WHERE)'
|
|
1107
|
+
]
|
|
1108
|
+
|
|
1109
|
+
for pattern in patterns:
|
|
1110
|
+
matches = re.findall(pattern, query, re.IGNORECASE | re.DOTALL)
|
|
1111
|
+
for match in matches:
|
|
1112
|
+
# Normalize pattern by removing extra whitespace and converting to uppercase
|
|
1113
|
+
normalized = re.sub(r'\s+', ' ', match).strip().upper()
|
|
1114
|
+
if len(normalized) < 50: # Only track reasonably sized patterns
|
|
1115
|
+
self.completion_usage[normalized] = self.completion_usage.get(normalized, 0) + 1
|
|
1116
|
+
|
|
1117
|
+
# Schedule an update of the completion model (but not too often to avoid performance issues)
|
|
1118
|
+
if not hasattr(self, '_last_completer_update') or \
|
|
1119
|
+
(datetime.now() - self._last_completer_update).total_seconds() > 30:
|
|
1120
|
+
self._last_completer_update = datetime.now()
|
|
1121
|
+
|
|
1122
|
+
# Use a timer to delay the update to avoid blocking the UI
|
|
1123
|
+
update_timer = QTimer()
|
|
1124
|
+
update_timer.setSingleShot(True)
|
|
1125
|
+
update_timer.timeout.connect(self.update_completer)
|
|
1126
|
+
update_timer.start(1000) # Update after 1 second
|
|
1127
|
+
|
|
1283
1128
|
def clear_query(self):
|
|
1284
1129
|
"""Clear the query editor with animation"""
|
|
1130
|
+
# Get the current tab
|
|
1131
|
+
current_tab = self.get_current_tab()
|
|
1132
|
+
if not current_tab:
|
|
1133
|
+
return
|
|
1134
|
+
|
|
1285
1135
|
# Save current text for animation
|
|
1286
|
-
current_text =
|
|
1136
|
+
current_text = current_tab.get_query_text()
|
|
1287
1137
|
if not current_text:
|
|
1288
1138
|
return
|
|
1289
1139
|
|
|
1290
1140
|
# Clear the editor
|
|
1291
|
-
|
|
1141
|
+
current_tab.set_query_text("")
|
|
1292
1142
|
|
|
1293
1143
|
# Show success message
|
|
1294
1144
|
self.statusBar().showMessage('Query cleared', 2000) # Show for 2 seconds
|
|
1295
1145
|
|
|
1296
1146
|
def show_table_preview(self, item):
|
|
1297
1147
|
"""Show a preview of the selected table"""
|
|
1298
|
-
if item:
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
results_title = self.findChild(QLabel, "header_label", Qt.FindChildOption.FindChildrenRecursively)
|
|
1311
|
-
if results_title and results_title.text() == "RESULTS":
|
|
1312
|
-
results_title.setText(f"PREVIEW: {table_name}")
|
|
1148
|
+
if not item:
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# Get the current tab
|
|
1152
|
+
current_tab = self.get_current_tab()
|
|
1153
|
+
if not current_tab:
|
|
1154
|
+
return
|
|
1155
|
+
|
|
1156
|
+
table_name = item.text().split(' (')[0]
|
|
1157
|
+
try:
|
|
1158
|
+
# Use the database manager to get a preview of the table
|
|
1159
|
+
preview_df = self.db_manager.get_table_preview(table_name)
|
|
1313
1160
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1161
|
+
self.populate_table(preview_df)
|
|
1162
|
+
self.statusBar().showMessage(f'Showing preview of table "{table_name}"')
|
|
1163
|
+
|
|
1164
|
+
# Update the results title to show which table is being previewed
|
|
1165
|
+
current_tab.results_title.setText(f"PREVIEW: {table_name}")
|
|
1166
|
+
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
current_tab.results_table.setRowCount(0)
|
|
1169
|
+
current_tab.results_table.setColumnCount(0)
|
|
1170
|
+
current_tab.row_count_label.setText("")
|
|
1171
|
+
self.statusBar().showMessage('Error showing table preview')
|
|
1172
|
+
|
|
1173
|
+
# Show error message with modern styling
|
|
1174
|
+
QMessageBox.critical(
|
|
1175
|
+
self,
|
|
1176
|
+
"Error",
|
|
1177
|
+
f"Error showing preview: {str(e)}",
|
|
1178
|
+
QMessageBox.StandardButton.Ok
|
|
1179
|
+
)
|
|
1327
1180
|
|
|
1328
1181
|
def load_test_data(self):
|
|
1329
1182
|
"""Generate and load test data"""
|
|
1330
1183
|
try:
|
|
1331
1184
|
# Ensure we have a DuckDB connection
|
|
1332
|
-
if not self.
|
|
1333
|
-
|
|
1334
|
-
self.
|
|
1335
|
-
self.db_info_label.setText("Connected to: in-memory DuckDB")
|
|
1185
|
+
if not self.db_manager.is_connected() or self.db_manager.connection_type != 'duckdb':
|
|
1186
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
1187
|
+
self.db_info_label.setText(connection_info)
|
|
1336
1188
|
|
|
1337
1189
|
# Show loading indicator
|
|
1338
1190
|
self.statusBar().showMessage('Generating test data...')
|
|
@@ -1350,28 +1202,20 @@ class SQLShell(QMainWindow):
|
|
|
1350
1202
|
customer_df.to_parquet('test_data/customer_data.parquet', index=False)
|
|
1351
1203
|
product_df.to_excel('test_data/product_catalog.xlsx', index=False)
|
|
1352
1204
|
|
|
1353
|
-
#
|
|
1354
|
-
self.
|
|
1355
|
-
self.
|
|
1356
|
-
self.
|
|
1357
|
-
|
|
1358
|
-
# Update loaded tables tracking
|
|
1359
|
-
self.loaded_tables['sample_sales_data'] = 'test_data/sample_sales_data.xlsx'
|
|
1360
|
-
self.loaded_tables['product_catalog'] = 'test_data/product_catalog.xlsx'
|
|
1361
|
-
self.loaded_tables['customer_data'] = 'test_data/customer_data.parquet'
|
|
1362
|
-
|
|
1363
|
-
# Store column names
|
|
1364
|
-
self.table_columns['sample_sales_data'] = sales_df.columns.tolist()
|
|
1365
|
-
self.table_columns['product_catalog'] = product_df.columns.tolist()
|
|
1366
|
-
self.table_columns['customer_data'] = customer_df.columns.tolist()
|
|
1205
|
+
# Register the tables in the database manager
|
|
1206
|
+
self.db_manager.register_dataframe(sales_df, 'sample_sales_data', 'test_data/sample_sales_data.xlsx')
|
|
1207
|
+
self.db_manager.register_dataframe(product_df, 'product_catalog', 'test_data/product_catalog.xlsx')
|
|
1208
|
+
self.db_manager.register_dataframe(customer_df, 'customer_data', 'test_data/customer_data.parquet')
|
|
1367
1209
|
|
|
1368
1210
|
# Update UI
|
|
1369
1211
|
self.tables_list.clear()
|
|
1370
|
-
for table_name, file_path in self.loaded_tables.items():
|
|
1212
|
+
for table_name, file_path in self.db_manager.loaded_tables.items():
|
|
1371
1213
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
1372
1214
|
|
|
1373
|
-
# Set the sample query
|
|
1374
|
-
|
|
1215
|
+
# Set the sample query in the current tab
|
|
1216
|
+
current_tab = self.get_current_tab()
|
|
1217
|
+
if current_tab:
|
|
1218
|
+
sample_query = """
|
|
1375
1219
|
SELECT
|
|
1376
1220
|
DISTINCT
|
|
1377
1221
|
c.customername
|
|
@@ -1381,7 +1225,7 @@ FROM
|
|
|
1381
1225
|
INNER JOIN product_catalog p ON p.productid = s.productid
|
|
1382
1226
|
LIMIT 10
|
|
1383
1227
|
"""
|
|
1384
|
-
|
|
1228
|
+
current_tab.set_query_text(sample_query.strip())
|
|
1385
1229
|
|
|
1386
1230
|
# Update completer
|
|
1387
1231
|
self.update_completer()
|
|
@@ -1397,7 +1241,12 @@ LIMIT 10
|
|
|
1397
1241
|
QMessageBox.critical(self, "Error", f"Failed to load test data: {str(e)}")
|
|
1398
1242
|
|
|
1399
1243
|
def export_to_excel(self):
|
|
1400
|
-
|
|
1244
|
+
# Get the current tab
|
|
1245
|
+
current_tab = self.get_current_tab()
|
|
1246
|
+
if not current_tab:
|
|
1247
|
+
return
|
|
1248
|
+
|
|
1249
|
+
if current_tab.results_table.rowCount() == 0:
|
|
1401
1250
|
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1402
1251
|
return
|
|
1403
1252
|
|
|
@@ -1415,21 +1264,21 @@ LIMIT 10
|
|
|
1415
1264
|
|
|
1416
1265
|
# Generate table name from file name
|
|
1417
1266
|
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1418
|
-
table_name = self.sanitize_table_name(base_name)
|
|
1267
|
+
table_name = self.db_manager.sanitize_table_name(base_name)
|
|
1419
1268
|
|
|
1420
1269
|
# Ensure unique table name
|
|
1421
1270
|
original_name = table_name
|
|
1422
1271
|
counter = 1
|
|
1423
|
-
while table_name in self.loaded_tables:
|
|
1272
|
+
while table_name in self.db_manager.loaded_tables:
|
|
1424
1273
|
table_name = f"{original_name}_{counter}"
|
|
1425
1274
|
counter += 1
|
|
1426
1275
|
|
|
1427
|
-
# Register the table in
|
|
1428
|
-
self.
|
|
1276
|
+
# Register the table in the database manager
|
|
1277
|
+
self.db_manager.register_dataframe(df, table_name, file_name)
|
|
1429
1278
|
|
|
1430
1279
|
# Update tracking
|
|
1431
|
-
self.loaded_tables[table_name] = file_name
|
|
1432
|
-
self.table_columns[table_name] = df.columns.tolist()
|
|
1280
|
+
self.db_manager.loaded_tables[table_name] = file_name
|
|
1281
|
+
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1433
1282
|
|
|
1434
1283
|
# Update UI
|
|
1435
1284
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
@@ -1450,7 +1299,12 @@ LIMIT 10
|
|
|
1450
1299
|
self.statusBar().showMessage('Error exporting data')
|
|
1451
1300
|
|
|
1452
1301
|
def export_to_parquet(self):
|
|
1453
|
-
|
|
1302
|
+
# Get the current tab
|
|
1303
|
+
current_tab = self.get_current_tab()
|
|
1304
|
+
if not current_tab:
|
|
1305
|
+
return
|
|
1306
|
+
|
|
1307
|
+
if current_tab.results_table.rowCount() == 0:
|
|
1454
1308
|
QMessageBox.warning(self, "No Data", "There is no data to export.")
|
|
1455
1309
|
return
|
|
1456
1310
|
|
|
@@ -1468,21 +1322,21 @@ LIMIT 10
|
|
|
1468
1322
|
|
|
1469
1323
|
# Generate table name from file name
|
|
1470
1324
|
base_name = os.path.splitext(os.path.basename(file_name))[0]
|
|
1471
|
-
table_name = self.sanitize_table_name(base_name)
|
|
1325
|
+
table_name = self.db_manager.sanitize_table_name(base_name)
|
|
1472
1326
|
|
|
1473
1327
|
# Ensure unique table name
|
|
1474
1328
|
original_name = table_name
|
|
1475
1329
|
counter = 1
|
|
1476
|
-
while table_name in self.loaded_tables:
|
|
1330
|
+
while table_name in self.db_manager.loaded_tables:
|
|
1477
1331
|
table_name = f"{original_name}_{counter}"
|
|
1478
1332
|
counter += 1
|
|
1479
1333
|
|
|
1480
|
-
# Register the table in
|
|
1481
|
-
self.
|
|
1334
|
+
# Register the table in the database manager
|
|
1335
|
+
self.db_manager.register_dataframe(df, table_name, file_name)
|
|
1482
1336
|
|
|
1483
1337
|
# Update tracking
|
|
1484
|
-
self.loaded_tables[table_name] = file_name
|
|
1485
|
-
self.table_columns[table_name] = df.columns.tolist()
|
|
1338
|
+
self.db_manager.loaded_tables[table_name] = file_name
|
|
1339
|
+
self.db_manager.table_columns[table_name] = df.columns.tolist()
|
|
1486
1340
|
|
|
1487
1341
|
# Update UI
|
|
1488
1342
|
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_name)})")
|
|
@@ -1504,12 +1358,17 @@ LIMIT 10
|
|
|
1504
1358
|
|
|
1505
1359
|
def get_table_data_as_dataframe(self):
|
|
1506
1360
|
"""Helper function to convert table widget data to a DataFrame"""
|
|
1507
|
-
|
|
1361
|
+
# Get the current tab
|
|
1362
|
+
current_tab = self.get_current_tab()
|
|
1363
|
+
if not current_tab:
|
|
1364
|
+
return pd.DataFrame()
|
|
1365
|
+
|
|
1366
|
+
headers = [current_tab.results_table.horizontalHeaderItem(i).text() for i in range(current_tab.results_table.columnCount())]
|
|
1508
1367
|
data = []
|
|
1509
|
-
for row in range(
|
|
1368
|
+
for row in range(current_tab.results_table.rowCount()):
|
|
1510
1369
|
row_data = []
|
|
1511
|
-
for column in range(
|
|
1512
|
-
item =
|
|
1370
|
+
for column in range(current_tab.results_table.columnCount()):
|
|
1371
|
+
item = current_tab.results_table.item(row, column)
|
|
1513
1372
|
row_data.append(item.text() if item else '')
|
|
1514
1373
|
data.append(row_data)
|
|
1515
1374
|
return pd.DataFrame(data, columns=headers)
|
|
@@ -1518,12 +1377,27 @@ LIMIT 10
|
|
|
1518
1377
|
"""Handle global keyboard shortcuts"""
|
|
1519
1378
|
# Execute query with Ctrl+Enter or Cmd+Enter (for Mac)
|
|
1520
1379
|
if event.key() == Qt.Key.Key_Return and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1521
|
-
self.
|
|
1380
|
+
self.execute_query()
|
|
1522
1381
|
return
|
|
1523
1382
|
|
|
1524
|
-
#
|
|
1525
|
-
if event.key() == Qt.Key.
|
|
1526
|
-
self.
|
|
1383
|
+
# Add new tab with Ctrl+T
|
|
1384
|
+
if event.key() == Qt.Key.Key_T and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1385
|
+
self.add_tab()
|
|
1386
|
+
return
|
|
1387
|
+
|
|
1388
|
+
# Close current tab with Ctrl+W
|
|
1389
|
+
if event.key() == Qt.Key.Key_W and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1390
|
+
self.close_current_tab()
|
|
1391
|
+
return
|
|
1392
|
+
|
|
1393
|
+
# Duplicate tab with Ctrl+D
|
|
1394
|
+
if event.key() == Qt.Key.Key_D and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1395
|
+
self.duplicate_current_tab()
|
|
1396
|
+
return
|
|
1397
|
+
|
|
1398
|
+
# Rename tab with Ctrl+R
|
|
1399
|
+
if event.key() == Qt.Key.Key_R and (event.modifiers() & Qt.KeyboardModifier.ControlModifier):
|
|
1400
|
+
self.rename_current_tab()
|
|
1527
1401
|
return
|
|
1528
1402
|
|
|
1529
1403
|
super().keyPressEvent(event)
|
|
@@ -1545,12 +1419,11 @@ LIMIT 10
|
|
|
1545
1419
|
event.ignore()
|
|
1546
1420
|
return
|
|
1547
1421
|
|
|
1422
|
+
# Save window state and settings
|
|
1423
|
+
self.save_recent_projects()
|
|
1424
|
+
|
|
1548
1425
|
# Close database connections
|
|
1549
|
-
|
|
1550
|
-
if self.current_connection_type == "duckdb":
|
|
1551
|
-
self.conn.close()
|
|
1552
|
-
else: # sqlite
|
|
1553
|
-
self.conn.close()
|
|
1426
|
+
self.db_manager.close_connection()
|
|
1554
1427
|
event.accept()
|
|
1555
1428
|
except Exception as e:
|
|
1556
1429
|
QMessageBox.warning(self, "Cleanup Warning",
|
|
@@ -1560,27 +1433,54 @@ LIMIT 10
|
|
|
1560
1433
|
def has_unsaved_changes(self):
|
|
1561
1434
|
"""Check if there are unsaved changes in the project"""
|
|
1562
1435
|
if not self.current_project_file:
|
|
1563
|
-
return
|
|
1436
|
+
return (self.tab_widget.count() > 0 and any(self.tab_widget.widget(i).get_query_text().strip()
|
|
1437
|
+
for i in range(self.tab_widget.count()))) or bool(self.db_manager.loaded_tables)
|
|
1564
1438
|
|
|
1565
1439
|
try:
|
|
1566
1440
|
# Load the last saved state
|
|
1567
1441
|
with open(self.current_project_file, 'r') as f:
|
|
1568
1442
|
saved_data = json.load(f)
|
|
1569
1443
|
|
|
1444
|
+
# Prepare current tab data
|
|
1445
|
+
current_tabs_data = []
|
|
1446
|
+
for i in range(self.tab_widget.count()):
|
|
1447
|
+
tab = self.tab_widget.widget(i)
|
|
1448
|
+
tab_data = {
|
|
1449
|
+
'title': self.tab_widget.tabText(i),
|
|
1450
|
+
'query': tab.get_query_text()
|
|
1451
|
+
}
|
|
1452
|
+
current_tabs_data.append(tab_data)
|
|
1453
|
+
|
|
1570
1454
|
# Compare current state with saved state
|
|
1571
1455
|
current_data = {
|
|
1572
1456
|
'tables': {
|
|
1573
1457
|
name: {
|
|
1574
1458
|
'file_path': path,
|
|
1575
|
-
'columns': self.table_columns.get(name, [])
|
|
1459
|
+
'columns': self.db_manager.table_columns.get(name, [])
|
|
1576
1460
|
}
|
|
1577
|
-
for name, path in self.loaded_tables.items()
|
|
1461
|
+
for name, path in self.db_manager.loaded_tables.items()
|
|
1578
1462
|
},
|
|
1579
|
-
'
|
|
1580
|
-
'connection_type': self.
|
|
1463
|
+
'tabs': current_tabs_data,
|
|
1464
|
+
'connection_type': self.db_manager.connection_type
|
|
1581
1465
|
}
|
|
1582
1466
|
|
|
1583
|
-
|
|
1467
|
+
# Compare tables and connection type
|
|
1468
|
+
if (current_data['connection_type'] != saved_data.get('connection_type') or
|
|
1469
|
+
len(current_data['tables']) != len(saved_data.get('tables', {}))):
|
|
1470
|
+
return True
|
|
1471
|
+
|
|
1472
|
+
# Compare tab data
|
|
1473
|
+
if 'tabs' not in saved_data or len(current_data['tabs']) != len(saved_data['tabs']):
|
|
1474
|
+
return True
|
|
1475
|
+
|
|
1476
|
+
for i, tab_data in enumerate(current_data['tabs']):
|
|
1477
|
+
saved_tab = saved_data['tabs'][i]
|
|
1478
|
+
if (tab_data['title'] != saved_tab.get('title', '') or
|
|
1479
|
+
tab_data['query'] != saved_tab.get('query', '')):
|
|
1480
|
+
return True
|
|
1481
|
+
|
|
1482
|
+
# If we get here, everything matches
|
|
1483
|
+
return False
|
|
1584
1484
|
|
|
1585
1485
|
except Exception:
|
|
1586
1486
|
# If there's any error reading the saved file, assume there are unsaved changes
|
|
@@ -1592,6 +1492,11 @@ LIMIT 10
|
|
|
1592
1492
|
if not item:
|
|
1593
1493
|
return
|
|
1594
1494
|
|
|
1495
|
+
# Get current tab
|
|
1496
|
+
current_tab = self.get_current_tab()
|
|
1497
|
+
if not current_tab:
|
|
1498
|
+
return
|
|
1499
|
+
|
|
1595
1500
|
# Get table name without the file info in parentheses
|
|
1596
1501
|
table_name = item.text().split(' (')[0]
|
|
1597
1502
|
|
|
@@ -1615,45 +1520,79 @@ LIMIT 10
|
|
|
1615
1520
|
# Add menu actions
|
|
1616
1521
|
select_from_action = context_menu.addAction("Select from")
|
|
1617
1522
|
add_to_editor_action = context_menu.addAction("Just add to editor")
|
|
1523
|
+
context_menu.addSeparator()
|
|
1524
|
+
rename_action = context_menu.addAction("Rename table...")
|
|
1525
|
+
delete_action = context_menu.addAction("Delete table")
|
|
1526
|
+
delete_action.setIcon(QIcon.fromTheme("edit-delete"))
|
|
1618
1527
|
|
|
1619
1528
|
# Show menu and get selected action
|
|
1620
1529
|
action = context_menu.exec(self.tables_list.mapToGlobal(position))
|
|
1621
1530
|
|
|
1622
1531
|
if action == select_from_action:
|
|
1623
1532
|
# Insert "SELECT * FROM table_name" at cursor position
|
|
1624
|
-
cursor =
|
|
1533
|
+
cursor = current_tab.query_edit.textCursor()
|
|
1625
1534
|
cursor.insertText(f"SELECT * FROM {table_name}")
|
|
1626
|
-
|
|
1535
|
+
current_tab.query_edit.setFocus()
|
|
1627
1536
|
elif action == add_to_editor_action:
|
|
1628
1537
|
# Just insert the table name at cursor position
|
|
1629
|
-
cursor =
|
|
1538
|
+
cursor = current_tab.query_edit.textCursor()
|
|
1630
1539
|
cursor.insertText(table_name)
|
|
1631
|
-
|
|
1540
|
+
current_tab.query_edit.setFocus()
|
|
1541
|
+
elif action == rename_action:
|
|
1542
|
+
# Show rename dialog
|
|
1543
|
+
new_name, ok = QInputDialog.getText(
|
|
1544
|
+
self,
|
|
1545
|
+
"Rename Table",
|
|
1546
|
+
"Enter new table name:",
|
|
1547
|
+
QLineEdit.EchoMode.Normal,
|
|
1548
|
+
table_name
|
|
1549
|
+
)
|
|
1550
|
+
if ok and new_name:
|
|
1551
|
+
if self.rename_table(table_name, new_name):
|
|
1552
|
+
# Update the item text
|
|
1553
|
+
source = item.text().split(' (')[1][:-1] # Get the source part
|
|
1554
|
+
item.setText(f"{new_name} ({source})")
|
|
1555
|
+
self.statusBar().showMessage(f'Table renamed to "{new_name}"')
|
|
1556
|
+
elif action == delete_action:
|
|
1557
|
+
# Show confirmation dialog
|
|
1558
|
+
reply = QMessageBox.question(
|
|
1559
|
+
self,
|
|
1560
|
+
"Delete Table",
|
|
1561
|
+
f"Are you sure you want to delete table '{table_name}'?",
|
|
1562
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
1563
|
+
)
|
|
1564
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
1565
|
+
self.remove_selected_table()
|
|
1632
1566
|
|
|
1633
1567
|
def new_project(self):
|
|
1634
1568
|
"""Create a new project by clearing current state"""
|
|
1635
|
-
if self.
|
|
1569
|
+
if self.db_manager.is_connected():
|
|
1636
1570
|
reply = QMessageBox.question(self, 'New Project',
|
|
1637
1571
|
'Are you sure you want to start a new project? All unsaved changes will be lost.',
|
|
1638
1572
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
1639
1573
|
if reply == QMessageBox.StandardButton.Yes:
|
|
1640
1574
|
# Close existing connection
|
|
1641
|
-
|
|
1642
|
-
self.conn.close()
|
|
1643
|
-
else: # sqlite
|
|
1644
|
-
self.conn.close()
|
|
1575
|
+
self.db_manager.close_connection()
|
|
1645
1576
|
|
|
1646
1577
|
# Reset state
|
|
1647
|
-
self.conn = None
|
|
1648
|
-
self.current_connection_type = None
|
|
1649
|
-
self.loaded_tables.clear()
|
|
1650
|
-
self.table_columns.clear()
|
|
1651
1578
|
self.tables_list.clear()
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
self.
|
|
1579
|
+
|
|
1580
|
+
# Clear all tabs except one
|
|
1581
|
+
while self.tab_widget.count() > 1:
|
|
1582
|
+
self.close_tab(1) # Always close tab at index 1 to keep at least one tab
|
|
1583
|
+
|
|
1584
|
+
# Clear the remaining tab
|
|
1585
|
+
first_tab = self.get_tab_at_index(0)
|
|
1586
|
+
if first_tab:
|
|
1587
|
+
first_tab.set_query_text("")
|
|
1588
|
+
first_tab.results_table.setRowCount(0)
|
|
1589
|
+
first_tab.results_table.setColumnCount(0)
|
|
1590
|
+
first_tab.row_count_label.setText("")
|
|
1591
|
+
first_tab.results_title.setText("RESULTS")
|
|
1592
|
+
|
|
1655
1593
|
self.current_project_file = None
|
|
1656
1594
|
self.setWindowTitle('SQL Shell')
|
|
1595
|
+
self.db_info_label.setText("No database connected")
|
|
1657
1596
|
self.statusBar().showMessage('New project created')
|
|
1658
1597
|
|
|
1659
1598
|
def save_project(self):
|
|
@@ -1683,14 +1622,29 @@ LIMIT 10
|
|
|
1683
1622
|
def save_project_to_file(self, file_name):
|
|
1684
1623
|
"""Save project data to a file"""
|
|
1685
1624
|
try:
|
|
1625
|
+
# Save tab information
|
|
1626
|
+
tabs_data = []
|
|
1627
|
+
for i in range(self.tab_widget.count()):
|
|
1628
|
+
tab = self.tab_widget.widget(i)
|
|
1629
|
+
tab_data = {
|
|
1630
|
+
'title': self.tab_widget.tabText(i),
|
|
1631
|
+
'query': tab.get_query_text()
|
|
1632
|
+
}
|
|
1633
|
+
tabs_data.append(tab_data)
|
|
1634
|
+
|
|
1686
1635
|
project_data = {
|
|
1687
1636
|
'tables': {},
|
|
1688
|
-
'
|
|
1689
|
-
'connection_type': self.
|
|
1637
|
+
'tabs': tabs_data,
|
|
1638
|
+
'connection_type': self.db_manager.connection_type,
|
|
1639
|
+
'database_path': None # Initialize to None
|
|
1690
1640
|
}
|
|
1691
1641
|
|
|
1642
|
+
# If we have a database connection, save the path
|
|
1643
|
+
if self.db_manager.is_connected() and hasattr(self.db_manager, 'database_path'):
|
|
1644
|
+
project_data['database_path'] = self.db_manager.database_path
|
|
1645
|
+
|
|
1692
1646
|
# Save table information
|
|
1693
|
-
for table_name, file_path in self.loaded_tables.items():
|
|
1647
|
+
for table_name, file_path in self.db_manager.loaded_tables.items():
|
|
1694
1648
|
# For database tables and query results, store the special identifier
|
|
1695
1649
|
if file_path in ['database', 'query_result']:
|
|
1696
1650
|
source_path = file_path
|
|
@@ -1700,103 +1654,683 @@ LIMIT 10
|
|
|
1700
1654
|
|
|
1701
1655
|
project_data['tables'][table_name] = {
|
|
1702
1656
|
'file_path': source_path,
|
|
1703
|
-
'columns': self.table_columns.get(table_name, [])
|
|
1657
|
+
'columns': self.db_manager.table_columns.get(table_name, [])
|
|
1704
1658
|
}
|
|
1705
1659
|
|
|
1706
1660
|
with open(file_name, 'w') as f:
|
|
1707
1661
|
json.dump(project_data, f, indent=4)
|
|
1708
1662
|
|
|
1663
|
+
# Add to recent projects
|
|
1664
|
+
self.add_recent_project(os.path.abspath(file_name))
|
|
1665
|
+
|
|
1709
1666
|
self.statusBar().showMessage(f'Project saved to {file_name}')
|
|
1710
1667
|
|
|
1711
1668
|
except Exception as e:
|
|
1712
1669
|
QMessageBox.critical(self, "Error",
|
|
1713
1670
|
f"Failed to save project:\n\n{str(e)}")
|
|
1714
1671
|
|
|
1715
|
-
def open_project(self):
|
|
1672
|
+
def open_project(self, file_name=None):
|
|
1716
1673
|
"""Open a project file"""
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1674
|
+
if not file_name:
|
|
1675
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
1676
|
+
self,
|
|
1677
|
+
"Open Project",
|
|
1678
|
+
"",
|
|
1679
|
+
"SQL Shell Project (*.sqls);;All Files (*)"
|
|
1680
|
+
)
|
|
1723
1681
|
|
|
1724
1682
|
if file_name:
|
|
1725
1683
|
try:
|
|
1684
|
+
# Create a progress dialog to keep UI responsive
|
|
1685
|
+
progress = QProgressDialog("Loading project...", "Cancel", 0, 100, self)
|
|
1686
|
+
progress.setWindowTitle("Opening Project")
|
|
1687
|
+
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
1688
|
+
progress.setMinimumDuration(500) # Show after 500ms delay
|
|
1689
|
+
progress.setValue(0)
|
|
1690
|
+
|
|
1691
|
+
# Load project data
|
|
1726
1692
|
with open(file_name, 'r') as f:
|
|
1727
1693
|
project_data = json.load(f)
|
|
1728
1694
|
|
|
1695
|
+
# Update progress
|
|
1696
|
+
progress.setValue(10)
|
|
1697
|
+
QApplication.processEvents()
|
|
1698
|
+
|
|
1729
1699
|
# Start fresh
|
|
1730
1700
|
self.new_project()
|
|
1701
|
+
progress.setValue(15)
|
|
1702
|
+
QApplication.processEvents()
|
|
1703
|
+
|
|
1704
|
+
# Check if there's a database path in the project
|
|
1705
|
+
has_database_path = 'database_path' in project_data and project_data['database_path']
|
|
1706
|
+
has_database_tables = any(table_info.get('file_path') == 'database'
|
|
1707
|
+
for table_info in project_data.get('tables', {}).values())
|
|
1708
|
+
|
|
1709
|
+
# Set a flag to track if database tables are loaded
|
|
1710
|
+
database_tables_loaded = False
|
|
1711
|
+
|
|
1712
|
+
# If the project contains database tables and a database path, try to connect to it
|
|
1713
|
+
progress.setLabelText("Connecting to database...")
|
|
1714
|
+
if has_database_path and has_database_tables:
|
|
1715
|
+
database_path = project_data['database_path']
|
|
1716
|
+
try:
|
|
1717
|
+
if os.path.exists(database_path):
|
|
1718
|
+
# Connect to the database
|
|
1719
|
+
self.db_manager.open_database(database_path)
|
|
1720
|
+
self.db_info_label.setText(self.db_manager.get_connection_info())
|
|
1721
|
+
self.statusBar().showMessage(f"Connected to database: {database_path}")
|
|
1722
|
+
|
|
1723
|
+
# Add all database tables to the tables list
|
|
1724
|
+
for table_name, source in self.db_manager.loaded_tables.items():
|
|
1725
|
+
if source == 'database':
|
|
1726
|
+
self.tables_list.addItem(f"{table_name} (database)")
|
|
1727
|
+
|
|
1728
|
+
# Mark database tables as loaded
|
|
1729
|
+
database_tables_loaded = True
|
|
1730
|
+
else:
|
|
1731
|
+
database_tables_loaded = False
|
|
1732
|
+
QMessageBox.warning(self, "Database Not Found",
|
|
1733
|
+
f"The project's database file was not found at:\n{database_path}\n\n"
|
|
1734
|
+
"Database tables will be shown but not accessible until you reconnect to the database.")
|
|
1735
|
+
except Exception as e:
|
|
1736
|
+
database_tables_loaded = False
|
|
1737
|
+
QMessageBox.warning(self, "Database Connection Error",
|
|
1738
|
+
f"Failed to connect to the project's database:\n{str(e)}\n\n"
|
|
1739
|
+
"Database tables will be shown but not accessible until you reconnect to the database.")
|
|
1740
|
+
else:
|
|
1741
|
+
# Create connection if needed (we don't have a specific database to connect to)
|
|
1742
|
+
database_tables_loaded = False
|
|
1743
|
+
if not self.db_manager.is_connected():
|
|
1744
|
+
connection_info = self.db_manager.create_memory_connection()
|
|
1745
|
+
self.db_info_label.setText(connection_info)
|
|
1746
|
+
elif 'connection_type' in project_data and project_data['connection_type'] != self.db_manager.connection_type:
|
|
1747
|
+
# If connected but with a different database type than what was saved in the project
|
|
1748
|
+
QMessageBox.warning(self, "Database Type Mismatch",
|
|
1749
|
+
f"The project was saved with a {project_data['connection_type']} database, but you're currently using {self.db_manager.connection_type}.\n\n"
|
|
1750
|
+
"Some database-specific features may not work correctly. Consider reconnecting to the correct database type.")
|
|
1731
1751
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1752
|
+
progress.setValue(30)
|
|
1753
|
+
QApplication.processEvents()
|
|
1754
|
+
|
|
1755
|
+
# Calculate progress steps for loading tables
|
|
1756
|
+
table_count = len(project_data.get('tables', {}))
|
|
1757
|
+
table_progress_start = 30
|
|
1758
|
+
table_progress_end = 70
|
|
1759
|
+
table_progress_step = (table_progress_end - table_progress_start) / max(1, table_count)
|
|
1760
|
+
current_progress = table_progress_start
|
|
1737
1761
|
|
|
1738
1762
|
# Load tables
|
|
1739
|
-
for table_name, table_info in project_data
|
|
1763
|
+
for table_name, table_info in project_data.get('tables', {}).items():
|
|
1764
|
+
if progress.wasCanceled():
|
|
1765
|
+
break
|
|
1766
|
+
|
|
1767
|
+
progress.setLabelText(f"Loading table: {table_name}")
|
|
1740
1768
|
file_path = table_info['file_path']
|
|
1741
1769
|
try:
|
|
1742
1770
|
if file_path == 'database':
|
|
1743
|
-
#
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1771
|
+
# Skip if we already loaded database tables by connecting to the database
|
|
1772
|
+
if database_tables_loaded:
|
|
1773
|
+
continue
|
|
1774
|
+
|
|
1775
|
+
# For database tables, we need to check if the original database is connected
|
|
1776
|
+
# Don't try to SELECT from non-existent tables
|
|
1777
|
+
# Instead just register the table name for UI display
|
|
1778
|
+
self.db_manager.loaded_tables[table_name] = 'database'
|
|
1779
|
+
|
|
1780
|
+
# If we have column information, use it
|
|
1781
|
+
if 'columns' in table_info:
|
|
1782
|
+
self.db_manager.table_columns[table_name] = table_info['columns']
|
|
1783
|
+
|
|
1784
|
+
# Add to the UI list
|
|
1749
1785
|
self.tables_list.addItem(f"{table_name} (database)")
|
|
1750
1786
|
elif file_path == 'query_result':
|
|
1751
1787
|
# For tables from query results, we'll need to re-run the query
|
|
1752
1788
|
# For now, just note it as a query result table
|
|
1753
|
-
self.loaded_tables[table_name] = 'query_result'
|
|
1789
|
+
self.db_manager.loaded_tables[table_name] = 'query_result'
|
|
1754
1790
|
self.tables_list.addItem(f"{table_name} (query result)")
|
|
1755
1791
|
elif os.path.exists(file_path):
|
|
1756
|
-
#
|
|
1757
|
-
|
|
1758
|
-
df =
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
else:
|
|
1792
|
+
# Use the database manager to load the file
|
|
1793
|
+
try:
|
|
1794
|
+
loaded_table_name, df = self.db_manager.load_file(file_path)
|
|
1795
|
+
self.tables_list.addItem(f"{loaded_table_name} ({os.path.basename(file_path)})")
|
|
1796
|
+
except Exception as e:
|
|
1797
|
+
QMessageBox.warning(self, "Warning",
|
|
1798
|
+
f"Failed to load file for table {table_name}:\n{str(e)}")
|
|
1764
1799
|
continue
|
|
1765
|
-
|
|
1766
|
-
# Register the table
|
|
1767
|
-
self.conn.register(table_name, df)
|
|
1768
|
-
self.loaded_tables[table_name] = file_path
|
|
1769
|
-
self.tables_list.addItem(f"{table_name} ({os.path.basename(file_path)})")
|
|
1770
1800
|
else:
|
|
1771
1801
|
QMessageBox.warning(self, "Warning",
|
|
1772
1802
|
f"Could not find file for table {table_name}: {file_path}")
|
|
1773
1803
|
continue
|
|
1774
1804
|
|
|
1775
|
-
# Store the columns
|
|
1776
|
-
self.table_columns[table_name] = table_info['columns']
|
|
1777
|
-
|
|
1778
1805
|
except Exception as e:
|
|
1779
1806
|
QMessageBox.warning(self, "Warning",
|
|
1780
1807
|
f"Failed to load table {table_name}:\n{str(e)}")
|
|
1781
1808
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1809
|
+
# Update progress for this table
|
|
1810
|
+
current_progress += table_progress_step
|
|
1811
|
+
progress.setValue(int(current_progress))
|
|
1812
|
+
QApplication.processEvents() # Keep UI responsive
|
|
1813
|
+
|
|
1814
|
+
# If the project had database tables but we couldn't connect automatically, notify the user
|
|
1815
|
+
if has_database_tables and not database_tables_loaded:
|
|
1816
|
+
QMessageBox.information(self, "Database Connection Required",
|
|
1817
|
+
"This project contains database tables. You need to reconnect to the database to use them.\n\n"
|
|
1818
|
+
"Use the 'Open Database' button to connect to your database file.")
|
|
1819
|
+
|
|
1820
|
+
# Check if the operation was canceled
|
|
1821
|
+
if progress.wasCanceled():
|
|
1822
|
+
self.statusBar().showMessage("Project loading was canceled")
|
|
1823
|
+
progress.close()
|
|
1824
|
+
return
|
|
1825
|
+
|
|
1826
|
+
progress.setValue(75)
|
|
1827
|
+
progress.setLabelText("Setting up tabs...")
|
|
1828
|
+
QApplication.processEvents()
|
|
1829
|
+
|
|
1830
|
+
# Load tabs in a more efficient way
|
|
1831
|
+
if 'tabs' in project_data and project_data['tabs']:
|
|
1832
|
+
try:
|
|
1833
|
+
# Temporarily disable signals
|
|
1834
|
+
self.tab_widget.blockSignals(True)
|
|
1835
|
+
|
|
1836
|
+
# First, pre-remove any existing tabs
|
|
1837
|
+
while self.tab_widget.count() > 0:
|
|
1838
|
+
widget = self.tab_widget.widget(0)
|
|
1839
|
+
self.tab_widget.removeTab(0)
|
|
1840
|
+
if widget in self.tabs:
|
|
1841
|
+
self.tabs.remove(widget)
|
|
1842
|
+
widget.deleteLater()
|
|
1843
|
+
|
|
1844
|
+
# Then create all tab widgets at once (empty)
|
|
1845
|
+
tab_count = len(project_data['tabs'])
|
|
1846
|
+
tab_progress_step = 15 / max(1, tab_count)
|
|
1847
|
+
progress.setValue(80)
|
|
1848
|
+
QApplication.processEvents()
|
|
1849
|
+
|
|
1850
|
+
# Create all tab widgets first without setting content
|
|
1851
|
+
for i, tab_data in enumerate(project_data['tabs']):
|
|
1852
|
+
# Create a new tab
|
|
1853
|
+
tab = QueryTab(self)
|
|
1854
|
+
self.tabs.append(tab)
|
|
1855
|
+
|
|
1856
|
+
# Add to tab widget
|
|
1857
|
+
title = tab_data.get('title', f'Query {i+1}')
|
|
1858
|
+
self.tab_widget.addTab(tab, title)
|
|
1859
|
+
|
|
1860
|
+
progress.setValue(int(80 + i * tab_progress_step/2))
|
|
1861
|
+
QApplication.processEvents()
|
|
1862
|
+
|
|
1863
|
+
# Now set the content for each tab
|
|
1864
|
+
for i, tab_data in enumerate(project_data['tabs']):
|
|
1865
|
+
# Get the tab and set its query text
|
|
1866
|
+
tab = self.tab_widget.widget(i)
|
|
1867
|
+
if tab and 'query' in tab_data:
|
|
1868
|
+
tab.set_query_text(tab_data['query'])
|
|
1869
|
+
|
|
1870
|
+
progress.setValue(int(87 + i * tab_progress_step/2))
|
|
1871
|
+
QApplication.processEvents()
|
|
1872
|
+
|
|
1873
|
+
# Re-enable signals
|
|
1874
|
+
self.tab_widget.blockSignals(False)
|
|
1875
|
+
|
|
1876
|
+
# Set current tab
|
|
1877
|
+
if self.tab_widget.count() > 0:
|
|
1878
|
+
self.tab_widget.setCurrentIndex(0)
|
|
1879
|
+
|
|
1880
|
+
except Exception as e:
|
|
1881
|
+
# If there's an error, ensure we restore signals
|
|
1882
|
+
self.tab_widget.blockSignals(False)
|
|
1883
|
+
self.statusBar().showMessage(f"Error loading tabs: {str(e)}")
|
|
1884
|
+
# Create a single default tab if all fails
|
|
1885
|
+
if self.tab_widget.count() == 0:
|
|
1886
|
+
self.add_tab()
|
|
1887
|
+
else:
|
|
1888
|
+
# Create default tab if no tabs in project
|
|
1889
|
+
self.add_tab()
|
|
1890
|
+
|
|
1891
|
+
progress.setValue(90)
|
|
1892
|
+
progress.setLabelText("Finishing up...")
|
|
1893
|
+
QApplication.processEvents()
|
|
1785
1894
|
|
|
1786
1895
|
# Update UI
|
|
1787
1896
|
self.current_project_file = file_name
|
|
1788
1897
|
self.setWindowTitle(f'SQL Shell - {os.path.basename(file_name)}')
|
|
1898
|
+
|
|
1899
|
+
# Add to recent projects
|
|
1900
|
+
self.add_recent_project(os.path.abspath(file_name))
|
|
1901
|
+
|
|
1902
|
+
# Defer the auto-completer update to after loading is complete
|
|
1903
|
+
# This helps prevent UI freezing during project loading
|
|
1904
|
+
progress.setValue(95)
|
|
1905
|
+
QApplication.processEvents()
|
|
1906
|
+
|
|
1907
|
+
# Use a timer to update the completer after the UI is responsive
|
|
1908
|
+
complete_timer = QTimer()
|
|
1909
|
+
complete_timer.setSingleShot(True)
|
|
1910
|
+
complete_timer.timeout.connect(self.update_completer)
|
|
1911
|
+
complete_timer.start(100) # Short delay before updating completer
|
|
1912
|
+
|
|
1913
|
+
# Queue another update for reliability - sometimes the first update might not fully complete
|
|
1914
|
+
failsafe_timer = QTimer()
|
|
1915
|
+
failsafe_timer.setSingleShot(True)
|
|
1916
|
+
failsafe_timer.timeout.connect(self.update_completer)
|
|
1917
|
+
failsafe_timer.start(2000) # Try again after 2 seconds to ensure completion is loaded
|
|
1918
|
+
|
|
1919
|
+
progress.setValue(100)
|
|
1920
|
+
QApplication.processEvents()
|
|
1921
|
+
|
|
1789
1922
|
self.statusBar().showMessage(f'Project loaded from {file_name}')
|
|
1790
|
-
|
|
1923
|
+
progress.close()
|
|
1791
1924
|
|
|
1792
1925
|
except Exception as e:
|
|
1793
1926
|
QMessageBox.critical(self, "Error",
|
|
1794
1927
|
f"Failed to open project:\n\n{str(e)}")
|
|
1795
1928
|
|
|
1929
|
+
def rename_table(self, old_name, new_name):
|
|
1930
|
+
"""Rename a table in the database and update tracking"""
|
|
1931
|
+
try:
|
|
1932
|
+
# Use the database manager to rename the table
|
|
1933
|
+
result = self.db_manager.rename_table(old_name, new_name)
|
|
1934
|
+
|
|
1935
|
+
if result:
|
|
1936
|
+
# Update completer
|
|
1937
|
+
self.update_completer()
|
|
1938
|
+
return True
|
|
1939
|
+
|
|
1940
|
+
return False
|
|
1941
|
+
|
|
1942
|
+
except Exception as e:
|
|
1943
|
+
QMessageBox.critical(self, "Error", f"Failed to rename table:\n\n{str(e)}")
|
|
1944
|
+
return False
|
|
1945
|
+
|
|
1946
|
+
def load_recent_projects(self):
|
|
1947
|
+
"""Load recent projects from settings file"""
|
|
1948
|
+
try:
|
|
1949
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
1950
|
+
if os.path.exists(settings_file):
|
|
1951
|
+
with open(settings_file, 'r') as f:
|
|
1952
|
+
settings = json.load(f)
|
|
1953
|
+
self.recent_projects = settings.get('recent_projects', [])
|
|
1954
|
+
|
|
1955
|
+
# Load window settings if available
|
|
1956
|
+
window_settings = settings.get('window', {})
|
|
1957
|
+
if window_settings:
|
|
1958
|
+
self.restore_window_state(window_settings)
|
|
1959
|
+
except Exception:
|
|
1960
|
+
self.recent_projects = []
|
|
1961
|
+
|
|
1962
|
+
def save_recent_projects(self):
|
|
1963
|
+
"""Save recent projects to settings file"""
|
|
1964
|
+
try:
|
|
1965
|
+
settings_file = os.path.join(os.path.expanduser('~'), '.sqlshell_settings.json')
|
|
1966
|
+
settings = {}
|
|
1967
|
+
if os.path.exists(settings_file):
|
|
1968
|
+
with open(settings_file, 'r') as f:
|
|
1969
|
+
settings = json.load(f)
|
|
1970
|
+
settings['recent_projects'] = self.recent_projects
|
|
1971
|
+
|
|
1972
|
+
# Save window settings
|
|
1973
|
+
window_settings = self.save_window_state()
|
|
1974
|
+
settings['window'] = window_settings
|
|
1975
|
+
|
|
1976
|
+
with open(settings_file, 'w') as f:
|
|
1977
|
+
json.dump(settings, f, indent=4)
|
|
1978
|
+
except Exception as e:
|
|
1979
|
+
print(f"Error saving recent projects: {e}")
|
|
1980
|
+
|
|
1981
|
+
def save_window_state(self):
|
|
1982
|
+
"""Save current window state"""
|
|
1983
|
+
window_settings = {
|
|
1984
|
+
'maximized': self.isMaximized(),
|
|
1985
|
+
'geometry': {
|
|
1986
|
+
'x': self.geometry().x(),
|
|
1987
|
+
'y': self.geometry().y(),
|
|
1988
|
+
'width': self.geometry().width(),
|
|
1989
|
+
'height': self.geometry().height()
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return window_settings
|
|
1993
|
+
|
|
1994
|
+
def restore_window_state(self, window_settings):
|
|
1995
|
+
"""Restore window state from settings"""
|
|
1996
|
+
try:
|
|
1997
|
+
# Check if we have valid geometry settings
|
|
1998
|
+
geometry = window_settings.get('geometry', {})
|
|
1999
|
+
if all(key in geometry for key in ['x', 'y', 'width', 'height']):
|
|
2000
|
+
x, y = geometry['x'], geometry['y']
|
|
2001
|
+
width, height = geometry['width'], geometry['height']
|
|
2002
|
+
|
|
2003
|
+
# Ensure the window is visible on the current screen
|
|
2004
|
+
screen = QApplication.primaryScreen()
|
|
2005
|
+
screen_geometry = screen.availableGeometry()
|
|
2006
|
+
|
|
2007
|
+
# Adjust if window would be off-screen
|
|
2008
|
+
if x < 0 or x + 100 > screen_geometry.width():
|
|
2009
|
+
x = 100
|
|
2010
|
+
if y < 0 or y + 100 > screen_geometry.height():
|
|
2011
|
+
y = 100
|
|
2012
|
+
|
|
2013
|
+
# Adjust if window is too large for the current screen
|
|
2014
|
+
if width > screen_geometry.width():
|
|
2015
|
+
width = int(screen_geometry.width() * 0.85)
|
|
2016
|
+
if height > screen_geometry.height():
|
|
2017
|
+
height = int(screen_geometry.height() * 0.85)
|
|
2018
|
+
|
|
2019
|
+
self.setGeometry(x, y, width, height)
|
|
2020
|
+
|
|
2021
|
+
# Set maximized state if needed
|
|
2022
|
+
if window_settings.get('maximized', False):
|
|
2023
|
+
self.showMaximized()
|
|
2024
|
+
self.was_maximized = True
|
|
2025
|
+
|
|
2026
|
+
except Exception as e:
|
|
2027
|
+
print(f"Error restoring window state: {e}")
|
|
2028
|
+
# Fall back to default geometry
|
|
2029
|
+
screen = QApplication.primaryScreen()
|
|
2030
|
+
screen_geometry = screen.availableGeometry()
|
|
2031
|
+
self.setGeometry(100, 100,
|
|
2032
|
+
min(1400, int(screen_geometry.width() * 0.85)),
|
|
2033
|
+
min(800, int(screen_geometry.height() * 0.85)))
|
|
2034
|
+
|
|
2035
|
+
def add_recent_project(self, project_path):
|
|
2036
|
+
"""Add a project to recent projects list"""
|
|
2037
|
+
if project_path in self.recent_projects:
|
|
2038
|
+
self.recent_projects.remove(project_path)
|
|
2039
|
+
self.recent_projects.insert(0, project_path)
|
|
2040
|
+
self.recent_projects = self.recent_projects[:self.max_recent_projects]
|
|
2041
|
+
self.save_recent_projects()
|
|
2042
|
+
self.update_recent_projects_menu()
|
|
2043
|
+
|
|
2044
|
+
def update_recent_projects_menu(self):
|
|
2045
|
+
"""Update the recent projects menu"""
|
|
2046
|
+
self.recent_projects_menu.clear()
|
|
2047
|
+
|
|
2048
|
+
if not self.recent_projects:
|
|
2049
|
+
no_recent = self.recent_projects_menu.addAction("No Recent Projects")
|
|
2050
|
+
no_recent.setEnabled(False)
|
|
2051
|
+
return
|
|
2052
|
+
|
|
2053
|
+
for project_path in self.recent_projects:
|
|
2054
|
+
if os.path.exists(project_path):
|
|
2055
|
+
action = self.recent_projects_menu.addAction(os.path.basename(project_path))
|
|
2056
|
+
action.setData(project_path)
|
|
2057
|
+
action.triggered.connect(lambda checked, path=project_path: self.open_recent_project(path))
|
|
2058
|
+
|
|
2059
|
+
if self.recent_projects:
|
|
2060
|
+
self.recent_projects_menu.addSeparator()
|
|
2061
|
+
clear_action = self.recent_projects_menu.addAction("Clear Recent Projects")
|
|
2062
|
+
clear_action.triggered.connect(self.clear_recent_projects)
|
|
2063
|
+
|
|
2064
|
+
def open_recent_project(self, project_path):
|
|
2065
|
+
"""Open a project from the recent projects list"""
|
|
2066
|
+
if os.path.exists(project_path):
|
|
2067
|
+
self.current_project_file = project_path
|
|
2068
|
+
self.open_project(project_path)
|
|
2069
|
+
else:
|
|
2070
|
+
QMessageBox.warning(self, "Warning",
|
|
2071
|
+
f"Project file not found:\n{project_path}")
|
|
2072
|
+
self.recent_projects.remove(project_path)
|
|
2073
|
+
self.save_recent_projects()
|
|
2074
|
+
self.update_recent_projects_menu()
|
|
2075
|
+
|
|
2076
|
+
def clear_recent_projects(self):
|
|
2077
|
+
"""Clear the list of recent projects"""
|
|
2078
|
+
self.recent_projects.clear()
|
|
2079
|
+
self.save_recent_projects()
|
|
2080
|
+
self.update_recent_projects_menu()
|
|
2081
|
+
|
|
2082
|
+
def add_tab(self, title="Query 1"):
|
|
2083
|
+
"""Add a new query tab"""
|
|
2084
|
+
# Ensure title is a string
|
|
2085
|
+
title = str(title)
|
|
2086
|
+
|
|
2087
|
+
# Create a new tab with a unique name if needed
|
|
2088
|
+
if title == "Query 1" and self.tab_widget.count() > 0:
|
|
2089
|
+
# Generate a unique tab name (Query 2, Query 3, etc.)
|
|
2090
|
+
# Use a more efficient approach to find a unique name
|
|
2091
|
+
base_name = "Query"
|
|
2092
|
+
existing_names = set()
|
|
2093
|
+
|
|
2094
|
+
# Collect existing tab names first (more efficient than checking each time)
|
|
2095
|
+
for i in range(self.tab_widget.count()):
|
|
2096
|
+
existing_names.add(self.tab_widget.tabText(i))
|
|
2097
|
+
|
|
2098
|
+
# Find the next available number
|
|
2099
|
+
counter = 1
|
|
2100
|
+
while f"{base_name} {counter}" in existing_names:
|
|
2101
|
+
counter += 1
|
|
2102
|
+
title = f"{base_name} {counter}"
|
|
2103
|
+
|
|
2104
|
+
# Create the tab content
|
|
2105
|
+
tab = QueryTab(self)
|
|
2106
|
+
|
|
2107
|
+
# Add to our list of tabs
|
|
2108
|
+
self.tabs.append(tab)
|
|
2109
|
+
|
|
2110
|
+
# Block signals temporarily to improve performance when adding many tabs
|
|
2111
|
+
was_blocked = self.tab_widget.blockSignals(True)
|
|
2112
|
+
|
|
2113
|
+
# Add tab to widget
|
|
2114
|
+
index = self.tab_widget.addTab(tab, title)
|
|
2115
|
+
self.tab_widget.setCurrentIndex(index)
|
|
2116
|
+
|
|
2117
|
+
# Restore signals
|
|
2118
|
+
self.tab_widget.blockSignals(was_blocked)
|
|
2119
|
+
|
|
2120
|
+
# Focus the new tab's query editor
|
|
2121
|
+
tab.query_edit.setFocus()
|
|
2122
|
+
|
|
2123
|
+
# Process events to keep UI responsive
|
|
2124
|
+
QApplication.processEvents()
|
|
2125
|
+
|
|
2126
|
+
return tab
|
|
2127
|
+
|
|
2128
|
+
def duplicate_current_tab(self):
|
|
2129
|
+
"""Duplicate the current tab"""
|
|
2130
|
+
if self.tab_widget.count() == 0:
|
|
2131
|
+
return self.add_tab()
|
|
2132
|
+
|
|
2133
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2134
|
+
if current_idx == -1:
|
|
2135
|
+
return
|
|
2136
|
+
|
|
2137
|
+
# Get current tab data
|
|
2138
|
+
current_tab = self.get_current_tab()
|
|
2139
|
+
current_title = self.tab_widget.tabText(current_idx)
|
|
2140
|
+
|
|
2141
|
+
# Create a new tab with "(Copy)" suffix
|
|
2142
|
+
new_title = f"{current_title} (Copy)"
|
|
2143
|
+
new_tab = self.add_tab(new_title)
|
|
2144
|
+
|
|
2145
|
+
# Copy query text
|
|
2146
|
+
new_tab.set_query_text(current_tab.get_query_text())
|
|
2147
|
+
|
|
2148
|
+
# Return focus to the new tab
|
|
2149
|
+
new_tab.query_edit.setFocus()
|
|
2150
|
+
|
|
2151
|
+
return new_tab
|
|
2152
|
+
|
|
2153
|
+
def rename_current_tab(self):
|
|
2154
|
+
"""Rename the current tab"""
|
|
2155
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2156
|
+
if current_idx == -1:
|
|
2157
|
+
return
|
|
2158
|
+
|
|
2159
|
+
current_title = self.tab_widget.tabText(current_idx)
|
|
2160
|
+
|
|
2161
|
+
new_title, ok = QInputDialog.getText(
|
|
2162
|
+
self,
|
|
2163
|
+
"Rename Tab",
|
|
2164
|
+
"Enter new tab name:",
|
|
2165
|
+
QLineEdit.EchoMode.Normal,
|
|
2166
|
+
current_title
|
|
2167
|
+
)
|
|
2168
|
+
|
|
2169
|
+
if ok and new_title:
|
|
2170
|
+
self.tab_widget.setTabText(current_idx, new_title)
|
|
2171
|
+
|
|
2172
|
+
def handle_tab_double_click(self, index):
|
|
2173
|
+
"""Handle double-clicking on a tab by starting rename immediately"""
|
|
2174
|
+
if index == -1:
|
|
2175
|
+
return
|
|
2176
|
+
|
|
2177
|
+
current_title = self.tab_widget.tabText(index)
|
|
2178
|
+
|
|
2179
|
+
new_title, ok = QInputDialog.getText(
|
|
2180
|
+
self,
|
|
2181
|
+
"Rename Tab",
|
|
2182
|
+
"Enter new tab name:",
|
|
2183
|
+
QLineEdit.EchoMode.Normal,
|
|
2184
|
+
current_title
|
|
2185
|
+
)
|
|
2186
|
+
|
|
2187
|
+
if ok and new_title:
|
|
2188
|
+
self.tab_widget.setTabText(index, new_title)
|
|
2189
|
+
|
|
2190
|
+
def close_tab(self, index):
|
|
2191
|
+
"""Close the tab at the given index"""
|
|
2192
|
+
if self.tab_widget.count() <= 1:
|
|
2193
|
+
# Don't close the last tab, just clear it
|
|
2194
|
+
tab = self.get_tab_at_index(index)
|
|
2195
|
+
if tab:
|
|
2196
|
+
tab.set_query_text("")
|
|
2197
|
+
tab.results_table.clearContents()
|
|
2198
|
+
tab.results_table.setRowCount(0)
|
|
2199
|
+
tab.results_table.setColumnCount(0)
|
|
2200
|
+
return
|
|
2201
|
+
|
|
2202
|
+
# Block signals temporarily to improve performance when removing multiple tabs
|
|
2203
|
+
was_blocked = self.tab_widget.blockSignals(True)
|
|
2204
|
+
|
|
2205
|
+
# Remove the tab
|
|
2206
|
+
widget = self.tab_widget.widget(index)
|
|
2207
|
+
self.tab_widget.removeTab(index)
|
|
2208
|
+
|
|
2209
|
+
# Restore signals
|
|
2210
|
+
self.tab_widget.blockSignals(was_blocked)
|
|
2211
|
+
|
|
2212
|
+
# Remove from our list of tabs
|
|
2213
|
+
if widget in self.tabs:
|
|
2214
|
+
self.tabs.remove(widget)
|
|
2215
|
+
|
|
2216
|
+
# Schedule the widget for deletion instead of immediate deletion
|
|
2217
|
+
widget.deleteLater()
|
|
2218
|
+
|
|
2219
|
+
# Process events to keep UI responsive
|
|
2220
|
+
QApplication.processEvents()
|
|
2221
|
+
|
|
2222
|
+
def close_current_tab(self):
|
|
2223
|
+
"""Close the current tab"""
|
|
2224
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2225
|
+
if current_idx != -1:
|
|
2226
|
+
self.close_tab(current_idx)
|
|
2227
|
+
|
|
2228
|
+
def get_current_tab(self):
|
|
2229
|
+
"""Get the currently active tab"""
|
|
2230
|
+
current_idx = self.tab_widget.currentIndex()
|
|
2231
|
+
if current_idx == -1:
|
|
2232
|
+
return None
|
|
2233
|
+
return self.tab_widget.widget(current_idx)
|
|
2234
|
+
|
|
2235
|
+
def get_tab_at_index(self, index):
|
|
2236
|
+
"""Get the tab at the specified index"""
|
|
2237
|
+
if index < 0 or index >= self.tab_widget.count():
|
|
2238
|
+
return None
|
|
2239
|
+
return self.tab_widget.widget(index)
|
|
2240
|
+
|
|
2241
|
+
def toggle_maximize_window(self):
|
|
2242
|
+
"""Toggle between maximized and normal window state"""
|
|
2243
|
+
if self.isMaximized():
|
|
2244
|
+
self.showNormal()
|
|
2245
|
+
self.was_maximized = False
|
|
2246
|
+
else:
|
|
2247
|
+
self.showMaximized()
|
|
2248
|
+
self.was_maximized = True
|
|
2249
|
+
|
|
2250
|
+
def change_zoom(self, factor):
|
|
2251
|
+
"""Change the zoom level of the application by adjusting font sizes"""
|
|
2252
|
+
try:
|
|
2253
|
+
# Update font sizes for SQL editors
|
|
2254
|
+
for i in range(self.tab_widget.count()):
|
|
2255
|
+
tab = self.tab_widget.widget(i)
|
|
2256
|
+
if hasattr(tab, 'query_edit'):
|
|
2257
|
+
# Get current font
|
|
2258
|
+
current_font = tab.query_edit.font()
|
|
2259
|
+
current_size = current_font.pointSizeF()
|
|
2260
|
+
|
|
2261
|
+
# Calculate new size with limits to prevent too small/large fonts
|
|
2262
|
+
new_size = current_size * factor
|
|
2263
|
+
if 6 <= new_size <= 72: # Reasonable limits
|
|
2264
|
+
current_font.setPointSizeF(new_size)
|
|
2265
|
+
tab.query_edit.setFont(current_font)
|
|
2266
|
+
|
|
2267
|
+
# Also update the line number area
|
|
2268
|
+
tab.query_edit.update_line_number_area_width(0)
|
|
2269
|
+
|
|
2270
|
+
# Update results table font if needed
|
|
2271
|
+
if hasattr(tab, 'results_table'):
|
|
2272
|
+
table_font = tab.results_table.font()
|
|
2273
|
+
table_size = table_font.pointSizeF()
|
|
2274
|
+
new_table_size = table_size * factor
|
|
2275
|
+
|
|
2276
|
+
if 6 <= new_table_size <= 72:
|
|
2277
|
+
table_font.setPointSizeF(new_table_size)
|
|
2278
|
+
tab.results_table.setFont(table_font)
|
|
2279
|
+
# Resize rows and columns to fit new font size
|
|
2280
|
+
tab.results_table.resizeColumnsToContents()
|
|
2281
|
+
tab.results_table.resizeRowsToContents()
|
|
2282
|
+
|
|
2283
|
+
# Update status bar
|
|
2284
|
+
self.statusBar().showMessage(f"Zoom level adjusted to {int(current_size * factor)}", 2000)
|
|
2285
|
+
|
|
2286
|
+
except Exception as e:
|
|
2287
|
+
self.statusBar().showMessage(f"Error adjusting zoom: {str(e)}", 2000)
|
|
2288
|
+
|
|
2289
|
+
def reset_zoom(self):
|
|
2290
|
+
"""Reset zoom level to default"""
|
|
2291
|
+
try:
|
|
2292
|
+
# Default font sizes
|
|
2293
|
+
sql_editor_size = 12
|
|
2294
|
+
table_size = 10
|
|
2295
|
+
|
|
2296
|
+
# Update all tabs
|
|
2297
|
+
for i in range(self.tab_widget.count()):
|
|
2298
|
+
tab = self.tab_widget.widget(i)
|
|
2299
|
+
|
|
2300
|
+
# Reset editor font
|
|
2301
|
+
if hasattr(tab, 'query_edit'):
|
|
2302
|
+
editor_font = tab.query_edit.font()
|
|
2303
|
+
editor_font.setPointSizeF(sql_editor_size)
|
|
2304
|
+
tab.query_edit.setFont(editor_font)
|
|
2305
|
+
tab.query_edit.update_line_number_area_width(0)
|
|
2306
|
+
|
|
2307
|
+
# Reset table font
|
|
2308
|
+
if hasattr(tab, 'results_table'):
|
|
2309
|
+
table_font = tab.results_table.font()
|
|
2310
|
+
table_font.setPointSizeF(table_size)
|
|
2311
|
+
tab.results_table.setFont(table_font)
|
|
2312
|
+
tab.results_table.resizeColumnsToContents()
|
|
2313
|
+
tab.results_table.resizeRowsToContents()
|
|
2314
|
+
|
|
2315
|
+
self.statusBar().showMessage("Zoom level reset to default", 2000)
|
|
2316
|
+
|
|
2317
|
+
except Exception as e:
|
|
2318
|
+
self.statusBar().showMessage(f"Error resetting zoom: {str(e)}", 2000)
|
|
2319
|
+
|
|
1796
2320
|
def main():
|
|
1797
2321
|
app = QApplication(sys.argv)
|
|
1798
2322
|
app.setStyle(QStyleFactory.create('Fusion'))
|
|
1799
2323
|
|
|
2324
|
+
# Set application icon
|
|
2325
|
+
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icon.png")
|
|
2326
|
+
if os.path.exists(icon_path):
|
|
2327
|
+
app.setWindowIcon(QIcon(icon_path))
|
|
2328
|
+
else:
|
|
2329
|
+
# Fallback to the main logo if the icon isn't found
|
|
2330
|
+
main_logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "sqlshell_logo.png")
|
|
2331
|
+
if os.path.exists(main_logo_path):
|
|
2332
|
+
app.setWindowIcon(QIcon(main_logo_path))
|
|
2333
|
+
|
|
1800
2334
|
# Ensure we have a valid working directory with pool.db
|
|
1801
2335
|
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1802
2336
|
working_dir = os.getcwd()
|
|
@@ -1812,22 +2346,71 @@ def main():
|
|
|
1812
2346
|
if os.path.exists(package_db):
|
|
1813
2347
|
shutil.copy2(package_db, working_dir)
|
|
1814
2348
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2349
|
+
try:
|
|
2350
|
+
# Show splash screen
|
|
2351
|
+
splash = AnimatedSplashScreen()
|
|
2352
|
+
splash.show()
|
|
2353
|
+
|
|
2354
|
+
# Process events immediately to ensure the splash screen appears
|
|
2355
|
+
app.processEvents()
|
|
2356
|
+
|
|
2357
|
+
# Create main window but don't show it yet
|
|
2358
|
+
print("Initializing main application...")
|
|
2359
|
+
window = SQLShell()
|
|
2360
|
+
|
|
2361
|
+
# Define the function to show main window and hide splash
|
|
2362
|
+
def show_main_window():
|
|
2363
|
+
# Properly finish the splash screen
|
|
2364
|
+
if splash:
|
|
2365
|
+
splash.finish(window)
|
|
2366
|
+
|
|
2367
|
+
# Show the main window
|
|
2368
|
+
window.show()
|
|
2369
|
+
timer.stop()
|
|
2370
|
+
|
|
2371
|
+
# Also stop the failsafe timer if it's still running
|
|
2372
|
+
if failsafe_timer.isActive():
|
|
2373
|
+
failsafe_timer.stop()
|
|
2374
|
+
|
|
2375
|
+
print("Main application started")
|
|
2376
|
+
|
|
2377
|
+
# Create a failsafe timer in case the splash screen fails to show
|
|
2378
|
+
def failsafe_show_window():
|
|
2379
|
+
if not window.isVisible():
|
|
2380
|
+
print("Failsafe timer activated - showing main window")
|
|
2381
|
+
if splash:
|
|
2382
|
+
try:
|
|
2383
|
+
# First try to use the proper finish method
|
|
2384
|
+
splash.finish(window)
|
|
2385
|
+
except Exception as e:
|
|
2386
|
+
print(f"Error in failsafe finish: {e}")
|
|
2387
|
+
try:
|
|
2388
|
+
# Fall back to direct close if finish fails
|
|
2389
|
+
splash.close()
|
|
2390
|
+
except Exception:
|
|
2391
|
+
pass
|
|
2392
|
+
window.show()
|
|
2393
|
+
|
|
2394
|
+
# Create and show main window after delay
|
|
2395
|
+
timer = QTimer()
|
|
2396
|
+
timer.setSingleShot(True) # Ensure it only fires once
|
|
2397
|
+
timer.timeout.connect(show_main_window)
|
|
2398
|
+
timer.start(2000) # 2 second delay
|
|
2399
|
+
|
|
2400
|
+
# Failsafe timer - show the main window after 5 seconds even if splash screen fails
|
|
2401
|
+
failsafe_timer = QTimer()
|
|
2402
|
+
failsafe_timer.setSingleShot(True)
|
|
2403
|
+
failsafe_timer.timeout.connect(failsafe_show_window)
|
|
2404
|
+
failsafe_timer.start(5000) # 5 second delay
|
|
2405
|
+
|
|
2406
|
+
sys.exit(app.exec())
|
|
2407
|
+
|
|
2408
|
+
except Exception as e:
|
|
2409
|
+
print(f"Error during startup: {e}")
|
|
2410
|
+
# If there's any error with the splash screen, just show the main window directly
|
|
2411
|
+
window = SQLShell()
|
|
1826
2412
|
window.show()
|
|
1827
|
-
|
|
1828
|
-
timer.stop()
|
|
1829
|
-
|
|
1830
|
-
sys.exit(app.exec())
|
|
2413
|
+
sys.exit(app.exec())
|
|
1831
2414
|
|
|
1832
2415
|
if __name__ == '__main__':
|
|
1833
2416
|
main()
|