feat: Expandable left sidebar with toggle button
NavigationRail now supports expanded/collapsed states: - Click menu toggle button to expand/collapse - Expanded: Shows icon + label (200px width) - Collapsed: Shows icon only (80px width) - Smooth animated width transition - Orange accent on active item maintained NavigationDestination updated: - Shows QLabel with text when expanded - Icon-only when collapsed - Proper styling for both states
This commit is contained in:
parent
447098b9fa
commit
d8a7ab6ba0
|
|
@ -329,21 +329,26 @@ class Card(Surface):
|
|||
|
||||
|
||||
class NavigationRail(QFrame):
|
||||
"""Material Design Navigation Rail with EU styling."""
|
||||
"""Material Design Navigation Rail with EU styling - Expandable with labels."""
|
||||
|
||||
destination_changed = pyqtSignal(str)
|
||||
|
||||
# Widths for collapsed/expanded states
|
||||
WIDTH_COLLAPSED = 80
|
||||
WIDTH_EXPANDED = 200
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.destinations = []
|
||||
self.active_destination = None
|
||||
self.icon_manager = get_icon_manager()
|
||||
self._expanded = False
|
||||
self._setup_style()
|
||||
self._setup_layout()
|
||||
|
||||
def _setup_style(self):
|
||||
"""Apply navigation rail styling."""
|
||||
self.setFixedWidth(80)
|
||||
self.setFixedWidth(self.WIDTH_COLLAPSED)
|
||||
self.setStyleSheet("""
|
||||
NavigationRail {
|
||||
background: rgba(20, 31, 35, 0.98);
|
||||
|
|
@ -352,21 +357,101 @@ class NavigationRail(QFrame):
|
|||
""")
|
||||
|
||||
def _setup_layout(self):
|
||||
"""Setup vertical layout."""
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(12, 24, 12, 24)
|
||||
self.layout.setSpacing(8)
|
||||
self.layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
|
||||
"""Setup vertical layout with expand toggle."""
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(0, 12, 0, 12)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
# Top toggle button
|
||||
self.toggle_btn = QPushButton()
|
||||
self.toggle_btn.setFixedSize(56, 32)
|
||||
self.toggle_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self.toggle_btn.setToolTip("Expand sidebar")
|
||||
self.toggle_btn.setIcon(self.icon_manager.get_icon("menu"))
|
||||
self.toggle_btn.setIconSize(QSize(20, 20))
|
||||
self.toggle_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
margin: 0 12px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background: rgba(255, 140, 66, 0.15);
|
||||
}}
|
||||
""")
|
||||
self.toggle_btn.clicked.connect(self._toggle_expanded)
|
||||
|
||||
toggle_container = QWidget()
|
||||
toggle_layout = QHBoxLayout(toggle_container)
|
||||
toggle_layout.setContentsMargins(0, 0, 0, 0)
|
||||
toggle_layout.addWidget(self.toggle_btn)
|
||||
toggle_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.main_layout.addWidget(toggle_container)
|
||||
|
||||
# Spacer after toggle
|
||||
self.main_layout.addSpacing(16)
|
||||
|
||||
# Destinations container
|
||||
self.destinations_widget = QWidget()
|
||||
self.destinations_layout = QVBoxLayout(self.destinations_widget)
|
||||
self.destinations_layout.setContentsMargins(12, 0, 12, 0)
|
||||
self.destinations_layout.setSpacing(8)
|
||||
self.destinations_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.main_layout.addWidget(self.destinations_widget)
|
||||
|
||||
# Add spacer at bottom
|
||||
self.layout.addStretch()
|
||||
self.main_layout.addStretch()
|
||||
|
||||
def _toggle_expanded(self):
|
||||
"""Toggle between collapsed and expanded states."""
|
||||
self._expanded = not self._expanded
|
||||
|
||||
# Animate width change
|
||||
self._animate_width()
|
||||
|
||||
# Update toggle button icon
|
||||
if self._expanded:
|
||||
self.toggle_btn.setToolTip("Collapse sidebar")
|
||||
self.toggle_btn.setIcon(self.icon_manager.get_icon("close")) # Or "collapse" icon
|
||||
else:
|
||||
self.toggle_btn.setToolTip("Expand sidebar")
|
||||
self.toggle_btn.setIcon(self.icon_manager.get_icon("menu"))
|
||||
|
||||
# Update all destinations to show/hide labels
|
||||
for dest in self.destinations:
|
||||
dest.set_expanded(self._expanded)
|
||||
|
||||
def _animate_width(self):
|
||||
"""Animate the width change."""
|
||||
target_width = self.WIDTH_EXPANDED if self._expanded else self.WIDTH_COLLAPSED
|
||||
|
||||
# Use QPropertyAnimation for smooth width transition
|
||||
self._width_anim = QPropertyAnimation(self, b"minimumWidth")
|
||||
self._width_anim.setDuration(DesignTokens.DURATION_NORMAL)
|
||||
self._width_anim.setStartValue(self.width())
|
||||
self._width_anim.setEndValue(target_width)
|
||||
self._width_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
self._width_anim2 = QPropertyAnimation(self, b"maximumWidth")
|
||||
self._width_anim2.setDuration(DesignTokens.DURATION_NORMAL)
|
||||
self._width_anim2.setStartValue(self.width())
|
||||
self._width_anim2.setEndValue(target_width)
|
||||
self._width_anim2.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# Update fixed width when done
|
||||
self._width_anim2.finished.connect(lambda: self.setFixedWidth(target_width))
|
||||
|
||||
self._width_anim.start()
|
||||
self._width_anim2.start()
|
||||
|
||||
def add_destination(self, icon_name: str, label: str, destination_id: str):
|
||||
"""Add a navigation destination with SVG icon."""
|
||||
btn = NavigationDestination(icon_name, label, destination_id, self.icon_manager)
|
||||
btn.clicked.connect(lambda: self._on_destination_clicked(destination_id))
|
||||
btn.set_expanded(self._expanded)
|
||||
self.destinations.append(btn)
|
||||
self.layout.insertWidget(len(self.destinations) - 1, btn)
|
||||
self.destinations_layout.addWidget(btn)
|
||||
|
||||
def _on_destination_clicked(self, destination_id: str):
|
||||
"""Handle destination selection."""
|
||||
|
|
@ -379,58 +464,135 @@ class NavigationRail(QFrame):
|
|||
for dest in self.destinations:
|
||||
dest.set_active(dest.destination_id == destination_id)
|
||||
|
||||
def is_expanded(self) -> bool:
|
||||
"""Return current expanded state."""
|
||||
return self._expanded
|
||||
|
||||
|
||||
class NavigationDestination(QPushButton):
|
||||
"""Single navigation destination with SVG icon."""
|
||||
"""Single navigation destination with SVG icon and optional label."""
|
||||
|
||||
def __init__(self, icon_name: str, label: str, destination_id: str, icon_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
self.destination_id = destination_id
|
||||
self.icon_manager = icon_manager
|
||||
self.icon_name = icon_name
|
||||
self.label = label
|
||||
self.label_text = label
|
||||
self._expanded = False
|
||||
self._active = False
|
||||
self._setup_style()
|
||||
self._create_content()
|
||||
|
||||
def _setup_style(self):
|
||||
"""Apply destination styling."""
|
||||
self.setFixedSize(56, 56)
|
||||
self.setMinimumHeight(48)
|
||||
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
||||
self._update_style(False)
|
||||
|
||||
def _create_content(self):
|
||||
"""Create icon from SVG."""
|
||||
self.setIcon(self.icon_manager.get_icon(self.icon_name))
|
||||
self.setIconSize(QSize(24, 24))
|
||||
self.setToolTip(self.label)
|
||||
"""Create icon and label."""
|
||||
# Horizontal layout for icon + label
|
||||
self.content_layout = QHBoxLayout(self)
|
||||
self.content_layout.setContentsMargins(12, 8, 12, 8)
|
||||
self.content_layout.setSpacing(12)
|
||||
|
||||
# Icon
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setPixmap(self.icon_manager.get_pixmap(self.icon_name, size=24))
|
||||
self.content_layout.addWidget(self.icon_label)
|
||||
|
||||
# Text label (hidden when collapsed)
|
||||
self.text_label = QLabel(self.label_text)
|
||||
self.text_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
""")
|
||||
self.text_label.hide()
|
||||
self.content_layout.addWidget(self.text_label)
|
||||
|
||||
self.content_layout.addStretch()
|
||||
|
||||
self.setToolTip(self.label_text)
|
||||
|
||||
def set_expanded(self, expanded: bool):
|
||||
"""Show or hide the text label."""
|
||||
self._expanded = expanded
|
||||
if expanded:
|
||||
self.text_label.show()
|
||||
self.setMinimumWidth(176) # Fill expanded rail width
|
||||
self.setMaximumWidth(176)
|
||||
else:
|
||||
self.text_label.hide()
|
||||
self.setFixedSize(56, 48)
|
||||
self._update_style(self._active)
|
||||
|
||||
def _update_style(self, active: bool):
|
||||
"""Update style based on active state with orange accent border."""
|
||||
c = get_all_colors()
|
||||
self._active = active
|
||||
orange = DesignTokens.EU_ORANGE
|
||||
|
||||
if active:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba(255, 140, 66, 0.15);
|
||||
border: none;
|
||||
border-left: 3px solid {orange};
|
||||
border-radius: 0 16px 16px 0;
|
||||
}}
|
||||
""")
|
||||
if self._expanded:
|
||||
# Expanded style - full width with left border
|
||||
if active:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba(255, 140, 66, 0.15);
|
||||
border: none;
|
||||
border-left: 3px solid {orange};
|
||||
border-radius: 0 12px 12px 0;
|
||||
text-align: left;
|
||||
padding-left: 9px;
|
||||
}}
|
||||
QLabel {{
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: 0 12px 12px 0;
|
||||
text-align: left;
|
||||
padding-left: 9px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left: 3px solid rgba(255, 140, 66, 0.3);
|
||||
}}
|
||||
QLabel {{
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left: 3px solid rgba(255, 140, 66, 0.3);
|
||||
}}
|
||||
""")
|
||||
# Collapsed style - icon only
|
||||
if active:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba(255, 140, 66, 0.15);
|
||||
border: none;
|
||||
border-left: 3px solid {orange};
|
||||
border-radius: 0 16px 16px 0;
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
self.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 3px solid transparent;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-left: 3px solid rgba(255, 140, 66, 0.3);
|
||||
}}
|
||||
""")
|
||||
|
||||
def set_active(self, active: bool):
|
||||
"""Set active state."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue