Add foldable sidebar with custom project icons

Implement a collapsible sidebar that can be folded to show just project
icons in a slim strip, or expanded to show full project details. Projects
can now have custom icons selected from a 6x6 grid of 36 symbolic icons.

Features:
- Foldable sidebar with toggle button (fold/unfold)
- Slim sidebar view showing only project icons when folded
- Custom project icons with 36 symbolic icon choices
- Icon picker dialog with 6x6 grid layout
- Edit Project dialog (renamed from Rename) with name and icon selection
- Project icons displayed in both full and slim sidebar views
- Smooth transitions between folded/unfolded states
- Click on slim project icon to unfold sidebar

Technical changes:
- Add icon field to Project model with default "folder-symbolic"
- Create constants.py with PROJECT_ICONS list (36 symbolic icons)
- Implement IconPickerDialog with grid layout and selection
- Create SlimProjectItem widget for folded sidebar view
- Update ProjectManager.update_project() to handle icon changes
- Restructure sidebar using GtkStack for full/slim view switching
- Update project-item.ui to display project icon
- Change "Rename" menu to "Edit Project" with icon picker
- Add fold/unfold buttons with sidebar-show icons
- Update build system with new files and resources
This commit is contained in:
Pavel Baksy 2025-12-18 15:37:29 +01:00
parent ecc9094514
commit 3cc96a82d6
14 changed files with 479 additions and 74 deletions

69
src/constants.py Normal file
View File

@ -0,0 +1,69 @@
# constants.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
# 36 symbolic icons for projects (6x6 grid)
PROJECT_ICONS = [
# Row 1: Folders & Organization
"folder-symbolic",
"folder-open-symbolic",
"folder-documents-symbolic",
"folder-download-symbolic",
"folder-music-symbolic",
"folder-pictures-symbolic",
# Row 2: Development & Code
"application-x-executable-symbolic",
"text-x-generic-symbolic",
"code-symbolic",
"utilities-terminal-symbolic",
"package-x-generic-symbolic",
"software-update-available-symbolic",
# Row 3: Network & Web
"network-server-symbolic",
"network-wireless-symbolic",
"weather-clear-symbolic",
"globe-symbolic",
"world-symbolic",
"web-browser-symbolic",
# Row 4: Tools & Actions
"document-edit-symbolic",
"preferences-system-symbolic",
"system-search-symbolic",
"view-list-symbolic",
"emblem-system-symbolic",
"edit-find-symbolic",
# Row 5: Objects & Concepts
"starred-symbolic",
"non-starred-symbolic",
"bookmark-new-symbolic",
"user-bookmarks-symbolic",
"tag-symbolic",
"mail-send-symbolic",
# Row 6: Status & Symbols
"emblem-ok-symbolic",
"dialog-warning-symbolic",
"dialog-information-symbolic",
"security-high-symbolic",
"changes-prevent-symbolic",
"document-properties-symbolic",
]

56
src/icon-picker-dialog.ui Normal file
View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.0"/>
<template class="IconPickerDialog" parent="AdwDialog">
<property name="title">Choose Icon</property>
<property name="content-width">400</property>
<property name="content-height">450</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="show-title">true</property>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label">Select an icon for your project:</property>
<property name="xalign">0</property>
</object>
</child>
<child>
<object class="GtkFlowBox" id="icon_flowbox">
<property name="column-spacing">6</property>
<property name="row-spacing">6</property>
<property name="homogeneous">true</property>
<property name="max-children-per-line">6</property>
<property name="min-children-per-line">6</property>
<property name="selection-mode">single</property>
<signal name="child-activated" handler="on_icon_selected"/>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</template>
</interface>

70
src/icon_picker_dialog.py Normal file
View File

