diff --git a/src/main-window.ui b/src/main-window.ui
index a31848a..f702db5 100644
--- a/src/main-window.ui
+++ b/src/main-window.ui
@@ -92,25 +92,29 @@
diff --git a/src/window.py b/src/window.py
index 316fd53..05aef8f 100644
--- a/src/window.py
+++ b/src/window.py
@@ -44,8 +44,8 @@ class RosterWindow(Adw.ApplicationWindow):
url_entry = Gtk.Template.Child()
send_button = Gtk.Template.Child()
new_request_button = Gtk.Template.Child()
- tab_bar_scroll = Gtk.Template.Child()
- tab_bar_container = Gtk.Template.Child()
+ tab_view = Gtk.Template.Child()
+ tab_bar = Gtk.Template.Child()
# Panes
main_pane = Gtk.Template.Child()
@@ -75,12 +75,14 @@ class RosterWindow(Adw.ApplicationWindow):
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)
+ # Map AdwTabPage to RequestTab - stores our tab data
+ self.page_to_tab = {} # AdwTabPage -> RequestTab
self.current_tab_id = None
+ # Connect to tab view signals
+ self.tab_view.connect('close-page', self._on_tab_close_page)
+ self.tab_view.connect('notify::selected-page', self._on_tab_selected)
+
# Create window actions
self._create_actions()
@@ -163,59 +165,12 @@ class RosterWindow(Adw.ApplicationWindow):
self.destroy()
def _setup_custom_css(self):
- """Setup custom CSS for enhanced tab styling."""
+ """Setup custom CSS for UI styling."""
css_provider = Gtk.CssProvider()
css_provider.load_from_data(b"""
- /* Document tabs in header bar */
- .tab-button {
- margin: 0 2px;
- padding: 0;
- border-radius: 6px;
- background: alpha(@window_fg_color, 0.08);
- transition: all 200ms ease;
- }
-
- .tab-button:hover {
- background: alpha(@window_fg_color, 0.12);
- }
-
- .tab-button-active {
- background: alpha(@accent_bg_color, 0.2);
- box-shadow: inset 0 -2px 0 0 @accent_bg_color;
- }
-
- .tab-button-active:hover {
- background: alpha(@accent_bg_color, 0.25);
- }
-
- /* Tab label button */
- .tab-label-btn {
- padding: 4px 12px;
- min-height: 28px;
- border-radius: 6px 0 0 6px;
- }
-
- .tab-button-active .tab-label-btn {
- font-weight: 600;
- }
-
- /* Tab close button */
- .tab-close-btn {
- padding: 4px 8px;
- min-width: 24px;
- min-height: 24px;
- margin: 2px 4px 2px 0;
- border-radius: 0 6px 6px 0;
- opacity: 0.7;
- }
-
- .tab-close-btn:hover {
- opacity: 1;
- background: alpha(@window_fg_color, 0.1);
- }
-
- .tab-button-active .tab-close-btn:hover {
- background: alpha(@accent_bg_color, 0.3);
+ /* Constrain tab bar to normal header height */
+ .tabbar-box {
+ min-height: 0;
}
/* Stack switchers styling (Headers/Body tabs) */
@@ -231,11 +186,6 @@ class RosterWindow(Adw.ApplicationWindow):
color: @accent_fg_color;
font-weight: 600;
}
-
- /* Add some polish to the tab bar container */
- #tab_bar_container {
- margin: 0 6px;
- }
""")
Gtk.StyleContext.add_provider_for_display(
@@ -320,17 +270,87 @@ class RosterWindow(Adw.ApplicationWindow):
body_buffer = self.body_sourceview.get_buffer()
body_buffer.connect("changed", self._on_request_changed)
+ def _on_tab_selected(self, tab_view, param):
+ """Handle tab selection change."""
+ page = tab_view.get_selected_page()
+ if not page:
+ return
+
+ # Get the RequestTab for this page
+ tab = self.page_to_tab.get(page)
+ if tab:
+ self.current_tab_id = tab.id
+ self._load_request_to_ui(tab.request)
+ if tab.response:
+ self._display_response(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("")
+
+ def _on_tab_close_page(self, tab_view, page):
+ """Handle tab close request."""
+ # Get the RequestTab for this page
+ tab = self.page_to_tab.get(page)
+ if not tab:
+ return True # Allow close
+
+ # Save current tab state if this is the active tab
+ if self.current_tab_id == tab.id:
+ tab.request = self._build_request_from_ui()
+
+ # Check if tab has unsaved changes
+ if 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")
+
+ def on_response(dlg, response):
+ if response == "close":
+ # Remove from our tracking
+ self.tab_manager.close_tab(tab.id)
+ del self.page_to_tab[page]
+ # Close the page
+ tab_view.close_page_finish(page, True)
+ else:
+ # Cancel the close
+ tab_view.close_page_finish(page, False)
+
+ dialog.connect("response", on_response)
+ dialog.present(self)
+ return True # Defer close decision to dialog
+ else:
+ # No unsaved changes, allow close
+ self.tab_manager.close_tab(tab.id)
+ del self.page_to_tab[page]
+
+ # If no tabs left, create a new one
+ if self.tab_view.get_n_pages() == 1: # This page is still counted
+ GLib.idle_add(self._create_new_tab)
+
+ return False # Allow close
+
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()
+ # Check if modified
+ current_tab.modified = current_tab.is_modified()
+ # Update tab indicator
+ self._update_tab_indicator(current_tab)
def _is_empty_new_request_tab(self):
"""Check if current tab is an empty 'New Request' tab."""
@@ -378,36 +398,8 @@ class RosterWindow(Adw.ApplicationWindow):
return f"{base_name} (copy {counter})"
- def _switch_to_tab(self, tab_id):
- """Switch to a specific tab."""
- tab = self.tab_manager.get_tab_by_id(tab_id)
- if not tab:
- return
-
- # Update tab manager
- self.tab_manager.set_active_tab(tab_id)
- self.current_tab_id = tab_id
-
- # Load the tab's request into UI
- self._load_request_to_ui(tab.request)
-
- # Restore response if available
- if tab.response:
- self._display_response(tab.response)
- else:
- # Clear response area
- 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 _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 a new tab using AdwTabView."""
# Create tab in tab manager
if not request:
request = HttpRequest(method="GET", url="", headers={}, body="", syntax="RAW")
@@ -415,140 +407,56 @@ class RosterWindow(Adw.ApplicationWindow):
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)
+ # Create a placeholder widget for the tab page
+ placeholder = Gtk.Box()
- # Update tab bar visibility
- self._update_tab_bar_visibility()
+ # Create AdwTabPage
+ page = self.tab_view.append(placeholder)
+ page.set_title(name)
+ page.set_indicator_activatable(False)
+
+ # Store mapping
+ self.page_to_tab[page] = tab
+
+ # Update indicator if modified
+ if tab.is_modified():
+ page.set_indicator_icon(Gio.ThemedIcon.new("dot-symbolic"))
+
+ # Select this new page
+ self.tab_view.set_selected_page(page)
+
+ # Load request into UI (will be triggered by selection change signal)
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_scroll.set_visible(num_tabs >= 2)
+ def _update_tab_indicator(self, tab):
+ """Update the indicator for a tab based on its modified state."""
+ # Find the page for this tab
+ page = None
+ for p, t in self.page_to_tab.items():
+ if t.id == tab.id:
+ page = p
+ break
- # 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=0)
- tab_box.add_css_class("tab-button")
-
- # Add active styling if this is the current tab
- if tab.id == self.current_tab_id:
- tab_box.add_css_class("tab-button-active")
-
- # Tab label with modified indicator
- tab_label = tab.name[:25] # 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")
- tab_label_btn.add_css_class("tab-label-btn")
- 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.add_css_class("tab-close-btn")
- 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()
+ if page:
+ if tab.is_modified():
+ page.set_indicator_icon(Gio.ThemedIcon.new("dot-symbolic"))
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()
+ page.set_indicator_icon(None)
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()
+ """Switch to a tab by its ID."""
+ # Find the page for this tab
+ for page, tab in self.page_to_tab.items():
+ if tab.id == tab_id:
+ self.tab_view.set_selected_page(page)
+ return
- # 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 _close_current_tab(self):
+ """Close the currently active tab."""
+ page = self.tab_view.get_selected_page()
+ if page:
+ self.tab_view.close_page(page)
def _load_request_to_ui(self, request):
"""Load a request into the UI."""
@@ -603,11 +511,6 @@ class RosterWindow(Adw.ApplicationWindow):
self.add_action(action)
self.get_application().set_accels_for_action("win.save-request", ["s"])
- 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()
@@ -1060,7 +963,6 @@ class RosterWindow(Adw.ApplicationWindow):
# 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
@@ -1312,8 +1214,8 @@ class RosterWindow(Adw.ApplicationWindow):
syntax=request.syntax
)
- # Update tab bar to remove modified indicator
- self._update_tab_bar_visibility()
+ # Update tab indicator to remove modified marker
+ self._update_tab_indicator(current_tab)
def _show_overwrite_dialog(self, project, name, existing_request_id, request):
"""Show dialog asking if user wants to overwrite existing request."""
@@ -1449,7 +1351,7 @@ class RosterWindow(Adw.ApplicationWindow):
if new_tab:
new_tab.original_request = None
new_tab.modified = True
- self._update_tab_bar_visibility()
+ self._update_tab_indicator(new_tab)
# Switch to headers tab
self.request_stack.set_visible_child_name("headers")