Tear-Off Tabs
Tear-off tabs provide Chrome-style detachable tab functionality, allowing users to drag tabs out to create floating windows and dock them into other windows.
Overview
The tear-off tab system enables a flexible multi-window workflow where users can:
Drag tabs out to create independent floating windows
Drag floating windows into other tab widgets to dock them
Organize workspace by distributing tabs across multiple windows
Restore tabs to their original location
Key Features
Chrome-style interaction: Drag tabs out vertically or horizontally to tear off
Cross-window docking: Drop tabs into any other TearOffTabWidget
Visual feedback: Drop indicator shows where tab will be inserted
Automatic cleanup: Empty floating windows close automatically
Window dragging: Move floating windows by dragging anywhere in the window
Persistent content: Widget state is preserved during tear-off/dock operations
Architecture
The tear-off tab system consists of four main components:
TearOffTabBar
Custom tab bar that detects tear-off gestures:
Tracks mouse press position for drag start
Detects drag distance exceeding threshold (30 pixels)
Detects drag outside tab bar bounds
Emits tear-off request with tab index and position
TearOffTabWidget
Main tab widget with tear-off and drop support:
Uses TearOffTabBar for tab bar
Accepts drops from other TearOffTabWidgets
Manages floating window lifecycle
Shows visual drop indicators
Emits signals for tear-off and dock events
TearOffRegistry
Global singleton registry for cross-window coordination:
Tracks current drag operation
Registers all tear-off capable widgets
Manages drop target detection
Coordinates drag hover state
FloatingTabWindow
Floating window containing torn-off tab content:
Contains single tab’s widget
Draggable by mouse
Detects hover over drop targets
Handles dock on release
Restores tab to source if closed without docking
Usage
Basic Tear-Off Tabs
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from pyqt_reactive.widgets.shared.tear_off_tab_widget import TearOffTabWidget
app = QApplication([])
# Create tab widget with tear-off support
tabs = TearOffTabWidget()
# Add tabs
tab1 = QWidget()
layout1 = QVBoxLayout(tab1)
layout1.addWidget(QLabel("Content for Tab 1"))
tabs.addTab(tab1, "Tab 1")
tab2 = QWidget()
layout2 = QVBoxLayout(tab2)
layout2.addWidget(QLabel("Content for Tab 2"))
tabs.addTab(tab2, "Tab 2")
# Show
tabs.show()
app.exec()
With Event Callbacks
tabs = TearOffTabWidget()
# Set callback for tab tear-off
tabs.on_tab_torn_off = lambda widget, text: print(f"Torn off: {text}")
# Set callback for tab dock
tabs.on_tab_docked = lambda widget, text, index: print(f"Docked: {text} at {index}")
# Or connect to signals
tabs.tab_torn_off.connect(on_tear_off)
tabs.tab_docked.connect(on_dock)
TabbedFormWidget Integration
TearOffTabWidget is used automatically by TabbedFormWidget:
from pyqt_reactive.widgets.shared.tabbed_form_widget import TabbedFormWidget, TabbedFormConfig
config = TabbedFormConfig(
form_field_configs=[...],
color_scheme=color_scheme
)
tabbed = TabbedFormWidget(config=config)
# Tabs can be torn off and docked elsewhere
Cross-Window Workflow
# Window 1
window1_tabs = TearOffTabWidget()
window1_tabs.addTab(content_a, "Tab A")
window1_tabs.addTab(content_b, "Tab B")
window1_tabs.show()
# Window 2
window2_tabs = TearOffTabWidget()
window2_tabs.addTab(content_c, "Tab C")
window2_tabs.show()
# User can drag Tab A from window1 to window2
# Tab A will be removed from window1 and added to window2
API Reference
TearOffTabWidget
- class TearOffTabWidget(parent=None)
Tab widget with tear-off and docking support.
- on_tab_torn_off
Callback when tab is torn off. Signature:
(widget, text) -> None
- on_tab_docked
Callback when tab is docked. Signature:
(widget, text, index) -> None
- tab_torn_off
Signal emitted when tab is torn off. Signature:
(QWidget, str)
- tab_docked
Signal emitted when tab is docked. Signature:
(QWidget, str, int)
TearOffRegistry
- class TearOffRegistry
Singleton registry for cross-window tear-off operations.
- classmethod register_drag(drag_data, floating_window)
Register a new drag operation.
- classmethod clear_drag()
Clear current drag operation.
- classmethod get_current_drag() TabDragData | None
Get current drag data.
- classmethod register_target(target)
Register widget as potential drop target.
- classmethod unregister_target(target)
Unregister drop target.
- classmethod check_hover(floating_window, global_pos)
Check if floating window is hovering over drop target.
- classmethod perform_drop(floating_window, target)
Perform drop operation.
Best Practices
Widget Lifecycle
Widgets maintain their state during tear-off/dock:
Widget is not deleted during tear-off
Widget parent changes from tab widget to floating window
Widget parent changes from floating window to new tab widget on dock
All widget state (selection, scroll position, etc.) is preserved
State Management
For complex widgets with ObjectState integration:
# ObjectState is preserved during tear-off
# Connections remain active
# Changes continue to work across tear-off/dock cycles
Window Management
Floating windows are top-level dialogs
They close automatically when tab is docked
They restore tab to source if closed without docking
Multiple floating windows can exist simultaneously
Performance Considerations
Drop target detection happens continuously during drag
Visual feedback is lightweight (QFrame indicator)
No expensive operations during drag
Cleanup is automatic and immediate