sqlshell 0.4.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlshell/__init__.py +84 -0
- sqlshell/__main__.py +4926 -0
- sqlshell/ai_autocomplete.py +392 -0
- sqlshell/ai_settings_dialog.py +337 -0
- sqlshell/context_suggester.py +768 -0
- sqlshell/create_test_data.py +152 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/db/__init__.py +6 -0
- sqlshell/db/database_manager.py +1318 -0
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor.py +1166 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/menus.py +262 -0
- sqlshell/notification_manager.py +370 -0
- sqlshell/query_tab.py +904 -0
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/space_invaders.py +501 -0
- sqlshell/splash_screen.py +405 -0
- sqlshell/sqlshell/__init__.py +5 -0
- sqlshell/sqlshell/create_test_data.py +118 -0
- sqlshell/sqlshell/create_test_databases.py +96 -0
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +330 -0
- sqlshell/syntax_highlighter.py +124 -0
- sqlshell/table_list.py +996 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +469 -0
- sqlshell/utils/__init__.py +16 -0
- sqlshell/utils/profile_cn2.py +1661 -0
- sqlshell/utils/profile_column.py +2635 -0
- sqlshell/utils/profile_distributions.py +616 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_foreign_keys.py +779 -0
- sqlshell/utils/profile_keys.py +2834 -0
- sqlshell/utils/profile_ohe.py +934 -0
- sqlshell/utils/profile_ohe_advanced.py +754 -0
- sqlshell/utils/profile_ohe_comparison.py +237 -0
- sqlshell/utils/profile_prediction.py +926 -0
- sqlshell/utils/profile_similarity.py +876 -0
- sqlshell/utils/search_in_df.py +90 -0
- sqlshell/widgets.py +400 -0
- sqlshell-0.4.4.dist-info/METADATA +441 -0
- sqlshell-0.4.4.dist-info/RECORD +54 -0
- sqlshell-0.4.4.dist-info/WHEEL +5 -0
- sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
- sqlshell-0.4.4.dist-info/top_level.txt +1 -0
sqlshell/menus.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Menu creation and management for SQLShell application.
|
|
3
|
+
This module contains functions to create and manage the application's menus.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_version():
|
|
10
|
+
"""Get the application version from pyproject.toml or __init__.py."""
|
|
11
|
+
try:
|
|
12
|
+
from sqlshell import __version__
|
|
13
|
+
return __version__
|
|
14
|
+
except ImportError:
|
|
15
|
+
return "0.3.3"
|
|
16
|
+
|
|
17
|
+
def create_file_menu(main_window):
|
|
18
|
+
"""Create the File menu with project management actions.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
main_window: The SQLShell main window instance
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The created File menu
|
|
25
|
+
"""
|
|
26
|
+
# Create File menu
|
|
27
|
+
file_menu = main_window.menuBar().addMenu('&File')
|
|
28
|
+
|
|
29
|
+
# Project management actions
|
|
30
|
+
new_project_action = file_menu.addAction('New Project')
|
|
31
|
+
new_project_action.setShortcut('Ctrl+N')
|
|
32
|
+
new_project_action.triggered.connect(main_window.new_project)
|
|
33
|
+
|
|
34
|
+
open_project_action = file_menu.addAction('Open Project...')
|
|
35
|
+
open_project_action.setShortcut('Ctrl+O')
|
|
36
|
+
open_project_action.triggered.connect(main_window.open_project)
|
|
37
|
+
|
|
38
|
+
# Add Recent Projects submenu
|
|
39
|
+
main_window.recent_projects_menu = file_menu.addMenu('Recent Projects')
|
|
40
|
+
main_window.update_recent_projects_menu()
|
|
41
|
+
|
|
42
|
+
# Add Quick Access submenu for files
|
|
43
|
+
main_window.quick_access_menu = file_menu.addMenu('Quick Access Files')
|
|
44
|
+
main_window.update_quick_access_menu()
|
|
45
|
+
|
|
46
|
+
save_project_action = file_menu.addAction('Save Project')
|
|
47
|
+
save_project_action.setShortcut('Ctrl+S')
|
|
48
|
+
save_project_action.triggered.connect(main_window.save_project)
|
|
49
|
+
|
|
50
|
+
save_project_as_action = file_menu.addAction('Save Project As...')
|
|
51
|
+
save_project_as_action.setShortcut('Ctrl+Shift+S')
|
|
52
|
+
save_project_as_action.triggered.connect(main_window.save_project_as)
|
|
53
|
+
|
|
54
|
+
file_menu.addSeparator()
|
|
55
|
+
|
|
56
|
+
# Load data action (databases, CSV, Excel, Parquet, etc.)
|
|
57
|
+
load_data_action = file_menu.addAction('Load Data...')
|
|
58
|
+
load_data_action.setShortcut('Ctrl+L')
|
|
59
|
+
load_data_action.triggered.connect(main_window.show_load_dialog)
|
|
60
|
+
|
|
61
|
+
file_menu.addSeparator()
|
|
62
|
+
|
|
63
|
+
exit_action = file_menu.addAction('Exit')
|
|
64
|
+
exit_action.setShortcut('Ctrl+Q')
|
|
65
|
+
exit_action.triggered.connect(main_window.close)
|
|
66
|
+
|
|
67
|
+
return file_menu
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_view_menu(main_window):
|
|
71
|
+
"""Create the View menu with window management options.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
main_window: The SQLShell main window instance
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The created View menu
|
|
78
|
+
"""
|
|
79
|
+
# Create View menu
|
|
80
|
+
view_menu = main_window.menuBar().addMenu('&View')
|
|
81
|
+
|
|
82
|
+
# Search action
|
|
83
|
+
search_action = view_menu.addAction('Search in Results...')
|
|
84
|
+
search_action.setShortcut('Ctrl+F')
|
|
85
|
+
search_action.triggered.connect(main_window.show_search_dialog)
|
|
86
|
+
|
|
87
|
+
view_menu.addSeparator()
|
|
88
|
+
|
|
89
|
+
# Toggle sidebar visibility
|
|
90
|
+
main_window.toggle_sidebar_action = view_menu.addAction('Toggle Sidebar')
|
|
91
|
+
main_window.toggle_sidebar_action.setShortcut('Ctrl+B')
|
|
92
|
+
main_window.toggle_sidebar_action.setCheckable(True)
|
|
93
|
+
main_window.toggle_sidebar_action.setChecked(True) # Sidebar visible by default
|
|
94
|
+
main_window.toggle_sidebar_action.triggered.connect(main_window.toggle_sidebar)
|
|
95
|
+
|
|
96
|
+
# Compact mode - reduces padding and hides secondary UI elements
|
|
97
|
+
main_window.compact_mode_action = view_menu.addAction('Compact Mode')
|
|
98
|
+
main_window.compact_mode_action.setShortcut('Ctrl+Shift+C')
|
|
99
|
+
main_window.compact_mode_action.setCheckable(True)
|
|
100
|
+
main_window.compact_mode_action.setChecked(False)
|
|
101
|
+
main_window.compact_mode_action.triggered.connect(main_window.toggle_compact_mode)
|
|
102
|
+
|
|
103
|
+
view_menu.addSeparator()
|
|
104
|
+
|
|
105
|
+
# Maximized window option
|
|
106
|
+
maximize_action = view_menu.addAction('Maximize Window')
|
|
107
|
+
maximize_action.setShortcut('F11')
|
|
108
|
+
maximize_action.triggered.connect(main_window.toggle_maximize_window)
|
|
109
|
+
|
|
110
|
+
# Zoom submenu
|
|
111
|
+
zoom_menu = view_menu.addMenu('Zoom')
|
|
112
|
+
|
|
113
|
+
zoom_in_action = zoom_menu.addAction('Zoom In')
|
|
114
|
+
zoom_in_action.setShortcut('Ctrl++')
|
|
115
|
+
zoom_in_action.triggered.connect(lambda: main_window.change_zoom(1.1))
|
|
116
|
+
|
|
117
|
+
zoom_out_action = zoom_menu.addAction('Zoom Out')
|
|
118
|
+
zoom_out_action.setShortcut('Ctrl+-')
|
|
119
|
+
zoom_out_action.triggered.connect(lambda: main_window.change_zoom(0.9))
|
|
120
|
+
|
|
121
|
+
reset_zoom_action = zoom_menu.addAction('Reset Zoom')
|
|
122
|
+
reset_zoom_action.setShortcut('Ctrl+0')
|
|
123
|
+
reset_zoom_action.triggered.connect(lambda: main_window.reset_zoom())
|
|
124
|
+
|
|
125
|
+
return view_menu
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_tab_menu(main_window):
|
|
129
|
+
"""Create the Tab menu with tab management actions.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
main_window: The SQLShell main window instance
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The created Tab menu
|
|
136
|
+
"""
|
|
137
|
+
# Create Tab menu
|
|
138
|
+
tab_menu = main_window.menuBar().addMenu('&Tab')
|
|
139
|
+
|
|
140
|
+
new_tab_action = tab_menu.addAction('New Tab')
|
|
141
|
+
new_tab_action.setShortcut('Ctrl+T')
|
|
142
|
+
new_tab_action.triggered.connect(main_window.add_tab)
|
|
143
|
+
|
|
144
|
+
duplicate_tab_action = tab_menu.addAction('Duplicate Current Tab')
|
|
145
|
+
duplicate_tab_action.setShortcut('Ctrl+D')
|
|
146
|
+
duplicate_tab_action.triggered.connect(main_window.duplicate_current_tab)
|
|
147
|
+
|
|
148
|
+
rename_tab_action = tab_menu.addAction('Rename Current Tab')
|
|
149
|
+
rename_tab_action.setShortcut('Ctrl+R')
|
|
150
|
+
rename_tab_action.triggered.connect(main_window.rename_current_tab)
|
|
151
|
+
|
|
152
|
+
close_tab_action = tab_menu.addAction('Close Current Tab')
|
|
153
|
+
close_tab_action.setShortcut('Ctrl+W')
|
|
154
|
+
close_tab_action.triggered.connect(main_window.close_current_tab)
|
|
155
|
+
|
|
156
|
+
return tab_menu
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_preferences_menu(main_window):
|
|
160
|
+
"""Create the Preferences menu with user settings.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
main_window: The SQLShell main window instance
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The created Preferences menu
|
|
167
|
+
"""
|
|
168
|
+
# Create Preferences menu
|
|
169
|
+
preferences_menu = main_window.menuBar().addMenu('&Preferences')
|
|
170
|
+
|
|
171
|
+
# Auto-load recent project option
|
|
172
|
+
auto_load_action = preferences_menu.addAction('Auto-load Most Recent Project')
|
|
173
|
+
auto_load_action.setCheckable(True)
|
|
174
|
+
auto_load_action.setChecked(main_window.auto_load_recent_project)
|
|
175
|
+
auto_load_action.triggered.connect(lambda checked: toggle_auto_load(main_window, checked))
|
|
176
|
+
|
|
177
|
+
preferences_menu.addSeparator()
|
|
178
|
+
|
|
179
|
+
# AI Autocomplete settings
|
|
180
|
+
ai_settings_action = preferences_menu.addAction('🤖 AI Autocomplete Settings...')
|
|
181
|
+
ai_settings_action.triggered.connect(lambda: show_ai_settings(main_window))
|
|
182
|
+
|
|
183
|
+
return preferences_menu
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def show_ai_settings(main_window):
|
|
187
|
+
"""Show the AI autocomplete settings dialog.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
main_window: The SQLShell main window instance
|
|
191
|
+
"""
|
|
192
|
+
from sqlshell.ai_settings_dialog import show_ai_settings_dialog
|
|
193
|
+
show_ai_settings_dialog(main_window)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def toggle_auto_load(main_window, checked):
|
|
197
|
+
"""Toggle the auto-load recent project setting.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
main_window: The SQLShell main window instance
|
|
201
|
+
checked: Boolean indicating whether the option is checked
|
|
202
|
+
"""
|
|
203
|
+
main_window.auto_load_recent_project = checked
|
|
204
|
+
main_window.save_recent_projects() # Save the preference
|
|
205
|
+
main_window.statusBar().showMessage(
|
|
206
|
+
f"Auto-load most recent project {'enabled' if checked else 'disabled'}",
|
|
207
|
+
2000
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def create_about_menu(main_window):
|
|
212
|
+
"""Create the About menu with version info and Easter egg.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
main_window: The SQLShell main window instance
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The created About menu
|
|
219
|
+
"""
|
|
220
|
+
# Create About menu
|
|
221
|
+
about_menu = main_window.menuBar().addMenu('&About')
|
|
222
|
+
|
|
223
|
+
# Version info action
|
|
224
|
+
version_action = about_menu.addAction(f'Version: {get_version()}')
|
|
225
|
+
version_action.setEnabled(False) # Just display, not clickable
|
|
226
|
+
|
|
227
|
+
about_menu.addSeparator()
|
|
228
|
+
|
|
229
|
+
# About SQLShell action (opens Space Invaders!)
|
|
230
|
+
about_action = about_menu.addAction('About SQLShell...')
|
|
231
|
+
about_action.triggered.connect(lambda: show_about_dialog(main_window))
|
|
232
|
+
|
|
233
|
+
return about_menu
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def show_about_dialog(main_window):
|
|
237
|
+
"""Show the About dialog with Space Invaders game.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
main_window: The SQLShell main window instance
|
|
241
|
+
"""
|
|
242
|
+
from sqlshell.space_invaders import show_space_invaders
|
|
243
|
+
show_space_invaders(main_window)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def setup_menubar(main_window):
|
|
247
|
+
"""Set up the complete menu bar for the application.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
main_window: The SQLShell main window instance
|
|
251
|
+
"""
|
|
252
|
+
# Create the menu bar (in case it doesn't exist)
|
|
253
|
+
menubar = main_window.menuBar()
|
|
254
|
+
|
|
255
|
+
# Create menus
|
|
256
|
+
file_menu = create_file_menu(main_window)
|
|
257
|
+
view_menu = create_view_menu(main_window)
|
|
258
|
+
tab_menu = create_tab_menu(main_window)
|
|
259
|
+
preferences_menu = create_preferences_menu(main_window)
|
|
260
|
+
about_menu = create_about_menu(main_window)
|
|
261
|
+
|
|
262
|
+
return menubar
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modern notification system for SQLShell.
|
|
3
|
+
Provides non-blocking, toast-style notifications instead of modal dialogs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QHBoxLayout,
|
|
7
|
+
QPushButton, QGraphicsEffect, QGraphicsDropShadowEffect,
|
|
8
|
+
QApplication)
|
|
9
|
+
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty, QRect, QPoint
|
|
10
|
+
from PyQt6.QtGui import QPainter, QColor, QPalette, QFont, QIcon, QBrush, QPen, QPainterPath
|
|
11
|
+
import time
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotificationType(Enum):
|
|
17
|
+
"""Types of notifications with different visual styles"""
|
|
18
|
+
INFO = "info"
|
|
19
|
+
SUCCESS = "success"
|
|
20
|
+
WARNING = "warning"
|
|
21
|
+
ERROR = "error"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NotificationWidget(QWidget):
|
|
25
|
+
"""A single notification widget with slide-in animation"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str, notification_type: NotificationType,
|
|
28
|
+
parent=None, duration: int = 5000):
|
|
29
|
+
super().__init__(parent)
|
|
30
|
+
self.message = message
|
|
31
|
+
self.notification_type = notification_type
|
|
32
|
+
self.duration = duration
|
|
33
|
+
self.parent_widget = parent
|
|
34
|
+
|
|
35
|
+
self.init_ui()
|
|
36
|
+
self.setup_animations()
|
|
37
|
+
|
|
38
|
+
def init_ui(self):
|
|
39
|
+
"""Initialize the notification UI"""
|
|
40
|
+
self.setFixedHeight(80)
|
|
41
|
+
self.setMinimumWidth(350)
|
|
42
|
+
self.setMaximumWidth(500)
|
|
43
|
+
|
|
44
|
+
# Make widget stay on top
|
|
45
|
+
self.setWindowFlags(Qt.WindowType.FramelessWindowHint |
|
|
46
|
+
Qt.WindowType.WindowStaysOnTopHint |
|
|
47
|
+
Qt.WindowType.Tool)
|
|
48
|
+
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
49
|
+
|
|
50
|
+
# Store colors for painting
|
|
51
|
+
self._bg_color = QColor('#E3F2FD')
|
|
52
|
+
self._border_color = QColor('#2196F3')
|
|
53
|
+
|
|
54
|
+
# Main layout
|
|
55
|
+
layout = QHBoxLayout(self)
|
|
56
|
+
layout.setContentsMargins(16, 12, 16, 12)
|
|
57
|
+
layout.setSpacing(12)
|
|
58
|
+
|
|
59
|
+
# Icon label
|
|
60
|
+
self.icon_label = QLabel()
|
|
61
|
+
self.icon_label.setFixedSize(24, 24)
|
|
62
|
+
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
63
|
+
layout.addWidget(self.icon_label)
|
|
64
|
+
|
|
65
|
+
# Message label
|
|
66
|
+
self.message_label = QLabel(self.message)
|
|
67
|
+
self.message_label.setWordWrap(True)
|
|
68
|
+
self.message_label.setAlignment(Qt.AlignmentFlag.AlignVCenter)
|
|
69
|
+
font = QFont()
|
|
70
|
+
font.setPointSize(10)
|
|
71
|
+
self.message_label.setFont(font)
|
|
72
|
+
layout.addWidget(self.message_label, 1)
|
|
73
|
+
|
|
74
|
+
# Close button
|
|
75
|
+
self.close_button = QPushButton("✕")
|
|
76
|
+
self.close_button.setFixedSize(24, 24)
|
|
77
|
+
self.close_button.clicked.connect(self.close_notification)
|
|
78
|
+
layout.addWidget(self.close_button)
|
|
79
|
+
|
|
80
|
+
# Apply styling based on notification type
|
|
81
|
+
self.apply_styling()
|
|
82
|
+
|
|
83
|
+
# Add drop shadow
|
|
84
|
+
shadow = QGraphicsDropShadowEffect()
|
|
85
|
+
shadow.setBlurRadius(15)
|
|
86
|
+
shadow.setOffset(0, 5)
|
|
87
|
+
shadow.setColor(QColor(0, 0, 0, 60))
|
|
88
|
+
self.setGraphicsEffect(shadow)
|
|
89
|
+
|
|
90
|
+
def apply_styling(self):
|
|
91
|
+
"""Apply styling based on notification type"""
|
|
92
|
+
styles = {
|
|
93
|
+
NotificationType.INFO: {
|
|
94
|
+
'bg_color': '#E3F2FD', # Light blue background
|
|
95
|
+
'text_color': '#0D47A1', # Dark blue text
|
|
96
|
+
'border_color': '#2196F3',
|
|
97
|
+
'icon': 'ℹ'
|
|
98
|
+
},
|
|
99
|
+
NotificationType.SUCCESS: {
|
|
100
|
+
'bg_color': '#E8F5E9', # Light green background
|
|
101
|
+
'text_color': '#1B5E20', # Dark green text
|
|
102
|
+
'border_color': '#4CAF50',
|
|
103
|
+
'icon': '✓'
|
|
104
|
+
},
|
|
105
|
+
NotificationType.WARNING: {
|
|
106
|
+
'bg_color': '#FFF3E0', # Light orange background
|
|
107
|
+
'text_color': '#E65100', # Dark orange text
|
|
108
|
+
'border_color': '#FF9800',
|
|
109
|
+
'icon': '⚠'
|
|
110
|
+
},
|
|
111
|
+
NotificationType.ERROR: {
|
|
112
|
+
'bg_color': '#FFEBEE', # Light red background
|
|
113
|
+
'text_color': '#B71C1C', # Dark red text
|
|
114
|
+
'border_color': '#F44336',
|
|
115
|
+
'icon': '✗'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
style = styles[self.notification_type]
|
|
120
|
+
|
|
121
|
+
# Store colors for custom painting (solid background)
|
|
122
|
+
self._bg_color = QColor(style['bg_color'])
|
|
123
|
+
self._border_color = QColor(style['border_color'])
|
|
124
|
+
|
|
125
|
+
# Set icon with improved visibility
|
|
126
|
+
self.icon_label.setText(style['icon'])
|
|
127
|
+
self.icon_label.setStyleSheet(f"""
|
|
128
|
+
QLabel {{
|
|
129
|
+
color: {style['text_color']};
|
|
130
|
+
font-size: 18px;
|
|
131
|
+
font-weight: bold;
|
|
132
|
+
font-family: "Arial", "Helvetica", sans-serif;
|
|
133
|
+
background: transparent;
|
|
134
|
+
}}
|
|
135
|
+
""")
|
|
136
|
+
|
|
137
|
+
# Set message styling with improved readability
|
|
138
|
+
self.message_label.setStyleSheet(f"""
|
|
139
|
+
QLabel {{
|
|
140
|
+
color: {style['text_color']};
|
|
141
|
+
background: transparent;
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
font-weight: bold;
|
|
144
|
+
font-family: "Arial", "Helvetica", sans-serif;
|
|
145
|
+
padding: 2px;
|
|
146
|
+
}}
|
|
147
|
+
""")
|
|
148
|
+
|
|
149
|
+
# Set close button styling
|
|
150
|
+
self.close_button.setStyleSheet(f"""
|
|
151
|
+
QPushButton {{
|
|
152
|
+
background: transparent;
|
|
153
|
+
border: none;
|
|
154
|
+
color: {style['text_color']};
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
font-weight: bold;
|
|
157
|
+
border-radius: 12px;
|
|
158
|
+
font-family: "Arial", "Helvetica", sans-serif;
|
|
159
|
+
}}
|
|
160
|
+
QPushButton:hover {{
|
|
161
|
+
background: rgba(0, 0, 0, 0.1);
|
|
162
|
+
}}
|
|
163
|
+
QPushButton:pressed {{
|
|
164
|
+
background: rgba(0, 0, 0, 0.2);
|
|
165
|
+
}}
|
|
166
|
+
""")
|
|
167
|
+
|
|
168
|
+
def paintEvent(self, event):
|
|
169
|
+
"""Paint a solid opaque background with rounded corners"""
|
|
170
|
+
painter = QPainter(self)
|
|
171
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
172
|
+
|
|
173
|
+
# Create rounded rectangle path
|
|
174
|
+
path = QPainterPath()
|
|
175
|
+
rect = self.rect().adjusted(2, 2, -2, -2) # Leave room for border
|
|
176
|
+
path.addRoundedRect(float(rect.x()), float(rect.y()),
|
|
177
|
+
float(rect.width()), float(rect.height()), 8, 8)
|
|
178
|
+
|
|
179
|
+
# Fill with solid opaque background
|
|
180
|
+
painter.fillPath(path, QBrush(self._bg_color))
|
|
181
|
+
|
|
182
|
+
# Draw border
|
|
183
|
+
painter.setPen(QPen(self._border_color, 3))
|
|
184
|
+
painter.drawPath(path)
|
|
185
|
+
|
|
186
|
+
def setup_animations(self):
|
|
187
|
+
"""Setup slide-in and fade-out animations"""
|
|
188
|
+
# Slide in animation
|
|
189
|
+
self.slide_animation = QPropertyAnimation(self, b"geometry")
|
|
190
|
+
self.slide_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
191
|
+
self.slide_animation.setDuration(300)
|
|
192
|
+
|
|
193
|
+
# Fade out animation
|
|
194
|
+
self.fade_animation = QPropertyAnimation(self, b"windowOpacity")
|
|
195
|
+
self.fade_animation.setEasingCurve(QEasingCurve.Type.InCubic)
|
|
196
|
+
self.fade_animation.setDuration(200)
|
|
197
|
+
self.fade_animation.finished.connect(self.hide)
|
|
198
|
+
|
|
199
|
+
# Auto-hide timer
|
|
200
|
+
if self.duration > 0:
|
|
201
|
+
self.auto_hide_timer = QTimer()
|
|
202
|
+
self.auto_hide_timer.timeout.connect(self.close_notification)
|
|
203
|
+
self.auto_hide_timer.setSingleShot(True)
|
|
204
|
+
|
|
205
|
+
def show_notification(self, position: QRect):
|
|
206
|
+
"""Show the notification with slide-in animation"""
|
|
207
|
+
# Position is already in global screen coordinates
|
|
208
|
+
# Start position: slide in from the right (off screen)
|
|
209
|
+
start_rect = QRect(position.x() + 400, position.y(),
|
|
210
|
+
self.width(), self.height())
|
|
211
|
+
end_rect = QRect(position.x(), position.y(),
|
|
212
|
+
self.width(), self.height())
|
|
213
|
+
|
|
214
|
+
self.setGeometry(start_rect)
|
|
215
|
+
self.show()
|
|
216
|
+
|
|
217
|
+
# Animate slide in
|
|
218
|
+
self.slide_animation.setStartValue(start_rect)
|
|
219
|
+
self.slide_animation.setEndValue(end_rect)
|
|
220
|
+
self.slide_animation.start()
|
|
221
|
+
|
|
222
|
+
# Start auto-hide timer
|
|
223
|
+
if self.duration > 0:
|
|
224
|
+
self.auto_hide_timer.start(self.duration)
|
|
225
|
+
|
|
226
|
+
def close_notification(self):
|
|
227
|
+
"""Close the notification with fade-out animation"""
|
|
228
|
+
if hasattr(self, 'auto_hide_timer'):
|
|
229
|
+
self.auto_hide_timer.stop()
|
|
230
|
+
|
|
231
|
+
self.fade_animation.setStartValue(1.0)
|
|
232
|
+
self.fade_animation.setEndValue(0.0)
|
|
233
|
+
self.fade_animation.start()
|
|
234
|
+
|
|
235
|
+
def enterEvent(self, event):
|
|
236
|
+
"""Pause auto-hide when mouse enters"""
|
|
237
|
+
if hasattr(self, 'auto_hide_timer'):
|
|
238
|
+
self.auto_hide_timer.stop()
|
|
239
|
+
super().enterEvent(event)
|
|
240
|
+
|
|
241
|
+
def leaveEvent(self, event):
|
|
242
|
+
"""Resume auto-hide when mouse leaves"""
|
|
243
|
+
if hasattr(self, 'auto_hide_timer') and self.duration > 0:
|
|
244
|
+
self.auto_hide_timer.start(2000) # Shorter duration after hover
|
|
245
|
+
super().leaveEvent(event)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class NotificationManager:
|
|
249
|
+
"""Manages multiple notifications and their positioning"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, parent_widget):
|
|
252
|
+
self.parent_widget = parent_widget
|
|
253
|
+
self.notifications: List[NotificationWidget] = []
|
|
254
|
+
self.notification_spacing = 10
|
|
255
|
+
|
|
256
|
+
def show_notification(self, message: str, notification_type: NotificationType,
|
|
257
|
+
duration: int = 5000) -> NotificationWidget:
|
|
258
|
+
"""Show a new notification"""
|
|
259
|
+
# Clean up any hidden notifications
|
|
260
|
+
self._cleanup_notifications()
|
|
261
|
+
|
|
262
|
+
# Create new notification
|
|
263
|
+
notification = NotificationWidget(
|
|
264
|
+
message=message,
|
|
265
|
+
notification_type=notification_type,
|
|
266
|
+
parent=self.parent_widget,
|
|
267
|
+
duration=duration
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Calculate position for this notification
|
|
271
|
+
position = self._calculate_position(notification)
|
|
272
|
+
|
|
273
|
+
# Connect cleanup when notification is hidden
|
|
274
|
+
notification.fade_animation.finished.connect(
|
|
275
|
+
lambda: self._remove_notification(notification)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Add to our list and show
|
|
279
|
+
self.notifications.append(notification)
|
|
280
|
+
notification.show_notification(position)
|
|
281
|
+
|
|
282
|
+
return notification
|
|
283
|
+
|
|
284
|
+
def show_info(self, message: str, duration: int = 5000) -> NotificationWidget:
|
|
285
|
+
"""Show an info notification"""
|
|
286
|
+
return self.show_notification(message, NotificationType.INFO, duration)
|
|
287
|
+
|
|
288
|
+
def show_success(self, message: str, duration: int = 4000) -> NotificationWidget:
|
|
289
|
+
"""Show a success notification"""
|
|
290
|
+
return self.show_notification(message, NotificationType.SUCCESS, duration)
|
|
291
|
+
|
|
292
|
+
def show_warning(self, message: str, duration: int = 6000) -> NotificationWidget:
|
|
293
|
+
"""Show a warning notification"""
|
|
294
|
+
return self.show_notification(message, NotificationType.WARNING, duration)
|
|
295
|
+
|
|
296
|
+
def show_error(self, message: str, duration: int = 8000) -> NotificationWidget:
|
|
297
|
+
"""Show an error notification"""
|
|
298
|
+
return self.show_notification(message, NotificationType.ERROR, duration)
|
|
299
|
+
|
|
300
|
+
def _calculate_position(self, notification: NotificationWidget) -> QRect:
|
|
301
|
+
"""Calculate the position for a new notification (in global screen coordinates)"""
|
|
302
|
+
# Get the parent widget's global position on screen
|
|
303
|
+
parent_global_pos = self.parent_widget.mapToGlobal(QPoint(0, 0))
|
|
304
|
+
parent_width = self.parent_widget.width()
|
|
305
|
+
|
|
306
|
+
# Calculate position relative to parent's right edge (in screen coordinates)
|
|
307
|
+
# Position notification inside the parent window's right side
|
|
308
|
+
x = parent_global_pos.x() + parent_width - notification.width() - 20
|
|
309
|
+
y = parent_global_pos.y() + 80 # Start below any toolbar/menubar
|
|
310
|
+
|
|
311
|
+
# Stack notifications vertically
|
|
312
|
+
for existing in self.notifications:
|
|
313
|
+
if existing.isVisible():
|
|
314
|
+
y += existing.height() + self.notification_spacing
|
|
315
|
+
|
|
316
|
+
return QRect(x, y, notification.width(), notification.height())
|
|
317
|
+
|
|
318
|
+
def _remove_notification(self, notification: NotificationWidget):
|
|
319
|
+
"""Remove a notification from the list"""
|
|
320
|
+
if notification in self.notifications:
|
|
321
|
+
self.notifications.remove(notification)
|
|
322
|
+
notification.deleteLater()
|
|
323
|
+
|
|
324
|
+
def _cleanup_notifications(self):
|
|
325
|
+
"""Remove any notifications that are no longer visible"""
|
|
326
|
+
self.notifications = [n for n in self.notifications if n.isVisible()]
|
|
327
|
+
|
|
328
|
+
def clear_all(self):
|
|
329
|
+
"""Clear all notifications"""
|
|
330
|
+
for notification in self.notifications[:]:
|
|
331
|
+
notification.close_notification()
|
|
332
|
+
self.notifications.clear()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# Global instance to be used throughout the application
|
|
336
|
+
_notification_manager: Optional[NotificationManager] = None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def init_notification_manager(parent_widget):
|
|
340
|
+
"""Initialize the global notification manager"""
|
|
341
|
+
global _notification_manager
|
|
342
|
+
_notification_manager = NotificationManager(parent_widget)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_notification_manager() -> NotificationManager:
|
|
346
|
+
"""Get the global notification manager instance"""
|
|
347
|
+
global _notification_manager
|
|
348
|
+
if _notification_manager is None:
|
|
349
|
+
raise RuntimeError("Notification manager not initialized. Call init_notification_manager() first.")
|
|
350
|
+
return _notification_manager
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def show_info_notification(message: str, duration: int = 5000):
|
|
354
|
+
"""Convenience function to show info notification"""
|
|
355
|
+
return get_notification_manager().show_info(message, duration)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def show_success_notification(message: str, duration: int = 4000):
|
|
359
|
+
"""Convenience function to show success notification"""
|
|
360
|
+
return get_notification_manager().show_success(message, duration)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def show_warning_notification(message: str, duration: int = 6000):
|
|
364
|
+
"""Convenience function to show warning notification"""
|
|
365
|
+
return get_notification_manager().show_warning(message, duration)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def show_error_notification(message: str, duration: int = 8000):
|
|
369
|
+
"""Convenience function to show error notification"""
|
|
370
|
+
return get_notification_manager().show_error(message, duration)
|