Add RequestTabWidget class and simplify main UI
- Create RequestTabWidget class with complete per-tab UI - Simplify main-window.ui to just AdwTabBar and AdwTabView - Set autohide=False to always show tabs - Add request_tab_widget.py to meson.build Next step: Refactor window.py to create RequestTabWidget instances for each tab.
This commit is contained in:
parent
d912f3479a
commit
99faf14983
@ -92,15 +92,6 @@
|
|||||||
<object class="GtkBox">
|
<object class="GtkBox">
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
|
|
||||||
<!-- AdwTabView (hidden - we use shared UI) -->
|
|
||||||
<child>
|
|
||||||
<object class="AdwTabView" id="tab_view">
|
|
||||||
<property name="visible">False</property>
|
|
||||||
<property name="vexpand">False</property>
|
|
||||||
<property name="height-request">0</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- Main Header Bar -->
|
<!-- Main Header Bar -->
|
||||||
<child>
|
<child>
|
||||||
<object class="AdwHeaderBar">
|
<object class="AdwHeaderBar">
|
||||||
@ -111,7 +102,7 @@
|
|||||||
<property name="title-widget">
|
<property name="title-widget">
|
||||||
<object class="AdwTabBar" id="tab_bar">
|
<object class="AdwTabBar" id="tab_bar">
|
||||||
<property name="view">tab_view</property>
|
<property name="view">tab_view</property>
|
||||||
<property name="autohide">True</property>
|
<property name="autohide">False</property>
|
||||||
<property name="expand-tabs">False</property>
|
<property name="expand-tabs">False</property>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
@ -153,188 +144,17 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- AdwTabView as main content area -->
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="AdwTabView" id="tab_view">
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
|
|
||||||
<!-- URL Input Section -->
|
|
||||||
<child>
|
|
||||||
<object class="AdwClamp">
|
|
||||||
<property name="maximum-size">1000</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">horizontal</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<property name="margin-start">12</property>
|
|
||||||
<property name="margin-end">12</property>
|
|
||||||
<property name="margin-top">12</property>
|
|
||||||
<property name="margin-bottom">12</property>
|
|
||||||
|
|
||||||
<!-- Method Dropdown -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkDropDown" id="method_dropdown">
|
|
||||||
<property name="enable-search">False</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- URL Entry -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkEntry" id="url_entry">
|
|
||||||
<property name="placeholder-text">Enter URL...</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- Send Button -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton" id="send_button">
|
|
||||||
<property name="label">Send</property>
|
|
||||||
<signal name="clicked" handler="on_send_clicked"/>
|
|
||||||
<style>
|
|
||||||
<class name="suggested-action"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- Vertical Paned: Main Content | History Panel -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkPaned" id="vertical_pane">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="vexpand">True</property>
|
<property name="vexpand">True</property>
|
||||||
<property name="position">600</property>
|
|
||||||
<property name="shrink-start-child">False</property>
|
|
||||||
<property name="shrink-end-child">True</property>
|
|
||||||
<property name="resize-start-child">True</property>
|
|
||||||
<property name="resize-end-child">True</property>
|
|
||||||
|
|
||||||
<!-- START: Request/Response Split -->
|
|
||||||
<property name="start-child">
|
|
||||||
<object class="GtkPaned" id="split_pane">
|
|
||||||
<property name="orientation">horizontal</property>
|
|
||||||
<property name="position">600</property>
|
|
||||||
<property name="shrink-start-child">False</property>
|
|
||||||
<property name="shrink-end-child">False</property>
|
|
||||||
<property name="resize-start-child">True</property>
|
|
||||||
<property name="resize-end-child">True</property>
|
|
||||||
|
|
||||||
<!-- LEFT: Request Configuration -->
|
|
||||||
<property name="start-child">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
|
|
||||||
<!-- Request Tabs Placeholder -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox" id="request_tabs_container">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
|
|
||||||
<!-- RIGHT: Response Display -->
|
|
||||||
<property name="end-child">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
|
|
||||||
<!-- Response Tabs Placeholder -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox" id="response_tabs_container">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- Status Bar -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">horizontal</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<property name="margin-start">12</property>
|
|
||||||
<property name="margin-end">12</property>
|
|
||||||
<property name="margin-top">6</property>
|
|
||||||
<property name="margin-bottom">6</property>
|
|
||||||
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="status_label">
|
|
||||||
<property name="label">Ready</property>
|
|
||||||
<style>
|
|
||||||
<class name="heading"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel" id="time_label">
|
|
||||||
<property name="label"></property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- Spacer -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
|
|
||||||
<!-- END: History Panel -->
|
|
||||||
<property name="end-child">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">vertical</property>
|
|
||||||
|
|
||||||
<!-- History Header -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">horizontal</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<property name="margin-start">12</property>
|
|
||||||
<property name="margin-end">12</property>
|
|
||||||
<property name="margin-top">6</property>
|
|
||||||
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label">Request History</property>
|
|
||||||
<property name="hexpand">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<style>
|
|
||||||
<class name="heading"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
<!-- History List -->
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow">
|
|
||||||
<property name="vexpand">True</property>
|
|
||||||
<property name="min-content-height">150</property>
|
|
||||||
|
|
||||||
<child>
|
|
||||||
<object class="GtkListBox" id="history_listbox">
|
|
||||||
<style>
|
|
||||||
<class name="boxed-list"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
<!-- Hidden history list for backward compatibility -->
|
||||||
|
<child>
|
||||||
|
<object class="GtkListBox" id="history_listbox">
|
||||||
|
<property name="visible">False</property>
|
||||||
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
|
|||||||
@ -38,6 +38,7 @@ roster_sources = [
|
|||||||
'constants.py',
|
'constants.py',
|
||||||
'icon_picker_dialog.py',
|
'icon_picker_dialog.py',
|
||||||
'preferences_dialog.py',
|
'preferences_dialog.py',
|
||||||
|
'request_tab_widget.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(roster_sources, install_dir: moduledir)
|
install_data(roster_sources, install_dir: moduledir)
|
||||||
|
|||||||
556
src/request_tab_widget.py
Normal file
556
src/request_tab_widget.py
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
# request_tab_widget.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, GtkSource
|
||||||
|
from .models import HttpRequest, HttpResponse
|
||||||
|
from .widgets.header_row import HeaderRow
|
||||||
|
import json
|
||||||
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTabWidget(Gtk.Box):
|
||||||
|
"""Widget representing a single request tab's UI."""
|
||||||
|
|
||||||
|
def __init__(self, tab_id, request=None, response=None, **kwargs):
|
||||||
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
|
||||||
|
|
||||||
|
self.tab_id = tab_id
|
||||||
|
self.request = request or HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
|
||||||
|
self.response = response
|
||||||
|
self.modified = False
|
||||||
|
self.original_request = None
|
||||||
|
|
||||||
|
# Build the UI
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
# Load initial request
|
||||||
|
if request:
|
||||||
|
self._load_request(request)
|
||||||
|
|
||||||
|
# Setup change tracking
|
||||||
|
self._setup_change_tracking()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
"""Build the complete UI for this tab."""
|
||||||
|
|
||||||
|
# URL Input Section
|
||||||
|
url_clamp = Adw.Clamp(maximum_size=1000)
|
||||||
|
url_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
url_box.set_margin_start(12)
|
||||||
|
url_box.set_margin_end(12)
|
||||||
|
url_box.set_margin_top(12)
|
||||||
|
url_box.set_margin_bottom(12)
|
||||||
|
|
||||||
|
# Method Dropdown
|
||||||
|
self.method_dropdown = Gtk.DropDown()
|
||||||
|
methods = Gtk.StringList()
|
||||||
|
for method in ["GET", "POST", "PUT", "DELETE"]:
|
||||||
|
methods.append(method)
|
||||||
|
self.method_dropdown.set_model(methods)
|
||||||
|
self.method_dropdown.set_selected(0)
|
||||||
|
self.method_dropdown.set_enable_search(False)
|
||||||
|
url_box.append(self.method_dropdown)
|
||||||
|
|
||||||
|
# URL Entry
|
||||||
|
self.url_entry = Gtk.Entry()
|
||||||
|
self.url_entry.set_placeholder_text("Enter URL...")
|
||||||
|
self.url_entry.set_hexpand(True)
|
||||||
|
url_box.append(self.url_entry)
|
||||||
|
|
||||||
|
# Send Button
|
||||||
|
self.send_button = Gtk.Button(label="Send")
|
||||||
|
self.send_button.add_css_class("suggested-action")
|
||||||
|
url_box.append(self.send_button)
|
||||||
|
|
||||||
|
url_clamp.set_child(url_box)
|
||||||
|
self.append(url_clamp)
|
||||||
|
|
||||||
|
# Vertical Paned: Main Content | History Panel
|
||||||
|
vpane = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
vpane.set_vexpand(True)
|
||||||
|
vpane.set_position(600)
|
||||||
|
vpane.set_shrink_start_child(False)
|
||||||
|
vpane.set_shrink_end_child(True)
|
||||||
|
vpane.set_resize_start_child(True)
|
||||||
|
vpane.set_resize_end_child(True)
|
||||||
|
|
||||||
|
# Horizontal Split: Request | Response
|
||||||
|
split_pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
split_pane.set_position(600)
|
||||||
|
split_pane.set_shrink_start_child(False)
|
||||||
|
split_pane.set_shrink_end_child(False)
|
||||||
|
split_pane.set_resize_start_child(True)
|
||||||
|
split_pane.set_resize_end_child(True)
|
||||||
|
|
||||||
|
# Request Panel
|
||||||
|
request_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
self.request_stack = Gtk.Stack()
|
||||||
|
self.request_stack.set_vexpand(True)
|
||||||
|
self.request_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
||||||
|
self.request_stack.set_transition_duration(150)
|
||||||
|
|
||||||
|
request_switcher = Gtk.StackSwitcher()
|
||||||
|
request_switcher.set_stack(self.request_stack)
|
||||||
|
request_switcher.set_halign(Gtk.Align.CENTER)
|
||||||
|
request_switcher.set_margin_top(8)
|
||||||
|
request_switcher.set_margin_bottom(8)
|
||||||
|
|
||||||
|
request_box.append(request_switcher)
|
||||||
|
request_box.append(self.request_stack)
|
||||||
|
|
||||||
|
# Headers tab
|
||||||
|
self._build_headers_tab()
|
||||||
|
|
||||||
|
# Body tab
|
||||||
|
self._build_body_tab()
|
||||||
|
|
||||||
|
split_pane.set_start_child(request_box)
|
||||||
|
|
||||||
|
# Response Panel
|
||||||
|
response_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
self.response_stack = Gtk.Stack()
|
||||||
|
self.response_stack.set_vexpand(True)
|
||||||
|
self.response_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
|
||||||
|
self.response_stack.set_transition_duration(150)
|
||||||
|
|
||||||
|
response_switcher = Gtk.StackSwitcher()
|
||||||
|
response_switcher.set_stack(self.response_stack)
|
||||||
|
response_switcher.set_halign(Gtk.Align.CENTER)
|
||||||
|
response_switcher.set_margin_top(8)
|
||||||
|
response_switcher.set_margin_bottom(8)
|
||||||
|
|
||||||
|
response_box.append(response_switcher)
|
||||||
|
response_box.append(self.response_stack)
|
||||||
|
|
||||||
|
# Response headers
|
||||||
|
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
|
||||||
|
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 theme
|
||||||
|
self._setup_sourceview_theme()
|
||||||
|
|
||||||
|
# Set font
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Status Bar
|
||||||
|
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
status_box.set_margin_start(12)
|
||||||
|
status_box.set_margin_end(12)
|
||||||
|
status_box.set_margin_top(6)
|
||||||
|
status_box.set_margin_bottom(6)
|
||||||
|
|
||||||
|
self.status_label = Gtk.Label(label="Ready")
|
||||||
|
self.status_label.add_css_class("heading")
|
||||||
|
status_box.append(self.status_label)
|
||||||
|
|
||||||
|
self.time_label = Gtk.Label(label="")
|
||||||
|
status_box.append(self.time_label)
|
||||||
|
|
||||||
|
# Spacer
|
||||||
|
spacer = Gtk.Box()
|
||||||
|
spacer.set_hexpand(True)
|
||||||
|
status_box.append(spacer)
|
||||||
|
|
||||||
|
response_box.append(status_box)
|
||||||
|
|
||||||
|
split_pane.set_end_child(response_box)
|
||||||
|
|
||||||
|
vpane.set_start_child(split_pane)
|
||||||
|
|
||||||
|
# History Panel (placeholder for now)
|
||||||
|
history_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
history_label = Gtk.Label(label="Request History")
|
||||||
|
history_label.add_css_class("heading")
|
||||||
|
history_label.set_margin_start(12)
|
||||||
|
history_label.set_margin_end(12)
|
||||||
|
history_label.set_margin_top(6)
|
||||||
|
history_label.set_xalign(0)
|
||||||
|
history_box.append(history_label)
|
||||||
|
|
||||||
|
vpane.set_end_child(history_box)
|
||||||
|
|
||||||
|
self.append(vpane)
|
||||||
|
|
||||||
|
def _build_headers_tab(self):
|
||||||
|
"""Build the headers tab."""
|
||||||
|
headers_scroll = Gtk.ScrolledWindow()
|
||||||
|
headers_scroll.set_vexpand(True)
|
||||||
|
headers_scroll.set_min_content_height(150)
|
||||||
|
|
||||||
|
self.headers_listbox = Gtk.ListBox()
|
||||||
|
self.headers_listbox.add_css_class("boxed-list")
|
||||||
|
headers_scroll.set_child(self.headers_listbox)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
add_header_button = Gtk.Button(label="Add Header")
|
||||||
|
add_header_button.connect("clicked", self._on_add_header_clicked)
|
||||||
|
headers_box.append(add_header_button)
|
||||||
|
|
||||||
|
self.request_stack.add_titled(headers_box, "headers", "Headers")
|
||||||
|
|
||||||
|
# Add initial empty header row
|
||||||
|
self._add_header_row()
|
||||||
|
|
||||||
|
def _build_body_tab(self):
|
||||||
|
"""Build the body tab."""
|
||||||
|
body_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Set font
|
||||||
|
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
|
||||||
|
self._setup_request_body_theme()
|
||||||
|
|
||||||
|
body_scroll.set_child(self.body_sourceview)
|
||||||
|
body_box.append(body_scroll)
|
||||||
|
|
||||||
|
# Bottom toolbar
|
||||||
|
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
|
||||||
|
spacer = Gtk.Box()
|
||||||
|
spacer.set_hexpand(True)
|
||||||
|
toolbar.append(spacer)
|
||||||
|
|
||||||
|
# Language selector
|
||||||
|
lang_label = Gtk.Label(label="Syntax:")
|
||||||
|
toolbar.append(lang_label)
|
||||||
|
|
||||||
|
lang_list = Gtk.StringList()
|
||||||
|
for lang in ["RAW", "JSON", "XML"]:
|
||||||
|
lang_list.append(lang)
|
||||||
|
|
||||||
|
self.body_language_dropdown = Gtk.DropDown(model=lang_list)
|
||||||
|
self.body_language_dropdown.set_selected(0)
|
||||||
|
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_sourceview_theme(self):
|
||||||
|
"""Set up GtkSourceView theme based on system color scheme."""
|
||||||
|
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.response_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 _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)
|
||||||
|
row.connect('changed', self._on_request_changed)
|
||||||
|
self.headers_listbox.append(row)
|
||||||
|
|
||||||
|
if key or value:
|
||||||
|
self._on_request_changed(None)
|
||||||
|
|
||||||
|
def _on_header_remove(self, header_row):
|
||||||
|
"""Handle header row removal."""
|
||||||
|
parent = header_row.get_parent()
|
||||||
|
if parent:
|
||||||
|
self.headers_listbox.remove(parent)
|
||||||
|
self._on_request_changed(None)
|
||||||
|
|
||||||
|
def _setup_change_tracking(self):
|
||||||
|
"""Setup change tracking for this tab."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
body_buffer = self.body_sourceview.get_buffer()
|
||||||
|
body_buffer.connect("changed", self._on_request_changed)
|
||||||
|
|
||||||
|
def _on_request_changed(self, widget, *args):
|
||||||
|
"""Mark this tab as modified."""
|
||||||
|
if not self.original_request:
|
||||||
|
return
|
||||||
|
|
||||||
|
current = self.get_request()
|
||||||
|
self.modified = self._is_different_from_original(current)
|
||||||
|
|
||||||
|
def _is_different_from_original(self, request):
|
||||||
|
"""Check if current request differs from original."""
|
||||||
|
if not self.original_request:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return (
|
||||||
|
request.method != self.original_request.method or
|
||||||
|
request.url != self.original_request.url or
|
||||||
|
request.body != self.original_request.body or
|
||||||
|
request.headers != self.original_request.headers or
|
||||||
|
request.syntax != self.original_request.syntax
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_request(self, request):
|
||||||
|
"""Load a request into this tab's 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 get_request(self):
|
||||||
|
"""Build and return HttpRequest from current UI state."""
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
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 this tab's UI."""
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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 this tab's 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)
|
||||||
|
|
||||||
|
def _extract_content_type(self, headers_text):
|
||||||
|
"""Extract content-type from response headers."""
|
||||||
|
if not headers_text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
for line in headers_text.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if line.lower().startswith('content-type:'):
|
||||||
|
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()
|
||||||
|
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
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:
|
||||||
|
print(f"Failed to format body: {e}")
|
||||||
|
|
||||||
|
return body
|
||||||
Loading…
x
Reference in New Issue
Block a user