# 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 . # # 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/bugsy/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