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