diff --git a/src/request_tab_widget.py b/src/request_tab_widget.py index f2343f1..fc9ca8d 100644 --- a/src/request_tab_widget.py +++ b/src/request_tab_widget.py @@ -80,28 +80,24 @@ class RequestTabWidget(Gtk.Box): def _build_ui(self) -> None: """Build the complete UI for this tab.""" - # URL Input Section - outer container (store as instance var for dynamic updates) + # URL Input Section self.url_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) self.url_container.set_margin_start(12) self.url_container.set_margin_end(12) self.url_container.set_margin_top(12) self.url_container.set_margin_bottom(12) - # Environment Selector (left-aligned, outside clamp) if self.project_id: self._build_environment_selector_inline(self.url_container) - # Add visual separator/spacer after environment self.env_separator = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL) self.env_separator.set_margin_start(12) self.env_separator.set_margin_end(12) self.url_container.append(self.env_separator) - # URL bar (method, URL, send) - centered in clamp url_clamp = Adw.Clamp(maximum_size=1000) url_clamp.set_hexpand(True) self.url_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - # Method Dropdown self.method_dropdown = Gtk.DropDown() methods = Gtk.StringList() for method in ["GET", "POST", "PUT", "DELETE"]: @@ -111,14 +107,12 @@ class RequestTabWidget(Gtk.Box): self.method_dropdown.set_enable_search(False) self.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) self.url_entry.add_css_class("url-entry") self.url_box.append(self.url_entry) - # Send Button self.send_button = Gtk.Button(icon_name="media-playback-start-symbolic") self.send_button.set_tooltip_text("Send Request") self.send_button.add_css_class("suggested-action") @@ -128,62 +122,89 @@ class RequestTabWidget(Gtk.Box): self.url_container.append(url_clamp) self.append(self.url_container) - # Horizontal Split: Request | Response - split_pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) - split_pane.set_vexpand(True) - split_pane.set_position(UI_PANE_REQUEST_RESPONSE_POSITION) - split_pane.set_shrink_start_child(True) # request can collapse to 0 - split_pane.set_shrink_end_child(False) # response stays - split_pane.set_resize_start_child(True) # request gives up space first - split_pane.set_resize_end_child(False) # response keeps its size + # Build content panels + self.request_panel = self._create_request_panel() + self.response_panel = self._create_response_panel() + + # Normal layout: horizontal split pane + self.split_pane = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) + self.split_pane.set_vexpand(True) + self.split_pane.set_position(UI_PANE_REQUEST_RESPONSE_POSITION) + self.split_pane.set_shrink_start_child(False) + self.split_pane.set_shrink_end_child(False) + self.split_pane.set_resize_start_child(True) + self.split_pane.set_resize_end_child(False) + self.split_pane.set_start_child(self.request_panel) + self.split_pane.set_end_child(self.response_panel) + + # Narrow layout: single panel with Request/Response toggle + self._build_narrow_layout() + self._narrow_mode = False + + self.append(self.split_pane) + + def _create_request_panel(self) -> Gtk.Box: + """Create the request panel with a responsive tab switcher.""" + request_panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + request_panel.set_size_request(220, -1) + + # Responsive tab switcher (custom, so orientation can change) + self.request_tab_switcher = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + self.request_tab_switcher.set_halign(Gtk.Align.CENTER) + self.request_tab_switcher.set_margin_top(8) + self.request_tab_switcher.set_margin_bottom(8) + self.request_tab_switcher.add_css_class("request-tab-switcher") - # 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 + # Build tab pages self._build_headers_tab() - - # Body tab self._build_body_tab() - - # Scripts tab self._build_scripts_tab() - split_pane.set_start_child(request_box) + # Build linked toggle buttons for each page + self._request_tab_buttons = {} + first_btn = None + for page_name, label in [("headers", "Headers"), ("body", "Body"), ("scripts", "Scripts")]: + btn = Gtk.ToggleButton(label=label) + btn.add_css_class("flat") + if first_btn is None: + btn.set_active(True) + first_btn = btn + else: + btn.set_group(first_btn) + btn.connect("toggled", self._on_request_tab_toggled, page_name) + self.request_tab_switcher.append(btn) + self._request_tab_buttons[page_name] = btn - # Response Panel + request_panel.append(self.request_tab_switcher) + request_panel.append(self.request_stack) + + # Switch switcher orientation based on available width + request_panel.connect("notify::width", self._on_request_panel_width_changed) + + return request_panel + + def _create_response_panel(self) -> Gtk.Box: + """Create the response panel.""" response_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) response_box.set_size_request(250, -1) - # Stack switcher (placed in bottom bar together with status info) response_switcher = Gtk.StackSwitcher() response_switcher.set_halign(Gtk.Align.START) response_switcher.set_valign(Gtk.Align.CENTER) - # Create a vertical paned for response stack and result panels self.response_main_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) self.response_main_paned.set_vexpand(True) self.response_main_paned.set_position(UI_PANE_RESPONSE_DETAILS_POSITION) self.response_main_paned.set_shrink_start_child(False) - # Don't allow results panels to shrink below their minimum size self.response_main_paned.set_shrink_end_child(False) self.response_main_paned.set_resize_start_child(True) self.response_main_paned.set_resize_end_child(True) - # Response Stack self.response_stack = Gtk.Stack() self.response_stack.set_vexpand(True) self.response_stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) @@ -192,7 +213,6 @@ class RequestTabWidget(Gtk.Box): response_switcher.set_stack(self.response_stack) self.response_main_paned.set_start_child(self.response_stack) - # Response headers headers_scroll = Gtk.ScrolledWindow() headers_scroll.set_vexpand(True) self.response_headers_textview = Gtk.TextView() @@ -205,7 +225,6 @@ class RequestTabWidget(Gtk.Box): 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() @@ -217,10 +236,8 @@ class RequestTabWidget(Gtk.Box): 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 { @@ -237,24 +254,19 @@ class RequestTabWidget(Gtk.Box): body_scroll.set_child(self.response_body_sourceview) self.response_stack.add_titled(body_scroll, "body", "Body") - # Create a second paned for preprocessing and script results self.results_paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) self.results_paned.set_vexpand(True) self.results_paned.set_position(UI_PANE_RESULTS_PANEL_POSITION) - self.results_paned.set_visible(False) # Initially hidden until scripts produce output - # Don't allow children to shrink below their minimum size + self.results_paned.set_visible(False) self.results_paned.set_shrink_start_child(False) self.results_paned.set_shrink_end_child(False) self.results_paned.set_resize_start_child(True) self.results_paned.set_resize_end_child(True) - # Preprocessing Results Panel (resizable) self.preprocessing_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - self.preprocessing_results_container.set_visible(False) # Initially hidden - # Set minimum height to ensure header is always visible (header ~40px + content min 60px) + self.preprocessing_results_container.set_visible(False) self.preprocessing_results_container.set_size_request(-1, 100) - # Header preprocessing_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) preprocessing_header.set_margin_start(12) preprocessing_header.set_margin_end(12) @@ -266,19 +278,16 @@ class RequestTabWidget(Gtk.Box): preprocessing_label.set_halign(Gtk.Align.START) preprocessing_header.append(preprocessing_label) - # Spacer preprocessing_spacer = Gtk.Box() preprocessing_spacer.set_hexpand(True) preprocessing_header.append(preprocessing_spacer) - # Status icon self.preprocessing_status_icon = Gtk.Image() self.preprocessing_status_icon.set_from_icon_name("object-select-symbolic") preprocessing_header.append(self.preprocessing_status_icon) self.preprocessing_results_container.append(preprocessing_header) - # Output text view (scrollable, resizable) self.preprocessing_output_scroll = Gtk.ScrolledWindow() self.preprocessing_output_scroll.set_vexpand(True) self.preprocessing_output_scroll.set_min_content_height(60) @@ -300,13 +309,10 @@ class RequestTabWidget(Gtk.Box): self.results_paned.set_start_child(self.preprocessing_results_container) - # Script Results Panel (resizable) self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - self.script_results_container.set_visible(False) # Initially hidden - # Set minimum height to ensure header is always visible (header ~40px + content min 60px) + self.script_results_container.set_visible(False) self.script_results_container.set_size_request(-1, 100) - # Header results_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) results_header.set_margin_start(12) results_header.set_margin_end(12) @@ -318,19 +324,16 @@ class RequestTabWidget(Gtk.Box): results_label.set_halign(Gtk.Align.START) results_header.append(results_label) - # Spacer results_spacer = Gtk.Box() results_spacer.set_hexpand(True) results_header.append(results_spacer) - # Status icon self.script_status_icon = Gtk.Image() self.script_status_icon.set_from_icon_name("object-select-symbolic") results_header.append(self.script_status_icon) self.script_results_container.append(results_header) - # Output text view (scrollable, resizable) self.script_output_scroll = Gtk.ScrolledWindow() self.script_output_scroll.set_vexpand(True) self.script_output_scroll.set_min_content_height(60) @@ -351,14 +354,10 @@ class RequestTabWidget(Gtk.Box): self.script_results_container.append(self.script_output_scroll) self.results_paned.set_end_child(self.script_results_container) - - # Set the results paned as the end child of the main response paned self.response_main_paned.set_end_child(self.results_paned) - # Add the main paned to response_box response_box.append(self.response_main_paned) - # Bottom bar: response tabs (left) + status info (right) bottom_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) bottom_bar.set_margin_start(6) bottom_bar.set_margin_end(12) @@ -383,12 +382,86 @@ class RequestTabWidget(Gtk.Box): status_box.append(self.size_label) bottom_bar.append(status_box) - response_box.append(bottom_bar) - split_pane.set_end_child(response_box) + return response_box - self.append(split_pane) + def _build_narrow_layout(self) -> None: + """Build the narrow single-panel layout with Request/Response toggle.""" + self.narrow_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.narrow_box.set_vexpand(True) + + toggle_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + toggle_bar.set_halign(Gtk.Align.CENTER) + toggle_bar.set_margin_top(6) + toggle_bar.set_margin_bottom(6) + toggle_bar.add_css_class("linked") + + self.narrow_request_btn = Gtk.ToggleButton(label="Request") + self.narrow_request_btn.set_active(True) + + self.narrow_response_btn = Gtk.ToggleButton(label="Response") + self.narrow_response_btn.set_group(self.narrow_request_btn) + + self.narrow_request_btn.connect("toggled", self._on_narrow_panel_toggled, "request") + self.narrow_response_btn.connect("toggled", self._on_narrow_panel_toggled, "response") + + toggle_bar.append(self.narrow_request_btn) + toggle_bar.append(self.narrow_response_btn) + self.narrow_box.append(toggle_bar) + + self.narrow_stack = Gtk.Stack() + self.narrow_stack.set_vexpand(True) + self.narrow_stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) + self.narrow_stack.set_transition_duration(200) + self.narrow_box.append(self.narrow_stack) + + def set_narrow_mode(self, narrow: bool) -> None: + """Switch between split-pane (normal) and single-panel (narrow) layout.""" + if narrow == self._narrow_mode: + return + self._narrow_mode = narrow + + if narrow: + self.split_pane.set_start_child(None) + self.split_pane.set_end_child(None) + self.remove(self.split_pane) + + self.narrow_stack.add_named(self.request_panel, "request") + self.narrow_stack.add_named(self.response_panel, "response") + self.append(self.narrow_box) + else: + self.narrow_stack.remove(self.request_panel) + self.narrow_stack.remove(self.response_panel) + self.remove(self.narrow_box) + + self.split_pane.set_start_child(self.request_panel) + self.split_pane.set_end_child(self.response_panel) + self.append(self.split_pane) + + def _on_request_tab_toggled(self, button: Gtk.ToggleButton, page_name: str) -> None: + """Handle request tab button toggle.""" + if button.get_active(): + self.request_stack.set_visible_child_name(page_name) + + def _on_request_panel_width_changed(self, widget, pspec) -> None: + """Switch request tab switcher to vertical when panel is narrow.""" + width = widget.get_width() + if width > 0 and width < 260: + if self.request_tab_switcher.get_orientation() != Gtk.Orientation.VERTICAL: + self.request_tab_switcher.set_orientation(Gtk.Orientation.VERTICAL) + self.request_tab_switcher.set_halign(Gtk.Align.START) + self.request_tab_switcher.set_margin_start(8) + elif width >= 260: + if self.request_tab_switcher.get_orientation() != Gtk.Orientation.HORIZONTAL: + self.request_tab_switcher.set_orientation(Gtk.Orientation.HORIZONTAL) + self.request_tab_switcher.set_halign(Gtk.Align.CENTER) + self.request_tab_switcher.set_margin_start(0) + + def _on_narrow_panel_toggled(self, button: Gtk.ToggleButton, panel_name: str) -> None: + """Handle narrow mode Request/Response toggle.""" + if button.get_active(): + self.narrow_stack.set_visible_child_name(panel_name) def _build_headers_tab(self) -> None: """Build the headers tab.""" @@ -931,6 +1004,11 @@ class RequestTabWidget(Gtk.Box): # Switch to body tab self.response_stack.set_visible_child_name("body") + # In narrow mode, automatically show the response panel + if self._narrow_mode: + self.narrow_stack.set_visible_child_name("response") + self.narrow_response_btn.set_active(True) + def display_error(self, error: str) -> None: """Display error in this tab's UI.""" self.status_label.set_text("Error") @@ -946,6 +1024,10 @@ class RequestTabWidget(Gtk.Box): source_buffer.set_text(error) source_buffer.set_language(None) + if self._narrow_mode: + self.narrow_stack.set_visible_child_name("response") + self.narrow_response_btn.set_active(True) + def display_script_results(self, script_result): """Display script execution results.""" from .script_executor import ScriptResult diff --git a/src/window.py b/src/window.py index 607f268..9114282 100644 --- a/src/window.py +++ b/src/window.py @@ -108,6 +108,9 @@ class RosterWindow(Adw.ApplicationWindow): GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE ) + # Switch tab widgets to narrow layout when sidebar collapses + self.split_view.connect("notify::collapsed", self._on_split_view_collapsed_changed) + # Setup UI self._setup_tab_system() self._load_projects() @@ -119,6 +122,12 @@ class RosterWindow(Adw.ApplicationWindow): # Create first tab self._create_new_tab() + def _on_split_view_collapsed_changed(self, split_view, pspec) -> None: + """Switch all tab widgets to narrow or normal layout based on sidebar state.""" + is_narrow = split_view.get_collapsed() + for widget in self.page_to_widget.values(): + widget.set_narrow_mode(is_narrow) + def _on_close_request(self, window) -> bool: """Handle window close request - warn if there are unsaved changes.""" # Check if any tabs have unsaved changes @@ -167,15 +176,15 @@ class RosterWindow(Adw.ApplicationWindow): /* AdwToolbarView handles header bar heights automatically */ /* Just add minimal custom styling for other elements */ - /* Stack switchers styling (Headers/Body tabs) */ - stackswitcher button { + /* Request tab switcher (Headers/Body/Scripts) */ + .request-tab-switcher button { padding: 6px 16px; min-height: 32px; border-radius: 6px; margin: 0 2px; } - stackswitcher button:checked { + .request-tab-switcher button:checked { font-weight: 900; } @@ -405,6 +414,10 @@ class RosterWindow(Adw.ApplicationWindow): # incorrectly marked all variables as undefined. widget._update_variable_indicators() + # Apply narrow mode if sidebar is currently collapsed + if self.split_view.get_collapsed(): + widget.set_narrow_mode(True) + # Connect to send button widget.send_button.connect("clicked", lambda btn: self._on_send_clicked(widget))