@ -0,0 +1,70 @@
# icon_picker_dialog.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw, Gtk, GObject
from .constants import PROJECT_ICONS
@Gtk.Template(resource_path='/cz/vesp/roster/icon-picker-dialog.ui')
class IconPickerDialog(Adw.Dialog):
"""Dialog for selecting a project icon."""
__gtype_name__ = 'IconPickerDialog'
icon_flowbox = Gtk.Template.Child()
__gsignals__ = {
'icon-selected': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
}
def __init__(self, current_icon=None):
super().__init__()
self.current_icon = current_icon
self._populate_icons()
def _populate_icons(self):
"""Populate the flowbox with icon buttons."""
for icon_name in PROJECT_ICONS:
button = Gtk.Button()
button.set_icon_name(icon_name)
button.set_tooltip_text(icon_name)
button.add_css_class("flat")
button.set_size_request(48, 48)
# Create icon image with larger size
icon = Gtk.Image.new_from_icon_name(icon_name)
icon.set_icon_size(Gtk.IconSize.LARGE)
button.set_child(icon)
# Store icon name as data
button.icon_name = icon_name
# Highlight current icon
if icon_name == self.current_icon:
button.add_css_class("suggested-action")
self.icon_flowbox.append(button)
@Gtk.Template.Callback()
def on_icon_selected(self, flowbox, child):
"""Handle icon selection."""
button = child.get_child()
if hasattr(button, 'icon_name'):
self.emit('icon-selected', button.icon_name)
self.close()

View File

@ -83,69 +83,136 @@
<!-- START: Sidebar for Projects --> <!-- START: Sidebar for Projects -->
<property name="start-child"> <property name="start-child">
<object class="GtkBox"> <object class="GtkStack" id="sidebar_stack">
<property name="orientation">vertical</property> <property name="transition-type">slide-left-right</property>
<property name="width-request">200</property>
<!-- Sidebar Header --> <!-- Full Sidebar -->
<child> <child>
<object class="GtkBox"> <object class="GtkStackPage">
<property name="orientation">horizontal</property> <property name="name">full</property>
<property name="spacing">6</property> <property name="child">
<property name="margin-start">12</property> <object class="GtkBox">
<property name="margin-end">12</property> <property name="orientation">vertical</property>
<property name="margin-top">6</property> <property name="width-request">200</property>
<property name="margin-bottom">6</property>
<child> <!-- Sidebar Header -->
<object class="GtkLabel"> <child>
<property name="label">Projects</property> <object class="GtkBox">
<property name="xalign">0</property> <property name="orientation">horizontal</property>
<property name="hexpand">True</property> <property name="spacing">6</property>
<style> <property name="margin-start">12</property>
<class name="heading"/> <property name="margin-end">12</property>
</style> <property name="margin-top">6</property>
</object> <property name="margin-bottom">6</property>
</child>
<child> <child>
<object class="GtkButton" id="add_project_button"> <object class="GtkLabel">
<property name="icon-name">list-add-symbolic</property> <property name="label">Projects</property>
<property name="tooltip-text">Add Project</property> <property name="xalign">0</property>
<signal name="clicked" handler="on_add_project_clicked"/> <property name="hexpand">True</property>
<style> <style>
<class name="flat"/> <class name="heading"/>
</style> </style>
</object>
</child>
<child>
<object class="GtkButton" id="add_project_button">
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text">Add Project</property>
<signal name="clicked" handler="on_add_project_clicked"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="fold_sidebar_button">
<property name="icon-name">sidebar-show-right-symbolic</property>
<property name="tooltip-text">Fold Sidebar</property>
<signal name="clicked" handler="on_fold_sidebar_clicked"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object>
</child>
<!-- Projects List -->
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">True</property>
<child>
<object class="GtkListBox" id="projects_listbox">
<style>
<class name="navigation-sidebar"/>
</style>
</object>
</child>
</object>
</child>
<!-- Save Current Request Button -->
<child>
<object class="GtkButton" id="save_request_button">
<property name="label">Save Current Request</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-bottom">12</property>
<signal name="clicked" handler="on_save_request_clicked"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object> </object>
</child> </property>
</object> </object>
</child> </child>
<!-- Projects List --> <!-- Slim Sidebar -->
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkStackPage">
<property name="vexpand">True</property> <property name="name">slim</property>
<child> <property name="child">
<object class="GtkListBox" id="projects_listbox"> <object class="GtkBox">
<style> <property name="orientation">vertical</property>
<class name="navigation-sidebar"/> <property name="width-request">60</property>
</style>
</object>
</child>
</object>
</child>
<!-- Save Current Request Button --> <!-- Unfold Button -->
<child> <child>
<object class="GtkButton" id="save_request_button"> <object class="GtkButton" id="unfold_sidebar_button">
<property name="label">Save Current Request</property> <property name="icon-name">sidebar-show-left-symbolic</property>
<property name="margin-start">12</property> <property name="tooltip-text">Unfold Sidebar</property>
<property name="margin-end">12</property> <property name="margin-start">6</property>
<property name="margin-bottom">12</property> <property name="margin-end">6</property>
<signal name="clicked" handler="on_save_request_clicked"/> <property name="margin-top">6</property>
<style> <property name="margin-bottom">6</property>
<class name="suggested-action"/> <signal name="clicked" handler="on_unfold_sidebar_clicked"/>
</style> <style>
<class name="flat"/>
</style>
</object>
</child>
<!-- Slim Projects List -->
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">True</property>
<child>
<object class="GtkBox" id="slim_projects_box">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object> </object>
</child> </child>
</object> </object>

