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 @@ vertical + + + + False + False + 0 + + + - False - - - - automatic - never - true - False - - - horizontal - 0 - - + False + False + + + + + tab_view + True + False - + @@ -137,6 +141,13 @@ primary_menu + + + + + end + + 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")