WIP: Initial AdwTabBar/AdwTabView implementation
This is a work-in-progress implementation of AdwTabBar/AdwTabView. Current issues: - Tab bar height is too large - Large gap on left side of header - Architecture needs refactoring: AdwTabView should contain per-tab UI widgets Next step: Properly refactor so each tab has its own complete UI widget tree.
This commit is contained in:
parent
0a5c1c59af
commit
d912f3479a
@ -92,25 +92,29 @@
|
||||
<object class="GtkBox">
|
||||
<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 -->
|
||||
<child>
|
||||
<object class="AdwHeaderBar">
|
||||
<property name="show-title">False</property>
|
||||
<!-- Tab bar in scrollable container (hidden when only 1 tab) -->
|
||||
<child type="start">
|
||||
<object class="GtkScrolledWindow" id="tab_bar_scroll">
|
||||
<property name="hscrollbar-policy">automatic</property>
|
||||
<property name="vscrollbar-policy">never</property>
|
||||
<property name="propagate-natural-width">true</property>
|
||||
<property name="visible">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="tab_bar_container">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">0</property>
|
||||
<property name="show-start-title-buttons">False</property>
|
||||
<property name="show-end-title-buttons">False</property>
|
||||
|
||||
<!-- Tab bar as title widget -->
|
||||
<property name="title-widget">
|
||||
<object class="AdwTabBar" id="tab_bar">
|
||||
<property name="view">tab_view</property>
|
||||
<property name="autohide">True</property>
|
||||
<property name="expand-tabs">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
|
||||
<!-- Right side buttons -->
|
||||
<child type="end">
|
||||
@ -137,6 +141,13 @@
|
||||
<property name="menu-model">primary_menu</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- Window Controls -->
|
||||
<child>
|
||||
<object class="GtkWindowControls">
|
||||
<property name="side">end</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
358
src/window.py
358
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 page:
|
||||
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)
|
||||
page.set_indicator_icon(Gio.ThemedIcon.new("dot-symbolic"))
|
||||
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()
|
||||
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", ["<Control>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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user