From cb57d038f19e4cefc38c9c483c83ff6c6edea6f6 Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Wed, 31 Dec 2025 00:02:29 +0100 Subject: [PATCH] Transpose environments table: variables as rows, environments as columns --- src/environments-dialog.ui | 84 +++++------------- src/environments_dialog.py | 89 +++++++++++-------- src/meson.build | 3 + src/roster.gresource.xml | 3 + src/widgets/__init__.py | 15 +++- src/widgets/environment-column-header.ui | 52 +++++++++++ src/widgets/environment-header-row.ui | 39 +++++++++ src/widgets/environment_column_header.py | 55 ++++++++++++ src/widgets/environment_header_row.py | 83 ++++++++++++++++++ src/widgets/variable-data-row.ui | 47 ++++++++++ src/widgets/variable_data_row.py | 106 +++++++++++++++++++++++ 11 files changed, 477 insertions(+), 99 deletions(-) create mode 100644 src/widgets/environment-column-header.ui create mode 100644 src/widgets/environment-header-row.ui create mode 100644 src/widgets/environment_column_header.py create mode 100644 src/widgets/environment_header_row.py create mode 100644 src/widgets/variable-data-row.ui create mode 100644 src/widgets/variable_data_row.py diff --git a/src/environments-dialog.ui b/src/environments-dialog.ui index 090209a..c79abd5 100644 --- a/src/environments-dialog.ui +++ b/src/environments-dialog.ui @@ -20,70 +20,20 @@ true - - vertical - 12 - 12 - 12 - 12 - 24 + + 1200 + 800 - - - - vertical - 12 - - - - horizontal - 12 - - - - Variables - 0 - True - - - - - - - list-add-symbolic - Add variable - - - - - - - - - - - - none - - - - - - - - - vertical + 12 + 12 + 12 + 12 12 + horizontal @@ -100,8 +50,21 @@ + + + Add Variable + list-add-symbolic + Add variable + + + + + + Add Environment list-add-symbolic Add environment @@ -113,11 +76,12 @@ + - - none + + vertical diff --git a/src/environments_dialog.py b/src/environments_dialog.py index 0a18859..775de95 100644 --- a/src/environments_dialog.py +++ b/src/environments_dialog.py @@ -20,6 +20,8 @@ from gi.repository import Adw, Gtk, GObject from .widgets.variable_row import VariableRow from .widgets.environment_row import EnvironmentRow +from .widgets.environment_header_row import EnvironmentHeaderRow +from .widgets.variable_data_row import VariableDataRow @Gtk.Template(resource_path='/cz/vesp/roster/environments-dialog.ui') @@ -28,9 +30,8 @@ class EnvironmentsDialog(Adw.Dialog): __gtype_name__ = 'EnvironmentsDialog' - variables_listbox = Gtk.Template.Child() + table_container = Gtk.Template.Child() add_variable_button = Gtk.Template.Child() - environments_listbox = Gtk.Template.Child() add_environment_button = Gtk.Template.Child() __gsignals__ = { @@ -41,35 +42,50 @@ class EnvironmentsDialog(Adw.Dialog): super().__init__() self.project = project self.project_manager = project_manager - self._populate_variables() - self._populate_environments() + self.size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) + self.header_row = None + self.data_rows = [] + self._populate_table() - def _populate_variables(self): - """Populate variables list.""" + def _populate_table(self): + """Populate the transposed environment-variable table.""" # Clear existing - while child := self.variables_listbox.get_first_child(): - self.variables_listbox.remove(child) + while child := self.table_container.get_first_child(): + self.table_container.remove(child) - # Add variables + self.data_rows = [] + + # Create header row + self.header_row = EnvironmentHeaderRow( + self.project.environments, + self.size_group + ) + self.header_row.connect('environment-edit-requested', + self._on_environment_edit) + self.header_row.connect('environment-delete-requested', + self._on_environment_delete) + self.table_container.append(self.header_row) + + # Add separator + separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + self.table_container.append(separator) + + # Create data rows for each variable for var_name in self.project.variable_names: - row = VariableRow(var_name) - row.connect('remove-requested', self._on_variable_remove, var_name) - row.connect('changed', self._on_variable_changed, var_name, row) - self.variables_listbox.append(row) + row = VariableDataRow( + var_name, + self.project.environments, + self.size_group + ) + row.connect('variable-changed', + self._on_variable_changed, var_name, row) + row.connect('variable-delete-requested', + self._on_variable_remove, var_name) + row.connect('value-changed', + self._on_value_changed, var_name) - def _populate_environments(self): - """Populate environments list.""" - # Clear existing - while child := self.environments_listbox.get_first_child(): - self.environments_listbox.remove(child) - - # Add environments - for env in self.project.environments: - row = EnvironmentRow(env, self.project.variable_names) - row.connect('edit-requested', self._on_environment_edit, env) - row.connect('delete-requested', self._on_environment_delete, env) - row.connect('value-changed', self._on_environment_value_changed, env) - self.environments_listbox.append(row) + self.table_container.append(row) + self.data_rows.append(row) @Gtk.Template.Callback() def on_add_variable_clicked(self, button): @@ -95,8 +111,7 @@ class EnvironmentsDialog(Adw.Dialog): self.project_manager.add_variable(self.project.id, name) # Reload project data self._reload_project() - self._populate_variables() - self._populate_environments() + self._populate_table() self.emit('environments-updated') dialog.connect("response", on_response) @@ -117,8 +132,7 @@ class EnvironmentsDialog(Adw.Dialog): if response == "delete": self.project_manager.delete_variable(self.project.id, var_name) self._reload_project() - self._populate_variables() - self._populate_environments() + self._populate_table() self.emit('environments-updated') dialog.connect("response", on_response) @@ -130,8 +144,7 @@ class EnvironmentsDialog(Adw.Dialog): if new_name and new_name != old_name and new_name not in self.project.variable_names: self.project_manager.rename_variable(self.project.id, old_name, new_name) self._reload_project() - self._populate_variables() - self._populate_environments() + self._populate_table() self.emit('environments-updated') @Gtk.Template.Callback() @@ -157,7 +170,7 @@ class EnvironmentsDialog(Adw.Dialog): if name: self.project_manager.add_environment(self.project.id, name) self._reload_project() - self._populate_environments() + self._populate_table() self.emit('environments-updated') dialog.connect("response", on_response) @@ -183,7 +196,7 @@ class EnvironmentsDialog(Adw.Dialog): if new_name: self.project_manager.update_environment(self.project.id, environment.id, name=new_name) self._reload_project() - self._populate_environments() + self._populate_table() self.emit('environments-updated') dialog.connect("response", on_response) @@ -213,15 +226,15 @@ class EnvironmentsDialog(Adw.Dialog): if response == "delete": self.project_manager.delete_environment(self.project.id, environment.id) self._reload_project() - self._populate_environments() + self._populate_table() self.emit('environments-updated') dialog.connect("response", on_response) dialog.present(self) - def _on_environment_value_changed(self, widget, var_name, value, environment): - """Handle environment variable value change.""" - self.project_manager.update_environment_variable(self.project.id, environment.id, var_name, value) + def _on_value_changed(self, widget, env_id, value, var_name): + """Handle value change in table cell.""" + self.project_manager.update_environment_variable(self.project.id, env_id, var_name, value) self.emit('environments-updated') def _reload_project(self): diff --git a/src/meson.build b/src/meson.build index 986d082..6d5aebb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -54,6 +54,9 @@ widgets_sources = [ 'widgets/request_item.py', 'widgets/variable_row.py', 'widgets/environment_row.py', + 'widgets/environment_column_header.py', + 'widgets/environment_header_row.py', + 'widgets/variable_data_row.py', ] install_data(widgets_sources, install_dir: moduledir / 'widgets') diff --git a/src/roster.gresource.xml b/src/roster.gresource.xml index 065e2e8..af734a0 100644 --- a/src/roster.gresource.xml +++ b/src/roster.gresource.xml @@ -12,5 +12,8 @@ widgets/request-item.ui widgets/variable-row.ui widgets/environment-row.ui + widgets/environment-column-header.ui + widgets/environment-header-row.ui + widgets/variable-data-row.ui diff --git a/src/widgets/__init__.py b/src/widgets/__init__.py index 97e06e1..405b308 100644 --- a/src/widgets/__init__.py +++ b/src/widgets/__init__.py @@ -23,5 +23,18 @@ from .project_item import ProjectItem from .request_item import RequestItem from .variable_row import VariableRow from .environment_row import EnvironmentRow +from .environment_column_header import EnvironmentColumnHeader +from .environment_header_row import EnvironmentHeaderRow +from .variable_data_row import VariableDataRow -__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem', 'VariableRow', 'EnvironmentRow'] +__all__ = [ + 'HeaderRow', + 'HistoryItem', + 'ProjectItem', + 'RequestItem', + 'VariableRow', + 'EnvironmentRow', + 'EnvironmentColumnHeader', + 'EnvironmentHeaderRow', + 'VariableDataRow', +] diff --git a/src/widgets/environment-column-header.ui b/src/widgets/environment-column-header.ui new file mode 100644 index 0000000..4fded58 --- /dev/null +++ b/src/widgets/environment-column-header.ui @@ -0,0 +1,52 @@ + + + + + diff --git a/src/widgets/environment-header-row.ui b/src/widgets/environment-header-row.ui new file mode 100644 index 0000000..7585fb4 --- /dev/null +++ b/src/widgets/environment-header-row.ui @@ -0,0 +1,39 @@ + + + + + diff --git a/src/widgets/environment_column_header.py b/src/widgets/environment_column_header.py new file mode 100644 index 0000000..fe9dd01 --- /dev/null +++ b/src/widgets/environment_column_header.py @@ -0,0 +1,55 @@ +# environment_column_header.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 . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Gtk, GObject + + +@Gtk.Template(resource_path='/cz/vesp/roster/widgets/environment-column-header.ui') +class EnvironmentColumnHeader(Gtk.Box): + """Widget for displaying an environment column header with edit/delete buttons.""" + + __gtype_name__ = 'EnvironmentColumnHeader' + + name_label = Gtk.Template.Child() + edit_button = Gtk.Template.Child() + delete_button = Gtk.Template.Child() + + __gsignals__ = { + 'edit-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), + } + + def __init__(self, environment): + super().__init__() + self.environment = environment + self.name_label.set_text(environment.name) + + @Gtk.Template.Callback() + def on_edit_clicked(self, button): + """Handle edit button click.""" + self.emit('edit-requested') + + @Gtk.Template.Callback() + def on_delete_clicked(self, button): + """Handle delete button click.""" + self.emit('delete-requested') + + def update_name(self, name): + """Update the environment name display.""" + self.name_label.set_text(name) diff --git a/src/widgets/environment_header_row.py b/src/widgets/environment_header_row.py new file mode 100644 index 0000000..e42e0a4 --- /dev/null +++ b/src/widgets/environment_header_row.py @@ -0,0 +1,83 @@ +# environment_header_row.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 . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Gtk, GObject +from .environment_column_header import EnvironmentColumnHeader + + +@Gtk.Template(resource_path='/cz/vesp/roster/widgets/environment-header-row.ui') +class EnvironmentHeaderRow(Gtk.Box): + """Widget for the header row containing all environment column headers.""" + + __gtype_name__ = 'EnvironmentHeaderRow' + + variable_cell = Gtk.Template.Child() + variable_label = Gtk.Template.Child() + columns_box = Gtk.Template.Child() + + __gsignals__ = { + 'environment-edit-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)), + 'environment-delete-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)), + } + + def __init__(self, environments, size_group): + super().__init__() + self.environments = environments + self.size_group = size_group + self.column_headers = {} + + # Add variable cell to size group for alignment + if size_group: + size_group.add_widget(self.variable_cell) + + self._populate() + + def _populate(self): + """Populate with environment column headers.""" + # Clear existing + while child := self.columns_box.get_first_child(): + self.columns_box.remove(child) + + self.column_headers = {} + + # Create column header for each environment + for env in self.environments: + header = EnvironmentColumnHeader(env) + header.connect('edit-requested', self._on_edit_requested, env) + header.connect('delete-requested', self._on_delete_requested, env) + + # Add to size group for alignment with data rows + if self.size_group: + self.size_group.add_widget(header) + + self.columns_box.append(header) + self.column_headers[env.id] = header + + def _on_edit_requested(self, widget, environment): + """Handle edit request from column header.""" + self.emit('environment-edit-requested', environment) + + def _on_delete_requested(self, widget, environment): + """Handle delete request from column header.""" + self.emit('environment-delete-requested', environment) + + def refresh(self, environments): + """Refresh with updated environments list.""" + self.environments = environments + self._populate() diff --git a/src/widgets/variable-data-row.ui b/src/widgets/variable-data-row.ui new file mode 100644 index 0000000..2aa3ed4 --- /dev/null +++ b/src/widgets/variable-data-row.ui @@ -0,0 +1,47 @@ + + + + + diff --git a/src/widgets/variable_data_row.py b/src/widgets/variable_data_row.py new file mode 100644 index 0000000..1ecade6 --- /dev/null +++ b/src/widgets/variable_data_row.py @@ -0,0 +1,106 @@ +# variable_data_row.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 . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from gi.repository import Gtk, GObject + + +@Gtk.Template(resource_path='/cz/vesp/roster/widgets/variable-data-row.ui') +class VariableDataRow(Gtk.Box): + """Widget for a data row with variable name and values for each environment.""" + + __gtype_name__ = 'VariableDataRow' + + variable_cell = Gtk.Template.Child() + name_entry = Gtk.Template.Child() + delete_button = Gtk.Template.Child() + values_box = Gtk.Template.Child() + + __gsignals__ = { + 'variable-changed': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'variable-delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'value-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, str)), # env_id, value + } + + def __init__(self, variable_name, environments, size_group): + super().__init__() + self.variable_name = variable_name + self.environments = environments + self.size_group = size_group + self.value_entries = {} + + # Set variable name + self.name_entry.set_text(variable_name) + + # Add variable cell to size group for alignment + if size_group: + size_group.add_widget(self.variable_cell) + + self._populate_values() + + def _populate_values(self): + """Populate value entries for each environment.""" + # Clear existing + while child := self.values_box.get_first_child(): + self.values_box.remove(child) + + self.value_entries = {} + + # Create entry for each environment + for env in self.environments: + # Create a box to hold the entry (for size group alignment) + entry_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + entry_box.set_width_request(200) + + entry = Gtk.Entry() + entry.set_placeholder_text(env.name) + entry.set_text(env.variables.get(self.variable_name, "")) + entry.set_hexpand(True) + entry.connect('changed', self._on_value_changed, env.id) + + entry_box.append(entry) + + # Add entry box to size group for alignment + if self.size_group: + self.size_group.add_widget(entry_box) + + self.values_box.append(entry_box) + self.value_entries[env.id] = entry + + def _on_value_changed(self, entry, env_id): + """Handle value entry changes.""" + self.emit('value-changed', env_id, entry.get_text()) + + @Gtk.Template.Callback() + def on_name_changed(self, entry): + """Handle variable name changes.""" + self.emit('variable-changed') + + @Gtk.Template.Callback() + def on_delete_clicked(self, button): + """Handle delete button click.""" + self.emit('variable-delete-requested') + + def get_variable_name(self): + """Return current variable name.""" + return self.name_entry.get_text().strip() + + def refresh(self, environments): + """Refresh with updated environments list.""" + self.environments = environments + self._populate_values()