roster/src/environments_dialog.py

285 lines
11 KiB
Python

# environments_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 .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')
class EnvironmentsDialog(Adw.Dialog):
"""Dialog for managing project environments and variables."""
__gtype_name__ = 'EnvironmentsDialog'
table_container = Gtk.Template.Child()
add_environment_button = Gtk.Template.Child()
__gsignals__ = {
'environments-updated': (GObject.SIGNAL_RUN_FIRST, None, ()),
}
def __init__(self, project, project_manager, recently_updated_variables=None):
super().__init__()
self.project = project
self.project_manager = project_manager
self.recently_updated_variables = recently_updated_variables or {}
self.size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
self.header_row = None
self.data_rows = []
self._populate_table()
def _populate_table(self):
"""Populate the transposed environment-variable table."""
# Clear existing
while child := self.table_container.get_first_child():
self.table_container.remove(child)
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:
# Check if variable was recently updated
update_timestamp = self.recently_updated_variables.get(var_name)
row = VariableDataRow(
var_name,
self.project.environments,
self.size_group,
update_timestamp=update_timestamp
)
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)
self.table_container.append(row)
self.data_rows.append(row)
# Add "Add Variable" button row at the bottom
add_var_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
add_var_row.set_margin_start(12)
add_var_row.set_margin_end(12)
add_var_row.set_margin_top(6)
add_var_row.set_margin_bottom(6)
# Variable cell with add button
var_cell = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
var_cell.set_size_request(150, -1)
if self.size_group:
self.size_group.add_widget(var_cell)
add_variable_button = Gtk.Button()
add_variable_button.set_icon_name("list-add-symbolic")
add_variable_button.set_tooltip_text("Add variable")
add_variable_button.connect('clicked', self.on_add_variable_clicked)
add_variable_button.add_css_class("flat")
add_variable_button.add_css_class("circular")
var_cell.append(add_variable_button)
add_var_row.append(var_cell)
# Empty space for value columns
empty_space = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
empty_space.set_hexpand(True)
add_var_row.append(empty_space)
self.table_container.append(add_var_row)
def on_add_variable_clicked(self, button):
"""Add new variable."""
dialog = Adw.AlertDialog()
dialog.set_heading("New Variable")
dialog.set_body("Enter variable name:")
entry = Gtk.Entry()
entry.set_placeholder_text("Variable name")
entry.set_activates_default(True)
dialog.set_extra_child(entry)
dialog.add_response("cancel", "Cancel")
dialog.add_response("add", "Add")
dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("add")
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "add":
name = entry.get_text().strip()
if name and name not in self.project.variable_names:
self.project_manager.add_variable(self.project.id, name)
# Reload project data
self._reload_project()
self._populate_table()
self.emit('environments-updated')
dialog.connect("response", on_response)
dialog.present(self)
def _on_variable_remove(self, widget, var_name):
"""Remove variable with confirmation."""
dialog = Adw.AlertDialog()
dialog.set_heading("Delete Variable?")
dialog.set_body(f"Delete variable '{var_name}' from all environments? This cannot be undone.")
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "delete":
self.project_manager.delete_variable(self.project.id, var_name)
self._reload_project()
self._populate_table()
self.emit('environments-updated')
dialog.connect("response", on_response)
dialog.present(self)
def _on_variable_changed(self, widget, old_name, row):
"""Handle variable name change."""
new_name = row.get_variable_name()
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_table()
self.emit('environments-updated')
@Gtk.Template.Callback()
def on_add_environment_clicked(self, button):
"""Add new environment."""
dialog = Adw.AlertDialog()
dialog.set_heading("New Environment")
dialog.set_body("Enter environment name:")
entry = Gtk.Entry()
entry.set_placeholder_text("Environment name (e.g., Production)")
entry.set_activates_default(True)
dialog.set_extra_child(entry)
dialog.add_response("cancel", "Cancel")
dialog.add_response("add", "Add")
dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("add")
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "add":
name = entry.get_text().strip()
if name:
self.project_manager.add_environment(self.project.id, name)
self._reload_project()
self._populate_table()
self.emit('environments-updated')
dialog.connect("response", on_response)
dialog.present(self)
def _on_environment_edit(self, widget, environment):
"""Edit environment name."""
dialog = Adw.AlertDialog()
dialog.set_heading("Edit Environment")
dialog.set_body("Enter new name:")
entry = Gtk.Entry()
entry.set_text(environment.name)
entry.set_activates_default(True)
dialog.set_extra_child(entry)
dialog.add_response("cancel", "Cancel")
dialog.add_response("save", "Save")
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("save")
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "save":
new_name = entry.get_text().strip()
if new_name:
self.project_manager.update_environment(self.project.id, environment.id, name=new_name)
self._reload_project()
self._populate_table()
self.emit('environments-updated')
dialog.connect("response", on_response)
dialog.present(self)
def _on_environment_delete(self, widget, environment):
"""Delete environment with confirmation."""
# Prevent deletion of last environment
if len(self.project.environments) <= 1:
dialog = Adw.AlertDialog()
dialog.set_heading("Cannot Delete")
dialog.set_body("Each project must have at least one environment.")
dialog.add_response("ok", "OK")
dialog.present(self)
return
dialog = Adw.AlertDialog()
dialog.set_heading("Delete Environment?")
dialog.set_body(f"Delete environment '{environment.name}'? This cannot be undone.")
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_close_response("cancel")
def on_response(dlg, response):
if response == "delete":
self.project_manager.delete_environment(self.project.id, environment.id)
self._reload_project()
self._populate_table()
self.emit('environments-updated')
dialog.connect("response", on_response)
dialog.present(self)
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):
"""Reload project data from manager."""
projects = self.project_manager.load_projects()
for p in projects:
if p.id == self.project.id:
self.project = p
break