View File

@ -34,6 +34,8 @@ roster_sources = [
'http_client.py', 'http_client.py',
'history_manager.py', 'history_manager.py',
'project_manager.py', 'project_manager.py',
'constants.py',
'icon_picker_dialog.py',
] ]
install_data(roster_sources, install_dir: moduledir) install_data(roster_sources, install_dir: moduledir)
@ -45,6 +47,7 @@ widgets_sources = [
'widgets/history_item.py', 'widgets/history_item.py',
'widgets/project_item.py', 'widgets/project_item.py',
'widgets/request_item.py', 'widgets/request_item.py',
'widgets/slim_project_item.py',
] ]
install_data(widgets_sources, install_dir: moduledir / 'widgets') install_data(widgets_sources, install_dir: moduledir / 'widgets')

View File

@ -124,6 +124,7 @@ class Project:
name: str name: str
requests: List[SavedRequest] requests: List[SavedRequest]
created_at: str # ISO format created_at: str # ISO format
icon: str = "folder-symbolic" # Icon name
def to_dict(self): def to_dict(self):
"""Convert to dictionary for JSON serialization.""" """Convert to dictionary for JSON serialization."""
@ -131,7 +132,8 @@ class Project:
'id': self.id, 'id': self.id,
'name': self.name, 'name': self.name,
'requests': [req.to_dict() for req in self.requests], 'requests': [req.to_dict() for req in self.requests],
'created_at': self.created_at 'created_at': self.created_at,
'icon': self.icon
} }
@classmethod @classmethod
@ -141,5 +143,6 @@ class Project:
id=data['id'], id=data['id'],
name=data['name'], name=data['name'],
requests=[SavedRequest.from_dict(r) for r in data.get('requests', [])], requests=[SavedRequest.from_dict(r) for r in data.get('requests', [])],
created_at=data['created_at'] created_at=data['created_at'],
icon=data.get('icon', 'folder-symbolic') # Default for old data
) )

View File

@ -76,12 +76,15 @@ class ProjectManager:
self.save_projects(projects) self.save_projects(projects)
return project return project
def rename_project(self, project_id: str, new_name: str): def update_project(self, project_id: str, new_name: str = None, new_icon: str = None):
"""Rename a project.""" """Update a project's name and/or icon."""
projects = self.load_projects() projects = self.load_projects()
for p in projects: for p in projects:
if p.id == project_id: if p.id == project_id:
p.name = new_name if new_name is not None:
p.name = new_name
if new_icon is not None:
p.icon = new_icon
break break
self.save_projects(projects) self.save_projects(projects)

