Transpose environments table: variables as rows, environments as columns

This commit is contained in:
Pavel Baksy 2025-12-31 00:02:29 +01:00
parent a62c37f5f2
commit cb57d038f1
11 changed files with 477 additions and 99 deletions

View File

@ -20,70 +20,20 @@
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="vexpand">true</property> <property name="vexpand">true</property>
<child> <child>
<object class="GtkBox"> <object class="AdwClamp">
<property name="orientation">vertical</property> <property name="maximum-size">1200</property>
<property name="margin-start">12</property> <property name="tightening-threshold">800</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="spacing">24</property>
<!-- Variables Section -->
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label">Variables</property>
<property name="xalign">0</property>
<property name="hexpand">True</property>
<style>
<class name="title-4"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="add_variable_button">
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text">Add variable</property>
<signal name="clicked" handler="on_add_variable_clicked"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkFrame">
<child>
<object class="GtkListBox" id="variables_listbox">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<!-- Environments Section -->
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">vertical</property> <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> <property name="spacing">12</property>
<!-- Header Section -->
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">horizontal</property> <property name="orientation">horizontal</property>
@ -100,8 +50,21 @@
</object> </object>
</child> </child>
<child>
<object class="GtkButton" id="add_variable_button">
<property name="label">Add Variable</property>
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text">Add variable</property>
<signal name="clicked" handler="on_add_variable_clicked"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
<child> <child>
<object class="GtkButton" id="add_environment_button"> <object class="GtkButton" id="add_environment_button">
<property name="label">Add Environment</property>
<property name="icon-name">list-add-symbolic</property> <property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text">Add environment</property> <property name="tooltip-text">Add environment</property>
<signal name="clicked" handler="on_add_environment_clicked"/> <signal name="clicked" handler="on_add_environment_clicked"/>
@ -113,11 +76,12 @@
</object> </object>
</child> </child>
<!-- Table Frame -->
<child> <child>
<object class="GtkFrame"> <object class="GtkFrame">
<child> <child>
<object class="GtkListBox" id="environments_listbox"> <object class="GtkBox" id="table_container">
<property name="selection-mode">none</property> <property name="orientation">vertical</property>
<style> <style>
<class name="boxed-list"/> <class name="boxed-list"/>
</style> </style>

View File

