feat: add workspace functionality with manager and UI integration#3
feat: add workspace functionality with manager and UI integration#3
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR introduces comprehensive workspace functionality with a tabbed editor interface, replacing the single-view approach with a multi-file workspace system. The implementation includes workspace management for saving/loading application states, a file panel for quick navigation, and enhanced UI layout with tabbed editing.
Key changes:
- Added workspace save/load/delete functionality with persistent JSON storage
- Replaced single table view with tabbed editor interface supporting multiple open files
- Added workspace file panel with drag-and-drop support for file management
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| workspaces.json | Configuration file storing saved workspace states with window geometry and view settings |
| workspace_manager.py | Core workspace management class providing JSON-based persistence for workspace snapshots |
| ui.py | Major UI overhaul replacing single table with tabbed interface, workspace panel, and multi-file support |
| main_v2.py | Added Qt logging filters to suppress debug noise during development |
| custom_widgets.py | Enhanced table views with better editor ownership tracking and debug capabilities |
| config.json | Updated configuration with new workspace settings and file tracking |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| "column_widths": [ | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100, | ||
| 100 | ||
| ], |
There was a problem hiding this comment.
The column_widths array contains 105 repeated values of 100. This repetitive data suggests the array could be replaced with a more compact representation like {count: 105, width: 100} or dynamically generated default values to improve maintainability.
| "column_widths": [ | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100, | |
| 100 | |
| ], | |
| "column_widths": { | |
| "count": 105, | |
| "width": 100 | |
| }, |
| except Exception: | ||
| pass |
There was a problem hiding this comment.
The broad exception handling with bare 'except Exception:' and 'pass' suppresses all errors including programming errors. Consider logging the exception or handling specific exception types that are expected during normal operation.
| except Exception: | |
| pass | |
| except Exception as e: | |
| logging.exception("Failed to set _d2te_index property on editor in ComboBoxDelegate.createEditor") |
| # Load the first selected into the editor | ||
| self._load_file_path(paths[0]) | ||
|
|
There was a problem hiding this comment.
The method silently loads only the first file when multiple files are selected (line 1119), which may be unexpected behavior for users. Consider either loading all selected files in tabs or clearly indicating in the UI that only the first file will be opened.
| # Load the first selected into the editor | |
| self._load_file_path(paths[0]) | |
| # Load all selected files into the editor (in tabs) | |
| for file_path in paths: | |
| self._load_file_path(file_path) |
|
|
||
| def on_tab_bar_clicked(self, index: int): | ||
| # Try to finalize edits in the current tab before switching | ||
| self._close_all_active_editors() | ||
|
|
||
| def _close_all_active_editors(self): | ||
| try: | ||
| from PyQt6.QtWidgets import QApplication | ||
| # Prefer closing the currently focused inline editor only; this avoids cross-view commits | ||
| fw = QApplication.focusWidget() | ||
| if fw is not None: | ||
| # Find owning QTableView and top-level editor widget | ||
| from PyQt6.QtWidgets import QTableView, QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit | ||
| editor_types = (QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit) | ||
| # Ascend from focus widget to find top-level editor | ||
| ed = fw | ||
| top_editor = None | ||
| depth = 0 | ||
| while ed is not None and depth < 10: # Increased depth | ||
| if isinstance(ed, editor_types): | ||
| top_editor = ed | ||
| ed = ed.parent() | ||
| depth += 1 | ||
| # If focus was inside editable combobox, use the combobox itself | ||
| if isinstance(top_editor, QLineEdit) and isinstance(top_editor.parent(), QComboBox): | ||
| top_editor = top_editor.parent() | ||
|
|
||
| # More thorough search for owning view - check all known table views | ||
| owner_view = None | ||
| all_views = list(self._tabs.values()) | ||
| if hasattr(self, 'tableView') and self.tableView is not None: | ||
| all_views.append(self.tableView) | ||
|
|
There was a problem hiding this comment.
The method imports QApplication inside the function and performs complex widget hierarchy traversal on every call. Consider moving the import to the module level and caching widget references to improve performance, especially since this method may be called frequently during tab switching.
| def on_tab_bar_clicked(self, index: int): | |
| # Try to finalize edits in the current tab before switching | |
| self._close_all_active_editors() | |
| def _close_all_active_editors(self): | |
| try: | |
| from PyQt6.QtWidgets import QApplication | |
| # Prefer closing the currently focused inline editor only; this avoids cross-view commits | |
| fw = QApplication.focusWidget() | |
| if fw is not None: | |
| # Find owning QTableView and top-level editor widget | |
| from PyQt6.QtWidgets import QTableView, QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit | |
| editor_types = (QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit) | |
| # Ascend from focus widget to find top-level editor | |
| ed = fw | |
| top_editor = None | |
| depth = 0 | |
| while ed is not None and depth < 10: # Increased depth | |
| if isinstance(ed, editor_types): | |
| top_editor = ed | |
| ed = ed.parent() | |
| depth += 1 | |
| # If focus was inside editable combobox, use the combobox itself | |
| if isinstance(top_editor, QLineEdit) and isinstance(top_editor.parent(), QComboBox): | |
| top_editor = top_editor.parent() | |
| # More thorough search for owning view - check all known table views | |
| owner_view = None | |
| all_views = list(self._tabs.values()) | |
| if hasattr(self, 'tableView') and self.tableView is not None: | |
| all_views.append(self.tableView) | |
| # Update cached views after tab change | |
| self._update_all_views_cache() | |
| def on_tab_bar_clicked(self, index: int): | |
| # Try to finalize edits in the current tab before switching | |
| self._close_all_active_editors() | |
| def _update_all_views_cache(self): | |
| # Cache all known table views for performance | |
| self._all_views_cache = list(self._tabs.values()) | |
| if hasattr(self, 'tableView') and self.tableView is not None: | |
| if self.tableView not in self._all_views_cache: | |
| self._all_views_cache.append(self.tableView) | |
| def _close_all_active_editors(self): | |
| try: | |
| # Prefer closing the currently focused inline editor only; this avoids cross-view commits | |
| fw = QApplication.focusWidget() | |
| if fw is not None: | |
| # Find owning QTableView and top-level editor widget | |
| # Ascend from focus widget to find top-level editor | |
| ed = fw | |
| top_editor = None | |
| depth = 0 | |
| while ed is not None and depth < 10: # Increased depth | |
| if isinstance(ed, EDITOR_WIDGET_TYPES): | |
| top_editor = ed | |
| ed = ed.parent() | |
| depth += 1 | |
| # If focus was inside editable combobox, use the combobox itself | |
| if isinstance(top_editor, QLineEdit) and isinstance(top_editor.parent(), QComboBox): | |
| top_editor = top_editor.parent() | |
| # More thorough search for owning view - check all known table views | |
| owner_view = None | |
| all_views = getattr(self, '_all_views_cache', list(self._tabs.values())) |
| debug_on = False | ||
| try: | ||
| debug_on = self.config_manager.get_setting("debug_mode_enabled", False) | ||
| except Exception: | ||
| debug_on = False |
There was a problem hiding this comment.
The nested try-catch blocks for retrieving debug settings create unnecessary complexity. Consider extracting this debug flag retrieval into a helper method since it's used multiple times throughout the class.
| debug_on = False | |
| try: | |
| debug_on = self.config_manager.get_setting("debug_mode_enabled", False) | |
| except Exception: | |
| debug_on = False | |
| debug_on = self._is_debug_enabled() |
| except Exception: | ||
| view = None | ||
| parent_to_use = parent |
There was a problem hiding this comment.
The broad exception handling suppresses all exceptions including potential programming errors. Consider handling specific exceptions like AttributeError that might occur during normal operation, and logging unexpected exceptions for debugging.
| except Exception: | |
| view = None | |
| parent_to_use = parent | |
| except (AttributeError, TypeError): | |
| view = None | |
| parent_to_use = parent | |
| except Exception as e: | |
| print(f"[ERROR] Unexpected exception in createEditor parent determination: {e}") | |
| view = None | |
| parent_to_use = parent |
No description provided.