View File

@ -3,9 +3,11 @@
<gresource prefix="/cz/vesp/roster"> <gresource prefix="/cz/vesp/roster">
<file preprocess="xml-stripblanks">main-window.ui</file> <file preprocess="xml-stripblanks">main-window.ui</file>
<file preprocess="xml-stripblanks">shortcuts-dialog.ui</file> <file preprocess="xml-stripblanks">shortcuts-dialog.ui</file>
<file preprocess="xml-stripblanks">icon-picker-dialog.ui</file>
<file preprocess="xml-stripblanks">widgets/header-row.ui</file> <file preprocess="xml-stripblanks">widgets/header-row.ui</file>
<file preprocess="xml-stripblanks">widgets/history-item.ui</file> <file preprocess="xml-stripblanks">widgets/history-item.ui</file>
<file preprocess="xml-stripblanks">widgets/project-item.ui</file> <file preprocess="xml-stripblanks">widgets/project-item.ui</file>
<file preprocess="xml-stripblanks">widgets/request-item.ui</file> <file preprocess="xml-stripblanks">widgets/request-item.ui</file>
<file preprocess="xml-stripblanks">widgets/slim-project-item.ui</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -21,5 +21,6 @@ from .header_row import HeaderRow
from .history_item import HistoryItem from .history_item import HistoryItem
from .project_item import ProjectItem from .project_item import ProjectItem
from .request_item import RequestItem from .request_item import RequestItem
from .slim_project_item import SlimProjectItem
__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem'] __all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem', 'SlimProjectItem']

View File

@ -10,8 +10,8 @@
<attribute name="action">project.add-request</attribute> <attribute name="action">project.add-request</attribute>
</item> </item>
<item> <item>
<attribute name="label">Rename</attribute> <attribute name="label">Edit Project</attribute>
<attribute name="action">project.rename</attribute> <attribute name="action">project.edit</attribute>
</item> </item>
<item> <item>
<attribute name="label">Delete</attribute> <attribute name="label">Delete</attribute>
@ -39,6 +39,12 @@
</object> </object>
</child> </child>
<child>
<object class="GtkImage" id="project_icon">
<property name="icon-name">folder-symbolic</property>
</object>
</child>
<child> <child>
<object class="GtkLabel" id="name_label"> <object class="GtkLabel" id="name_label">
<property name="xalign">0</property> <property name="xalign">0</property>

View File