@ -20,6 +20,8 @@
from gi.repository import Adw, Gtk, GObject from gi.repository import Adw, Gtk, GObject
from .widgets.variable_row import VariableRow from .widgets.variable_row import VariableRow
from .widgets.environment_row import EnvironmentRow 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') @Gtk.Template(resource_path='/cz/vesp/roster/environments-dialog.ui')
@ -28,9 +30,8 @@ class EnvironmentsDialog(Adw.Dialog):
__gtype_name__ = 'EnvironmentsDialog' __gtype_name__ = 'EnvironmentsDialog'
variables_listbox = Gtk.Template.Child() table_container = Gtk.Template.Child()
add_variable_button = Gtk.Template.Child() add_variable_button = Gtk.Template.Child()
environments_listbox = Gtk.Template.Child()
add_environment_button = Gtk.Template.Child() add_environment_button = Gtk.Template.Child()
__gsignals__ = { __gsignals__ = {
@ -41,35 +42,50 @@ class EnvironmentsDialog(Adw.Dialog):
super().__init__() super().__init__()
self.project = project self.project = project
self.project_manager = project_manager self.project_manager = project_manager
self._populate_variables() self.size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
self._populate_environments() self.header_row = None
self.data_rows = []
self._populate_table()
def _populate_variables(self): def _populate_table(self):
"""Populate variables list.""" """Populate the transposed environment-variable table."""
# Clear existing # Clear existing
while child := self.variables_listbox.get_first_child(): while child := self.table_container.get_first_child():
self.variables_listbox.remove(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: for var_name in self.project.variable_names:
row = VariableRow(var_name) row = VariableDataRow(
row.connect('remove-requested', self._on_variable_remove, var_name) var_name,
row.connect('changed', self._on_variable_changed, var_name, row) self.project.environments,
self.variables_listbox.append(row) 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): self.table_container.append(row)
"""Populate environments list.""" self.data_rows.append(row)
# 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)
@Gtk.Template.Callback() @Gtk.Template.Callback()
def on_add_variable_clicked(self, button): def on_add_variable_clicked(self, button):
@ -95,8 +111,7 @@ class EnvironmentsDialog(Adw.Dialog):
self.project_manager.add_variable(self.project.id, name) self.project_manager.add_variable(self.project.id, name)
# Reload project data # Reload project data
self._reload_project() self._reload_project()
self._populate_variables() self._populate_table()
self._populate_environments()
self.emit('environments-updated') self.emit('environments-updated')
dialog.connect("response", on_response) dialog.connect("response", on_response)
@ -117,8 +132,7 @@ class EnvironmentsDialog(Adw.Dialog):
if response == "delete": if response == "delete":
self.project_manager.delete_variable(self.project.id, var_name) self.project_manager.delete_variable(self.project.id, var_name)
self._reload_project() self._reload_project()
self._populate_variables() self._populate_table()
self._populate_environments()
self.emit('environments-updated') self.emit('environments-updated')
dialog.connect("response", on_response) 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: 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.project_manager.rename_variable(self.project.id, old_name, new_name)
self._reload_project() self._reload_project()
self._populate_variables() self._populate_table()
self._populate_environments()
self.emit('environments-updated') self.emit('environments-updated')
@Gtk.Template.Callback() @Gtk.Template.Callback()
@ -157,7 +170,7 @@ class EnvironmentsDialog(Adw.Dialog):
if name: if name:
self.project_manager.add_environment(self.project.id, name) self.project_manager.add_environment(self.project.id, name)
self._reload_project() self._reload_project()
self._populate_environments() self._populate_table()
self.emit('environments-updated') self.emit('environments-updated')
dialog.connect("response", on_response) dialog.connect("response", on_response)
@ -183,7 +196,7 @@ class EnvironmentsDialog(Adw.Dialog):
if new_name: if new_name:
self.project_manager.update_environment(self.project.id, environment.id, name=new_name) self.project_manager.update_environment(self.project.id, environment.id, name=new_name)
self._reload_project() self._reload_project()
self._populate_environments() self._populate_table()
self.emit('environments-updated') self.emit('environments-updated')
dialog.connect("response", on_response) dialog.connect("response", on_response)
@ -213,15 +226,15 @@ class EnvironmentsDialog(Adw.Dialog):
if response == "delete": if response == "delete":
self.project_manager.delete_environment(self.project.id, environment.id) self.project_manager.delete_environment(self.project.id, environment.id)
self._reload_project() self._reload_project()
self._populate_environments() self._populate_table()
self.emit('environments-updated') self.emit('environments-updated')
dialog.connect("response", on_response) dialog.connect("response", on_response)
dialog.present(self) dialog.present(self)
def _on_environment_value_changed(self, widget, var_name, value, environment): def _on_value_changed(self, widget, env_id, value, var_name):
"""Handle environment variable value change.""" """Handle value change in table cell."""
self.project_manager.update_environment_variable(self.project.id, environment.id, var_name, value) self.project_manager.update_environment_variable(self.project.id, env_id, var_name, value)
self.emit('environments-updated') self.emit('environments-updated')
def _reload_project(self): def _reload_project(self):

View File

@ -54,6 +54,9 @@ widgets_sources = [
'widgets/request_item.py', 'widgets/request_item.py',
'widgets/variable_row.py', 'widgets/variable_row.py',
'widgets/environment_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') install_data(widgets_sources, install_dir: moduledir / 'widgets')

View File

@ -12,5 +12,8 @@
<file preprocess="xml-stripblanks">widgets/request-item.ui</file> <file preprocess="xml-stripblanks">widgets/request-item.ui</file>
<file preprocess="xml-stripblanks">widgets/variable-row.ui</file> <file preprocess="xml-stripblanks">widgets/variable-row.ui</file>
<file preprocess="xml-stripblanks">widgets/environment-row.ui</file> <file preprocess="xml-stripblanks">widgets/environment-row.ui</file>
<file preprocess="xml-stripblanks">widgets/environment-column-header.ui</file>
<file preprocess="xml-stripblanks">widgets/environment-header-row.ui</file>
<file preprocess="xml-stripblanks">widgets/variable-data-row.ui</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -23,5 +23,18 @@ from .project_item import ProjectItem
from .request_item import RequestItem from .request_item import RequestItem
from .variable_row import VariableRow from .variable_row import VariableRow
from .environment_row import EnvironmentRow 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',
]

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="EnvironmentColumnHeader" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<property name="width-request">200</property>
<child>
<object class="GtkLabel" id="name_label">
<property name="xalign">0.5</property>
<property name="ellipsize">end</property>
<property name="max-width-chars">20</property>
<style>
<class name="heading"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="halign">center</property>
<property name="spacing">4</property>
<child>
<object class="GtkButton" id="edit_button">
<property name="icon-name">document-edit-symbolic</property>
<property name="tooltip-text">Edit environment</property>
<signal name="clicked" handler="on_edit_clicked"/>
<style>
<class name="flat"/>
<class name="circular"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="delete_button">
<property name="icon-name">edit-delete-symbolic</property>
<property name="tooltip-text">Delete environment</property>
<signal name="clicked" handler="on_delete_clicked"/>
<style>
<class name="flat"/>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
</template>
</interface>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="EnvironmentHeaderRow" parent="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkBox" id="variable_cell">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="width-request">150</property>
<child>
<object class="GtkLabel" id="variable_label">
<property name="label">Variable</property>
<property name="xalign">0</property>
<property name="hexpand">True</property>
<style>
<class name="heading"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="columns_box">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="hexpand">True</property>
</object>
</child>
</template>
</interface>

View File

@ -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 <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/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)

View File

@ -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 <https://www.gnu.org/licenses/>.
#
# 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()

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="VariableDataRow" parent="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<child>
<object class="GtkBox" id="variable_cell">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="width-request">150</property>
<child>
<object class="GtkEntry" id="name_entry">
<property name="placeholder-text">Variable name</property>
<property name="hexpand">True</property>
<signal name="changed" handler="on_name_changed"/>
</object>
</child>
<child>
<object class="GtkButton" id="delete_button">
<property name="icon-name">edit-delete-symbolic</property>
<property name="tooltip-text">Delete variable</property>
<signal name="clicked" handler="on_delete_clicked"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="values_box">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="hexpand">True</property>
</object>
</child>
</template>
</interface>

View File

@ -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 <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/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()