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:
parent
a73b838c7f
commit
6418aa0695
69
src/constants.py
Normal file
69
src/constants.py
Normal 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
56
src/icon-picker-dialog.ui
Normal 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
70
src/icon_picker_dialog.py
Normal 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()
|
||||||
@ -83,6 +83,14 @@
|
|||||||
|
|
||||||
<!-- START: Sidebar for Projects -->
|
<!-- START: Sidebar for Projects -->
|
||||||
<property name="start-child">
|
<property name="start-child">
|
||||||
|
<object class="GtkStack" id="sidebar_stack">
|
||||||
|
<property name="transition-type">slide-left-right</property>
|
||||||
|
|
||||||
|
<!-- Full Sidebar -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkStackPage">
|
||||||
|
<property name="name">full</property>
|
||||||
|
<property name="child">
|
||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="width-request">200</property>
|
<property name="width-request">200</property>
|
||||||
@ -118,6 +126,17 @@
|
|||||||
</style>
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</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>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
@ -150,6 +169,54 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
|
||||||
|
<!-- Slim Sidebar -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkStackPage">
|
||||||
|
<property name="name">slim</property>
|
||||||
|
<property name="child">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="width-request">60</property>
|
||||||
|
|
||||||
|
<!-- Unfold Button -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="unfold_sidebar_button">
|
||||||
|
<property name="icon-name">sidebar-show-left-symbolic</property>
|
||||||
|
<property name="tooltip-text">Unfold Sidebar</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<signal name="clicked" handler="on_unfold_sidebar_clicked"/>
|
||||||
|
<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>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</property>
|
||||||
|
|
||||||
<!-- END: Request/Response Split -->
|
<!-- END: Request/Response Split -->
|
||||||
<property name="end-child">
|
<property name="end-child">
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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:
|
||||||
|
if new_name is not None:
|
||||||
p.name = new_name
|
p.name = new_name
|
||||||
|
if new_icon is not None:
|
||||||
|
p.icon = new_icon
|
||||||
break
|
break
|
||||||
self.save_projects(projects)
|
self.save_projects(projects)
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
21
src/widgets/slim-project-item.ui
Normal file
21
src/widgets/slim-project-item.ui
Normal 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>
|
||||||
44
src/widgets/slim_project_item.py
Normal file
44
src/widgets/slim_project_item.py
Normal 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')
|
||||||
@ -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")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user