@ -28,12 +28,13 @@ class ProjectItem(Gtk.Box):
__gtype_name__ = 'ProjectItem' __gtype_name__ = 'ProjectItem'
expand_icon = Gtk.Template.Child() expand_icon = Gtk.Template.Child()
project_icon = Gtk.Template.Child()
name_label = Gtk.Template.Child() name_label = Gtk.Template.Child()
requests_revealer = Gtk.Template.Child() requests_revealer = Gtk.Template.Child()
requests_listbox = Gtk.Template.Child() requests_listbox = Gtk.Template.Child()
__gsignals__ = { __gsignals__ = {
'rename-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), 'edit-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), 'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'add-request-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), 'add-request-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'request-load-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)), 'request-load-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
@ -60,8 +61,8 @@ class ProjectItem(Gtk.Box):
action.connect("activate", lambda *_: self.emit('add-request-requested')) action.connect("activate", lambda *_: self.emit('add-request-requested'))
actions.add_action(action) actions.add_action(action)
action = Gio.SimpleAction.new("rename", None) action = Gio.SimpleAction.new("edit", None)
action.connect("activate", lambda *_: self.emit('rename-requested')) action.connect("activate", lambda *_: self.emit('edit-requested'))
actions.add_action(action) actions.add_action(action)
action = Gio.SimpleAction.new("delete", None) action = Gio.SimpleAction.new("delete", None)
@ -73,6 +74,7 @@ class ProjectItem(Gtk.Box):
def _populate(self): def _populate(self):
"""Populate with project data.""" """Populate with project data."""
self.name_label.set_text(self.project.name) self.name_label.set_text(self.project.name)
self.project_icon.set_from_icon_name(self.project.icon)
# Clear and add requests # Clear and add requests
while child := self.requests_listbox.get_first_child(): while child := self.requests_listbox.get_first_child():

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="SlimProjectItem" parent="GtkButton">
<property name="tooltip-text">Project</property>
<property name="width-request">48</property>
<property name="height-request">48</property>
<signal name="clicked" handler="on_clicked"/>
<style>
<class name="flat"/>
</style>
<child>
<object class="GtkImage" id="project_icon">
<property name="icon-name">folder-symbolic</property>
<property name="icon-size">large</property>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,44 @@
# slim_project_item.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Gtk, GObject
@Gtk.Template(resource_path='/cz/vesp/roster/widgets/slim-project-item.ui')
class SlimProjectItem(Gtk.Button):
"""Slim widget showing only project icon (for folded sidebar)."""
__gtype_name__ = 'SlimProjectItem'
project_icon = Gtk.Template.Child()
__gsignals__ = {
'project-clicked': (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, project):
super().__init__()
self.project = project
self.project_icon.set_from_icon_name(project.icon)
self.set_tooltip_text(project.name)
@Gtk.Template.Callback()
def on_clicked(self, button):
"""Handle click - unfold sidebar and show this project."""
self.emit('project-clicked')

View File

@ -22,9 +22,11 @@ from .models import HttpRequest, HttpResponse, HistoryEntry
from .http_client import HttpClient from .http_client import HttpClient
from .history_manager import HistoryManager from .history_manager import HistoryManager
from .project_manager import ProjectManager from .project_manager import ProjectManager
from .icon_picker_dialog import IconPickerDialog
from .widgets.header_row import HeaderRow from .widgets.header_row import HeaderRow
from .widgets.history_item import HistoryItem from .widgets.history_item import HistoryItem
from .widgets.project_item import ProjectItem from .widgets.project_item import ProjectItem
from .widgets.slim_project_item import SlimProjectItem
from datetime import datetime from datetime import datetime
import threading import threading
@ -43,9 +45,13 @@ class RosterWindow(Adw.ApplicationWindow):
split_pane = Gtk.Template.Child() split_pane = Gtk.Template.Child()
# Sidebar widgets # Sidebar widgets
sidebar_stack = Gtk.Template.Child()
projects_listbox = Gtk.Template.Child() projects_listbox = Gtk.Template.Child()
slim_projects_box = Gtk.Template.Child()
add_project_button = Gtk.Template.Child() add_project_button = Gtk.Template.Child()
save_request_button = Gtk.Template.Child() save_request_button = Gtk.Template.Child()
fold_sidebar_button = Gtk.Template.Child()
unfold_sidebar_button = Gtk.Template.Child()
# Containers for tabs # Containers for tabs
request_tabs_container = Gtk.Template.Child() request_tabs_container = Gtk.Template.Child()
@ -452,18 +458,26 @@ class RosterWindow(Adw.ApplicationWindow):
# Clear existing # Clear existing
while child := self.projects_listbox.get_first_child(): while child := self.projects_listbox.get_first_child():
self.projects_listbox.remove(child) self.projects_listbox.remove(child)
while child := self.slim_projects_box.get_first_child():
self.slim_projects_box.remove(child)
# Load and populate # Load and populate
projects = self.project_manager.load_projects() projects = self.project_manager.load_projects()
for project in projects: for project in projects:
# Full project item
item = ProjectItem(project) item = ProjectItem(project)
item.connect('rename-requested', self._on_project_rename, project) item.connect('edit-requested', self._on_project_edit, project)
item.connect('delete-requested', self._on_project_delete, project) item.connect('delete-requested', self._on_project_delete, project)
item.connect('add-request-requested', self._on_add_to_project, project) item.connect('add-request-requested', self._on_add_to_project, project)
item.connect('request-load-requested', self._on_load_request) item.connect('request-load-requested', self._on_load_request)
item.connect('request-delete-requested', self._on_delete_request, project) item.connect('request-delete-requested', self._on_delete_request, project)
self.projects_listbox.append(item) self.projects_listbox.append(item)
# Slim project item
slim_item = SlimProjectItem(project)
slim_item.connect('project-clicked', self._on_slim_project_clicked)
self.slim_projects_box.append(slim_item)
@Gtk.Template.Callback() @Gtk.Template.Callback()
def on_add_project_clicked(self, button): def on_add_project_clicked(self, button):
"""Show dialog to create project.""" """Show dialog to create project."""
@ -491,25 +505,53 @@ class RosterWindow(Adw.ApplicationWindow):
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(self) dialog.present(self)
def _on_project_rename(self, widget, project): def _on_project_edit(self, widget, project):
"""Show rename dialog.""" """Show edit project dialog with icon picker."""
dialog = Adw.AlertDialog() dialog = Adw.AlertDialog()
dialog.set_heading("Rename Project") dialog.set_heading("Edit Project")
dialog.set_body(f"Rename '{project.name}' to:") dialog.set_body(f"Edit '{project.name}':")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
# Project name entry
name_label = Gtk.Label(label="Name:", xalign=0)
entry = Gtk.Entry() entry = Gtk.Entry()
entry.set_text(project.name) entry.set_text(project.name)
dialog.set_extra_child(entry) box.append(name_label)
box.append(entry)
# Icon selector button
icon_label = Gtk.Label(label="Icon:", xalign=0)
icon_button = Gtk.Button()
icon_button.set_child(Gtk.Image.new_from_icon_name(project.icon))
icon_button.selected_icon = project.icon # Store current selection
def on_icon_button_clicked(btn):
icon_dialog = IconPickerDialog(current_icon=btn.selected_icon)
def on_icon_selected(dlg, icon_name):
btn.selected_icon = icon_name
btn.set_child(Gtk.Image.new_from_icon_name(icon_name))
icon_dialog.connect('icon-selected', on_icon_selected)
icon_dialog.present(self)
icon_button.connect('clicked', on_icon_button_clicked)
box.append(icon_label)
box.append(icon_button)
dialog.set_extra_child(box)
dialog.add_response("cancel", "Cancel") dialog.add_response("cancel", "Cancel")
dialog.add_response("rename", "Rename") dialog.add_response("save", "Save")
dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED) dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
def on_response(dlg, response): def on_response(dlg, response):
if response == "rename": if response == "save":
new_name = entry.get_text().strip() new_name = entry.get_text().strip()
new_icon = icon_button.selected_icon
if new_name: if new_name:
self.project_manager.rename_project(project.id, new_name) self.project_manager.update_project(project.id, new_name, new_icon)
self._load_projects() self._load_projects()
dialog.connect("response", on_response) dialog.connect("response", on_response)
@ -667,3 +709,19 @@ class RosterWindow(Adw.ApplicationWindow):
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(self) dialog.present(self)
# Sidebar fold/unfold methods
@Gtk.Template.Callback()
def on_fold_sidebar_clicked(self, button):
"""Fold the sidebar to slim view."""
self.sidebar_stack.set_visible_child_name("slim")
@Gtk.Template.Callback()
def on_unfold_sidebar_clicked(self, button):
"""Unfold the sidebar to full view."""
self.sidebar_stack.set_visible_child_name("full")
def _on_slim_project_clicked(self, slim_item):
"""Handle click on slim project item - unfold sidebar."""
self.sidebar_stack.set_visible_child_name("full")