From 643e93bf2cf36ca34a27bb8429a766dead584aa9 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Tue, 21 Oct 2025 18:41:53 +0200 Subject: [PATCH 01/14] pattern_completer_widget --- .../utils/widgets/pattern_completer.py | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 src/pymodaq_gui/utils/widgets/pattern_completer.py diff --git a/src/pymodaq_gui/utils/widgets/pattern_completer.py b/src/pymodaq_gui/utils/widgets/pattern_completer.py new file mode 100644 index 00000000..3155f93a --- /dev/null +++ b/src/pymodaq_gui/utils/widgets/pattern_completer.py @@ -0,0 +1,602 @@ +from PyQt6.QtWidgets import ( + QWidget, + QCompleter, + QLineEdit, + QTextEdit, + QPlainTextEdit, + QStyledItemDelegate, + QListView, # For word wrap +) +from PyQt6.QtCore import Qt, QRect +from PyQt6.QtGui import QStandardItemModel, QStandardItem, QTextCursor, QFontMetrics + + +class PatternCompleter: + """ + Mixin class that adds pattern completion to any text widget. + + Requirements for the widget: + - Must have: text(), setText(), cursorPosition() or textCursor() + - Must emit: textChanged signal + - Must support: keyPressEvent override + + Usage: + class MyLineEdit(QLineEdit, PatternCompleterMixin): + def __init__(self, parent=None): + super().__init__(parent) + self.init_pattern_completer() + """ + + def init_pattern_completer(self, **kwargs): + """ + Initialize the pattern completer system. + + Args: + **kwargs: Global configuration options + - min_width (int): Minimum popup width in pixels (default: 150) + - max_width (int): Maximum popup width in pixels (default: 500) + - visual_indicator (bool): Enable visual indicator globally (default: False) + - case_sensitive (bool): Case sensitive completion (default: False) + - completion_mode (str): 'popup' or 'inline' (default: 'popup') + - auto_resize (bool): Auto-resize popup to content (default: True) + - word_wrap (bool): Enable word wrap in popup (default: False) + """ + # Initialize all attributes first + self: QWidget # Type hint for IDEs + self.completers = {} + self.active_pattern = None + self.trigger_start_pos = -1 + self.inserting_completion = False + self._is_destroyed = False + + # Global configuration with defaults + self.global_config = { + "min_width": kwargs.get("min_width", 150), + "max_width": kwargs.get("max_width", 500), + "visual_indicator": kwargs.get("visual_indicator", False), + "case_sensitive": kwargs.get("case_sensitive", False), + "completion_mode": kwargs.get("completion_mode", "popup"), + "auto_resize": kwargs.get("auto_resize", True), + "word_wrap": kwargs.get("word_wrap", False), + } + + # Connect to text changes + if hasattr(self, "textChanged"): + try: + self.textChanged.connect(self._pattern_on_text_changed) + except Exception as e: + print(f"Error connecting textChanged signal: {e}") + + def add_completer(self, pattern, completions, **kwargs): + """ + Add a completer for a specific trigger pattern. + + Args: + pattern (str): Trigger string (e.g., '@', '#', '::') + completions (list): List of completion strings + **kwargs: Per-pattern configuration (overrides global config) + - visual_indicator (bool): Show visual indicator for this pattern + - case_sensitive (bool): Case sensitive completion + - min_width (int): Minimum popup width + - max_width (int): Maximum popup width + - completion_mode (str): 'popup' or 'inline' + - auto_resize (bool): Auto-resize popup + - word_wrap (bool): Word wrap in popup + - padding (int): Extra padding for width calculation (default: 20) + """ + model = QStandardItemModel() + for item in completions: + model.appendRow(QStandardItem(item)) + + completer = QCompleter(model, self) + + # Apply configuration (pattern-specific overrides global) + config = {**self.global_config, **kwargs} + + case_sensitivity = ( + Qt.CaseSensitivity.CaseSensitive + if config.get("case_sensitive", False) + else Qt.CaseSensitivity.CaseInsensitive + ) + completer.setCaseSensitivity(case_sensitivity) + + completion_mode_str = config.get("completion_mode", "popup") + if completion_mode_str == "inline": + completer.setCompletionMode(QCompleter.CompletionMode.InlineCompletion) + else: + completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + + completer.activated.connect(self._pattern_insert_completion) + + # Configure popup + popup = completer.popup() + min_width = config.get("min_width", 150) + max_width = config.get("max_width", 500) + popup.setMinimumWidth(min_width) + popup.setMaximumWidth(max_width) + + if isinstance(popup, QListView): + word_wrap = config.get("word_wrap", False) + popup.setWordWrap(word_wrap) + popup.setTextElideMode( + Qt.TextElideMode.ElideNone + if not word_wrap + else Qt.TextElideMode.ElideRight + ) + popup.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + popup.setResizeMode(QListView.ResizeMode.Adjust) + + self.completers[pattern] = { + "completer": completer, + "model": model, + "completions": completions, + "config": config, + } + + def update_completions(self, pattern, completions): + """Update completion list for a pattern""" + if pattern not in self.completers: + return + + config = self.completers[pattern] + config["completions"] = completions + config["model"].clear() + for item in completions: + config["model"].appendRow(QStandardItem(item)) + + def update_completer_config(self, pattern, **kwargs): + """ + Update configuration for a specific pattern completer. + + Args: + pattern (str): The pattern to update + **kwargs: Configuration options to update + """ + if pattern not in self.completers: + return + + config = self.completers[pattern] + config["config"].update(kwargs) + + # Apply updates to completer + completer: QCompleter = config["completer"] + + if "case_sensitive" in kwargs: + case_sensitivity = ( + Qt.CaseSensitivity.CaseSensitive + if kwargs["case_sensitive"] + else Qt.CaseSensitivity.CaseInsensitive + ) + completer.setCaseSensitivity(case_sensitivity) + + if "completion_mode" in kwargs: + mode_str = kwargs["completion_mode"] + if mode_str == "inline": + completer.setCompletionMode(QCompleter.CompletionMode.InlineCompletion) + else: + completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + + # Update popup settings + if any(k in kwargs for k in ["min_width", "max_width", "word_wrap"]): + popup = completer.popup() + + if "min_width" in kwargs: + popup.setMinimumWidth(kwargs["min_width"]) + if "max_width" in kwargs: + popup.setMaximumWidth(kwargs["max_width"]) + if "word_wrap" in kwargs: + from PyQt6.QtWidgets import QListView + + if isinstance(popup, QListView): + popup.setWordWrap(kwargs["word_wrap"]) + + def set_global_config(self, **kwargs): + """Update global configuration for all completers""" + self.global_config.update(kwargs) + + def set_visual_indicator(self, enabled): + """Enable/disable visual indicator globally""" + self.global_config["visual_indicator"] = enabled + + def cleanup_pattern_completer(self): + """Clean up completer resources""" + # Disconnect text changed signal first + if hasattr(self, "textChanged"): + try: + self.textChanged.disconnect(self._pattern_on_text_changed) + except (TypeError, RuntimeError): + pass + + # Clean up completers + for pattern, config in list(self.completers.items()): + try: + completer: QCompleter = config.get("completer") + if completer: + # Hide and disconnect popup + popup = completer.popup() + if popup and popup.isVisible(): + popup.hide() + + # Disconnect signals + try: + completer.activated.disconnect() + except (TypeError, RuntimeError): + pass + + # Delete completer + completer.setWidget(None) + completer.setModel(None) + completer.deleteLater() + except (TypeError, RuntimeError, AttributeError) as e: + print(f"Error cleaning up completer for pattern '{pattern}': {e}") + pass + + self.completers.clear() + + def _get_text_and_cursor(self): + """Get text and cursor position (works for different widget types)""" + text = self.toPlainText() if hasattr(self, "toPlainText") else self.text() + + if hasattr(self, "textCursor"): + cursor_pos: QTextCursor = self.textCursor().position() + else: + cursor_pos = self.cursorPosition() + + return text, cursor_pos + + def _set_text_with_cursor(self, text, cursor_pos): + """Set text and cursor position (works for different widget types)""" + if hasattr(self, "setPlainText"): + self.setPlainText(text) + cursor: QTextCursor = self.textCursor() + cursor.setPosition(min(cursor_pos, len(text))) + self.setTextCursor(cursor) + else: + self.setText(text) + self.setCursorPosition(min(cursor_pos, len(text))) + + def _find_active_trigger(self, text, cursor_pos): + """Find which trigger pattern is currently active (optimized)""" + active_pattern = None + last_trigger_pos = -1 + + search_text: str = text[:cursor_pos] + + for pattern in self.completers.keys(): + # Search backwards from cursor for efficiency + pos = search_text.rfind(pattern) + + while pos >= 0: + end_pos = pos + len(pattern) + + # Validate end_pos + if end_pos > len(text): + pos = search_text.rfind(pattern, 0, pos) + continue + + text_after = text[end_pos:cursor_pos] + + # Check if there's no space or newline after trigger + if " " not in text_after and "\n" not in text_after: + if pos > last_trigger_pos: + last_trigger_pos = pos + active_pattern = pattern + break + + # Search for earlier occurrence + pos = search_text.rfind(pattern, 0, pos) + + return active_pattern, last_trigger_pos + + def _apply_visual_indicator(self, active): + """Apply visual styling""" + config = self.global_config + if not config.get("visual_indicator", False): + return + + if active: + # Use object name for more reliable styling + if not self.objectName(): + self.setObjectName("pattern_completer_widget") + self.setStyleSheet( + "#pattern_completer_widget { border: 2px solid #4CAF50; border-radius: 3px; }" + ) + else: + self.setStyleSheet("") + + def _pattern_on_text_changed(self): + """Handle text changes""" + if self.inserting_completion: + return + + try: + text, cursor_pos = self._get_text_and_cursor() + except (RuntimeError, AttributeError): + return + + active_pattern, trigger_pos = self._find_active_trigger(text, cursor_pos) + + if active_pattern and trigger_pos >= 0: + self.active_pattern = active_pattern + self.trigger_start_pos = trigger_pos + + pattern_config = self.completers[active_pattern] + completer: QCompleter = pattern_config["completer"] + config = pattern_config["config"] + + pattern_len = len(active_pattern) + prefix = text[trigger_pos + pattern_len : cursor_pos] + + completer.setCompletionPrefix(prefix) + completer.setWidget(self) + + # Calculate optimal width based on content + popup = completer.popup() + popup.setUpdatesEnabled(False) + + # Position the popup at the cursor for multi-line widgets + if hasattr(self, "cursorRect"): + # QTextEdit/QPlainTextEdit - position at cursor + cursor_rect: QRect = self.cursorRect() + popup_pos = self.mapToGlobal(cursor_rect.bottomLeft()) + popup.move(popup_pos) + completer.complete(cursor_rect) + else: + # QLineEdit - default positioning is fine + completer.complete() + + # Auto-resize popup width to fit content using font metrics + if config.get("auto_resize", True) and completer.completionCount() > 0: + # Get font metrics from the popup + font_metrics = QFontMetrics(popup.font()) + + max_width = config.get("min_width", 150) + padding = config.get("padding", 20) + + for i in range(completer.completionCount()): + index = completer.completionModel().index(i, 0) + item_text = completer.completionModel().data(index) + if item_text: + # Get actual pixel width of the text + text_width = font_metrics.horizontalAdvance(str(item_text)) + max_width = max(max_width, text_width + padding) + + # Set width with limits + max_limit = config.get("max_width", 500) + max_width = min(max_width, max_limit) + popup.setFixedWidth(max_width) + + popup.setUpdatesEnabled(True) + + if config.get("visual_indicator", False): + self._apply_visual_indicator(True) + else: + self.active_pattern = None + self.trigger_start_pos = -1 + + for pattern_config in self.completers.values(): + try: + if pattern_config["completer"].popup().isVisible(): + pattern_config["completer"].popup().hide() + except (RuntimeError, AttributeError): + pass + + self._apply_visual_indicator(False) + + def _pattern_insert_completion(self, completion): + """Insert the selected completion""" + if self.trigger_start_pos < 0 or not self.active_pattern: + return + + self.inserting_completion = True + + try: + # Remove the trigger pattern and any text after it up to cursor + text, cursor_pos = self._get_text_and_cursor() + + # Replace with just the completion (without the pattern prefix) + new_text = text[: self.trigger_start_pos] + completion + text[cursor_pos:] + + new_cursor_pos = self.trigger_start_pos + len(completion) + self._set_text_with_cursor(new_text, new_cursor_pos) + + # Reset state BEFORE hiding popup to prevent re-triggering + self.trigger_start_pos = -1 + self.active_pattern = None + + # Hide all popups + for pattern_config in self.completers.values(): + try: + if pattern_config["completer"].popup().isVisible(): + pattern_config["completer"].popup().hide() + except (RuntimeError, AttributeError): + pass + + self._apply_visual_indicator(False) + finally: + self.inserting_completion = False + + def _pattern_key_press_event(self, event): + """ + Handle pattern completion keys. + Call this from your widget's keyPressEvent BEFORE calling super(). + + Returns: + bool: True if event was handled (don't call super), False otherwise + """ + # Check for active completer + completer_visible = False + active_completer: QCompleter = None + + for pattern, pattern_config in self.completers.items(): + if pattern_config["completer"].popup().isVisible(): + completer_visible = True + if pattern == self.active_pattern: + active_completer = pattern_config["completer"] + break + + if completer_visible and active_completer: + if event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): + index = active_completer.popup().currentIndex() + if index.isValid(): + completion = active_completer.completionModel().data(index) + self._pattern_insert_completion(completion) + event.accept() + return True # Event handled + elif event.key() == Qt.Key.Key_Escape: + active_completer.popup().hide() + self._apply_visual_indicator(False) + event.accept() + return True # Event handled + + return False # Event not handled, continue normal processing + + +class PatternLineEdit(QLineEdit, PatternCompleter): + """QLineEdit with pattern completion""" + + def __init__(self, parent=None, **kwargs): + super().__init__(parent) + self.init_pattern_completer(**kwargs) + + def keyPressEvent(self, event): + """Override to handle completion keys""" + if not self._pattern_key_press_event(event): + # Event not handled by pattern completer, process normally + super().keyPressEvent(event) + + +class PatternTextEdit(QTextEdit, PatternCompleter): + """QTextEdit with pattern completion""" + + def __init__(self, parent=None, **kwargs): + super().__init__(parent) + self.init_pattern_completer(**kwargs) + + def keyPressEvent(self, event): + """Override to handle completion keys""" + if not self._pattern_key_press_event(event): + # Event not handled by pattern completer, process normally + super().keyPressEvent(event) + + +class PatternPlainTextEdit(QPlainTextEdit, PatternCompleter): + """QPlainTextEdit with pattern completion""" + + def __init__(self, parent=None, **kwargs): + super().__init__(parent) + self.init_pattern_completer(**kwargs) + + def keyPressEvent(self, event): + """Override to handle completion keys""" + if not self._pattern_key_press_event(event): + # Event not handled by pattern completer, process normally + super().keyPressEvent(event) + + +class PatternCompleterDelegate(QStyledItemDelegate): + """ + Custom delegate for QTableWidget that uses PatternLineEdit with mixin. + + Usage: + delegate = PatternCompleterDelegate(min_width=200, max_width=600) + delegate.add_completer('@', ['USA', 'Canada', 'Mexico']) + delegate.add_completer('#', ['Python', 'Java', 'C++'], case_sensitive=True) + table.setItemDelegateForColumn(0, delegate) + """ + + def __init__(self, parent=None, **kwargs): + """ + Initialize delegate with global configuration. + + Args: + **kwargs: Global configuration options (same as init_pattern_completer) + """ + super().__init__(parent) + self.completer_configs = {} # pattern -> config dict + self.global_kwargs = kwargs + + def add_completer(self, pattern, completions, **kwargs): + """ + Add a completer pattern for this delegate. + + Args: + pattern: Trigger string (e.g., '@', '#') + completions: List of completion strings + **kwargs: Pattern-specific configuration (overrides global) + """ + self.completer_configs[pattern] = { + "completions": completions, + "kwargs": kwargs, + } + + def update_completions(self, pattern, completions): + """Update the completion list for a specific pattern""" + if pattern in self.completer_configs: + self.completer_configs[pattern]["completions"] = completions + + def update_completer_config(self, pattern, **kwargs): + """Update configuration for a specific pattern""" + if pattern in self.completer_configs: + self.completer_configs[pattern]["kwargs"].update(kwargs) + + def set_global_config(self, **kwargs): + """Update global configuration""" + self.global_kwargs.update(kwargs) + + def createEditor(self, parent, option, index): + """Create a PatternLineEdit when editing starts""" + try: + editor = PatternLineEdit(parent, **self.global_kwargs) + + # Add all configured completers + for pattern, config in self.completer_configs.items(): + editor.add_completer( + pattern, config["completions"], **config.get("kwargs", {}) + ) + + return editor + except Exception as e: + print(f"Error creating editor: {e}") + # Fallback to basic QLineEdit + return QLineEdit(parent) + + def setEditorData(self, editor: PatternLineEdit, index): + """Load data from model into editor""" + try: + if not editor or not index.isValid(): + return + value = index.model().data(index, Qt.ItemDataRole.DisplayRole) + if value is not None: + editor.setText(str(value)) + else: + editor.clear() + except Exception as e: + print(f"Error setting editor data: {e}") + pass + + def setModelData(self, editor: PatternLineEdit, model, index): + """Save data from editor back to model""" + try: + if not editor or not model or not index.isValid(): + return + text = editor.text() + model.setData(index, text, Qt.ItemDataRole.EditRole) + except Exception as e: + print(f"Error setting model data: {e}") + pass + + def destroyEditor(self, editor: PatternLineEdit, index): + """Clean up editor when done""" + try: + if editor and hasattr(editor, "cleanup_pattern_completer"): + editor.cleanup_pattern_completer() + except Exception as e: + print(f"Error destroying editor: {e}") + pass + + try: + super().destroyEditor(editor, index) + except Exception as e: + print(f"Error in super destroyEditor: {e}") + pass From d17e3a69e0aa23def5f9c9fa2e4d4497ad0d20cc Mon Sep 17 00:00:00 2001 From: Ashwola Date: Tue, 21 Oct 2025 18:46:04 +0200 Subject: [PATCH 02/14] adding demo --- .../examples/pattern_completer_demo.py | 393 ++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 src/pymodaq_gui/examples/pattern_completer_demo.py diff --git a/src/pymodaq_gui/examples/pattern_completer_demo.py b/src/pymodaq_gui/examples/pattern_completer_demo.py new file mode 100644 index 00000000..15080c9d --- /dev/null +++ b/src/pymodaq_gui/examples/pattern_completer_demo.py @@ -0,0 +1,393 @@ +""" +PatternCompleter Examples - PyQt6 Auto-completion System + +This demonstrates various use cases for the PatternCompleter mixin class. +All examples are accessible through tabs in a single window. +""" + +from PyQt6.QtWidgets import ( + QApplication, + QMainWindow, + QVBoxLayout, + QWidget, + QLabel, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QHBoxLayout, + QPushButton, +) +from PyQt6.QtCore import QTimer +from pattern_completer import ( + PatternLineEdit, + PatternTextEdit, + PatternPlainTextEdit, + PatternCompleterDelegate, +) +import sys + + +# ============================================================================ +# EXAMPLE 1: Basic Usage with QLineEdit +# ============================================================================ +def create_basic_example(): + """Simple mention system with @ trigger""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 1: Basic Usage")) + layout.addWidget(QLabel("Type @ to mention someone")) + + # Create a line edit with pattern completion + line_edit = PatternLineEdit() + + # Add @ mentions completer + users = ["Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince"] + line_edit.add_completer("@", users) + + line_edit.setPlaceholderText("Type @ to mention someone...") + layout.addWidget(line_edit) + layout.addStretch() + + return widget + + +# ============================================================================ +# EXAMPLE 2: Multiple Patterns +# ============================================================================ +def create_multiple_patterns_example(): + """Text editor with multiple completion triggers""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 2: Multiple Patterns")) + layout.addWidget(QLabel("Try typing @ for mentions, # for hashtags, :: for emojis")) + + text_edit = PatternTextEdit() + + # @ for user mentions + users = ["Alice", "Bob", "Charlie", "Diana"] + text_edit.add_completer("@", users) + + # # for hashtags + tags = ["python", "pyqt6", "programming", "development", "tutorial"] + text_edit.add_completer("#", tags) + + # :: for emojis + emojis = ["smile 😊", "heart ❤️", "thumbsup 👍", "fire 🔥", "rocket 🚀"] + text_edit.add_completer("::", emojis) + + text_edit.setPlaceholderText( + "Try typing:\n @ for mentions\n # for hashtags\n :: for emojis" + ) + layout.addWidget(text_edit) + + return widget + + +# ============================================================================ +# EXAMPLE 3: Global Configuration +# ============================================================================ +def create_global_config_example(): + """Configure appearance and behavior globally""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 3: Global Configuration")) + layout.addWidget( + QLabel("Notice the green border when typing @ (visual indicator enabled)") + ) + + # Initialize with global settings + line_edit = PatternLineEdit( + min_width=200, # Minimum popup width + max_width=600, # Maximum popup width + visual_indicator=True, # Show green border when active + case_sensitive=False, # Case-insensitive by default + auto_resize=True, # Auto-resize popup to fit content + word_wrap=False, # Don't wrap long items + ) + + countries = ["United States", "United Kingdom", "Canada", "Australia", "Germany"] + line_edit.add_completer("@", countries) + + line_edit.setPlaceholderText("Type @ - notice the green border!") + layout.addWidget(line_edit) + layout.addStretch() + + return widget + + +# ============================================================================ +# EXAMPLE 4: Per-Pattern Configuration +# ============================================================================ +def create_per_pattern_config_example(): + """Different settings for each pattern""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 4: Per-Pattern Configuration")) + layout.addWidget(QLabel("@ is case-insensitive, :: is case-sensitive")) + + text_edit = PatternTextEdit() + + # Case-insensitive user mentions with visual indicator + users = ["Alice", "Bob", "Charlie"] + text_edit.add_completer("@", users, visual_indicator=True, case_sensitive=False) + + # Case-sensitive programming keywords + keywords = ["def", "class", "import", "return", "if", "else"] + text_edit.add_completer( + "::", + keywords, + case_sensitive=True, # Exact case matching + min_width=150, + max_width=300, + ) + + text_edit.setPlaceholderText( + "@ mentions are case-insensitive (try '@ali' or '@ALI')\n" + ":: keywords are case-sensitive (try '::def' vs '::DEF')" + ) + layout.addWidget(text_edit) + + return widget + + +# ============================================================================ +# EXAMPLE 5: Word Wrap Example +# ============================================================================ +def create_word_wrap_example(): + """ + Demonstrates word wrap feature for long completion items + """ + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 5: Word Wrap")) + + info_label = QLabel( + "What is Word Wrap?
" + "• word_wrap=False (default): Long items are truncated or need scrolling
" + "• word_wrap=True: Long items wrap to multiple lines in popup

" + "Click buttons below to toggle word wrap and see the difference!" + ) + layout.addWidget(info_label) + + # Button controls + button_layout = QHBoxLayout() + btn_no_wrap = QPushButton("Without Word Wrap") + btn_with_wrap = QPushButton("With Word Wrap") + button_layout.addWidget(btn_no_wrap) + button_layout.addWidget(btn_with_wrap) + layout.addLayout(button_layout) + + line_edit = PatternLineEdit() + + # Long completion items that benefit from word wrap + long_items = [ + "Alice Johnson - Senior Developer at Tech Corp", + "Bob Smith - Product Manager with 10 years experience", + "Charlie Brown - UX Designer specializing in mobile applications", + "Diana Prince - Data Scientist working on machine learning projects", + ] + + # Start without word wrap + line_edit.add_completer("@", long_items, word_wrap=False, max_width=300) + + def set_no_wrap(): + line_edit.update_completer_config("@", word_wrap=False) + line_edit.setPlaceholderText("Type @ - items are truncated (word_wrap=False)") + + def set_with_wrap(): + line_edit.update_completer_config("@", word_wrap=True) + line_edit.setPlaceholderText( + "Type @ - items wrap to multiple lines (word_wrap=True)" + ) + + btn_no_wrap.clicked.connect(set_no_wrap) + btn_with_wrap.clicked.connect(set_with_wrap) + + line_edit.setPlaceholderText("Type @ - items are truncated (word_wrap=False)") + layout.addWidget(line_edit) + layout.addStretch() + + return widget + + +# ============================================================================ +# EXAMPLE 6: Dynamic Updates +# ============================================================================ +def create_dynamic_updates_example(): + """Update completions dynamically""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 6: Dynamic Updates")) + counter_label = QLabel("Completions update every 2 seconds!") + layout.addWidget(counter_label) + + text_edit = PatternTextEdit() + text_edit.add_completer("@", ["Alice", "Bob"]) + text_edit.setPlaceholderText("Type @ to see completions. Watch them change!") + layout.addWidget(text_edit) + + # Simulate dynamic updates + counter = [0] + + def update_users(): + counter[0] += 1 + new_users = [f"User{i}" for i in range(counter[0], counter[0] + 5)] + text_edit.update_completions("@", new_users) + counter_label.setText(f"Update #{counter[0]}: {', '.join(new_users)}") + + timer = QTimer(widget) + timer.timeout.connect(update_users) + timer.start(2000) + + return widget + + +# ============================================================================ +# EXAMPLE 7: Table Widget with Delegate +# ============================================================================ +def create_table_delegate_example(): + """Use PatternCompleter in table cells""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 7: Table Delegate")) + layout.addWidget(QLabel("Double-click cells and type @ for users or # for tags")) + + table = QTableWidget(5, 2) + table.setHorizontalHeaderLabels(["Assigned To", "Tags"]) + + # Create delegate with pattern completion + delegate = PatternCompleterDelegate(min_width=200, visual_indicator=True) + # Add completers for the delegate + users = ["Alice", "Bob", "Charlie", "Diana"] + tags = ["urgent", "review", "bug", "feature", "documentation"] + delegate.add_completer("@", users) + delegate.add_completer("#", tags) + + # Apply delegate to both columns + table.setItemDelegateForColumn(0, delegate) + table.setItemDelegateForColumn(1, delegate) + # It is possible to use different delegates for each column but one has to keep the instance alive (with self. ...) + + # Add some sample data + for i in range(5): + table.setItem(i, 0, QTableWidgetItem(f"Task {i + 1}")) + table.setItem(i, 1, QTableWidgetItem("")) + + layout.addWidget(table) + + return widget + + +# ============================================================================ +# EXAMPLE 8: Code Editor Style +# ============================================================================ +def create_code_editor_example(): + """IDE-like code completion""" + widget = QWidget() + layout = QVBoxLayout(widget) + + layout.addWidget(QLabel("Example 8: Code Editor Style")) + layout.addWidget(QLabel("Python-style completion: :keyword or ::builtin")) + + editor = PatternPlainTextEdit( + min_width=250, max_width=500, case_sensitive=True, auto_resize=True + ) + + # Python keywords + keywords = [ + "def", + "class", + "import", + "from", + "return", + "if", + "else", + "elif", + "for", + "while", + "try", + "except", + "with", + "as", + "pass", + "break", + ] + + # Built-in functions + builtins = [ + "print()", + "len()", + "range()", + "enumerate()", + "zip()", + "map()", + "filter()", + "sorted()", + "sum()", + "max()", + "min()", + ] + + editor.add_completer(":", keywords, case_sensitive=True) + editor.add_completer("::", builtins) + + editor.setPlaceholderText( + "Python-style completion:\n" + " :def → keywords\n" + " ::print → built-in functions\n\n" + "Try typing ':for' or '::pri'" + ) + + layout.addWidget(editor) + + return widget + + +# ============================================================================ +# Main Application with Tabs +# ============================================================================ +class PatternCompleterDemo(QMainWindow): + """Main demo window with all examples in tabs""" + + def __init__(self): + super().__init__() + self.setWindowTitle("PatternCompleter Examples - Interactive Demo") + self.setGeometry(100, 100, 800, 600) + + # Create tab widget + tabs = QTabWidget() + + # Add all example tabs + tabs.addTab(create_basic_example(), "1. Basic") + tabs.addTab(create_multiple_patterns_example(), "2. Multiple Patterns") + tabs.addTab(create_global_config_example(), "3. Global Config") + tabs.addTab(create_per_pattern_config_example(), "4. Per-Pattern Config") + tabs.addTab(create_word_wrap_example(), "5. Word Wrap") + tabs.addTab(create_dynamic_updates_example(), "6. Dynamic Updates") + tabs.addTab(create_table_delegate_example(), "7. Table Delegate") + tabs.addTab(create_code_editor_example(), "8. Code Editor") + + self.setCentralWidget(tabs) + + +# ============================================================================ +# Run Application +# ============================================================================ +if __name__ == "__main__": + app = QApplication(sys.argv) + + # Set application style + app.setStyle("Fusion") + + # Create and show demo window + demo = PatternCompleterDemo() + demo.show() + + sys.exit(app.exec()) From 59e6a71a2ce34b86f255b085ab9cfd77553c2911 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 09:22:13 +0200 Subject: [PATCH 03/14] using qtpy --- src/pymodaq_gui/utils/widgets/pattern_completer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_gui/utils/widgets/pattern_completer.py b/src/pymodaq_gui/utils/widgets/pattern_completer.py index 3155f93a..36d0a03d 100644 --- a/src/pymodaq_gui/utils/widgets/pattern_completer.py +++ b/src/pymodaq_gui/utils/widgets/pattern_completer.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import ( +from qtpy.QtWidgets import ( QWidget, QCompleter, QLineEdit, @@ -7,8 +7,8 @@ QStyledItemDelegate, QListView, # For word wrap ) -from PyQt6.QtCore import Qt, QRect -from PyQt6.QtGui import QStandardItemModel, QStandardItem, QTextCursor, QFontMetrics +from qtpy.QtCore import Qt, QRect +from qtpy.QtGui import QStandardItemModel, QStandardItem, QTextCursor, QFontMetrics class PatternCompleter: From 9bce7f280669bdd940c2a9fea09200d98863e607 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:23:02 +0200 Subject: [PATCH 04/14] Adding delegates and CustomTableWidget using it --- src/pymodaq_gui/utils/widgets/delegates.py | 329 ++++++++++++++++++ .../utils/widgets/managed_table.py | 285 +++++++++++++++ 2 files changed, 614 insertions(+) create mode 100644 src/pymodaq_gui/utils/widgets/delegates.py create mode 100644 src/pymodaq_gui/utils/widgets/managed_table.py diff --git a/src/pymodaq_gui/utils/widgets/delegates.py b/src/pymodaq_gui/utils/widgets/delegates.py new file mode 100644 index 00000000..78b57a42 --- /dev/null +++ b/src/pymodaq_gui/utils/widgets/delegates.py @@ -0,0 +1,329 @@ +""" +Custom delegates for widget editing. +""" + +from qtpy.QtWidgets import ( + QStyledItemDelegate, + QSpinBox, + QDoubleSpinBox, + QComboBox, + QLineEdit, + QCheckBox, + QStyle, +) +from qtpy.QtCore import Qt, QEvent +from pymodaq_gui.utils.widgets.pattern_completer import PatternLineEdit +from pyqtgraph.widgets.SpinBox import SpinBox +from pymodaq_data import Q_ + +class NumericDelegate(QStyledItemDelegate): + """Delegate for numeric input with spinbox.""" + + def __init__(self, min_val=0, max_val=100, decimals=2): + super().__init__() + self.min_val = min_val + self.max_val = max_val + self.decimals = decimals + + def createEditor(self, parent, option, index): + if self.decimals > 0: + editor = QDoubleSpinBox(parent) + editor.setDecimals(self.decimals) + else: + editor = QSpinBox(parent) + + editor.setMinimum(self.min_val) + editor.setMaximum(self.max_val) + return editor + + def setEditorData(self, editor, index): + value = index.model().data(index, Qt.EditRole) + try: + editor.setValue(float(value) if value else 0) + except (ValueError, TypeError): + editor.setValue(0) + + def setModelData(self, editor, model, index): + editor.interpretText() + value = editor.value() + model.setData(index, str(value), Qt.EditRole) + + +class ComboBoxDelegate(QStyledItemDelegate): + """Delegate for dropdown selection.""" + + def __init__(self, items): + super().__init__() + self.items = items + + def createEditor(self, parent, option, index): + editor = QComboBox(parent) + editor.addItems(self.items) + # Auto-commit on selection change + editor.currentIndexChanged.connect(lambda: self._commit_and_close(editor)) + return editor + + def setEditorData(self, editor, index): + value = index.model().data(index, Qt.EditRole) + # Block signals during initial setup to avoid premature commit + editor.blockSignals(True) + idx = editor.findText(value) + if idx >= 0: + editor.setCurrentIndex(idx) + editor.blockSignals(False) + # Show popup immediately + editor.showPopup() + + def setModelData(self, editor, model, index): + value = editor.currentText() + model.setData(index, value, Qt.EditRole) + + def _commit_and_close(self, editor): + """Commit and close editor when selection changes.""" + self.commitData.emit(editor) + self.closeEditor.emit(editor) + + +class ColumnSpecificDelegate(QStyledItemDelegate): + """Delegate that applies different delegates to different columns.""" + + def __init__(self, column_delegates): + """ + Parameters + ---------- + column_delegates : dict + Dictionary mapping column indices to delegate instances. + Example: {0: NumericDelegate(), 1: ComboBoxDelegate(['A', 'B'])} + """ + super().__init__() + self.column_delegates = column_delegates + + def createEditor(self, parent, option, index): + col = index.column() + if col in self.column_delegates: + return self.column_delegates[col].createEditor(parent, option, index) + return super().createEditor(parent, option, index) + + def setEditorData(self, editor, index): + col = index.column() + if col in self.column_delegates: + self.column_delegates[col].setEditorData(editor, index) + else: + super().setEditorData(editor, index) + + def setModelData(self, editor, model, index): + col = index.column() + if col in self.column_delegates: + self.column_delegates[col].setModelData(editor, model, index) + else: + super().setModelData(editor, model, index) + + +class ReadOnlyDelegate(QStyledItemDelegate): + """Delegate that makes cells read-only.""" + + def createEditor(self, parent, option, index): + return None + + +class ColumnReadOnlyDelegate(QStyledItemDelegate): + """Delegate that makes specific columns read-only.""" + + def __init__(self, readonly_columns): + """ + Parameters + ---------- + readonly_columns : list + List of column indices that should be read-only. + """ + super().__init__() + self.readonly_columns = set(readonly_columns) + + def createEditor(self, parent, option, index): + if index.column() in self.readonly_columns: + return None + return super().createEditor(parent, option, index) + + +class PatternCompleterDelegate(QStyledItemDelegate): + """ + Custom delegate for QTableWidget that uses PatternLineEdit with mixin. + + Usage: + delegate = PatternCompleterDelegate(min_width=200, max_width=600) + delegate.add_completer('@', ['USA', 'Canada', 'Mexico']) + delegate.add_completer('#', ['Python', 'Java', 'C++'], case_sensitive=True) + table.setItemDelegateForColumn(0, delegate) + """ + + def __init__(self, parent=None, **kwargs): + """ + Initialize delegate with global configuration. + + Args: + **kwargs: Global configuration options (same as init_pattern_completer) + """ + super().__init__(parent) + self.completer_configs = {} # pattern -> config dict + self.global_kwargs = kwargs + + def add_completer(self, pattern, completions, **kwargs): + """ + Add a completer pattern for this delegate. + + Args: + pattern: Trigger string (e.g., '@', '#') + completions: List of completion strings + **kwargs: Pattern-specific configuration (overrides global) + """ + self.completer_configs[pattern] = { + "completions": completions, + "kwargs": kwargs, + } + + def update_completions(self, pattern, completions): + """Update the completion list for a specific pattern""" + if pattern in self.completer_configs: + self.completer_configs[pattern]["completions"] = completions + + def update_completer_config(self, pattern, **kwargs): + """Update configuration for a specific pattern""" + if pattern in self.completer_configs: + self.completer_configs[pattern]["kwargs"].update(kwargs) + + def set_global_config(self, **kwargs): + """Update global configuration""" + self.global_kwargs.update(kwargs) + + def createEditor(self, parent, option, index): + """Create a PatternLineEdit when editing starts""" + try: + editor = PatternLineEdit(parent, **self.global_kwargs) + + # Add all configured completers + for pattern, config in self.completer_configs.items(): + editor.add_completer( + pattern, config["completions"], **config.get("kwargs", {}) + ) + + return editor + except Exception as e: + print(f"Error creating editor: {e}") + # Fallback to basic QLineEdit + return QLineEdit(parent) + + def setEditorData(self, editor: PatternLineEdit, index): + """Load data from model into editor""" + try: + if not editor or not index.isValid(): + return + value = index.model().data(index, Qt.ItemDataRole.DisplayRole) + if value is not None: + editor.setText(str(value)) + else: + editor.clear() + except Exception as e: + print(f"Error setting editor data: {e}") + pass + + def setModelData(self, editor: PatternLineEdit, model, index): + """Save data from editor back to model""" + try: + if not editor or not model or not index.isValid(): + return + text = editor.text() + model.setData(index, text, Qt.ItemDataRole.EditRole) + except Exception as e: + print(f"Error setting model data: {e}") + pass + + def destroyEditor(self, editor: PatternLineEdit, index): + """Clean up editor when done""" + try: + if editor and hasattr(editor, "cleanup_pattern_completer"): + editor.cleanup_pattern_completer() + except Exception as e: + print(f"Error destroying editor: {e}") + pass + + try: + super().destroyEditor(editor, index) + except Exception as e: + print(f"Error in super destroyEditor: {e}") + pass + + +class SpinBoxDelegate(QStyledItemDelegate): + def __init__(self, parent=None, decimals=4, min=-1e6, max=1e6, units=None): + self.decimals = decimals + self.min = min + self.max = max + self.units = units + super().__init__(parent) + + def createEditor(self, parent, option, index): + doubleSpinBox = SpinBox(parent) + doubleSpinBox.setDecimals(self.decimals) + doubleSpinBox.setMaximum(self.min) + doubleSpinBox.setMaximum(self.max) + if self.units is not None: + doubleSpinBox.setSuffix(self.units) + return doubleSpinBox + + def setEditorData(self, editor: SpinBox, index): + data = index.data() if index.data() else 0 + editor.setValue(Q_(data).magnitude) + # editor.setSuffix(Q_(index.data()).units) + + def setModelData(self, editor: SpinBox, model, index): + model.setData( + index, + f"{editor.value()} {editor.opts['suffix']}" + if self.units is not None + else f"{editor.value()}", + Qt.ItemDataRole.EditRole, + ) + + +class BooleanDelegate(QStyledItemDelegate): + """ + TO implement custom widget editor for cells in a tableview + """ + + def __init__(self, check_symbol="True", cross_symbol="False", parent=None): + super().__init__(parent) + self.check_symbol = check_symbol + self.cross_symbol = cross_symbol + + def createEditor(self, parent, option, index): + boolean = QCheckBox(parent) + return boolean + + def setEditorData(self, editor, index): + value = str(index.data()).lower() in ("true", "1", "yes") + editor.setChecked(value) + + def setModelData(self, editor, model, index): + value = "True" if editor.isChecked() else "False" + model.setData(index, value, Qt.ItemDataRole.EditRole) + + def displayText(self, value, locale): + """Convert boolean to checkmark/cross.""" + is_true = str(value).lower() in ("true", "1", "yes") + return self.check_symbol if is_true else self.cross_symbol + + +class YesNoDelegate(BooleanDelegate): + """Boolean delegate showing 'Yes'/'No' (more readable than True/False).""" + + def __init__(self, parent=None): + super().__init__(check_symbol="Yes", cross_symbol="No", parent=parent) + + +class CheckmarkDelegate(BooleanDelegate): + """Boolean delegate showing ✓/✗ symbols (compact, visual).""" + + def __init__(self, parent=None): + super().__init__(check_symbol="✓", cross_symbol="✗",parent=parent) + + diff --git a/src/pymodaq_gui/utils/widgets/managed_table.py b/src/pymodaq_gui/utils/widgets/managed_table.py new file mode 100644 index 00000000..e50d01fe --- /dev/null +++ b/src/pymodaq_gui/utils/widgets/managed_table.py @@ -0,0 +1,285 @@ +""" +Standalone table widget with row management and validation. +Can be used independently or wrapped by TableParameter. +""" + +from qtpy.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QPushButton, + QStyle, +) +from qtpy.QtCore import Signal + + +class ManagedTableWidget(QWidget): + """Reusable table widget with row controls and delegate support. + + Shape is inferred from data if provided, otherwise uses columns/rows parameters. + """ + + valueChanged = Signal(list) # Emits list of lists + + def __init__( + self, + data=None, + columns=None, + rows=None, + enable_row_controls=True, + max_display_rows=None, + delegate=None, + parent=None, + ): + super().__init__(parent) + + # Handle dict format: {column_name: [values]} + if isinstance(data, dict): + data, columns = self._dict_to_list(data, columns) + + # Infer shape from data if provided + if data is not None and len(data) > 0: + self._initial_data = data + if columns is None: + # Infer column count from first row + num_cols = len(data[0]) if data else 3 + columns = [f"Column {i + 1}" for i in range(num_cols)] + if rows is None: + rows = len(data) + else: + self._initial_data = None + if columns is None: + columns = ["Column 1", "Column 2", "Column 3"] + if rows is None: + rows = 5 + + self.columns = columns if isinstance(columns, list) else list(columns) + self._delegate = delegate + + self._setup_ui(rows, enable_row_controls, max_display_rows or rows) + + def _setup_ui(self, rows, enable_row_controls, max_display_rows): + """Setup the UI components.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + + # Table + self.table = QTableWidget(rows, len(self.columns)) + self.table.setHorizontalHeaderLabels(self.columns) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode( + QHeaderView.ResizeMode.Interactive + ) + self.table.setAlternatingRowColors(False) + + # Height constraint + row_height = self.table.verticalHeader().defaultSectionSize() + header_height = self.table.horizontalHeader().height() + max_height = header_height + (row_height * max_display_rows) + 10 + self.table.setMaximumHeight(max_height) + + # Delegate + if self._delegate: + self.table.setItemDelegate(self._delegate) + + # Signals + self.table.itemChanged.connect(self._on_item_changed) + + layout.addWidget(self.table) + + # Load initial data if provided + if self._initial_data is not None: + self.setValue(self._initial_data) + + # Row controls + if enable_row_controls: + button_layout = QHBoxLayout() + button_layout.setContentsMargins(0, 0, 0, 0) + + btn_add = QPushButton() + btn_add.setIcon( + self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon) + ) + btn_add.setToolTip("Add row") + btn_add.setMaximumWidth(30) + btn_add.clicked.connect(self.addRow) + + btn_remove = QPushButton() + btn_remove.setIcon( + self.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon) + ) + btn_remove.setToolTip("Remove selected row(s)") + btn_remove.setMaximumWidth(30) + btn_remove.clicked.connect(self.removeSelectedRows) + + btn_clear = QPushButton() + btn_clear.setIcon( + self.style().standardIcon(QStyle.StandardPixmap.SP_DialogResetButton) + ) + btn_clear.setToolTip("Clear all") + btn_clear.setMaximumWidth(30) + btn_clear.clicked.connect(self.clearAll) + + button_layout.addWidget(btn_add) + button_layout.addWidget(btn_remove) + button_layout.addWidget(btn_clear) + button_layout.addStretch() + + layout.addLayout(button_layout) + + def _on_item_changed(self): + """Emit value changed signal.""" + self.valueChanged.emit(self.value()) + + @staticmethod + def _dict_to_list(data_dict, columns=None): + """Convert dict format to list of lists. + + Args: + data_dict: Dict with {column_name: [values]} format + columns: Optional column order. If None, uses dict keys order. + + Returns: + tuple: (list_of_lists, column_names) + """ + if not data_dict: + return [], [] + + # Determine column order + if columns is None: + columns = list(data_dict.keys()) + + # Verify all columns exist in dict + for col in columns: + if col not in data_dict: + raise ValueError(f"Column '{col}' not found in data dict") + + # Get max length to handle uneven columns + max_len = max(len(data_dict[col]) for col in columns) + + # Convert to list of lists + list_data = [] + for i in range(max_len): + row = [] + for col in columns: + col_data = data_dict[col] + row.append(str(col_data[i]) if i < len(col_data) else "") + list_data.append(row) + + return list_data, columns + + @staticmethod + def _list_to_dict(list_data, columns): + """Convert list of lists to dict format. + + Args: + list_data: List of lists + columns: Column names + + Returns: + dict: {column_name: [values]} format + """ + result = {col: [] for col in columns} + for row in list_data: + for col_idx, col_name in enumerate(columns): + if col_idx < len(row): + result[col_name].append(row[col_idx]) + else: + result[col_name].append("") + return result + + def setValue(self, data): + """Set table data from list of lists or dict.""" + if isinstance(data, dict): + data, _ = self._dict_to_list(data, self.columns) + + if not data: + return + + self.table.blockSignals(True) + + if len(data) != self.table.rowCount(): + self.table.setRowCount(len(data)) + + for row_idx, row_data in enumerate(data): + for col_idx, cell_value in enumerate(row_data): + if col_idx >= self.table.columnCount(): + break + item = QTableWidgetItem(str(cell_value)) + self.table.setItem(row_idx, col_idx, item) + + self.table.blockSignals(False) + + def value(self): + """Get table data as list of lists.""" + data = [] + for row in range(self.table.rowCount()): + row_data = [] + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + row_data.append(item.text() if item else "") + data.append(row_data) + return data + + def valueAsDict(self): + """Get table data as dict with column names as keys.""" + return self._list_to_dict(self.value(), self.columns) + + def addRow(self, row_data=None): + """Add a new row.""" + self.table.blockSignals(True) + row_count = self.table.rowCount() + self.table.insertRow(row_count) + + if not row_data: + row_data = [""] * self.table.columnCount() + + for col, value in enumerate(row_data): + if col < self.table.columnCount(): + self.table.setItem(row_count, col, QTableWidgetItem(str(value))) + + self.table.blockSignals(False) + self._on_item_changed() + + def removeSelectedRows(self): + """Remove selected rows.""" + selected_rows = set(item.row() for item in self.table.selectedItems()) + if not selected_rows: + return + + self.table.blockSignals(True) + for row in sorted(selected_rows, reverse=True): + self.table.removeRow(row) + self.table.blockSignals(False) + self._on_item_changed() + + def removeRow(self, row_index): + """Remove row by index.""" + if 0 <= row_index < self.table.rowCount(): + self.table.blockSignals(True) + self.table.removeRow(row_index) + self.table.blockSignals(False) + self._on_item_changed() + + def clearAll(self): + """Clear all cell data.""" + self.table.blockSignals(True) + for row in range(self.table.rowCount()): + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + if item: + item.setText("") + else: + self.table.setItem(row, col, QTableWidgetItem("")) + self.table.blockSignals(False) + self._on_item_changed() + + def setDelegate(self, delegate): + """Set item delegate for validation.""" + self._delegate = delegate + if delegate: + self.table.setItemDelegate(delegate) From 2c9c4de547bdfd68df943377d0f74a82591ce021 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:24:26 +0200 Subject: [PATCH 05/14] Changed parameter item, table becomes table_dict and table uses the new table_widget --- .../parameter/pymodaq_ptypes/table.py | 257 ++++++++++-------- .../parameter/pymodaq_ptypes/table_dict.py | 134 +++++++++ 2 files changed, 274 insertions(+), 117 deletions(-) create mode 100644 src/pymodaq_gui/parameter/pymodaq_ptypes/table_dict.py diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py index 08d664b4..76c4ed1a 100644 --- a/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py @@ -1,135 +1,158 @@ -from qtpy import QtWidgets, QtCore -from collections import OrderedDict -from pyqtgraph.parametertree.parameterTypes.basetypes import WidgetParameterItem -from pyqtgraph.parametertree import Parameter +""" +Table parameter wrapping ManagedTableWidget. +""" +from pyqtgraph.parametertree.parameterTypes import WidgetParameterItem, SimpleParameter -class TableWidget(QtWidgets.QTableWidget): - """ - ============== =========================== - *Attributes** **Type** - *valuechanged* instance of pyqt Signal - *QtWidgets* instance of QTableWidget - ============== =========================== - """ +# from managed_table_widget import ManagedTableWidget +from pymodaq_gui.utils.widgets.managed_table import ManagedTableWidget - valuechanged = QtCore.Signal(OrderedDict) - - def __init__(self): - super().__init__() +class TableParameterItem(WidgetParameterItem): + """Widget item wrapping ManagedTableWidget.""" - def get_table_value(self): - """ - Get the contents of the self coursed table. + def __init__(self, param, depth): + super().__init__(param, depth) + self.hideWidget = False - Returns - ------- - data : ordered dictionnary - The getted values dictionnary. - """ - data = OrderedDict([]) - for ind in range(self.rowCount()): - item0 = self.item(ind, 0) - item1 = self.item(ind, 1) - if item0 is not None and item1 is not None: - try: - data[item0.text()] = float(item1.text()) - except Exception: - data[item0.text()] = item1.text() - return data - - def set_table_value(self, data_dict): - """ - Set the data values dictionnary to the custom table. + def makeWidget(self): + """Create ManagedTableWidget.""" + opts = self.param.opts + self.asSubItem = True - =============== ====================== ================================================ - **Parameters** **Type** **Description** - *data_dict* ordered dictionnary the contents to be stored in the custom table - =============== ====================== ================================================ - """ + # Get initial value/data + initial_value = self.param.value() + + widget = ManagedTableWidget( + data=initial_value, + columns=opts.get("columns", None), # Let widget infer if not provided + rows=opts.get("rows", None), + enable_row_controls=opts.get("enable_row_controls", True), + max_display_rows=opts.get("max_display_rows", None), + delegate=opts.get("delegate")() if "delegate" in opts else None, + ) + # widget.setFocusPolicy(Qt.FocusPolicy.NoFocus) + # widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False) + widget.setStyleSheet(""" + ManagedTableWidget { + background: transparent; + border: none; + } + """) + # Connect signals + widget.valueChanged.connect(self.widgetValueChanged) + widget.sigChanged = widget.valueChanged + + self.widget = widget + return widget + + def widgetValueChanged(self, data): + """Handle widget value changes.""" try: - self.setRowCount(len(data_dict)) - self.setColumnCount(2) - for ind, (key, value) in enumerate(data_dict.items()): - item0 = QtWidgets.QTableWidgetItem(key) - item0.setFlags(item0.flags() ^ QtCore.Qt.ItemIsEditable) - if isinstance(value, float): - item1 = QtWidgets.QTableWidgetItem('{:.3e}'.format(value)) - else: - item1 = QtWidgets.QTableWidgetItem(str(value)) - item1.setFlags(item1.flags() ^ QtCore.Qt.ItemIsEditable) - self.setItem(ind, 0, item0) - self.setItem(ind, 1, item1) - # self.valuechanged.emit(data_dict) - + self.param.setValue(data) except Exception as e: - pass + print(f"Error updating parameter: {e}") + def setValue(self, val): + """Set widget value.""" + self.widget.setValue(val) -class TableParameterItem(WidgetParameterItem): + def value(self): + """Get widget value.""" + return self.widget.value() - # def treeWidgetChanged(self): - # """ - # Check for changement in the Widget tree. - # """ - # # # TODO: fix so that superclass method can be called - # # # (WidgetParameter should just natively support this style) - # # WidgetParameterItem.treeWidgetChanged(self) - # self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) - # self.treeWidget().setItemWidget(self.subItem, 0, self.widget) - # - # # for now, these are copied from ParameterItem.treeWidgetChanged - # self.setHidden(not self.param.opts.get('visible', True)) - # self.setExpanded(self.param.opts.get('expanded', True)) + def optsChanged(self, param, opts): + """Handle option changes.""" + super().optsChanged(param, opts) - def makeWidget(self): - """ - Make and initialize an instance of TableWidget. + if "value" in opts: + self.widget.setValue(opts["value"]) - Returns - ------- - table : instance of TableWidget. - The initialized table. + if "delegate" in opts: + delegate = opts["delegate"]() + self.widget.setDelegate(delegate) - See Also - -------- - TableWidget - """ - self.asSubItem = True - self.hideWidget = False - opts = self.param.opts - w = TableWidget() - if 'tip' in opts: - w.setToolTip(opts['tip']) - w.setColumnCount(2) - if 'header' in opts: - w.setHorizontalHeaderLabels(self.param.opts['header']) - if 'height' not in opts: - opts['height'] = 200 - w.setMaximumHeight(opts['height']) - w.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) - # self.table.setReadOnly(self.param.opts.get('readonly', False)) - w.value = w.get_table_value - w.setValue = w.set_table_value - w.sigChanged = w.itemChanged - return w - - -class TableParameter(Parameter): - """ - =============== ================================= - **Attributes** **Type** - *itemClass* instance of TableParameterItem - *Parameter* instance of pyqtgraph parameter - =============== ================================= + +class TableParameter(SimpleParameter): + """Table parameter with row management and validation. + + Can be initialized with: + - data: Infers shape from data + - columns + rows: Empty table with specified shape + - value: Legacy support (same as data) """ - itemClass = TableParameterItem - """Editable string; displayed as large text box in the tree.""" - # def __init(self): - # super(TableParameter,self).__init__() + itemClass = TableParameterItem - def setValue(self, value): - self.opts['value'] = value - self.sigValueChanged.emit(self, value) + def __init__(self, **opts): + # Priority: data > value > columns/rows + if "data" in opts: + opts["value"] = opts.pop("data") + + if "value" not in opts: + rows = opts.get("rows", 5) + columns = opts.get("columns", ["Column 1", "Column 2", "Column 3"]) + opts["value"] = [[""] * len(columns) for _ in range(rows)] + + opts["expanded"] = False + super().__init__(**opts) + + def valueIsDefault(self): + return True + + def hasDefault(self): + return False + + def setOpts(self, **opts): + """Override to trigger optsChanged.""" + super().setOpts(**opts) + for item in self.items: + if hasattr(item, "optsChanged"): + item.optsChanged(self, opts) + + def setValue(self, value, blockSignal=None): + """Override to update widget.""" + super().setValue(value, blockSignal=blockSignal) + for item in self.items: + if hasattr(item, "setValue"): + item.setValue(value) + + def addRow(self, row_data=None): + """Add row programmatically.""" + current_value = self.value() + if row_data is None: + columns = self.opts.get("columns", ["Column 1", "Column 2", "Column 3"]) + row_data = [""] * len(columns) + new_value = current_value + [row_data] + self.setValue(new_value) + + def removeRow(self, row_index): + """Remove row by index.""" + current_value = self.value() + if 0 <= row_index < len(current_value): + new_value = current_value[:row_index] + current_value[row_index + 1 :] + self.setValue(new_value) + + def clearRows(self): + """Clear all row data.""" + current_value = self.value() + new_value = [[""] * len(row) for row in current_value] + self.setValue(new_value) + + def valueAsDict(self): + """Get value as dict with column names as keys. + + Returns: + dict: {column_name: [column_values]} + """ + columns = self.opts.get("columns", ["Column 1", "Column 2", "Column 3"]) + data = self.value() + + result = {col: [] for col in columns} + for row in data: + for col_idx, col_name in enumerate(columns): + if col_idx < len(row): + result[col_name].append(row[col_idx]) + else: + result[col_name].append("") + return result diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/table_dict.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/table_dict.py new file mode 100644 index 00000000..8bc54ee8 --- /dev/null +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/table_dict.py @@ -0,0 +1,134 @@ +from qtpy import QtWidgets, QtCore +from collections import OrderedDict +from pyqtgraph.parametertree.parameterTypes.basetypes import WidgetParameterItem +from pyqtgraph.parametertree import Parameter + + +class TableWidget(QtWidgets.QTableWidget): + """ + ============== =========================== + *Attributes** **Type** + *valuechanged* instance of pyqt Signal + *QtWidgets* instance of QTableWidget + ============== =========================== + """ + + valuechanged = QtCore.Signal(OrderedDict) + + def __init__(self): + super().__init__() + + def get_table_value(self): + """ + Get the contents of the self coursed table. + + Returns + ------- + data : ordered dictionnary + The getted values dictionnary. + """ + data = OrderedDict([]) + for ind in range(self.rowCount()): + item0 = self.item(ind, 0) + item1 = self.item(ind, 1) + if item0 is not None and item1 is not None: + try: + data[item0.text()] = float(item1.text()) + except Exception: + data[item0.text()] = item1.text() + return data + + def set_table_value(self, data_dict): + """ + Set the data values dictionnary to the custom table. + + =============== ====================== ================================================ + **Parameters** **Type** **Description** + *data_dict* ordered dictionnary the contents to be stored in the custom table + =============== ====================== ================================================ + """ + try: + self.setRowCount(len(data_dict)) + self.setColumnCount(2) + for ind, (key, value) in enumerate(data_dict.items()): + item0 = QtWidgets.QTableWidgetItem(key) + item0.setFlags(item0.flags() ^ QtCore.Qt.ItemIsEditable) + if isinstance(value, float): + item1 = QtWidgets.QTableWidgetItem("{:.3e}".format(value)) + else: + item1 = QtWidgets.QTableWidgetItem(str(value)) + item1.setFlags(item1.flags() ^ QtCore.Qt.ItemIsEditable) + self.setItem(ind, 0, item0) + self.setItem(ind, 1, item1) + # self.valuechanged.emit(data_dict) + + except Exception as e: + pass + + +class TableParameterItem(WidgetParameterItem): + # def treeWidgetChanged(self): + # """ + # Check for changement in the Widget tree. + # """ + # # # TODO: fix so that superclass method can be called + # # # (WidgetParameter should just natively support this style) + # # WidgetParameterItem.treeWidgetChanged(self) + # self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) + # self.treeWidget().setItemWidget(self.subItem, 0, self.widget) + # + # # for now, these are copied from ParameterItem.treeWidgetChanged + # self.setHidden(not self.param.opts.get('visible', True)) + # self.setExpanded(self.param.opts.get('expanded', True)) + + def makeWidget(self): + """ + Make and initialize an instance of TableWidget. + + Returns + ------- + table : instance of TableWidget. + The initialized table. + + See Also + -------- + TableWidget + """ + self.asSubItem = True + self.hideWidget = False + opts = self.param.opts + w = TableWidget() + if "tip" in opts: + w.setToolTip(opts["tip"]) + w.setColumnCount(2) + if "header" in opts: + w.setHorizontalHeaderLabels(self.param.opts["header"]) + if "height" not in opts: + opts["height"] = 200 + w.setMaximumHeight(opts["height"]) + w.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) + # self.table.setReadOnly(self.param.opts.get('readonly', False)) + w.value = w.get_table_value + w.setValue = w.set_table_value + w.sigChanged = w.itemChanged + return w + + +class DoubleColumnTableParameter(Parameter): + """ + =============== ================================= + **Attributes** **Type** + *itemClass* instance of TableParameterItem + *Parameter* instance of pyqtgraph parameter + =============== ================================= + """ + + itemClass = TableParameterItem + """Editable string; displayed as large text box in the tree.""" + + # def __init(self): + # super(TableParameter,self).__init__() + + def setValue(self, value): + self.opts["value"] = value + self.sigValueChanged.emit(self, value) From 18fb5995a280130d5eaecf0766a1d893d9bdbff2 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:24:41 +0200 Subject: [PATCH 06/14] updating parameter_ex --- src/pymodaq_gui/examples/parameter_ex.py | 71 +++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_gui/examples/parameter_ex.py b/src/pymodaq_gui/examples/parameter_ex.py index 80840523..866a515b 100644 --- a/src/pymodaq_gui/examples/parameter_ex.py +++ b/src/pymodaq_gui/examples/parameter_ex.py @@ -10,6 +10,16 @@ from pymodaq_gui.utils.utils import create_nested_menu from pymodaq_gui.parameter.pymodaq_ptypes import GroupParameter, registerParameterType from pymodaq_gui.managers.parameter_manager import ParameterManager +from pymodaq_gui.utils.widgets.delegates import ( + NumericDelegate, + ComboBoxDelegate, + ColumnSpecificDelegate, + PatternCompleterDelegate, + BooleanDelegate, + SpinBoxDelegate, +) +from pyqtgraph.parametertree import ParameterTree, Parameter + class ScalableGroup(GroupParameter): def __init__(self, **opts): @@ -23,6 +33,64 @@ def addNew(self, full_path:tuple): # Need to register a new type to properly trigger addNew registerParameterType('groupedit', ScalableGroup, override=True) +def create_comprehensive_table(): + """ + Create a table showcasing all delegate types: + - Column 0: Plain text (no delegate) + - Column 1: Numeric spinbox (0-100) + - Column 2: ComboBox dropdown + - Column 3: Pattern completer (@mentions, #tags) + - Column 4: Numeric with decimals (0-10, 2 decimals) + - Column 5: Boolean checkbox + """ + + # Setup pattern completer delegate + pattern_delegate = PatternCompleterDelegate() + pattern_delegate.add_completer("@", ["Alice", "Bob", "Charlie", "David", "Eve"]) + pattern_delegate.add_completer( + "#", ["important", "urgent", "review", "done", "todo"] + ) + + # Create column-specific delegate + delegate = ColumnSpecificDelegate( + { + 1: NumericDelegate(min_val=0, max_val=100, decimals=0), + 2: ComboBoxDelegate(["Type A", "Type B", "Type C", "Type D"]), + 3: pattern_delegate, + 4: SpinBoxDelegate(decimals=4, min=-1e6, max=1e6, units="s"), + 5: BooleanDelegate(), + } + ) + + table_params = { + "title": "Multi-Delegate Table", + "name": "multi_delegate_table", + "type": "table", + "columns": [ + "Name (Plain)", + "Score (NumericDelegate 0-100)", + "Status (ComboBoxDelegate)", + "Tags (PatternDelegate @/#)", + "Rating (SpinBoxDelegate 0-10)", + "Checked (BooleanDelegate True/False)", + ], + "rows": 5, + "delegate": lambda: delegate, + "max_display_rows": 6, + "value": [ + ["Sample 1", "75", "Type A", "@Alice #important", "8.5s", True], + ["Sample 2", "50", "Type B", "@Bob #urgent", "7.2s", True], + ["Sample 3", "90", "Type C", "@Charlie #review", "9.1s", False], + ["Sample 4", "60", "Type A", "", "6.8s"], + ["Sample 5", "", "", "", "", ""], + ], + "enable_row_controls": True, + } + + + return table_params + # return Parameter.create(name="params", type="group", children=params) + class ParameterEx(ParameterManager): params = [ @@ -122,8 +190,9 @@ class ParameterEx(ParameterManager): ]}, {'title': 'Tables:', 'name': 'tables', 'type': 'group', 'children': [ - {'title': 'Table widget', 'name': 'tablewidget', 'type': 'table', 'value': + {'title': 'Table widget', 'name': 'tablewidget', 'type': 'table_dict', 'value': OrderedDict(key1='data1', key2=24), 'header': ['keys', 'limits'], 'height': 100}, + create_comprehensive_table(), {'title': 'Table view', 'name': 'tabular_table', 'type': 'table_view', 'delegate': table.SpinBoxDelegate, 'menu': True, 'value': table.TableModel([[0.1, 0.2, 0.3]], ['value1', 'value2', 'value3']), From 8b5bd8ba27168bacb172abc02b531a10141680db Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:25:10 +0200 Subject: [PATCH 07/14] cleaning pattern_completer --- .../utils/widgets/pattern_completer.py | 109 ------------------ 1 file changed, 109 deletions(-) diff --git a/src/pymodaq_gui/utils/widgets/pattern_completer.py b/src/pymodaq_gui/utils/widgets/pattern_completer.py index 36d0a03d..aa49fcbe 100644 --- a/src/pymodaq_gui/utils/widgets/pattern_completer.py +++ b/src/pymodaq_gui/utils/widgets/pattern_completer.py @@ -4,7 +4,6 @@ QLineEdit, QTextEdit, QPlainTextEdit, - QStyledItemDelegate, QListView, # For word wrap ) from qtpy.QtCore import Qt, QRect @@ -492,111 +491,3 @@ def keyPressEvent(self, event): if not self._pattern_key_press_event(event): # Event not handled by pattern completer, process normally super().keyPressEvent(event) - - -class PatternCompleterDelegate(QStyledItemDelegate): - """ - Custom delegate for QTableWidget that uses PatternLineEdit with mixin. - - Usage: - delegate = PatternCompleterDelegate(min_width=200, max_width=600) - delegate.add_completer('@', ['USA', 'Canada', 'Mexico']) - delegate.add_completer('#', ['Python', 'Java', 'C++'], case_sensitive=True) - table.setItemDelegateForColumn(0, delegate) - """ - - def __init__(self, parent=None, **kwargs): - """ - Initialize delegate with global configuration. - - Args: - **kwargs: Global configuration options (same as init_pattern_completer) - """ - super().__init__(parent) - self.completer_configs = {} # pattern -> config dict - self.global_kwargs = kwargs - - def add_completer(self, pattern, completions, **kwargs): - """ - Add a completer pattern for this delegate. - - Args: - pattern: Trigger string (e.g., '@', '#') - completions: List of completion strings - **kwargs: Pattern-specific configuration (overrides global) - """ - self.completer_configs[pattern] = { - "completions": completions, - "kwargs": kwargs, - } - - def update_completions(self, pattern, completions): - """Update the completion list for a specific pattern""" - if pattern in self.completer_configs: - self.completer_configs[pattern]["completions"] = completions - - def update_completer_config(self, pattern, **kwargs): - """Update configuration for a specific pattern""" - if pattern in self.completer_configs: - self.completer_configs[pattern]["kwargs"].update(kwargs) - - def set_global_config(self, **kwargs): - """Update global configuration""" - self.global_kwargs.update(kwargs) - - def createEditor(self, parent, option, index): - """Create a PatternLineEdit when editing starts""" - try: - editor = PatternLineEdit(parent, **self.global_kwargs) - - # Add all configured completers - for pattern, config in self.completer_configs.items(): - editor.add_completer( - pattern, config["completions"], **config.get("kwargs", {}) - ) - - return editor - except Exception as e: - print(f"Error creating editor: {e}") - # Fallback to basic QLineEdit - return QLineEdit(parent) - - def setEditorData(self, editor: PatternLineEdit, index): - """Load data from model into editor""" - try: - if not editor or not index.isValid(): - return - value = index.model().data(index, Qt.ItemDataRole.DisplayRole) - if value is not None: - editor.setText(str(value)) - else: - editor.clear() - except Exception as e: - print(f"Error setting editor data: {e}") - pass - - def setModelData(self, editor: PatternLineEdit, model, index): - """Save data from editor back to model""" - try: - if not editor or not model or not index.isValid(): - return - text = editor.text() - model.setData(index, text, Qt.ItemDataRole.EditRole) - except Exception as e: - print(f"Error setting model data: {e}") - pass - - def destroyEditor(self, editor: PatternLineEdit, index): - """Clean up editor when done""" - try: - if editor and hasattr(editor, "cleanup_pattern_completer"): - editor.cleanup_pattern_completer() - except Exception as e: - print(f"Error destroying editor: {e}") - pass - - try: - super().destroyEditor(editor, index) - except Exception as e: - print(f"Error in super destroyEditor: {e}") - pass From 6c42c187f4bc04db91bb1b2a740be9c6091a295c Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:29:17 +0200 Subject: [PATCH 08/14] adding DoubleColumnTableParameter --- .../parameter/pymodaq_ptypes/__init__.py | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/__init__.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/__init__.py index 9b76874d..1ebef6f7 100644 --- a/src/pymodaq_gui/parameter/pymodaq_ptypes/__init__.py +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/__init__.py @@ -1,11 +1,15 @@ from pyqtgraph.parametertree.parameterTypes.basetypes import ( - SimpleParameter, GroupParameter, GroupParameterItem) # to be imported from elsewhere + SimpleParameter, + GroupParameter, + GroupParameterItem, +) # to be imported from elsewhere from .bool import BoolPushParameter from .pixmap import PixmapParameter, PixmapCheckParameter from .slide import SliderSpinBox, SliderParameter from .led import LedPushParameter, LedParameter from .date import DateParameter, DateTimeParameter, TimeParameter from .list import ListParameter +from .table_dict import DoubleColumnTableParameter from .table import TableParameter from .tableview import TableViewParameter, TableViewCustom from .itemselect import ItemSelectParameter @@ -13,29 +17,34 @@ from .text import PlainTextPbParameter from .numeric import NumericParameter from .group import GroupParameter -from pyqtgraph.parametertree.Parameter import registerParameterType, registerParameterItemType, Parameter +from .text_pattern import PatternParameter +from pyqtgraph.parametertree.Parameter import ( + registerParameterType, + registerParameterItemType, + Parameter, +) -registerParameterType('float', NumericParameter, override=True) -registerParameterType('int', NumericParameter, override=True) -registerParameterType('bool_push', BoolPushParameter, override=True) -registerParameterType('pixmap', PixmapParameter, override=True) -registerParameterType('pixmap_check', PixmapCheckParameter, override=True) +registerParameterType("float", NumericParameter, override=True) +registerParameterType("int", NumericParameter, override=True) +registerParameterType("bool_push", BoolPushParameter, override=True) +registerParameterType("pixmap", PixmapParameter, override=True) +registerParameterType("pixmap_check", PixmapCheckParameter, override=True) -registerParameterType('slide', SliderParameter, override=True) +registerParameterType("slide", SliderParameter, override=True) -registerParameterType('led', LedParameter, override=True) -registerParameterType('led_push', LedPushParameter, override=True) -registerParameterType('date', DateParameter, override=True) -registerParameterType('date_time', DateTimeParameter, override=True) -registerParameterType('time', TimeParameter, override=True) +registerParameterType("led", LedParameter, override=True) +registerParameterType("led_push", LedPushParameter, override=True) +registerParameterType("date", DateParameter, override=True) +registerParameterType("date_time", DateTimeParameter, override=True) +registerParameterType("time", TimeParameter, override=True) -registerParameterType('list', ListParameter, override=True) -registerParameterType('table', TableParameter, override=True) +registerParameterType("list", ListParameter, override=True) +registerParameterType("table_dict", DoubleColumnTableParameter, override=True) +registerParameterType("table", TableParameter, override=True) -registerParameterType('table_view', TableViewParameter, override=True) -registerParameterType('itemselect', ItemSelectParameter, override=True) -registerParameterType('browsepath', FileDirParameter, override=True) -registerParameterType('text_pb', PlainTextPbParameter, override=True) - -registerParameterType('text_pb', PlainTextPbParameter, override=True) -registerParameterType('group', GroupParameter, override=True) +registerParameterType("table_view", TableViewParameter, override=True) +registerParameterType("itemselect", ItemSelectParameter, override=True) +registerParameterType("browsepath", FileDirParameter, override=True) +registerParameterType("text_pb", PlainTextPbParameter, override=True) +registerParameterType("text_pattern", PatternParameter) +registerParameterType("group", GroupParameter, override=True) \ No newline at end of file From c283751510e8033ec4cc467db0842af175aba6f4 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:43:30 +0200 Subject: [PATCH 09/14] adding text_pattern --- .../parameter/pymodaq_ptypes/text_pattern.py | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py new file mode 100644 index 00000000..9fb037a6 --- /dev/null +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py @@ -0,0 +1,484 @@ +""" +Pattern Completer Parameter Item for pyqtgraph + +This module provides a custom parameter item that integrates pattern completion +functionality into pyqtgraph's parameter tree system. + +Features: +- Multiple trigger patterns (e.g., '@' for users, '#' for tags) +- Dynamic updates via convenience methods or direct setOpts +- Configurable completion behavior (case sensitivity, popup size, etc.) +- Full integration with pyqtgraph's parameter system +""" + +from pyqtgraph.parametertree import Parameter +from pyqtgraph.parametertree.parameterTypes import TextParameterItem, SimpleParameter +from pymodaq_gui.utils.widgets.pattern_completer import PatternPlainTextEdit + + +class PatternParameterItem(TextParameterItem): + """ + A parameter item that provides pattern completion in the text editor. + + This extends pyqtgraph's TextParameterItem to add pattern-based autocompletion. + You can configure multiple completion patterns (e.g., '@' for mentions, '#' for tags). + """ + + def __init__(self, param, depth): + super().__init__(param, depth) + + def makeWidget(self): + """Create the pattern-enabled line edit widget""" + self.hideWidget = False + self.asSubItem = True + completer_config = self.param.opts.get("completer_config", {}) + + # Create the widget with global configuration + widget = PatternPlainTextEdit(**completer_config) + + # Add pattern completers + patterns = self.param.opts.get("patterns", {}) + for pattern, completions in patterns.items(): + # Per-pattern config can override global config + pattern_config = self.param.opts.get(f"pattern_config_{pattern}", {}) + widget.add_completer(pattern, completions, **pattern_config) + + widget.value = widget.toPlainText + widget.setValue = widget.setPlainText + widget.sigChanged = widget.textChanged + + self.widget = widget + + return widget + + def optsChanged(self, param, opts): + """Handle parameter option changes - this is triggered by setOpts""" + super().optsChanged(param, opts) + + # Update patterns if changed + if "patterns" in opts and hasattr(self, "widget") and self.widget is not None: + patterns = opts["patterns"] + + # First, handle removed patterns (in widget but not in new opts) + if hasattr(self.widget, "completers"): + widget_patterns = set(self.widget.completers.keys()) + opts_patterns = set(patterns.keys()) + removed_patterns = widget_patterns - opts_patterns + + for pattern in removed_patterns: + del self.widget.completers[pattern] + + # Then, add new patterns or update existing ones + for pattern, completions in patterns.items(): + # Check if pattern exists in widget's completers + if ( + hasattr(self.widget, "completers") + and pattern in self.widget.completers + ): + # Pattern exists, just update completions + self.widget.update_completions(pattern, completions) + else: + # New pattern, add it + pattern_config = self.param.opts.get( + f"pattern_config_{pattern}", {} + ) + self.widget.add_completer(pattern, completions, **pattern_config) + + # Update completer config if changed + if ( + "completer_config" in opts + and hasattr(self, "widget") + and self.widget is not None + ): + self.widget.set_global_config(**opts["completer_config"]) + + +class PatternParameter(SimpleParameter): + """ + Parameter class for pattern completion. + + This parameter type allows text input with pattern-based autocompletion. + Uses pyqtgraph's setOpts for all updates. + + Users can update patterns in two ways: + 1. Convenience methods: update_completions(), add_pattern(), remove_pattern() + 2. Direct setOpts: parameter.setOpts(patterns=new_patterns_dict) + """ + + itemClass = PatternParameterItem + + def __init__(self, **opts): + # Set default options + opts.setdefault("patterns", {}) + opts.setdefault("completer_config", {}) + super().__init__(**opts) + + def add_pattern(self, pattern, completions, **config): + """ + Add or update a completion pattern after initialization. + + Args: + pattern (str): Trigger string (e.g., '@', '#') + completions (list): List of completion strings + **config: Pattern-specific configuration + """ + # Create new patterns dict with the added/updated pattern + new_patterns = dict(self.opts.get("patterns", {})) + new_patterns[pattern] = list( + completions + ) # Use list() to create a new list object + + # Build opts dict for setOpts + opts_to_set = {"patterns": new_patterns} + + # Add pattern-specific config if provided + if config: + opts_to_set[f"pattern_config_{pattern}"] = config + + # Let setOpts handle everything + self.setOpts(**opts_to_set) + + def update_completions(self, pattern, completions): + """ + Update the completion list for a specific pattern. + + Args: + pattern (str): The pattern to update + completions (list): New list of completion strings + """ + if pattern not in self.opts.get("patterns", {}): + print( + f"Warning: Pattern '{pattern}' not found. Use add_pattern() to add it first." + ) + return + + # Create new patterns dict with the updated pattern + new_patterns = dict(self.opts.get("patterns", {})) + new_patterns[pattern] = list( + completions + ) # Use list() to create a new list object + + # Let setOpts handle everything + self.setOpts(patterns=new_patterns) + + def remove_pattern(self, pattern): + """ + Remove a completion pattern. + + Args: + pattern (str): The pattern to remove + """ + if pattern not in self.opts.get("patterns", {}): + return + + # Create new patterns dict without the removed pattern + new_patterns = dict(self.opts.get("patterns", {})) + del new_patterns[pattern] + + # Let setOpts handle everything (optsChanged will handle widget cleanup) + self.setOpts(patterns=new_patterns) + + def set_completer_config(self, **config): + """ + Update global completer configuration. + + Args: + **config: Configuration options (min_width, max_width, etc.) + """ + # Create new config dict with updates + new_config = dict(self.opts.get("completer_config", {})) + new_config.update(config) + + # Let setOpts handle everything + self.setOpts(completer_config=new_config) + + +# Example usage and demo +if __name__ == "__main__": + import sys + from qtpy.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QPushButton, + QLabel, + QGroupBox, + ) + from pyqtgraph.parametertree import ParameterTree + + app = QApplication(sys.argv) + + # Create a parameter tree with pattern parameters + params = [ + { + "name": "Text Editing", + "type": "group", + "children": [ + { + "name": "Message", + "type": "text_pattern", + "value": "", + "patterns": { + "@": ["alice", "bob", "charlie"], + "#": ["python", "javascript", "cpp"], + }, + "completer_config": { + "min_width": 200, + "max_width": 400, + "case_sensitive": False, + "visual_indicator": True, + }, + }, + { + "name": "Tags", + "type": "text_pattern", + "value": "", + "patterns": {"#": ["urgent", "todo", "done", "in-progress"]}, + "completer_config": {"min_width": 150, "case_sensitive": False}, + }, + ], + }, + ] + + # Create parameter tree + p = Parameter.create(name="params", type="group", children=params) + tree = ParameterTree() + tree.setParameters(p, showTop=False) + + # Get the Message parameter for examples + message_param = p.child("Text Editing").child("Message") + + # ======================================================================== + # EXAMPLE 1: Using convenience methods + # ======================================================================== + def example_convenience_update(): + """Update completions using the convenience method""" + print("\n" + "=" * 60) + print("EXAMPLE 1: Update using convenience method") + print("=" * 60) + new_completions = ["alice", "bob", "charlie", "david", "eve"] + message_param.update_completions("@", new_completions) + print(f"✓ Updated @ completions: {new_completions}") + print("=" * 60 + "\n") + + def example_convenience_add(): + """Add pattern using the convenience method""" + print("\n" + "=" * 60) + print("EXAMPLE 2: Add pattern using convenience method") + print("=" * 60) + message_param.add_pattern("$", ["dollar", "euro", "pound", "yen"]) + print(f"✓ Added $ pattern with currency completions") + print("=" * 60 + "\n") + + def example_convenience_remove(): + """Remove pattern using the convenience method""" + print("\n" + "=" * 60) + print("EXAMPLE 3: Remove pattern using convenience method") + print("=" * 60) + message_param.remove_pattern("#") + print(f"✓ Removed # pattern") + print("=" * 60 + "\n") + + # ======================================================================== + # EXAMPLE 2: Using direct setOpts (standard pyqtgraph approach) + # ======================================================================== + def example_setopts_update(): + """Update completions using setOpts directly""" + print("\n" + "=" * 60) + print("EXAMPLE 4: Update using setOpts directly") + print("=" * 60) + + # Get current patterns + current_patterns = message_param.opts.get("patterns", {}) + + # Create new patterns dict with updated completions + new_patterns = dict(current_patterns) + new_patterns["@"] = ["alice", "bob", "charlie", "frank", "grace"] + + # Apply changes + message_param.setOpts(patterns=new_patterns) + print(f"✓ Updated @ completions using setOpts") + print(f" Code: message_param.setOpts(patterns=new_patterns)") + print("=" * 60 + "\n") + + def example_setopts_add(): + """Add pattern using setOpts directly""" + print("\n" + "=" * 60) + print("EXAMPLE 5: Add pattern using setOpts directly") + print("=" * 60) + + # Get current patterns and add new one + current_patterns = message_param.opts.get("patterns", {}) + new_patterns = dict(current_patterns) + new_patterns[":"] = ["smile", "heart", "fire", "star"] + + # Apply changes + message_param.setOpts(patterns=new_patterns) + print(f"✓ Added : pattern using setOpts") + print(f" Code: message_param.setOpts(patterns=new_patterns)") + print("=" * 60 + "\n") + + def example_setopts_remove(): + """Remove pattern using setOpts directly""" + print("\n" + "=" * 60) + print("EXAMPLE 6: Remove pattern using setOpts directly") + print("=" * 60) + + # Get current patterns and remove one + current_patterns = message_param.opts.get("patterns", {}) + new_patterns = dict(current_patterns) + if "$" in new_patterns: + del new_patterns["$"] + + # Apply changes + message_param.setOpts(patterns=new_patterns) + print(f"✓ Removed $ pattern using setOpts") + print(f" Code: message_param.setOpts(patterns=new_patterns)") + print("=" * 60 + "\n") + + def example_setopts_config(): + """Update config using setOpts directly""" + print("\n" + "=" * 60) + print("EXAMPLE 7: Update config using setOpts directly") + print("=" * 60) + + # Get current config and update it + current_config = message_param.opts.get("completer_config", {}) + new_config = dict(current_config) + new_config["min_width"] = 300 + new_config["max_width"] = 600 + + # Apply changes + message_param.setOpts(completer_config=new_config) + print(f"✓ Updated completer config using setOpts") + print(f" Code: message_param.setOpts(completer_config=new_config)") + print("=" * 60 + "\n") + + # ======================================================================== + # EXAMPLE 3: Programmatic/automated updates + # ======================================================================== + def example_auto_update(): + """Example of automated updates (e.g., from a timer or callback)""" + print("\n" + "=" * 60) + print("EXAMPLE 8: Automated update simulation") + print("=" * 60) + + # Simulate loading user list from a database + users_from_db = [ + "alice", + "bob", + "charlie", + "david", + "eve", + "frank", + "grace", + "henry", + ] + + current_patterns = message_param.opts.get("patterns", {}) + new_patterns = dict(current_patterns) + new_patterns["@"] = users_from_db + message_param.setOpts(patterns=new_patterns) + + print(f"✓ Updated @ with {len(users_from_db)} users from 'database'") + print("=" * 60 + "\n") + + # Create window + window = QWidget() + main_layout = QVBoxLayout() + + # Add instructions + instructions = QLabel( + "Pattern Completer Parameter - Interactive Demo

" + "How to use:
" + "• Type '@' in Message field to see user completions
" + "• Type '#' to see tag completions
" + "• Click buttons below to dynamically update patterns

" + "Two approaches available:
" + "1. Convenience methods: update_completions(), add_pattern(), remove_pattern()
" + "2. Direct setOpts(): Standard pyqtgraph approach - message_param.setOpts(patterns=...)" + ) + instructions.setWordWrap(True) + main_layout.addWidget(instructions) + + # Add parameter tree + main_layout.addWidget(tree) + + # Group 1: Convenience Methods + group1 = QGroupBox("Method 1: Convenience Methods") + group1_layout = QVBoxLayout() + + btn1_1 = QPushButton("Update @ completions (add david, eve)") + btn1_1.clicked.connect(example_convenience_update) + group1_layout.addWidget(btn1_1) + + btn1_2 = QPushButton("Add $ pattern (currencies)") + btn1_2.clicked.connect(example_convenience_add) + group1_layout.addWidget(btn1_2) + + btn1_3 = QPushButton("Remove # pattern") + btn1_3.clicked.connect(example_convenience_remove) + group1_layout.addWidget(btn1_3) + + group1.setLayout(group1_layout) + main_layout.addWidget(group1) + + # Group 2: Direct setOpts + group2 = QGroupBox("Method 2: Direct setOpts (Standard PyQtGraph)") + group2_layout = QVBoxLayout() + + btn2_1 = QPushButton("Update @ with setOpts (add frank, grace)") + btn2_1.clicked.connect(example_setopts_update) + group2_layout.addWidget(btn2_1) + + btn2_2 = QPushButton("Add : pattern with setOpts (emojis)") + btn2_2.clicked.connect(example_setopts_add) + group2_layout.addWidget(btn2_2) + + btn2_3 = QPushButton("Remove $ with setOpts") + btn2_3.clicked.connect(example_setopts_remove) + group2_layout.addWidget(btn2_3) + + btn2_4 = QPushButton("Update config with setOpts (wider popup)") + btn2_4.clicked.connect(example_setopts_config) + group2_layout.addWidget(btn2_4) + + group2.setLayout(group2_layout) + main_layout.addWidget(group2) + + # Group 3: Advanced + group3 = QGroupBox("Advanced Examples") + group3_layout = QVBoxLayout() + + btn3_1 = QPushButton("Simulate loading users from database") + btn3_1.clicked.connect(example_auto_update) + group3_layout.addWidget(btn3_1) + + group3.setLayout(group3_layout) + main_layout.addWidget(group3) + + window.setLayout(main_layout) + window.setWindowTitle("Pattern Completer Parameter - Demo") + window.resize(800, 700) + window.show() + + # Print initial state + print("\n" + "=" * 70) + print("PATTERN COMPLETER PARAMETER - INTERACTIVE DEMO") + print("=" * 70) + print("\nInitial state:") + print(f" Message patterns: {list(message_param.opts.get('patterns', {}).keys())}") + print(f" @ completions: {message_param.opts['patterns']['@']}") + print(f" # completions: {message_param.opts['patterns']['#']}") + print("\nTry typing '@' or '#' in the Message field to see completions!") + print("Then click buttons to see dynamic updates in action.") + print("=" * 70 + "\n") + + # Monitor value changes + def value_changed(param, value): + if value: # Only print non-empty values + print(f"[Value Changed] {param.name()}: {value}") + + p.sigTreeStateChanged.connect(value_changed) + + sys.exit(app.exec_()) From 7269763d5a1390f75cb22fb1482288ee31e84981 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:48:20 +0200 Subject: [PATCH 10/14] updating parameter_ex --- src/pymodaq_gui/examples/parameter_ex.py | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/pymodaq_gui/examples/parameter_ex.py b/src/pymodaq_gui/examples/parameter_ex.py index 866a515b..75391213 100644 --- a/src/pymodaq_gui/examples/parameter_ex.py +++ b/src/pymodaq_gui/examples/parameter_ex.py @@ -90,8 +90,29 @@ def create_comprehensive_table(): return table_params # return Parameter.create(name="params", type="group", children=params) - - +def create_text_parameter(): + text_params = { + "name": "Text Editing with pattern completion", + "type": "group", + "children": [ + { + "name": "Message", + "type": "text_pattern", + "value": "", + "patterns": { + "@": ["alice", "bob", "charlie"], + "#": ["python", "javascript", "cpp"], + }, + "completer_config": { + "min_width": 200, + "max_width": 400, + "case_sensitive": False, + "visual_indicator": True, + }, + }, + ], + } + return text_params class ParameterEx(ParameterManager): params = [ {'title': 'Groups:', 'name': 'groups', 'type': 'group', 'children': [ @@ -185,6 +206,7 @@ class ParameterEx(ParameterManager): {'title': 'Plain text:', 'name': 'texts', 'type': 'group', 'children': [ {'title': 'Standard str', 'name': 'atte', 'type': 'str', 'value': 'this is a string you can edit'}, {'title': 'Plain text', 'name': 'text', 'type': 'text', 'value': 'this is some text'}, + create_text_parameter(), {'title': 'Plain text', 'name': 'textpb', 'type': 'text_pb', 'value': 'this is some text', 'tip': 'If text_pb type is used, user can add text to the parameter'}, ]}, @@ -192,7 +214,7 @@ class ParameterEx(ParameterManager): {'title': 'Tables:', 'name': 'tables', 'type': 'group', 'children': [ {'title': 'Table widget', 'name': 'tablewidget', 'type': 'table_dict', 'value': OrderedDict(key1='data1', key2=24), 'header': ['keys', 'limits'], 'height': 100}, - create_comprehensive_table(), + # create_comprehensive_table(), {'title': 'Table view', 'name': 'tabular_table', 'type': 'table_view', 'delegate': table.SpinBoxDelegate, 'menu': True, 'value': table.TableModel([[0.1, 0.2, 0.3]], ['value1', 'value2', 'value3']), From f7b89cdcb38274337f897735948277671c1cdf30 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 18:58:00 +0200 Subject: [PATCH 11/14] Adding resize to content and example in parameter_ex --- src/pymodaq_gui/examples/parameter_ex.py | 2 +- src/pymodaq_gui/parameter/pymodaq_ptypes/table.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_gui/examples/parameter_ex.py b/src/pymodaq_gui/examples/parameter_ex.py index 75391213..9bceaf61 100644 --- a/src/pymodaq_gui/examples/parameter_ex.py +++ b/src/pymodaq_gui/examples/parameter_ex.py @@ -214,7 +214,7 @@ class ParameterEx(ParameterManager): {'title': 'Tables:', 'name': 'tables', 'type': 'group', 'children': [ {'title': 'Table widget', 'name': 'tablewidget', 'type': 'table_dict', 'value': OrderedDict(key1='data1', key2=24), 'header': ['keys', 'limits'], 'height': 100}, - # create_comprehensive_table(), + create_comprehensive_table(), {'title': 'Table view', 'name': 'tabular_table', 'type': 'table_view', 'delegate': table.SpinBoxDelegate, 'menu': True, 'value': table.TableModel([[0.1, 0.2, 0.3]], ['value1', 'value2', 'value3']), diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py index 76c4ed1a..29873923 100644 --- a/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/table.py @@ -3,8 +3,6 @@ """ from pyqtgraph.parametertree.parameterTypes import WidgetParameterItem, SimpleParameter - -# from managed_table_widget import ManagedTableWidget from pymodaq_gui.utils.widgets.managed_table import ManagedTableWidget class TableParameterItem(WidgetParameterItem): @@ -48,7 +46,7 @@ def makeWidget(self): def widgetValueChanged(self, data): """Handle widget value changes.""" try: - self.param.setValue(data) + self.param.setValue(data) except Exception as e: print(f"Error updating parameter: {e}") @@ -66,6 +64,7 @@ def optsChanged(self, param, opts): if "value" in opts: self.widget.setValue(opts["value"]) + self.widget.table.resizeColumnsToContents() if "delegate" in opts: delegate = opts["delegate"]() @@ -93,7 +92,7 @@ def __init__(self, **opts): columns = opts.get("columns", ["Column 1", "Column 2", "Column 3"]) opts["value"] = [[""] * len(columns) for _ in range(rows)] - opts["expanded"] = False + opts["expanded"] = True super().__init__(**opts) def valueIsDefault(self): From a1b9e20e0e5c740e0011e5d62fe38fd084b4e415 Mon Sep 17 00:00:00 2001 From: Ashwola Date: Wed, 22 Oct 2025 19:12:00 +0200 Subject: [PATCH 12/14] wrong addition --- .../examples/pattern_completer_demo.py | 393 ------------------ 1 file changed, 393 deletions(-) delete mode 100644 src/pymodaq_gui/examples/pattern_completer_demo.py diff --git a/src/pymodaq_gui/examples/pattern_completer_demo.py b/src/pymodaq_gui/examples/pattern_completer_demo.py deleted file mode 100644 index 15080c9d..00000000 --- a/src/pymodaq_gui/examples/pattern_completer_demo.py +++ /dev/null @@ -1,393 +0,0 @@ -""" -PatternCompleter Examples - PyQt6 Auto-completion System - -This demonstrates various use cases for the PatternCompleter mixin class. -All examples are accessible through tabs in a single window. -""" - -from PyQt6.QtWidgets import ( - QApplication, - QMainWindow, - QVBoxLayout, - QWidget, - QLabel, - QTableWidget, - QTableWidgetItem, - QTabWidget, - QHBoxLayout, - QPushButton, -) -from PyQt6.QtCore import QTimer -from pattern_completer import ( - PatternLineEdit, - PatternTextEdit, - PatternPlainTextEdit, - PatternCompleterDelegate, -) -import sys - - -# ============================================================================ -# EXAMPLE 1: Basic Usage with QLineEdit -# ============================================================================ -def create_basic_example(): - """Simple mention system with @ trigger""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 1: Basic Usage")) - layout.addWidget(QLabel("Type @ to mention someone")) - - # Create a line edit with pattern completion - line_edit = PatternLineEdit() - - # Add @ mentions completer - users = ["Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince"] - line_edit.add_completer("@", users) - - line_edit.setPlaceholderText("Type @ to mention someone...") - layout.addWidget(line_edit) - layout.addStretch() - - return widget - - -# ============================================================================ -# EXAMPLE 2: Multiple Patterns -# ============================================================================ -def create_multiple_patterns_example(): - """Text editor with multiple completion triggers""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 2: Multiple Patterns")) - layout.addWidget(QLabel("Try typing @ for mentions, # for hashtags, :: for emojis")) - - text_edit = PatternTextEdit() - - # @ for user mentions - users = ["Alice", "Bob", "Charlie", "Diana"] - text_edit.add_completer("@", users) - - # # for hashtags - tags = ["python", "pyqt6", "programming", "development", "tutorial"] - text_edit.add_completer("#", tags) - - # :: for emojis - emojis = ["smile 😊", "heart ❤️", "thumbsup 👍", "fire 🔥", "rocket 🚀"] - text_edit.add_completer("::", emojis) - - text_edit.setPlaceholderText( - "Try typing:\n @ for mentions\n # for hashtags\n :: for emojis" - ) - layout.addWidget(text_edit) - - return widget - - -# ============================================================================ -# EXAMPLE 3: Global Configuration -# ============================================================================ -def create_global_config_example(): - """Configure appearance and behavior globally""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 3: Global Configuration")) - layout.addWidget( - QLabel("Notice the green border when typing @ (visual indicator enabled)") - ) - - # Initialize with global settings - line_edit = PatternLineEdit( - min_width=200, # Minimum popup width - max_width=600, # Maximum popup width - visual_indicator=True, # Show green border when active - case_sensitive=False, # Case-insensitive by default - auto_resize=True, # Auto-resize popup to fit content - word_wrap=False, # Don't wrap long items - ) - - countries = ["United States", "United Kingdom", "Canada", "Australia", "Germany"] - line_edit.add_completer("@", countries) - - line_edit.setPlaceholderText("Type @ - notice the green border!") - layout.addWidget(line_edit) - layout.addStretch() - - return widget - - -# ============================================================================ -# EXAMPLE 4: Per-Pattern Configuration -# ============================================================================ -def create_per_pattern_config_example(): - """Different settings for each pattern""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 4: Per-Pattern Configuration")) - layout.addWidget(QLabel("@ is case-insensitive, :: is case-sensitive")) - - text_edit = PatternTextEdit() - - # Case-insensitive user mentions with visual indicator - users = ["Alice", "Bob", "Charlie"] - text_edit.add_completer("@", users, visual_indicator=True, case_sensitive=False) - - # Case-sensitive programming keywords - keywords = ["def", "class", "import", "return", "if", "else"] - text_edit.add_completer( - "::", - keywords, - case_sensitive=True, # Exact case matching - min_width=150, - max_width=300, - ) - - text_edit.setPlaceholderText( - "@ mentions are case-insensitive (try '@ali' or '@ALI')\n" - ":: keywords are case-sensitive (try '::def' vs '::DEF')" - ) - layout.addWidget(text_edit) - - return widget - - -# ============================================================================ -# EXAMPLE 5: Word Wrap Example -# ============================================================================ -def create_word_wrap_example(): - """ - Demonstrates word wrap feature for long completion items - """ - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 5: Word Wrap")) - - info_label = QLabel( - "What is Word Wrap?
" - "• word_wrap=False (default): Long items are truncated or need scrolling
" - "• word_wrap=True: Long items wrap to multiple lines in popup

" - "Click buttons below to toggle word wrap and see the difference!" - ) - layout.addWidget(info_label) - - # Button controls - button_layout = QHBoxLayout() - btn_no_wrap = QPushButton("Without Word Wrap") - btn_with_wrap = QPushButton("With Word Wrap") - button_layout.addWidget(btn_no_wrap) - button_layout.addWidget(btn_with_wrap) - layout.addLayout(button_layout) - - line_edit = PatternLineEdit() - - # Long completion items that benefit from word wrap - long_items = [ - "Alice Johnson - Senior Developer at Tech Corp", - "Bob Smith - Product Manager with 10 years experience", - "Charlie Brown - UX Designer specializing in mobile applications", - "Diana Prince - Data Scientist working on machine learning projects", - ] - - # Start without word wrap - line_edit.add_completer("@", long_items, word_wrap=False, max_width=300) - - def set_no_wrap(): - line_edit.update_completer_config("@", word_wrap=False) - line_edit.setPlaceholderText("Type @ - items are truncated (word_wrap=False)") - - def set_with_wrap(): - line_edit.update_completer_config("@", word_wrap=True) - line_edit.setPlaceholderText( - "Type @ - items wrap to multiple lines (word_wrap=True)" - ) - - btn_no_wrap.clicked.connect(set_no_wrap) - btn_with_wrap.clicked.connect(set_with_wrap) - - line_edit.setPlaceholderText("Type @ - items are truncated (word_wrap=False)") - layout.addWidget(line_edit) - layout.addStretch() - - return widget - - -# ============================================================================ -# EXAMPLE 6: Dynamic Updates -# ============================================================================ -def create_dynamic_updates_example(): - """Update completions dynamically""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 6: Dynamic Updates")) - counter_label = QLabel("Completions update every 2 seconds!") - layout.addWidget(counter_label) - - text_edit = PatternTextEdit() - text_edit.add_completer("@", ["Alice", "Bob"]) - text_edit.setPlaceholderText("Type @ to see completions. Watch them change!") - layout.addWidget(text_edit) - - # Simulate dynamic updates - counter = [0] - - def update_users(): - counter[0] += 1 - new_users = [f"User{i}" for i in range(counter[0], counter[0] + 5)] - text_edit.update_completions("@", new_users) - counter_label.setText(f"Update #{counter[0]}: {', '.join(new_users)}") - - timer = QTimer(widget) - timer.timeout.connect(update_users) - timer.start(2000) - - return widget - - -# ============================================================================ -# EXAMPLE 7: Table Widget with Delegate -# ============================================================================ -def create_table_delegate_example(): - """Use PatternCompleter in table cells""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 7: Table Delegate")) - layout.addWidget(QLabel("Double-click cells and type @ for users or # for tags")) - - table = QTableWidget(5, 2) - table.setHorizontalHeaderLabels(["Assigned To", "Tags"]) - - # Create delegate with pattern completion - delegate = PatternCompleterDelegate(min_width=200, visual_indicator=True) - # Add completers for the delegate - users = ["Alice", "Bob", "Charlie", "Diana"] - tags = ["urgent", "review", "bug", "feature", "documentation"] - delegate.add_completer("@", users) - delegate.add_completer("#", tags) - - # Apply delegate to both columns - table.setItemDelegateForColumn(0, delegate) - table.setItemDelegateForColumn(1, delegate) - # It is possible to use different delegates for each column but one has to keep the instance alive (with self. ...) - - # Add some sample data - for i in range(5): - table.setItem(i, 0, QTableWidgetItem(f"Task {i + 1}")) - table.setItem(i, 1, QTableWidgetItem("")) - - layout.addWidget(table) - - return widget - - -# ============================================================================ -# EXAMPLE 8: Code Editor Style -# ============================================================================ -def create_code_editor_example(): - """IDE-like code completion""" - widget = QWidget() - layout = QVBoxLayout(widget) - - layout.addWidget(QLabel("Example 8: Code Editor Style")) - layout.addWidget(QLabel("Python-style completion: :keyword or ::builtin")) - - editor = PatternPlainTextEdit( - min_width=250, max_width=500, case_sensitive=True, auto_resize=True - ) - - # Python keywords - keywords = [ - "def", - "class", - "import", - "from", - "return", - "if", - "else", - "elif", - "for", - "while", - "try", - "except", - "with", - "as", - "pass", - "break", - ] - - # Built-in functions - builtins = [ - "print()", - "len()", - "range()", - "enumerate()", - "zip()", - "map()", - "filter()", - "sorted()", - "sum()", - "max()", - "min()", - ] - - editor.add_completer(":", keywords, case_sensitive=True) - editor.add_completer("::", builtins) - - editor.setPlaceholderText( - "Python-style completion:\n" - " :def → keywords\n" - " ::print → built-in functions\n\n" - "Try typing ':for' or '::pri'" - ) - - layout.addWidget(editor) - - return widget - - -# ============================================================================ -# Main Application with Tabs -# ============================================================================ -class PatternCompleterDemo(QMainWindow): - """Main demo window with all examples in tabs""" - - def __init__(self): - super().__init__() - self.setWindowTitle("PatternCompleter Examples - Interactive Demo") - self.setGeometry(100, 100, 800, 600) - - # Create tab widget - tabs = QTabWidget() - - # Add all example tabs - tabs.addTab(create_basic_example(), "1. Basic") - tabs.addTab(create_multiple_patterns_example(), "2. Multiple Patterns") - tabs.addTab(create_global_config_example(), "3. Global Config") - tabs.addTab(create_per_pattern_config_example(), "4. Per-Pattern Config") - tabs.addTab(create_word_wrap_example(), "5. Word Wrap") - tabs.addTab(create_dynamic_updates_example(), "6. Dynamic Updates") - tabs.addTab(create_table_delegate_example(), "7. Table Delegate") - tabs.addTab(create_code_editor_example(), "8. Code Editor") - - self.setCentralWidget(tabs) - - -# ============================================================================ -# Run Application -# ============================================================================ -if __name__ == "__main__": - app = QApplication(sys.argv) - - # Set application style - app.setStyle("Fusion") - - # Create and show demo window - demo = PatternCompleterDemo() - demo.show() - - sys.exit(app.exec()) From 6b1338d097f186464e860782ce5b071ebdebe2cf Mon Sep 17 00:00:00 2001 From: Ashwola Date: Thu, 23 Oct 2025 09:01:07 +0200 Subject: [PATCH 13/14] updating ROI_manager with new type --- src/pymodaq_gui/managers/roi_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_gui/managers/roi_manager.py b/src/pymodaq_gui/managers/roi_manager.py index aec50155..cc5f9d93 100644 --- a/src/pymodaq_gui/managers/roi_manager.py +++ b/src/pymodaq_gui/managers/roi_manager.py @@ -207,7 +207,7 @@ def setupUI(self): self.roiwidget.setMaximumWidth(300) params = [ - {'title': 'Measurements:', 'name': 'measurements', 'type': 'table', 'value': OrderedDict([]), 'Ncol': 2, + {'title': 'Measurements:', 'name': 'measurements', 'type': 'table_dict', 'value': OrderedDict([]), 'Ncol': 2, 'header': ["LO", "Value"]}, ROIScalableGroup(roi_type=self.ROI_type, name="ROIs")] self.settings = Parameter.create(title='ROIs Settings', name='rois_settings', type='group', children=params) From e075f3ec827f7bf3817316824f4534bb3bbb829a Mon Sep 17 00:00:00 2001 From: Ashwola Date: Thu, 23 Oct 2025 09:02:22 +0200 Subject: [PATCH 14/14] Simplifying main of text_pattern --- src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py b/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py index 9fb037a6..5066a374 100644 --- a/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py +++ b/src/pymodaq_gui/parameter/pymodaq_ptypes/text_pattern.py @@ -229,13 +229,6 @@ def set_completer_config(self, **config): "visual_indicator": True, }, }, - { - "name": "Tags", - "type": "text_pattern", - "value": "", - "patterns": {"#": ["urgent", "todo", "done", "in-progress"]}, - "completer_config": {"min_width": 150, "case_sensitive": False}, - }, ], }, ]