sqlshell 0.4.4__py3-none-any.whl

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