793 lines
28 KiB
Python
793 lines
28 KiB
Python
# 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 <https://www.gnu.org/licenses/>.
|
|
#
|
|
# 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
|
|
from .http_client import HttpClient
|
|
from .history_manager import HistoryManager
|
|
from .project_manager import ProjectManager
|
|
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
|
|
|
|
|
|
@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()
|
|
|
|
# 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()
|
|
|
|
# Create window actions
|
|
self._create_actions()
|
|
|
|
# Setup UI
|
|
self._setup_method_dropdown()
|
|
self._setup_request_tabs()
|
|
self._setup_response_tabs()
|
|
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)
|
|
|
|
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 _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 = 'classic-dark' if is_dark else 'classic'
|
|
scheme = style_scheme_manager.get_scheme(scheme_name)
|
|
|
|
if scheme:
|
|
self.response_body_sourceview.get_buffer().set_style_scheme(scheme)
|
|
|
|
def _create_actions(self):
|
|
"""Create window-level actions."""
|
|
pass
|
|
|
|
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
|
|
body_scroll = Gtk.ScrolledWindow()
|
|
body_scroll.set_vexpand(True)
|
|
self.body_textview = Gtk.TextView()
|
|
self.body_textview.set_monospace(True)
|
|
self.body_textview.set_left_margin(12)
|
|
self.body_textview.set_right_margin(12)
|
|
self.body_textview.set_top_margin(12)
|
|
self.body_textview.set_bottom_margin(12)
|
|
body_scroll.set_child(self.body_textview)
|
|
|
|
self.request_stack.add_titled(body_scroll, "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")
|
|
|
|
# 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_textview.get_buffer()
|
|
body = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False)
|
|
|
|
return HttpRequest(method=method, url=url, headers=headers, body=body)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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."""
|
|
# Show warning dialog
|
|
dialog = Adw.AlertDialog()
|
|
dialog.set_heading("Load Request?")
|
|
dialog.set_body("This will replace your current request. Any unsaved changes will be lost.")
|
|
dialog.add_response("cancel", "Cancel")
|
|
dialog.add_response("load", "Load")
|
|
dialog.set_response_appearance("load", Adw.ResponseAppearance.SUGGESTED)
|
|
dialog.set_default_response("cancel")
|
|
dialog.set_close_response("cancel")
|
|
|
|
dialog.connect("response", self._on_load_dialog_response, entry)
|
|
dialog.present(self)
|
|
|
|
def _on_load_dialog_response(self, dialog, response, entry):
|
|
"""Handle load dialog response."""
|
|
if response == "load":
|
|
self._load_request_from_entry(entry)
|
|
|
|
def _load_request_from_entry(self, entry):
|
|
"""Load request from history entry into UI."""
|
|
request = entry.request
|
|
|
|
# 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 existing headers
|
|
while True:
|
|
child = self.headers_listbox.get_first_child()
|
|
if child is None:
|
|
break
|
|
self.headers_listbox.remove(child)
|
|
|
|
# Set headers
|
|
for key, value in request.headers.items():
|
|
self._add_header_row(key, value)
|
|
|
|
# Add one empty row if no headers
|
|
if not request.headers:
|
|
self._add_header_row()
|
|
|
|
# Set body
|
|
buffer = self.body_textview.get_buffer()
|
|
buffer.set_text(request.body)
|
|
|
|
# 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 into UI (direct load, no warning)."""
|
|
req = saved_request.request
|
|
|
|
# Set method
|
|
methods = ["GET", "POST", "PUT", "DELETE"]
|
|
if req.method in methods:
|
|
self.method_dropdown.set_selected(methods.index(req.method))
|
|
|
|
# Set URL
|
|
self.url_entry.set_text(req.url)
|
|
|
|
# Clear headers
|
|
while child := self.headers_listbox.get_first_child():
|
|
self.headers_listbox.remove(child)
|
|
|
|
# Set headers
|
|
for key, value in req.headers.items():
|
|
self._add_header_row(key, value)
|
|
|
|
if not req.headers:
|
|
self._add_header_row()
|
|
|
|
# Set body
|
|
buffer = self.body_textview.get_buffer()
|
|
buffer.set_text(req.body)
|
|
|
|
# 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)
|