# 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 .widgets.header_row import HeaderRow from .widgets.history_item import HistoryItem from .widgets.project_item import ProjectItem from datetime import datetime import threading import json import xml.dom.minidom import uuid @Gtk.Template(resource_path='/cz/vesp/roster/main-window.ui') class RosterWindow(Adw.ApplicationWindow): __gtype_name__ = 'RosterWindow' # Top bar widgets method_dropdown = Gtk.Template.Child() url_entry = Gtk.Template.Child() send_button = Gtk.Template.Child() new_request_button = Gtk.Template.Child() tab_bar_container = Gtk.Template.Child() # Panes main_pane = Gtk.Template.Child() split_pane = Gtk.Template.Child() # Sidebar widgets projects_listbox = Gtk.Template.Child() add_project_button = Gtk.Template.Child() save_request_button = Gtk.Template.Child() # Containers for tabs request_tabs_container = Gtk.Template.Child() response_tabs_container = Gtk.Template.Child() # Status bar status_label = Gtk.Template.Child() time_label = Gtk.Template.Child() # History 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() # Tab tracking - maps tab_id to UI state self.tab_notebook = Gtk.Notebook() self.tab_notebook.set_show_tabs(False) # We'll use custom tab bar self.tab_notebook.set_show_border(False) self.current_tab_id = None # Create window actions self._create_actions() # Setup UI self._setup_method_dropdown() self._setup_request_tabs() self._setup_response_tabs() self._setup_tab_system() self._load_history() self._load_projects() # Add initial header row self._add_header_row() # Set split pane position to center after window is shown self.connect("map", self._on_window_mapped) # 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_window_mapped(self, widget): """Set split pane position to center when window is mapped.""" # Use idle_add to ensure the widget is fully allocated GLib.idle_add(self._center_split_pane) def _center_split_pane(self): """Center the split pane divider.""" # Get the allocated width of the paned widget allocation = self.split_pane.get_allocation() if allocation.width > 0: self.split_pane.set_position(allocation.width // 2) return False # Don't repeat def _on_close_request(self, window): """Handle window close request - warn if there are unsaved changes.""" # Save current tab state first if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: current_tab.request = self._build_request_from_ui() # Check if any tabs have unsaved changes modified_tabs = [tab for tab in self.tab_manager.tabs if tab.is_modified()] 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_sourceview_theme(self): """Set up GtkSourceView theme based on system color scheme.""" # Get the style manager to detect dark mode style_manager = Adw.StyleManager.get_default() is_dark = style_manager.get_dark() # Get the appropriate color scheme (using classic for more subtle colors) # You can change these to customize the color theme: # - 'classic' / 'classic-dark' (subtle, muted colors) # - 'Adwaita' / 'Adwaita-dark' (GNOME default) # - 'tango' (colorful) # - 'solarized-light' / 'solarized-dark' (popular alternative) # - 'kate' / 'kate-dark' style_scheme_manager = GtkSource.StyleSchemeManager.get_default() scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita' scheme = style_scheme_manager.get_scheme(scheme_name) if scheme: self.response_body_sourceview.get_buffer().set_style_scheme(scheme) # Listen for theme changes style_manager.connect('notify::dark', self._on_theme_changed) def _on_theme_changed(self, style_manager, param): """Handle system theme changes.""" is_dark = style_manager.get_dark() style_scheme_manager = GtkSource.StyleSchemeManager.get_default() scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita' scheme = style_scheme_manager.get_scheme(scheme_name) if scheme: self.response_body_sourceview.get_buffer().set_style_scheme(scheme) # Also update request body theme self.body_sourceview.get_buffer().set_style_scheme(scheme) def _setup_request_body_theme(self): """Set up GtkSourceView theme for request body.""" style_manager = Adw.StyleManager.get_default() is_dark = style_manager.get_dark() style_scheme_manager = GtkSource.StyleSchemeManager.get_default() scheme_name = 'Adwaita-dark' if is_dark else 'Adwaita' scheme = style_scheme_manager.get_scheme(scheme_name) if scheme: self.body_sourceview.get_buffer().set_style_scheme(scheme) def _on_request_body_language_changed(self, dropdown, param): """Handle request body language selection change.""" selected = dropdown.get_selected() language_manager = GtkSource.LanguageManager.get_default() buffer = self.body_sourceview.get_buffer() if selected == 0: # RAW buffer.set_language(None) elif selected == 1: # JSON language = language_manager.get_language('json') buffer.set_language(language) elif selected == 2: # XML language = language_manager.get_language('xml') buffer.set_language(language) 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) # Connect change tracking to detect modifications self.url_entry.connect("changed", self._on_request_changed) self.method_dropdown.connect("notify::selected", self._on_request_changed) self.body_language_dropdown.connect("notify::selected", self._on_request_changed) # Track body changes body_buffer = self.body_sourceview.get_buffer() body_buffer.connect("changed", self._on_request_changed) def _on_request_changed(self, widget, *args): """Track changes to mark tab as modified.""" if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: # Check if modified current_tab.modified = current_tab.is_modified() # Update the current request in the tab current_tab.request = self._build_request_from_ui() # Update tab bar to show modified indicator self._update_tab_bar_visibility() 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 current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if not current_tab: return False # Check if it's a "New Request" (not from saved request) if current_tab.saved_request_id: return False # Check if it's empty (no URL, no body, no headers) request = self._build_request_from_ui() is_empty = ( not request.url.strip() and not request.body.strip() and not request.headers ) return is_empty def _create_new_tab(self, name="New Request", request=None, saved_request_id=None): """Create a new tab (simplified version - just clears current request for now).""" # 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) self.current_tab_id = tab.id # For simplified version, just load this request into UI self._load_request_to_ui(request) # Update tab bar visibility self._update_tab_bar_visibility() return tab.id def _update_tab_bar_visibility(self): """Show/hide tab bar based on number of tabs.""" num_tabs = len(self.tab_manager.tabs) # Show tab bar only if we have 2 or more tabs self.tab_bar_container.set_visible(num_tabs >= 2) # Update tab bar with buttons if num_tabs >= 2: # Clear existing children child = self.tab_bar_container.get_first_child() while child: next_child = child.get_next_sibling() self.tab_bar_container.remove(child) child = next_child # Add tab buttons with close button for tab in self.tab_manager.tabs: # Create a box for tab label + close button tab_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) # Tab label with modified indicator tab_label = tab.name[:20] # Truncate long names if tab.is_modified(): tab_label += " •" # Add dot for unsaved changes # Tab label button tab_label_btn = Gtk.Button(label=tab_label) tab_label_btn.add_css_class("flat") if tab.id == self.current_tab_id: tab_label_btn.add_css_class("suggested-action") tab_label_btn.connect("clicked", lambda btn, tid=tab.id: self._switch_to_tab(tid)) tab_box.append(tab_label_btn) # Close button close_btn = Gtk.Button() close_btn.set_icon_name("window-close-symbolic") close_btn.add_css_class("flat") close_btn.add_css_class("circular") close_btn.set_tooltip_text("Close tab") close_btn.connect("clicked", lambda btn, tid=tab.id: self._close_tab(tid)) tab_box.append(close_btn) self.tab_bar_container.append(tab_box) def _close_tab(self, tab_id): """Close a tab with unsaved change warning.""" # Update current tab state if self.current_tab_id == tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: current_tab.request = self._build_request_from_ui() # Check if tab has unsaved changes tab = self.tab_manager.get_tab_by_id(tab_id) if tab and tab.is_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") dialog.connect("response", lambda d, r: self._on_close_tab_dialog_response(r, tab_id)) dialog.present(self) else: # No unsaved changes, close directly self._do_close_tab(tab_id) def _on_close_tab_dialog_response(self, response, tab_id): """Handle close tab dialog response.""" if response == "close": self._do_close_tab(tab_id) def _do_close_tab(self, tab_id): """Actually close the tab (after confirmation if needed).""" # Close the tab success = self.tab_manager.close_tab(tab_id) if success: # If we closed the last tab, create a new one if len(self.tab_manager.tabs) == 0: self._create_new_tab() else: # Switch to the active tab (tab_manager already updated active_tab_id) active_tab = self.tab_manager.get_active_tab() if active_tab: self.current_tab_id = active_tab.id self._load_request_to_ui(active_tab.request) if active_tab.response: self._display_response(active_tab.response) else: # Clear response display self.status_label.set_text("Ready") self.time_label.set_text("") buffer = self.response_headers_textview.get_buffer() buffer.set_text("") buffer = self.response_body_sourceview.get_buffer() buffer.set_text("") # Update tab bar self._update_tab_bar_visibility() def _switch_to_tab(self, tab_id): """Switch to a different tab.""" # Save current tab state if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: current_tab.request = self._build_request_from_ui() # Load new tab tab = self.tab_manager.get_tab_by_id(tab_id) if tab: self.current_tab_id = tab_id self._load_request_to_ui(tab.request) if tab.response: self._display_response(tab.response) self._update_tab_bar_visibility() def _load_request_to_ui(self, request): """Load a request into the UI.""" # Set method methods = ["GET", "POST", "PUT", "DELETE"] if request.method in methods: self.method_dropdown.set_selected(methods.index(request.method)) # Set URL self.url_entry.set_text(request.url) # Clear and set headers while child := self.headers_listbox.get_first_child(): self.headers_listbox.remove(child) for key, value in request.headers.items(): self._add_header_row(key, value) if not request.headers: self._add_header_row() # Set body buffer = self.body_sourceview.get_buffer() buffer.set_text(request.body) # Set syntax syntax_options = ["RAW", "JSON", "XML"] if request.syntax in syntax_options: self.body_language_dropdown.set_selected(syntax_options.index(request.syntax)) 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"]) def _close_current_tab(self): """Close the currently active tab.""" if self.current_tab_id: self._close_tab(self.current_tab_id) def _setup_method_dropdown(self): """Populate HTTP method dropdown.""" methods = Gtk.StringList() methods.append("GET") methods.append("POST") methods.append("PUT") methods.append("DELETE") self.method_dropdown.set_model(methods) self.method_dropdown.set_selected(0) # Default to GET def _setup_request_tabs(self): """Create request tabs programmatically.""" # Create stack for switching between pages self.request_stack = Gtk.Stack() self.request_stack.set_vexpand(True) # Create stack switcher for the tabs request_switcher = Gtk.StackSwitcher() request_switcher.set_stack(self.request_stack) request_switcher.set_halign(Gtk.Align.CENTER) # Add switcher and stack to container self.request_tabs_container.append(request_switcher) self.request_tabs_container.append(self.request_stack) # Headers tab headers_scroll = Gtk.ScrolledWindow() self.headers_listbox = Gtk.ListBox() self.headers_listbox.add_css_class("boxed-list") headers_scroll.set_child(self.headers_listbox) # Add header button container headers_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) headers_box.set_margin_start(12) headers_box.set_margin_end(12) headers_box.set_margin_top(12) headers_box.set_margin_bottom(12) headers_box.append(headers_scroll) self.add_header_button = Gtk.Button(label="Add Header") self.add_header_button.connect("clicked", self.on_add_header_clicked) headers_box.append(self.add_header_button) self.request_stack.add_titled(headers_box, "headers", "Headers") # Body tab with syntax highlighting body_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) # SourceView for request body body_scroll = Gtk.ScrolledWindow() body_scroll.set_vexpand(True) self.body_sourceview = GtkSource.View() self.body_sourceview.set_editable(True) self.body_sourceview.set_show_line_numbers(True) self.body_sourceview.set_highlight_current_line(True) self.body_sourceview.set_left_margin(12) self.body_sourceview.set_right_margin(12) self.body_sourceview.set_top_margin(12) self.body_sourceview.set_bottom_margin(12) # Apply same font styling as response body css_provider = Gtk.CssProvider() css_provider.load_from_data(b""" textview { font-family: "Source Code Pro"; font-size: 12pt; line-height: 1.2; } """) self.body_sourceview.get_style_context().add_provider( css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) # Set up theme for request body self._setup_request_body_theme() body_scroll.set_child(self.body_sourceview) body_box.append(body_scroll) # Bottom toolbar with language selector toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) toolbar.set_margin_start(12) toolbar.set_margin_end(12) toolbar.set_margin_top(6) toolbar.set_margin_bottom(6) # Spacer to push dropdown to the right spacer = Gtk.Box() spacer.set_hexpand(True) toolbar.append(spacer) # Language selector label and dropdown lang_label = Gtk.Label(label="Syntax:") toolbar.append(lang_label) lang_list = Gtk.StringList() lang_list.append("RAW") lang_list.append("JSON") lang_list.append("XML") self.body_language_dropdown = Gtk.DropDown(model=lang_list) self.body_language_dropdown.set_selected(0) # Default to RAW self.body_language_dropdown.connect("notify::selected", self._on_request_body_language_changed) toolbar.append(self.body_language_dropdown) body_box.append(toolbar) self.request_stack.add_titled(body_box, "body", "Body") def _setup_response_tabs(self): """Create response tabs programmatically.""" # Create stack for switching between pages self.response_stack = Gtk.Stack() self.response_stack.set_vexpand(True) # Create stack switcher for the tabs response_switcher = Gtk.StackSwitcher() response_switcher.set_stack(self.response_stack) response_switcher.set_halign(Gtk.Align.CENTER) # Add switcher and stack to container self.response_tabs_container.append(response_switcher) self.response_tabs_container.append(self.response_stack) # Response Headers tab headers_scroll = Gtk.ScrolledWindow() headers_scroll.set_vexpand(True) self.response_headers_textview = Gtk.TextView() self.response_headers_textview.set_editable(False) self.response_headers_textview.set_monospace(True) self.response_headers_textview.set_left_margin(12) self.response_headers_textview.set_right_margin(12) self.response_headers_textview.set_top_margin(12) self.response_headers_textview.set_bottom_margin(12) headers_scroll.set_child(self.response_headers_textview) self.response_stack.add_titled(headers_scroll, "headers", "Headers") # Response Body tab with syntax highlighting body_scroll = Gtk.ScrolledWindow() body_scroll.set_vexpand(True) self.response_body_sourceview = GtkSource.View() self.response_body_sourceview.set_editable(False) self.response_body_sourceview.set_show_line_numbers(True) self.response_body_sourceview.set_highlight_current_line(True) self.response_body_sourceview.set_left_margin(12) self.response_body_sourceview.set_right_margin(12) self.response_body_sourceview.set_top_margin(12) self.response_body_sourceview.set_bottom_margin(12) # Set up syntax highlighting with system theme colors self._setup_sourceview_theme() # Set font family and size # You can customize these values: # - font-family: Change to any font (e.g., "Monospace", "JetBrains Mono", "Fira Code") # - font-size: Change to any size (e.g., 10pt, 12pt, 14pt, 16pt) css_provider = Gtk.CssProvider() css_provider.load_from_data(b""" textview { font-family: "Source Code Pro"; font-size: 12pt; line-height: 1.2; } """) self.response_body_sourceview.get_style_context().add_provider( css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) body_scroll.set_child(self.response_body_sourceview) self.response_stack.add_titled(body_scroll, "body", "Body") @Gtk.Template.Callback() def on_send_clicked(self, button): """Handle Send button click.""" # Validate URL url = self.url_entry.get_text().strip() if not url: self._show_toast("Please enter a URL") return # Build request from UI request = self._build_request_from_ui() # Disable send button during request self.send_button.set_sensitive(False) self.send_button.set_label("Sending...") self.status_label.set_text("Sending...") self.time_label.set_text("") # Execute in thread to avoid blocking UI thread = threading.Thread(target=self._execute_request_thread, args=(request,)) thread.daemon = True thread.start() def _execute_request_thread(self, request): """Execute request in background thread.""" response, error = self.http_client.execute_request(request) # Update UI in main thread GLib.idle_add(self._handle_response, request, response, error) def _handle_response(self, request, response, error): """Handle response in main thread.""" # Re-enable send button self.send_button.set_sensitive(True) self.send_button.set_label("Send") # Save response to current tab if self.current_tab_id: current_tab = self.tab_manager.get_tab_by_id(self.current_tab_id) if current_tab: current_tab.response = response # Create history entry entry = HistoryEntry( timestamp=datetime.now().isoformat(), request=request, response=response, error=error ) # Save to history self.history_manager.add_entry(entry) # Update UI if response: self._display_response(response) else: self._display_error(error) # Refresh history list self._load_history() def _build_request_from_ui(self): """Build HttpRequest from UI inputs.""" method = self.method_dropdown.get_selected_item().get_string() url = self.url_entry.get_text().strip() # Collect headers headers = {} child = self.headers_listbox.get_first_child() while child is not None: # In GTK4, ListBox wraps children in ListBoxRow, so we need to get the actual child if isinstance(child, Gtk.ListBoxRow): header_row = child.get_child() if isinstance(header_row, HeaderRow): key, value = header_row.get_header() if key and value: headers[key] = value child = child.get_next_sibling() # Get body buffer = self.body_sourceview.get_buffer() body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) # Get syntax selection syntax_index = self.body_language_dropdown.get_selected() syntax_options = ["RAW", "JSON", "XML"] syntax = syntax_options[syntax_index] return HttpRequest(method=method, url=url, headers=headers, body=body, syntax=syntax) def _display_response(self, response): """Display response in UI.""" # Update status status_text = f"{response.status_code} {response.status_text}" self.status_label.set_text(status_text) # Update time time_text = f"{response.response_time_ms:.0f} ms" self.time_label.set_text(time_text) # Update response headers buffer = self.response_headers_textview.get_buffer() buffer.set_text(response.headers) # Extract content-type and format body content_type = self._extract_content_type(response.headers) formatted_body = self._format_response_body(response.body, content_type) # Update response body with syntax highlighting source_buffer = self.response_body_sourceview.get_buffer() source_buffer.set_text(formatted_body) # Set language for syntax highlighting language = self._get_language_from_content_type(content_type) source_buffer.set_language(language) def _display_error(self, error): """Display error in UI.""" self.status_label.set_text("Error") self.time_label.set_text("") # Clear response headers buffer = self.response_headers_textview.get_buffer() buffer.set_text("") # Display error in body source_buffer = self.response_body_sourceview.get_buffer() source_buffer.set_text(error) source_buffer.set_language(None) # No syntax highlighting for errors # Show toast self._show_toast(f"Request failed: {error}") def _extract_content_type(self, headers_text): """Extract content-type from response headers.""" if not headers_text: return "" # Parse headers line by line for line in headers_text.split('\n'): line = line.strip() if line.lower().startswith('content-type:'): # Extract value after colon return line.split(':', 1)[1].strip().lower() return "" def _get_language_from_content_type(self, content_type): """Get GtkSourceView language ID from content type.""" if not content_type: return None language_manager = GtkSource.LanguageManager.get_default() # Map content types to language IDs if 'application/json' in content_type or 'text/json' in content_type: return language_manager.get_language('json') elif 'application/xml' in content_type or 'text/xml' in content_type: return language_manager.get_language('xml') elif 'text/html' in content_type: return language_manager.get_language('html') elif 'application/javascript' in content_type or 'text/javascript' in content_type: return language_manager.get_language('js') elif 'text/css' in content_type: return language_manager.get_language('css') return None def _format_response_body(self, body, content_type): """Format response body based on content type.""" if not body or not body.strip(): return body try: # JSON formatting if 'application/json' in content_type or 'text/json' in content_type: parsed = json.loads(body) return json.dumps(parsed, indent=2, ensure_ascii=False) # XML formatting elif 'application/xml' in content_type or 'text/xml' in content_type: dom = xml.dom.minidom.parseString(body) return dom.toprettyxml(indent=" ") except Exception as e: # If formatting fails, return original body print(f"Failed to format body: {e}") # Return original for other content types or if formatting failed return body def on_add_header_clicked(self, button): """Add new header row.""" self._add_header_row() def _add_header_row(self, key='', value=''): """Add a header row to the list.""" row = HeaderRow() row.set_header(key, value) row.connect('remove-requested', self._on_header_remove) self.headers_listbox.append(row) # Mark as changed if we're adding a non-empty header if key or value: self._on_request_changed(None) def _on_header_remove(self, header_row): """Handle header row removal.""" # In GTK4, we need to remove the parent ListBoxRow, not the HeaderRow itself parent = header_row.get_parent() if parent: self.headers_listbox.remove(parent) # Mark as changed self._on_request_changed(None) 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) 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 _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 self._load_request_to_ui(request) self._update_tab_bar_visibility() 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 ) # Switch to headers tab self.request_stack.set_visible_child_name("headers") 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) 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) @Gtk.Template.Callback() def on_save_request_clicked(self, button): """Save current request to a project.""" request = self._build_request_from_ui() 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) # Project dropdown project_list = Gtk.StringList() for p in projects: project_list.append(p.name) dropdown = Gtk.DropDown(model=project_list) box.append(dropdown) # Name entry entry = Gtk.Entry() entry.set_placeholder_text("Request name") 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] self.project_manager.add_request(project.id, name, request) self._load_projects() dialog.connect("response", on_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: self.project_manager.add_request(project.id, name, request) self._load_projects() 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 # 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 = saved_request.name current_tab.request = req current_tab.saved_request_id = saved_request.id current_tab.original_request = HttpRequest( method=req.method, url=req.url, headers=req.headers.copy(), body=req.body, syntax=req.syntax ) current_tab.modified = False # Load into UI self._load_request_to_ui(req) self._update_tab_bar_visibility() else: # Current tab has changes or is not a "New Request" # Create a new tab self._create_new_tab( name=saved_request.name, request=req, saved_request_id=saved_request.id ) # Switch to headers tab self.request_stack.set_visible_child_name("headers") 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)