Source code for eye.widgets.tabs

# this project is licensed under the WTFPLv2, see COPYING.txt for details

"""Tab widget
"""

from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QPolygon, QDrag, QIcon
from PyQt5.QtWidgets import QTabWidget, QTabBar, QStackedWidget, QToolButton, QMenu

from eye import consts
from eye.connector import CategoryMixin, disabled, register_setup
from eye.helpers import buffers
from eye.qt import Signal, Slot, override
from eye.widgets.droparea import DropAreaMixin, BandMixin
from eye.widgets.helpers import WidgetMixin, parent_tab_widget

__all__ = (
	'TabWidget', 'TabBar', 'SplitButton',
	'auto_create_corner_splitter',
)


TAB_MIME = 'application/x.eye.tab'


def is_tab_drop_event(ev):
	mdata = ev.mimeData()
	return mdata.hasFormat(TAB_MIME)


def take_widget(widget):
	tw = parent_tab_widget(widget)
	tw.removeTab(tw.indexOf(widget))


def drop_get_widget(ev):
	tb = ev.source()
	tw = tb.parent()
	return tw.widget(tb.tab_drag)


[docs] class TabBar(QTabBar, BandMixin, CategoryMixin): def __init__(self, **kwargs): super().__init__(**kwargs) self.setTabsClosable(True) #~ self.setMovable(True) self.setUsesScrollButtons(True) self.tab_drag = None self.add_category('tabbar') ## drag and drop events
[docs] @override def mousePressEvent(self, ev): super().mousePressEvent(ev) self.tab_drag = self.tabAt(ev.pos())
[docs] @override def mouseMoveEvent(self, ev): mdata = QMimeData() mdata.setData(TAB_MIME, b'x') drag = QDrag(self) drag.setMimeData(mdata) drag.exec_(Qt.CopyAction | Qt.MoveAction, Qt.MoveAction)
def _show_band(self, ev): idx = self.tabAt(ev.pos()) if idx >= 0: self.showBand(self.tabRect(idx)) else: self.showBand(self.rect())
[docs] @override def dragEnterEvent(self, ev): if not is_tab_drop_event(ev): return super().dragEnterEvent(ev) ev.acceptProposedAction() self._show_band(ev)
[docs] @override def dragMoveEvent(self, ev): if not is_tab_drop_event(ev): return super().dragMoveEvent(ev) ev.acceptProposedAction() self._show_band(ev)
[docs] @override def dragLeaveEvent(self, ev): self.hide_band()
[docs] @override def dropEvent(self, ev): if not is_tab_drop_event(ev): return super().dropEvent(ev) self.hide_band() idx = self.tabAt(ev.pos()) assert isinstance(self.parent(), TabWidget) widget = drop_get_widget(ev) if ev.proposedAction() == Qt.MoveAction: ev.acceptProposedAction() take_widget(widget) self.parent().insert_widget(idx, widget) self.parent().setCurrentWidget(widget) elif ev.proposedAction() == Qt.CopyAction: ev.acceptProposedAction() new = buffers.new_editor_share(widget, parent_tab_bar=self.parent()) # FIXME put at right place new.give_focus()
[docs] class TabWidget(DropAreaMixin, QTabWidget, WidgetMixin, BandMixin): """Tab widget class By default, instances of this class have the category `"tabwidget"` (see :doc:`eye.connector`). """ last_tab_closed = Signal() """Signal last_tab_closed() This signal is emitted when the last tab of this tab widget has been closed. """ file_dropped = Signal(str) def __init__(self, **kwargs): super().__init__(**kwargs) self.hide_bar_if_single_tab = False self.tabCloseRequested.connect(self._tab_close_requested) self.currentChanged.connect(self._current_changed) bar = TabBar() self.setTabBar(bar) self.add_category('tabwidget')
[docs] def current_buffer(self): """Return the widget from the current tab""" return self.currentWidget()
def _idx_container_of(self, widget): while widget is not self: idx = self.indexOf(widget) if idx >= 0: return idx widget = widget.parent() return -1 ## add/remove tabs
[docs] def close_tab(self, ed): """Close the tab containing the specified widget and return True if it can be The tab can't be closed if the widget has a `close_file()` method which returns `True` when it is called. This method allows a tab content to reject closing if a file wasn't saved. """ assert self.isAncestorOf(ed) if hasattr(ed, 'close_file'): if not ed.close_file(): return False idx = self._idx_container_of(ed) self.removeTab(idx) return True
[docs] def add_widget(self, widget): """Add a new tab with the specified widget""" assert not self.isAncestorOf(widget) idx = self.addTab(widget, widget.windowIcon(), widget.windowTitle()) widget.windowTitleChanged.connect(self._sub_title_changed) widget.windowIconChanged.connect(self._sub_icon_changed) self.setTabToolTip(idx, widget.toolTip())
[docs] def insert_widget(self, idx, widget): assert not self.isAncestorOf(widget) self.insertTab(idx, widget, widget.windowIcon(), widget.windowTitle()) widget.windowTitleChanged.connect(self._sub_title_changed) widget.windowIconChanged.connect(self._sub_icon_changed) self.setTabToolTip(idx, widget.toolTip())
remove_widget = close_tab ## tab change
[docs] @override def setCurrentWidget(self, widget): """Select the tab containing the specified widget""" assert self.isAncestorOf(widget) idx = self._idx_container_of(widget) if idx >= 0: self.setCurrentIndex(idx)
[docs] @Slot() def select_prev_tab(self, rotate=False): """Select previous tab. :param rotate: if `True` and the current tab is the first tab, the last tab is selected. :type rotate: bool """ cur = self.currentIndex() self._select_tab(-1, cur - 1, -1, rotate, self.count() - 1, cur)
[docs] @Slot() def select_prev_tab_rotate(self): """Select previous tab or last if current is the first tab""" self.select_prev_tab(True)
[docs] @Slot() def select_next_tab(self, rotate=False): """Select next tab :param rotate: if `True` and the current tab is the last tab, the first tab is selected. :type rotate: bool """ cur = self.currentIndex() self._select_tab(1, cur + 1, self.count(), rotate, 0, cur)
[docs] @Slot() def select_next_tab_rotate(self): """Select next tab or first tab if current is the last tab""" self.select_next_tab(True)
def _select_tab(self, step, s1, e1, rotate, s2, e2): for idx in range(s1, e1, step): if self.isTabEnabled(idx): self.setCurrentIndex(idx) return if not rotate: return for idx in range(s2, e2, step): if self.isTabEnabled(idx): self.setCurrentIndex(idx) return
[docs] @Slot() def swap_next_tab(self): idx = self.currentIndex() if idx + 1 < self.count(): self.tabBar().moveTab(idx, idx + 1)
[docs] @Slot() def swap_prev_tab(self): idx = self.currentIndex() if idx >= 1: self.tabBar().moveTab(idx, idx - 1)
## close management
[docs] @override def closeEvent(self, ev): for _ in range(self.count()): w = self.widget(0) if w.close(): self.removeTab(0) else: ev.ignore() return ev.accept()
[docs] def can_close(self): """Returns True if all sub-widgets can be closed""" return all(not w.isWindowModified() for w in self.widgets())
## private @Slot(int) def _tab_close_requested(self, idx): widget = self.widget(idx) if not widget.close_file(): return self.removeTab(idx) @Slot(int) def _current_changed(self, idx): hadFocus = self.hasFocus() self.setFocusProxy(self.widget(idx)) self.tabBar().setFocusProxy(self.widget(idx)) if hadFocus: self.setFocus() @Slot(str) def _sub_title_changed(self, title): w = self.sender() idx = self.indexOf(w) if idx < 0: return self.setTabText(idx, title) self.setTabToolTip(idx, w.toolTip()) @Slot(QIcon) def _sub_icon_changed(self, icon): w = self.sender() idx = self.indexOf(w) if idx < 0: return self.setTabIcon(idx, icon) ## override
[docs] @override def tabInserted(self, idx): super().tabInserted(idx) self._change_tab_bar_visibility()
[docs] @override def tabRemoved(self, idx): super().tabRemoved(idx) self._change_tab_bar_visibility() w = self._find_removed_widget() w.setParent(None) if self.count() == 0: self.last_tab_closed.emit() elif self.currentIndex() == idx: self.currentWidget().give_focus()
def _find_removed_widget(self): # implementation detail, but no access to the removed widget base = self.findChild(QStackedWidget) for c in base.children(): if not c.isWidgetType(): continue if self.indexOf(c) < 0: return c
[docs] def set_hide_bar_if_single_tab(self, b): """Set whether the tab bar should be hidden if there's only one tab. """ self.hide_bar_if_single_tab = b self._change_tab_bar_visibility()
def _change_tab_bar_visibility(self): if self.hide_bar_if_single_tab: visible = (self.count() > 1) else: visible = True self.tabBar().setVisible(visible) ## misc
[docs] @Slot() def refocus(self): """Give focus to the widget inside the current tab""" self.currentWidget().setFocus(Qt.OtherFocusReason)
[docs] def widgets(self): """Return widgets contained in tabs""" return [self.widget(i) for i in range(self.count())]
## drag and drop events def _show_band(self, pos): quad = widget_quadrant(self.rect(), pos) r = widget_half(self.rect(), quad) self.showBand(r)
[docs] @override def dragEnterEvent(self, ev): if is_tab_drop_event(ev): self.tabBar().setVisible(True) ev.acceptProposedAction() self._show_band(ev.pos()) else: super().dragEnterEvent(ev)
[docs] @override def dragMoveEvent(self, ev): if is_tab_drop_event(ev): ev.acceptProposedAction() self._show_band(ev.pos()) else: super().dragMoveEvent(ev)
[docs] @override def dragLeaveEvent(self, ev): super().dragLeaveEvent(ev) self.hide_band() self._change_tab_bar_visibility()
[docs] @override def dropEvent(self, ev): if is_tab_drop_event(ev): self.hide_band() splitmanager = self.parent().parent_manager() quad = widget_quadrant(self.rect(), ev.pos()) widget = drop_get_widget(ev) old_tw = parent_tab_widget(widget) if ev.proposedAction() == Qt.MoveAction: ev.acceptProposedAction() if old_tw.count() == 1: if old_tw is self: return splitmanager.split_at(self, quad, old_tw) else: take_widget(widget) tabs = TabWidget() tabs.add_widget(widget) splitmanager.split_at(self, quad, tabs) elif ev.proposedAction() == Qt.CopyAction: ev.acceptProposedAction() new = buffers.new_editor_share(widget, parent_tab_bar=self) take_widget(new) tabs = TabWidget() tabs.add_widget(new) splitmanager.split_at(self, quad, tabs) else: super().dropEvent(ev)
[docs] class SplitButton(QToolButton, WidgetMixin): """Button for splitting When clicked, the button shows a popup menu to choose between horizontal split and vertical split. The button is suitable for using as `cornerWidget` of :any:`TabWidget`. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.setText('\u25ea') menu = QMenu() action = menu.addAction('Split &horizontally ―') action.triggered.connect(self.split_horizontal) action = menu.addAction('Split &vertically |') action.triggered.connect(self.split_vertical) self.setMenu(menu) self.setPopupMode(self.InstantPopup)
[docs] @Slot() def split_horizontal(self): assert isinstance(self.parent(), TabWidget) win = self.window() win.buffer_split_horizontal(self.parent())
[docs] @Slot() def split_vertical(self): assert isinstance(self.parent(), TabWidget) win = self.window() win.buffer_split_vertical(self.parent())
[docs] @register_setup('tabwidget') @disabled def auto_create_corner_splitter(tw): """When enabled, will create a corner popup button for splitting. .. seealso:: :any:`eye.widgets.splitter` """ button = SplitButton() tw.setCornerWidget(button, Qt.TopRightCorner) button.show()
def widget_quadrant(rect, point): center = rect.center() if QPolygon([rect.topLeft(), rect.topRight(), center]).containsPoint(point, 0): return consts.UP if QPolygon([rect.bottomLeft(), rect.bottomRight(), center]).containsPoint(point, 0): return consts.DOWN if QPolygon([rect.topLeft(), rect.bottomLeft(), center]).containsPoint(point, 0): return consts.LEFT if QPolygon([rect.bottomRight(), rect.topRight(), center]).containsPoint(point, 0): return consts.RIGHT def widget_half(rect, quadrant): if quadrant in (consts.UP, consts.DOWN): rect.setHeight(rect.height() // 2) if quadrant == consts.DOWN: rect.translate(0, rect.height()) elif quadrant in (consts.LEFT, consts.RIGHT): rect.setWidth(rect.width() // 2) if quadrant == consts.RIGHT: rect.translate(rect.width(), 0) return rect