From a1b6af0bde7ca0857360c9ddd7c6f061d983d59e Mon Sep 17 00:00:00 2001 From: HuangMason320 Date: Thu, 31 Jul 2025 01:23:40 +0800 Subject: [PATCH] feat: Optimize properties panel layout to prevent horizontal scrolling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add smart path truncation for long file paths (preserves filename and parent folder) - Set maximum width constraints on all UI components (QPushButton, QComboBox, QSpinBox, QDoubleSpinBox, QLineEdit) - Add tooltips showing full paths for truncated file path buttons - Disable horizontal scrollbar and optimize right panel width (320-380px) - Improve styling for all property widgets with consistent theme - Add better placeholder text for input fields Key improvements: - File paths like "C:/Very/Long/Path/.../filename.nef" → "...Long/Path/filename.nef" - All widgets limited to 250px max width to prevent panel expansion - Enhanced hover and focus states for better UX - Properties panel now fits within fixed width without horizontal scroll 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- cluster4npu_ui/ui/windows/dashboard.py | 238 +++++++++++++++++++++++-- 1 file changed, 224 insertions(+), 14 deletions(-) diff --git a/cluster4npu_ui/ui/windows/dashboard.py b/cluster4npu_ui/ui/windows/dashboard.py index 28d0b1a..83310c2 100644 --- a/cluster4npu_ui/ui/windows/dashboard.py +++ b/cluster4npu_ui/ui/windows/dashboard.py @@ -361,10 +361,10 @@ class IntegratedPipelineDashboard(QMainWindow): # Middle: Pipeline Editor (50% width) - without its own status bar middle_panel = self.create_pipeline_editor_panel() - # Right side: Configuration panels (25% width) + # Right side: Configuration panels (25% width) - optimized for no horizontal scroll right_panel = self.create_configuration_panel() - right_panel.setMinimumWidth(300) - right_panel.setMaximumWidth(400) + right_panel.setMinimumWidth(320) + right_panel.setMaximumWidth(380) # Add widgets to splitter main_splitter.addWidget(left_panel) @@ -795,8 +795,15 @@ class IntegratedPipelineDashboard(QMainWindow): def create_node_properties_panel(self) -> QWidget: """Create node properties editing panel.""" widget = QScrollArea() + + # Configure scroll area to prevent horizontal scrolling + widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + widget.setWidgetResizable(True) + content = QWidget() layout = QVBoxLayout(content) + layout.setContentsMargins(10, 10, 10, 10) # Add some padding # Header header = QLabel("Node Properties") @@ -1271,6 +1278,41 @@ class IntegratedPipelineDashboard(QMainWindow): return widget + def truncate_path_smart(self, path: str, max_length: int = 35) -> str: + """ + Smart path truncation that preserves important parts. + Shows: ...drive/important_folder/filename.ext + """ + if not path or len(path) <= max_length: + return path + + import os + + # Split path into components + drive, path_without_drive = os.path.splitdrive(path) + path_parts = path_without_drive.replace('\\', '/').split('/') + + if len(path_parts) <= 2: + # Very short path, just truncate from start + return '...' + path[-(max_length-3):] + + filename = path_parts[-1] if path_parts[-1] else path_parts[-2] + + # Always keep filename and one parent directory if possible + if len(filename) > max_length - 10: + # Filename itself is too long + return '...' + filename[-(max_length-3):] + + # Try to keep parent folder + filename + parent_dir = path_parts[-2] if len(path_parts) >= 2 else '' + short_end = f"/{parent_dir}/{filename}" if parent_dir else f"/{filename}" + + if len(short_end) <= max_length - 3: + return '...' + short_end + else: + # Just keep filename + return '.../' + filename + def create_property_widget_enhanced(self, node, prop_name: str, prop_value): """Create enhanced property widget with better type detection.""" # Create widget based on property name and value @@ -1284,9 +1326,35 @@ class IntegratedPipelineDashboard(QMainWindow): # Check for file path properties first (from prop_options or name pattern) if (prop_options and isinstance(prop_options, dict) and prop_options.get('type') == 'file_path') or \ prop_name in ['model_path', 'source_path', 'destination']: - # File path property with filters from prop_options or defaults - widget = QPushButton(str(prop_value) if prop_value else 'Select File...') - widget.setStyleSheet("text-align: left; padding: 5px;") + # File path property with smart truncation and width limits + display_text = self.truncate_path_smart(str(prop_value)) if prop_value else 'Select File...' + widget = QPushButton(display_text) + + # Set fixed width and styling to prevent expansion + widget.setMaximumWidth(250) # Limit button width + widget.setMinimumWidth(200) + widget.setStyleSheet(""" + QPushButton { + text-align: left; + padding: 5px 8px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 10px; + } + QPushButton:hover { + background-color: #585b70; + border-color: #74c7ec; + } + QPushButton:pressed { + background-color: #313244; + } + """) + + # Store full path for tooltip and internal use + full_path = str(prop_value) if prop_value else '' + widget.setToolTip(f"Full path: {full_path}\n\nClick to browse for {prop_name.replace('_', ' ')}") def browse_file(): # Use filter from prop_options if available, otherwise use defaults @@ -1295,7 +1363,9 @@ class IntegratedPipelineDashboard(QMainWindow): else: # Fallback to original filters filters = { - 'model_path': 'Model files (*.onnx *.tflite *.pb)', + 'model_path': 'NEF Model files (*.nef)', + 'scpu_fw_path': 'SCPU Firmware files (*.bin)', + 'ncpu_fw_path': 'NCPU Firmware files (*.bin)', 'source_path': 'Media files (*.mp4 *.avi *.mov *.mkv *.wav *.mp3)', 'destination': 'Output files (*.json *.xml *.csv *.txt)' } @@ -1303,7 +1373,12 @@ class IntegratedPipelineDashboard(QMainWindow): file_path, _ = QFileDialog.getOpenFileName(self, f'Select {prop_name}', '', file_filter) if file_path: - widget.setText(file_path) + # Update button text with truncated path + truncated_text = self.truncate_path_smart(file_path) + widget.setText(truncated_text) + # Update tooltip with full path + widget.setToolTip(f"Full path: {file_path}\n\nClick to browse for {prop_name.replace('_', ' ')}") + # Set property with full path if hasattr(node, 'set_property'): node.set_property(prop_name, file_path) @@ -1312,9 +1387,43 @@ class IntegratedPipelineDashboard(QMainWindow): # Check for dropdown properties (list options from prop_options or predefined) elif (prop_options and isinstance(prop_options, list)) or \ prop_name in ['source_type', 'dongle_series', 'output_format', 'format', 'output_type', 'resolution']: - # Dropdown property + # Dropdown property with width limits widget = QComboBox() + # Set maximum width to prevent expansion + widget.setMaximumWidth(250) + widget.setMinimumWidth(150) + widget.setStyleSheet(""" + QComboBox { + padding: 4px 8px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 11px; + } + QComboBox:hover { + border-color: #74c7ec; + } + QComboBox::drop-down { + border: none; + width: 20px; + } + QComboBox::down-arrow { + image: none; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #cdd6f4; + margin-right: 4px; + } + QComboBox QAbstractItemView { + background-color: #313244; + color: #cdd6f4; + selection-background-color: #89b4fa; + border: 1px solid #585b70; + } + """) + # Use options from prop_options if available, otherwise use defaults if prop_options and isinstance(prop_options, list): items = prop_options @@ -1401,10 +1510,48 @@ class IntegratedPipelineDashboard(QMainWindow): widget.stateChanged.connect(on_change) elif isinstance(prop_value, int): - # Integer property + # Integer property with width limits widget = QSpinBox() widget.setValue(prop_value) + # Set width limits to prevent expansion + widget.setMaximumWidth(120) + widget.setMinimumWidth(80) + widget.setStyleSheet(""" + QSpinBox { + padding: 4px 6px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 11px; + } + QSpinBox:hover { + border-color: #74c7ec; + } + QSpinBox:focus { + border-color: #89b4fa; + } + QSpinBox::up-button, QSpinBox::down-button { + width: 16px; + background-color: #585b70; + border: none; + } + QSpinBox::up-button:hover, QSpinBox::down-button:hover { + background-color: #6c7086; + } + QSpinBox::up-arrow { + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-bottom: 3px solid #cdd6f4; + } + QSpinBox::down-arrow { + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 3px solid #cdd6f4; + } + """) + # Set range from prop_options if available, otherwise use defaults if prop_options and isinstance(prop_options, dict) and 'min' in prop_options and 'max' in prop_options: widget.setRange(prop_options['min'], prop_options['max']) @@ -1429,11 +1576,49 @@ class IntegratedPipelineDashboard(QMainWindow): widget.valueChanged.connect(on_change) elif isinstance(prop_value, float): - # Float property + # Float property with width limits widget = QDoubleSpinBox() widget.setValue(prop_value) widget.setDecimals(2) + # Set width limits to prevent expansion + widget.setMaximumWidth(120) + widget.setMinimumWidth(80) + widget.setStyleSheet(""" + QDoubleSpinBox { + padding: 4px 6px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 11px; + } + QDoubleSpinBox:hover { + border-color: #74c7ec; + } + QDoubleSpinBox:focus { + border-color: #89b4fa; + } + QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { + width: 16px; + background-color: #585b70; + border: none; + } + QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover { + background-color: #6c7086; + } + QDoubleSpinBox::up-arrow { + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-bottom: 3px solid #cdd6f4; + } + QDoubleSpinBox::down-arrow { + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 3px solid #cdd6f4; + } + """) + # Set range and step from prop_options if available, otherwise use defaults if prop_options and isinstance(prop_options, dict): if 'min' in prop_options and 'max' in prop_options: @@ -1462,15 +1647,40 @@ class IntegratedPipelineDashboard(QMainWindow): widget.valueChanged.connect(on_change) else: - # String property (default) + # String property (default) with width limits widget = QLineEdit() widget.setText(str(prop_value)) + # Set width limits to prevent expansion + widget.setMaximumWidth(250) + widget.setMinimumWidth(150) + widget.setStyleSheet(""" + QLineEdit { + padding: 4px 8px; + background-color: #45475a; + color: #cdd6f4; + border: 1px solid #585b70; + border-radius: 4px; + font-size: 11px; + } + QLineEdit:hover { + border-color: #74c7ec; + } + QLineEdit:focus { + border-color: #89b4fa; + } + QLineEdit::placeholder { + color: #6c7086; + } + """) + # Set placeholders for specific properties placeholders = { - 'model_path': 'Path to model file (.nef, .onnx, etc.)', + 'model_path': 'Path to model file', 'destination': 'Output file path', - 'resolution': 'e.g., 1920x1080' + 'resolution': 'e.g., 1920x1080', + 'port_id': 'e.g., 6,7,8 or auto', + 'operations': 'e.g., resize,normalize' } if prop_name in placeholders: