# window.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 import gi gi.require_version('GtkSource', '5') from gi.repository import Adw, Gtk, GLib, Gio, GtkSource from .models import HttpRequest, HttpResponse, HistoryEntry, RequestTab from .http_client import HttpClient from .history_manager import HistoryManager from .project_manager import ProjectManager from .tab_manager import TabManager from .icon_picker_dialog import IconPickerDialog from .environments_dialog import EnvironmentsDialog from .request_tab_widget import RequestTabWidget from .widgets.history_item import HistoryItem from .widgets.project_item import ProjectItem from datetime import datetime import json import uuid @Gtk.Template(resource_path='/cz/vesp/roster/main-window.ui') class RosterWindow(Adw.ApplicationWindow): __gtype_name__ = 'RosterWindow' # Top bar widgets save_request_button = Gtk.Template.Child() new_request_button = Gtk.Template.Child() tab_view = Gtk.Template.Child() tab_bar = Gtk.Template.Child() # Panes main_pane = Gtk.Template.Child() # Sidebar widgets projects_listbox = Gtk.Template.Child() add_project_button = Gtk.Template.Child() # History (hidden but kept for compatibility) history_listbox = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) self.http_client = HttpClient() self.history_manager = HistoryManager() self.project_manager = ProjectManager() self.tab_manager = TabManager() # Map AdwTabPage to tab data self.page_to_widget = {} # AdwTabPage -> RequestTabWidget self.page_to_tab = {} # AdwTabPage -> RequestTab self.current_tab_id = None # Connect to tab view signals self.tab_view.connect('close-page', self._on_tab_close_page) self.tab_view.connect('notify::selected-page', self._on_tab_selected) # Create window actions self._create_actions() # Setup custom CSS self._setup_custom_css() # Setup UI self._setup_tab_system() self._load_projects() self._load_history() # Connect to close-request to warn about unsaved changes self.connect("close-request", self._on_close_request) # Create first tab self._create_new_tab() def _on_close_request(self, window): """Handle window close request - warn if there are unsaved changes.""" # Check if any tabs have unsaved changes modified_tabs = [] for page, widget in self.page_to_widget.items(): if widget.modified: tab = self.page_to_tab.get(page) if tab: modified_tabs.append(tab) if modified_tabs: # Show warning dialog dialog = Adw.AlertDialog() dialog.set_heading("Unsaved Changes") if len(modified_tabs) == 1: dialog.set_body(f"'{modified_tabs[0].name}' has unsaved changes. Close anyway?") else: dialog.set_body(f"{len(modified_tabs)} requests have unsaved changes. Close anyway?") dialog.add_response("cancel", "Cancel") dialog.add_response("close", "Close") dialog.set_response_appearance("close", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_default_response("cancel") dialog.set_close_response("cancel") dialog.connect("response", self._on_close_app_dialog_response) dialog.present(self) # Prevent closing for now - will close if user confirms return True # No unsaved changes, allow closing return False def _on_close_app_dialog_response(self, dialog, response): """Handle close application dialog response.""" if response == "close": # User confirmed - close the window self.destroy() def _setup_custom_css(self): """Setup custom CSS for UI styling.""" css_provider = Gtk.CssProvider() css_provider.load_from_data(b""" /* AdwToolbarView handles header bar heights automatically */ /* Just add minimal custom styling for other elements */ /* Stack switchers styling (Headers/Body tabs) */ stackswitcher button { padding: 6px 16px; min-height: 32px; border-radius: 6px; margin: 0 2px; } stackswitcher button:checked { background: @accent_bg_color; color: @accent_fg_color; font-weight: 600; } /* Warning styling for undefined variables */ entry.warning { background-color: mix(@warning_bg_color, @view_bg_color, 0.3); } """) Gtk.StyleContext.add_provider_for_display( self.get_display(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) def _setup_tab_system(self): """Set up the tab system.""" # Connect new request button self.new_request_button.connect("clicked", self._on_new_request_clicked) def _on_tab_selected(self, tab_view, param): """Handle tab selection change.""" page = tab_view.get_selected_page() if not page: return # Get the RequestTab for this page tab = self.page_to_tab.get(page) if tab: self.current_tab_id = tab.id def _on_tab_close_page(self, tab_view, page): """Handle tab close request.""" # Get the RequestTab and widget for this page tab = self.page_to_tab.get(page) widget = self.page_to_widget.get(page) # If already removed, allow close (prevents double-processing) if not tab or not widget: return False # Allow close # Check if tab has unsaved changes if widget.modified: # Show warning dialog dialog = Adw.AlertDialog() dialog.set_heading("Close Unsaved Request?") dialog.set_body(f"'{tab.name}' has unsaved changes. Close anyway?") dialog.add_response("cancel", "Cancel") dialog.add_response("close", "Close") dialog.set_response_appearance("close", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_default_response("cancel") dialog.set_close_response("cancel") def on_response(dlg, response): if response == "close": # Remove from our tracking FIRST to prevent re-triggering self.tab_manager.close_tab(tab.id) del self.page_to_tab[page] del self.page_to_widget[page] # Check if we need to create a new tab remaining_tabs = len(self.page_to_tab) if remaining_tabs == 0: # Schedule creating exactly one new tab GLib.timeout_add(50, self._create_new_tab_once) # Close the page tab_view.close_page_finish(page, True) else: # Cancel the close tab_view.close_page_finish(page, False) dialog.connect("response", on_response) dialog.present(self) return True # Defer close decision to dialog else: # No unsaved changes, allow close self.tab_manager.close_tab(tab.id) del self.page_to_tab[page] del self.page_to_widget[page] # If this will be the last tab, create a new one # Check after we've removed our tracking to get accurate count remaining_tabs = len(self.page_to_tab) if remaining_tabs == 0: # Use a single-shot timer to create exactly one new tab GLib.timeout_add(50, self._create_new_tab_once) return False # Allow close def _create_new_tab_once(self): """Create a new tab (one-time callback).""" # Only create if there really are no tabs if self.tab_view.get_n_pages() == 0: self._create_new_tab() return False # Don't repeat def _is_empty_new_request_tab(self): """Check if current tab is an empty 'New Request' tab.""" if not self.current_tab_id: return False # Find the current page page = self.tab_view.get_selected_page() if not page: return False tab = self.page_to_tab.get(page) widget = self.page_to_widget.get(page) if not tab or not widget: return False # Check if it's a "New Request" (not from saved request) if tab.saved_request_id: return False # Check if it's empty request = widget.get_request() is_empty = ( not request.url.strip() and not request.body.strip() and not request.headers ) return is_empty def _find_tab_by_saved_request_id(self, saved_request_id): """Find a tab by its saved_request_id. Returns None if not found.""" for tab in self.tab_manager.tabs: if tab.saved_request_id == saved_request_id: return tab return None def _generate_copy_name(self, base_name): """Generate a unique copy name like 'ReqA (copy)', 'ReqA (copy 2)', etc.""" # Check if "Name (copy)" exists existing_names = {tab.name for tab in self.tab_manager.tabs} copy_name = f"{base_name} (copy)" if copy_name not in existing_names: return copy_name # Find the highest number used counter = 2 while f"{base_name} (copy {counter})" in existing_names: counter += 1 return f"{base_name} (copy {counter})" def _create_new_tab(self, name="New Request", request=None, saved_request_id=None, response=None, project_id=None, selected_environment_id=None): """Create a new tab with RequestTabWidget.""" # Create tab in tab manager if not request: request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW") tab = self.tab_manager.create_tab(name, request, saved_request_id, response, project_id, selected_environment_id) self.current_tab_id = tab.id # Create RequestTabWidget for this tab widget = RequestTabWidget(tab.id, request, response, project_id, selected_environment_id) widget.original_request = tab.original_request widget.project_manager = self.project_manager # Inject project manager # Populate environment dropdown if project_id is set if project_id and hasattr(widget, '_populate_environment_dropdown'): widget._populate_environment_dropdown() # Connect to send button widget.send_button.connect("clicked", lambda btn: self._on_send_clicked(widget)) # Create AdwTabPage page = self.tab_view.append(widget) page.set_title(name) page.set_indicator_activatable(False) # Store mappings self.page_to_tab[page] = tab self.page_to_widget[page] = widget # Connect to modified state changes widget.connect('modified-changed', lambda w, modified: self._on_tab_modified_changed(page, modified)) # Select this new page self.tab_view.set_selected_page(page) return tab.id def _get_project_for_saved_request(self, saved_request_id): """Find which project contains a saved request. Returns (project_id, default_env_id) or (None, None).""" if not saved_request_id: return None, None projects = self.project_manager.load_projects() for project in projects: for request in project.requests: if request.id == saved_request_id: # Found the project - get default environment (first one) default_env_id = project.environments[0].id if project.environments else None return project.id, default_env_id return None, None def _on_tab_modified_changed(self, page, modified): """Update tab indicator when modified state changes.""" # Update the tab title with/without modified indicator tab = self.page_to_tab.get(page) if tab: if modified: # Add star to title page.set_title(f"*{tab.name}") else: # Remove star from title page.set_title(tab.name) def _switch_to_tab(self, tab_id): """Switch to a tab by its ID.""" # Find the page for this tab for page, tab in self.page_to_tab.items(): if tab.id == tab_id: self.tab_view.set_selected_page(page) return def _close_current_tab(self): """Close the currently active tab.""" page = self.tab_view.get_selected_page() if page: self.tab_view.close_page(page) def _on_new_request_clicked(self, button): """Handle New Request button click.""" self._create_new_tab() def _create_actions(self): """Create window-level actions.""" # New tab shortcut (Ctrl+T) action = Gio.SimpleAction.new("new-tab", None) action.connect("activate", lambda a, p: self._on_new_request_clicked(None)) self.add_action(action) self.get_application().set_accels_for_action("win.new-tab", ["t"]) # Close tab shortcut (Ctrl+W) action = Gio.SimpleAction.new("close-tab", None) action.connect("activate", lambda a, p: self._close_current_tab()) self.add_action(action) self.get_application().set_accels_for_action("win.close-tab", ["w"]) # Save request shortcut (Ctrl+S) action = Gio.SimpleAction.new("save-request", None) action.connect("activate", lambda a, p: self.on_save_request_clicked(None)) self.add_action(action) self.get_application().set_accels_for_action("win.save-request", ["s"]) def _on_send_clicked(self, widget): """Handle Send button click from a tab widget.""" # Validate URL request = widget.get_request() if not request.url.strip(): self._show_toast("Please enter a URL") return # Apply variable substitution if environment is selected substituted_request = request if widget.selected_environment_id: env = widget.get_selected_environment() if env: from .variable_substitution import VariableSubstitution substituted_request, undefined = VariableSubstitution.substitute_request(request, env) # Log undefined variables for debugging if undefined: print(f"Warning: Undefined variables in request: {', '.join(undefined)}") # Disable send button during request widget.send_button.set_sensitive(False) # Execute async def callback(response, error, user_data): """Callback runs on main thread.""" # Re-enable send button widget.send_button.set_sensitive(True) # Update tab with response if response: widget.display_response(response) else: widget.display_error(error or "Unknown error") # Save response to tab page = self.tab_view.get_selected_page() if page: tab = self.page_to_tab.get(page) if tab: tab.response = response # Create history entry (save the substituted request with actual values) entry = HistoryEntry( timestamp=datetime.now().isoformat(), request=substituted_request, response=response, error=error ) self.history_manager.add_entry(entry) # Refresh history panel to show new entry self._load_history() self.http_client.execute_request_async(substituted_request, callback, None) # History and Project Management def _load_history(self): """Load history from file and populate list.""" # Clear existing history items while True: child = self.history_listbox.get_first_child() if child is None: break self.history_listbox.remove(child) # Load and display history entries = self.history_manager.load_history() for entry in entries: item = HistoryItem(entry) item.connect('load-requested', self._on_history_load_requested, entry) item.connect('delete-requested', self._on_history_delete_requested, entry) self.history_listbox.append(item) def _on_history_load_requested(self, widget, entry): """Handle load request from history item.""" # Smart loading: replaces empty tabs or creates new tab if modified self._load_request_from_entry(entry) def _on_history_delete_requested(self, widget, entry): """Handle delete request from history item.""" # Delete the entry from history self.history_manager.delete_entry(entry) # Refresh the history panel self._load_history() def _load_request_from_entry(self, entry): """Load request from history entry - smart loading based on current tab state.""" request = entry.request # Generate name from method and URL url_parts = request.url.split('/') url_name = url_parts[-1] if url_parts else request.url name = f"{request.method} {url_name[:30]}" if url_name else f"{request.method} Request" # Check if current tab is an empty "New Request" if self._is_empty_new_request_tab(): # Replace the empty tab with this request if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: # Update the tab current_tab.name = name current_tab.request = request current_tab.saved_request_id = None # Not from saved requests current_tab.original_request = HttpRequest( method=request.method, url=request.url, headers=request.headers.copy(), body=request.body, syntax=getattr(request, 'syntax', 'RAW') ) current_tab.modified = False # Load into UI - get current widget and update it page = self.tab_view.get_selected_page() if page: widget = self.page_to_widget.get(page) if widget: widget._load_request(request) widget.original_request = current_tab.original_request widget.modified = False # Update tab page title page.set_title(name) else: # Current tab has changes or is not a "New Request" # Create a new tab self._create_new_tab( name=name, request=request, saved_request_id=None ) self._show_toast("Request loaded from history") def _show_toast(self, message): """Show a toast notification.""" toast = Adw.Toast() toast.set_title(message) toast.set_timeout(3) # Get the toast overlay (we need to add one) # For now, just print to console print(f"Toast: {message}") # Project Management Methods def _load_projects(self): """Load and display projects.""" # Clear existing while child := self.projects_listbox.get_first_child(): self.projects_listbox.remove(child) # Load and populate projects = self.project_manager.load_projects() for project in projects: item = ProjectItem(project) item.connect('edit-requested', self._on_project_edit, project) item.connect('delete-requested', self._on_project_delete, project) item.connect('add-request-requested', self._on_add_to_project, project) item.connect('request-load-requested', self._on_load_request) item.connect('request-delete-requested', self._on_delete_request, project) item.connect('manage-environments-requested', self._on_manage_environments, project) self.projects_listbox.append(item) @Gtk.Template.Callback() def on_add_project_clicked(self, button): """Show dialog to create project.""" dialog = Adw.AlertDialog() dialog.set_heading("New Project") dialog.set_body("Enter a name for the new project:") entry = Gtk.Entry() entry.set_placeholder_text("Project name") dialog.set_extra_child(entry) dialog.add_response("cancel", "Cancel") dialog.add_response("create", "Create") dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED) dialog.set_default_response("create") dialog.set_close_response("cancel") def on_response(dlg, response): if response == "create": name = entry.get_text().strip() if name: self.project_manager.add_project(name) self._load_projects() dialog.connect("response", on_response) dialog.present(self) def _on_project_edit(self, widget, project): """Show edit project dialog with icon picker.""" dialog = Adw.AlertDialog() dialog.set_heading("Edit Project") 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.set_text(project.name) 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("save", "Save") dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED) def on_response(dlg, response): if response == "save": new_name = entry.get_text().strip() new_icon = icon_button.selected_icon if new_name: self.project_manager.update_project(project.id, new_name, new_icon) self._load_projects() dialog.connect("response", on_response) dialog.present(self) def _on_project_delete(self, widget, project): """Show delete confirmation.""" dialog = Adw.AlertDialog() dialog.set_heading("Delete Project?") dialog.set_body(f"Delete '{project.name}' and all its saved requests? 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_project(project.id) self._load_projects() dialog.connect("response", on_response) dialog.present(self) def _on_manage_environments(self, widget, project): """Show environments management dialog.""" dialog = EnvironmentsDialog(project, self.project_manager) def on_environments_updated(dlg): # Reload projects to reflect changes self._load_projects() dialog.connect('environments-updated', on_environments_updated) dialog.present(self) @Gtk.Template.Callback() def on_save_request_clicked(self, button): """Save current request to a project.""" # Get request from current tab widget page = self.tab_view.get_selected_page() if not page: self._show_toast("No active request") return widget = self.page_to_widget.get(page) if not widget: self._show_toast("No active request") return request = widget.get_request() if not request.url.strip(): self._show_toast("Cannot save: URL is empty") return projects = self.project_manager.load_projects() if not projects: # Show error - no projects dialog = Adw.AlertDialog() dialog.set_heading("No Projects") dialog.set_body("Create a project first before saving requests.") dialog.add_response("ok", "OK") dialog.present(self) return # Create save dialog dialog = Adw.AlertDialog() dialog.set_heading("Save Request") dialog.set_body("Choose a project and enter a name:") box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) # Check if current tab has a saved request (for pre-filling) current_tab = None preselect_project_index = 0 prefill_name = "" if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: if current_tab.saved_request_id: # Find which project this request belongs to for i, p in enumerate(projects): for req in p.requests: if req.id == current_tab.saved_request_id: preselect_project_index = i prefill_name = current_tab.name break elif current_tab.name and current_tab.name != "New Request": # Copy tab or named unsaved tab - prefill with tab name prefill_name = current_tab.name # Project dropdown project_list = Gtk.StringList() for p in projects: project_list.append(p.name) dropdown = Gtk.DropDown(model=project_list) dropdown.set_selected(preselect_project_index) # Pre-select project box.append(dropdown) # Name entry entry = Gtk.Entry() entry.set_placeholder_text("Request name") if prefill_name: entry.set_text(prefill_name) # Pre-fill name # Select all text for easy editing entry.select_region(0, -1) box.append(entry) dialog.set_extra_child(box) dialog.add_response("cancel", "Cancel") dialog.add_response("save", "Save") dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED) def on_response(dlg, response): if response == "save": name = entry.get_text().strip() if name: selected = dropdown.get_selected() project = projects[selected] # Check for duplicate name existing = self.project_manager.find_request_by_name(project.id, name) if existing: # Show overwrite confirmation self._show_overwrite_dialog(project, name, existing.id, request) else: # No duplicate, save normally saved_request = self.project_manager.add_request(project.id, name, request) self._load_projects() self._show_toast(f"Saved as '{name}'") # Clear modified flag on current tab self._mark_tab_as_saved(saved_request.id, name, request) dialog.connect("response", on_response) dialog.present(self) def _mark_tab_as_saved(self, saved_request_id, name, request): """Mark the current tab as saved (clear modified flag).""" page = self.tab_view.get_selected_page() if not page: return tab = self.page_to_tab.get(page) widget = self.page_to_widget.get(page) if tab and widget: # Update tab metadata tab.saved_request_id = saved_request_id tab.name = name tab.modified = False # Update original_request to match saved state original = HttpRequest( method=request.method, url=request.url, headers=request.headers.copy(), body=request.body, syntax=request.syntax ) tab.original_request = original # Update widget state widget.original_request = original widget.modified = False # Update tab page title (widget.modified is now False, so no star) page.set_title(name) def _show_overwrite_dialog(self, project, name, existing_request_id, request): """Show dialog asking if user wants to overwrite existing request.""" dialog = Adw.AlertDialog() dialog.set_heading("Request Already Exists") dialog.set_body(f"A request named '{name}' already exists in '{project.name}'.\n\nDo you want to overwrite it?") dialog.add_response("cancel", "Cancel") dialog.add_response("overwrite", "Overwrite") dialog.set_response_appearance("overwrite", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_default_response("cancel") dialog.set_close_response("cancel") def on_overwrite_response(dlg, response): if response == "overwrite": # Update the existing request self.project_manager.update_request(project.id, existing_request_id, name, request) self._load_projects() self._show_toast(f"Updated '{name}'") # Clear modified flag on current tab self._mark_tab_as_saved(existing_request_id, name, request) dialog.connect("response", on_overwrite_response) dialog.present(self) def _on_add_to_project(self, widget, project): """Save current request to specific project.""" request = self._build_request_from_ui() if not request.url.strip(): self._show_toast("Cannot save: URL is empty") return dialog = Adw.AlertDialog() dialog.set_heading(f"Save to {project.name}") dialog.set_body("Enter a name for this request:") entry = Gtk.Entry() entry.set_placeholder_text("Request name") dialog.set_extra_child(entry) dialog.add_response("cancel", "Cancel") dialog.add_response("save", "Save") dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED) def on_response(dlg, response): if response == "save": name = entry.get_text().strip() if name: # Check for duplicate name existing = self.project_manager.find_request_by_name(project.id, name) if existing: # Show overwrite confirmation self._show_overwrite_dialog(project, name, existing.id, request) else: # No duplicate, save normally saved_request = self.project_manager.add_request(project.id, name, request) self._load_projects() self._show_toast(f"Saved as '{name}'") # Clear modified flag on current tab self._mark_tab_as_saved(saved_request.id, name, request) dialog.connect("response", on_response) dialog.present(self) def _on_load_request(self, widget, saved_request): """Load saved request - smart loading based on current tab state.""" req = saved_request.request # First, check if this request is already open in an unmodified tab existing_tab = self._find_tab_by_saved_request_id(saved_request.id) if existing_tab and not existing_tab.is_modified(): # Switch to the existing unmodified tab self._switch_to_tab(existing_tab.id) self._show_toast(f"Switched to '{saved_request.name}'") return # Check if there's a modified tab with this saved_request_id # If so, this is a copy and should not be linked is_copy = existing_tab and existing_tab.is_modified() if is_copy: # Generate numbered copy name tab_name = self._generate_copy_name(saved_request.name) link_to_saved = None else: tab_name = saved_request.name link_to_saved = saved_request.id # Check if current tab is an empty "New Request" if self._is_empty_new_request_tab(): # Replace the empty tab with this request page = self.tab_view.get_selected_page() if page: widget = self.page_to_widget.get(page) current_tab = self.page_to_tab.get(page) if widget and current_tab: # Get project context project_id, default_env_id = self._get_project_for_saved_request(link_to_saved) # Update the tab metadata current_tab.name = tab_name current_tab.request = req current_tab.saved_request_id = link_to_saved current_tab.project_id = project_id current_tab.selected_environment_id = default_env_id # Update widget with project context widget.project_id = project_id widget.selected_environment_id = default_env_id if project_id and hasattr(widget, '_show_environment_selector'): widget._show_environment_selector() # Update the tab page title page.set_title(tab_name) # Load request into widget widget._load_request(req) if is_copy: # This is a copy - mark as unsaved current_tab.original_request = None widget.original_request = None widget.modified = True else: # This is linked to saved request original = HttpRequest( method=req.method, url=req.url, headers=req.headers.copy(), body=req.body, syntax=req.syntax ) current_tab.original_request = original widget.original_request = original widget.modified = False else: # Current tab has changes or is not a "New Request" # Create a new tab project_id, default_env_id = self._get_project_for_saved_request(link_to_saved) tab_id = self._create_new_tab( name=tab_name, request=req, saved_request_id=link_to_saved, project_id=project_id, selected_environment_id=default_env_id ) # If it's a copy, clear the original_request to mark as unsaved if is_copy: new_tab = self.tab_manager.get_tab_by_id(tab_id) if new_tab: new_tab.original_request = None new_tab.modified = True # Also update the widget page = self.tab_view.get_selected_page() if page: widget = self.page_to_widget.get(page) if widget: widget.original_request = None widget.modified = True def _on_delete_request(self, widget, saved_request, project): """Delete saved request with confirmation.""" dialog = Adw.AlertDialog() dialog.set_heading("Delete Request?") dialog.set_body(f"Delete '{saved_request.name}'? This cannot be undone.") dialog.add_response("cancel", "Cancel") dialog.add_response("delete", "Delete") dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE) def on_response(dlg, response): if response == "delete": self.project_manager.delete_request(project.id, saved_request.id) self._load_projects() dialog.connect("response", on_response) dialog.present(self)