diff -r f99d60d6b59b -r 2602857055c5 eric6/Plugins/ViewManagerPlugins/Tabview/Tabview.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/ViewManagerPlugins/Tabview/Tabview.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,1444 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a tabbed viewmanager class. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot, QPoint, QFileInfo, pyqtSignal, QEvent, \ + QByteArray, QMimeData, Qt, QSize +from PyQt5.QtGui import QColor, QDrag, QPixmap +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QSplitter, QTabBar, \ + QApplication, QToolButton, QMenu, QLabel + +from E5Gui.E5Application import e5App + +from ViewManager.ViewManager import ViewManager + +import QScintilla.Editor +from QScintilla.Editor import Editor +from QScintilla.EditorAssembly import EditorAssembly + +import UI.PixmapCache + +from E5Gui.E5TabWidget import E5TabWidget, E5WheelTabBar +from E5Gui.E5Led import E5Led + +import Preferences +from Globals import isMacPlatform + +from eric6config import getConfig + + +class TabBar(E5WheelTabBar): + """ + Class implementing a customized tab bar supporting drag & drop. + + @signal tabMoveRequested(int, int) emitted to signal a tab move request + giving the old and new index position + @signal tabRelocateRequested(str, int, int) emitted to signal a tab + relocation request giving the string encoded id of the old tab widget, + the index in the old tab widget and the new index position + @signal tabCopyRequested(str, int, int) emitted to signal a clone request + giving the string encoded id of the source tab widget, the index in the + source tab widget and the new index position + @signal tabCopyRequested(int, int) emitted to signal a clone request giving + the old and new index position + """ + tabMoveRequested = pyqtSignal(int, int) + tabRelocateRequested = pyqtSignal(str, int, int) + tabCopyRequested = pyqtSignal((str, int, int), (int, int)) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(TabBar, self).__init__(parent) + self.setAcceptDrops(True) + + self.__dragStartPos = QPoint() + + def mousePressEvent(self, event): + """ + Protected method to handle mouse press events. + + @param event reference to the mouse press event + @type QMouseEvent + """ + if event.button() == Qt.LeftButton: + self.__dragStartPos = QPoint(event.pos()) + super(TabBar, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + """ + Protected method to handle mouse move events. + + @param event reference to the mouse move event + @type QMouseEvent + """ + if event.buttons() == Qt.MouseButtons(Qt.LeftButton) and \ + (event.pos() - self.__dragStartPos).manhattanLength() > \ + QApplication.startDragDistance(): + drag = QDrag(self) + mimeData = QMimeData() + index = self.tabAt(event.pos()) + mimeData.setText(self.tabText(index)) + mimeData.setData("action", b"tab-reordering") + mimeData.setData("tabbar-id", str(id(self)).encode("utf-8")) + mimeData.setData( + "source-index", + QByteArray.number(self.tabAt(self.__dragStartPos))) + mimeData.setData( + "tabwidget-id", + str(id(self.parentWidget())).encode("utf-8")) + drag.setMimeData(mimeData) + if event.modifiers() == Qt.KeyboardModifiers(Qt.ShiftModifier): + drag.exec_(Qt.DropActions(Qt.CopyAction)) + elif event.modifiers() == Qt.KeyboardModifiers(Qt.NoModifier): + drag.exec_(Qt.DropActions(Qt.MoveAction)) + super(TabBar, self).mouseMoveEvent(event) + + def dragEnterEvent(self, event): + """ + Protected method to handle drag enter events. + + @param event reference to the drag enter event + @type QDragEnterEvent + """ + mimeData = event.mimeData() + formats = mimeData.formats() + if "action" in formats and \ + mimeData.data("action") == b"tab-reordering" and \ + "tabbar-id" in formats and \ + "source-index" in formats and \ + "tabwidget-id" in formats: + event.acceptProposedAction() + super(TabBar, self).dragEnterEvent(event) + + def dropEvent(self, event): + """ + Protected method to handle drop events. + + @param event reference to the drop event + @type QDropEvent + """ + mimeData = event.mimeData() + oldID = int(mimeData.data("tabbar-id")) + fromIndex = int(mimeData.data("source-index")) + toIndex = self.tabAt(event.pos()) + if oldID != id(self): + parentID = int(mimeData.data("tabwidget-id")) + if event.proposedAction() == Qt.MoveAction: + self.tabRelocateRequested.emit( + str(parentID), fromIndex, toIndex) + event.acceptProposedAction() + elif event.proposedAction() == Qt.CopyAction: + self.tabCopyRequested[str, int, int].emit( + str(parentID), fromIndex, toIndex) + event.acceptProposedAction() + else: + if fromIndex != toIndex: + if event.proposedAction() == Qt.MoveAction: + self.tabMoveRequested.emit(fromIndex, toIndex) + event.acceptProposedAction() + elif event.proposedAction() == Qt.CopyAction: + self.tabCopyRequested[int, int].emit(fromIndex, toIndex) + event.acceptProposedAction() + super(TabBar, self).dropEvent(event) + + +class TabWidget(E5TabWidget): + """ + Class implementing a custimized tab widget. + """ + def __init__(self, vm): + """ + Constructor + + @param vm view manager widget + @type Tabview + """ + super(TabWidget, self).__init__() + + self.__tabBar = TabBar(self) + self.setTabBar(self.__tabBar) + iconSize = self.__tabBar.iconSize() + self.__tabBar.setIconSize( + QSize(2 * iconSize.width(), iconSize.height())) + + self.setUsesScrollButtons(True) + self.setElideMode(Qt.ElideNone) + if isMacPlatform(): + self.setDocumentMode(True) + + self.__tabBar.tabMoveRequested.connect(self.moveTab) + self.__tabBar.tabRelocateRequested.connect(self.__relocateTab) + self.__tabBar.tabCopyRequested[str, int, int].connect( + self.__copyTabOther) + self.__tabBar.tabCopyRequested[int, int].connect(self.__copyTab) + + self.vm = vm + self.editors = [] + + self.indicator = E5Led(self) + self.setCornerWidget(self.indicator, Qt.TopLeftCorner) + + self.rightCornerWidget = QWidget(self) + self.rightCornerWidgetLayout = QHBoxLayout(self.rightCornerWidget) + self.rightCornerWidgetLayout.setContentsMargins(0, 0, 0, 0) + self.rightCornerWidgetLayout.setSpacing(0) + + self.__navigationMenu = QMenu(self) + self.__navigationMenu.aboutToShow.connect(self.__showNavigationMenu) + self.__navigationMenu.triggered.connect(self.__navigationMenuTriggered) + + self.navigationButton = QToolButton(self) + self.navigationButton.setIcon(UI.PixmapCache.getIcon("1downarrow.png")) + self.navigationButton.setToolTip(self.tr("Show a navigation menu")) + self.navigationButton.setPopupMode(QToolButton.InstantPopup) + self.navigationButton.setMenu(self.__navigationMenu) + self.navigationButton.setEnabled(False) + self.rightCornerWidgetLayout.addWidget(self.navigationButton) + + if Preferences.getUI("SingleCloseButton") or \ + not hasattr(self, 'setTabsClosable'): + self.closeButton = QToolButton(self) + self.closeButton.setIcon(UI.PixmapCache.getIcon("close.png")) + self.closeButton.setToolTip( + self.tr("Close the current editor")) + self.closeButton.setEnabled(False) + self.closeButton.clicked[bool].connect(self.__closeButtonClicked) + self.rightCornerWidgetLayout.addWidget(self.closeButton) + else: + self.tabCloseRequested.connect(self.__closeRequested) + self.closeButton = None + + self.setCornerWidget(self.rightCornerWidget, Qt.TopRightCorner) + + self.__initMenu() + self.contextMenuEditor = None + self.contextMenuIndex = -1 + + self.setTabContextMenuPolicy(Qt.CustomContextMenu) + self.customTabContextMenuRequested.connect(self.__showContextMenu) + + ericPic = QPixmap( + os.path.join(getConfig('ericPixDir'), 'eric_small.png')) + self.emptyLabel = QLabel() + self.emptyLabel.setPixmap(ericPic) + self.emptyLabel.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) + super(TabWidget, self).addTab( + self.emptyLabel, + UI.PixmapCache.getIcon("empty.png"), "") + + def __initMenu(self): + """ + Private method to initialize the tab context menu. + """ + self.__menu = QMenu(self) + self.leftMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("1leftarrow.png"), + self.tr('Move Left'), self.__contextMenuMoveLeft) + self.rightMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("1rightarrow.png"), + self.tr('Move Right'), self.__contextMenuMoveRight) + self.firstMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("2leftarrow.png"), + self.tr('Move First'), self.__contextMenuMoveFirst) + self.lastMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("2rightarrow.png"), + self.tr('Move Last'), self.__contextMenuMoveLast) + self.__menu.addSeparator() + self.__menu.addAction( + UI.PixmapCache.getIcon("tabClose.png"), + self.tr('Close'), self.__contextMenuClose) + self.closeOthersMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("tabCloseOther.png"), + self.tr("Close Others"), self.__contextMenuCloseOthers) + self.__menu.addAction( + self.tr('Close All'), self.__contextMenuCloseAll) + self.__menu.addSeparator() + self.saveMenuAct = self.__menu.addAction( + UI.PixmapCache.getIcon("fileSave.png"), + self.tr('Save'), self.__contextMenuSave) + self.__menu.addAction( + UI.PixmapCache.getIcon("fileSaveAs.png"), + self.tr('Save As...'), self.__contextMenuSaveAs) + self.__menu.addAction( + UI.PixmapCache.getIcon("fileSaveAll.png"), + self.tr('Save All'), self.__contextMenuSaveAll) + self.__menu.addSeparator() + self.openRejectionsMenuAct = self.__menu.addAction( + self.tr("Open 'rejection' file"), + self.__contextMenuOpenRejections) + self.__menu.addSeparator() + self.__menu.addAction( + UI.PixmapCache.getIcon("print.png"), + self.tr('Print'), self.__contextMenuPrintFile) + self.__menu.addSeparator() + self.copyPathAct = self.__menu.addAction( + self.tr("Copy Path to Clipboard"), + self.__contextMenuCopyPathToClipboard) + + def __showContextMenu(self, coord, index): + """ + Private slot to show the tab context menu. + + @param coord the position of the mouse pointer + @type QPoint + @param index index of the tab the menu is requested for + @type int + """ + if self.editors: + widget = self.widget(index) + if widget is not None: + self.contextMenuEditor = widget.getEditor() + if self.contextMenuEditor: + self.saveMenuAct.setEnabled( + self.contextMenuEditor.isModified()) + fileName = self.contextMenuEditor.getFileName() + self.copyPathAct.setEnabled(bool(fileName)) + if fileName: + rej = "{0}.rej".format(fileName) + self.openRejectionsMenuAct.setEnabled( + os.path.exists(rej)) + else: + self.openRejectionsMenuAct.setEnabled(False) + + self.contextMenuIndex = index + self.leftMenuAct.setEnabled(index > 0) + self.rightMenuAct.setEnabled(index < self.count() - 1) + self.firstMenuAct.setEnabled(index > 0) + self.lastMenuAct.setEnabled(index < self.count() - 1) + + self.closeOthersMenuAct.setEnabled(self.count() > 1) + + coord = self.mapToGlobal(coord) + self.__menu.popup(coord) + + def __showNavigationMenu(self): + """ + Private slot to show the navigation button menu. + """ + self.__navigationMenu.clear() + for index in range(self.count()): + act = self.__navigationMenu.addAction(self.tabIcon(index), + self.tabText(index)) + act.setData(index) + + def __navigationMenuTriggered(self, act): + """ + Private slot called to handle the navigation button menu selection. + + @param act reference to the selected action + @type QAction + """ + index = act.data() + if index is not None: + self.setCurrentIndex(index) + + def showIndicator(self, on): + """ + Public slot to set the indicator on or off. + + @param on flag indicating the state of the indicator + @type bool + """ + if on: + self.indicator.setColor(QColor("green")) + else: + self.indicator.setColor(QColor("red")) + + def addTab(self, assembly, title): + """ + Public method to add a new tab. + + @param assembly editor assembly object to be added + @type QScintilla.EditorAssembly.EditorAssembly + @param title title for the new tab + @type str + """ + editor = assembly.getEditor() + super(TabWidget, self).addTab( + assembly, UI.PixmapCache.getIcon("empty.png"), title) + if self.closeButton: + self.closeButton.setEnabled(True) + else: + self.setTabsClosable(True) + self.navigationButton.setEnabled(True) + + if editor not in self.editors: + self.editors.append(editor) + editor.captionChanged.connect(self.__captionChange) + editor.cursorLineChanged.connect( + lambda lineno: self.__cursorLineChanged(lineno, editor)) + + emptyIndex = self.indexOf(self.emptyLabel) + if emptyIndex > -1: + self.removeTab(emptyIndex) + + def insertWidget(self, index, assembly, title): + """ + Public method to insert a new tab. + + @param index index position for the new tab + @type int + @param assembly editor assembly object to be added + @type QScintilla.EditorAssembly.EditorAssembly + @param title title for the new tab + @type str + @return index of the inserted tab + @rtype int + """ + editor = assembly.getEditor() + newIndex = super(TabWidget, self).insertTab( + index, assembly, + UI.PixmapCache.getIcon("empty.png"), + title) + if self.closeButton: + self.closeButton.setEnabled(True) + else: + self.setTabsClosable(True) + self.navigationButton.setEnabled(True) + + if editor not in self.editors: + self.editors.append(editor) + editor.captionChanged.connect(self.__captionChange) + editor.cursorLineChanged.connect( + lambda lineno: self.__cursorLineChanged(lineno, editor)) + emptyIndex = self.indexOf(self.emptyLabel) + if emptyIndex > -1: + self.removeTab(emptyIndex) + + return newIndex + + def __captionChange(self, cap, editor): + """ + Private slot to handle Caption change signals from the editor. + + Updates the tab text and tooltip text to reflect the new caption + information. + + @param cap Caption for the editor + @type str + @param editor Editor to update the caption for + @type Editor + """ + fn = editor.getFileName() + if fn: + if Preferences.getUI("TabViewManagerFilenameOnly"): + txt = os.path.basename(fn) + else: + txt = e5App().getObject("Project").getRelativePath(fn) + + maxFileNameChars = Preferences.getUI( + "TabViewManagerFilenameLength") + if len(txt) > maxFileNameChars: + txt = "...{0}".format(txt[-maxFileNameChars:]) + if editor.isReadOnly(): + txt = self.tr("{0} (ro)").format(txt) + + assembly = editor.parent() + index = self.indexOf(assembly) + if index > -1: + self.setTabText(index, txt) + self.setTabToolTip(index, fn) + + def __cursorLineChanged(self, lineno, editor): + """ + Private slot to handle a change of the current editor's cursor line. + + @param lineno line number of the editor's cursor (zero based) + @type int + @param editor reference to the editor + @type Editor + """ + if editor and isinstance(editor, QScintilla.Editor.Editor): + fn = editor.getFileName() + if fn: + self.vm.editorLineChanged.emit(fn, lineno + 1) + + def removeWidget(self, widget): + """ + Public method to remove a widget. + + @param widget widget to be removed + @type QWidget + """ + if isinstance(widget, QScintilla.Editor.Editor): + widget.cursorLineChanged.disconnect() + widget.captionChanged.disconnect() + self.editors.remove(widget) + index = self.indexOf(widget.parent()) + else: + index = self.indexOf(widget) + if index > -1: + self.removeTab(index) + + if not self.editors: + super(TabWidget, self).addTab( + self.emptyLabel, UI.PixmapCache.getIcon("empty.png"), "") + self.emptyLabel.show() + if self.closeButton: + self.closeButton.setEnabled(False) + else: + self.setTabsClosable(False) + self.navigationButton.setEnabled(False) + + def __relocateTab(self, sourceId, sourceIndex, targetIndex): + """ + Private method to relocate an editor from another TabWidget. + + @param sourceId id of the TabWidget to get the editor from + @type str + @param sourceIndex index of the tab in the old tab widget + @type int + @param targetIndex index position to place it to + @type int + """ + tw = self.vm.getTabWidgetById(int(sourceId)) + if tw is not None: + # step 1: get data of the tab of the source + toolTip = tw.tabToolTip(sourceIndex) + text = tw.tabText(sourceIndex) + icon = tw.tabIcon(sourceIndex) + whatsThis = tw.tabWhatsThis(sourceIndex) + assembly = tw.widget(sourceIndex) + + # step 2: relocate the tab + tw.removeWidget(assembly.getEditor()) + self.insertWidget(targetIndex, assembly, text) + + # step 3: set the tab data again + self.setTabIcon(targetIndex, icon) + self.setTabToolTip(targetIndex, toolTip) + self.setTabWhatsThis(targetIndex, whatsThis) + + # step 4: set current widget + self.setCurrentIndex(targetIndex) + + def __copyTabOther(self, sourceId, sourceIndex, targetIndex): + """ + Private method to copy an editor from another TabWidget. + + @param sourceId id of the TabWidget to get the editor from + @type str + @param sourceIndex index of the tab in the old tab widget + @type int + @param targetIndex index position to place it to + @type int + """ + tw = self.vm.getTabWidgetById(int(sourceId)) + if tw is not None: + editor = tw.widget(sourceIndex).getEditor() + newEditor, assembly = self.vm.cloneEditor( + editor, editor.getFileType(), editor.getFileName()) + self.vm.insertView(assembly, self, targetIndex, + editor.getFileName(), editor.getNoName()) + + def __copyTab(self, sourceIndex, targetIndex): + """ + Private method to copy an editor. + + @param sourceIndex index of the tab + @type int + @param targetIndex index position to place it to + @type int + """ + editor = self.widget(sourceIndex).getEditor() + newEditor, assembly = self.vm.cloneEditor( + editor, editor.getFileType(), editor.getFileName()) + self.vm.insertView(assembly, self, targetIndex, + editor.getFileName(), editor.getNoName()) + + def currentWidget(self): + """ + Public method to return a reference to the current page. + + @return reference to the current page + @rtype Editor + """ + if not self.editors: + return None + else: + return super(TabWidget, self).currentWidget() + + def setCurrentWidget(self, assembly): + """ + Public method to set the current tab by the given editor assembly. + + @param assembly editor assembly to determine current tab from + @type EditorAssembly.EditorAssembly + """ + super(TabWidget, self).setCurrentWidget(assembly) + + def indexOf(self, widget): + """ + Public method to get the tab index of the given editor. + + @param widget widget to get the index for + @type QLabel or Editor + @return tab index of the editor + @rtype int + """ + if isinstance(widget, QScintilla.Editor.Editor): + widget = widget.parent() + return super(TabWidget, self).indexOf(widget) + + def hasEditor(self, editor): + """ + Public method to check for an editor. + + @param editor editor object to check for + @type Editor + @return flag indicating, whether the editor to be checked belongs + to the list of editors managed by this tab widget. + @rtype bool + """ + return editor in self.editors + + def hasEditors(self): + """ + Public method to test, if any editor is managed. + + @return flag indicating editors are managed + @rtype bool + """ + return len(self.editors) > 0 + + def __contextMenuClose(self): + """ + Private method to close the selected tab. + """ + if self.contextMenuEditor: + self.vm.closeEditorWindow(self.contextMenuEditor) + + def __contextMenuCloseOthers(self): + """ + Private method to close the other tabs. + """ + index = self.contextMenuIndex + for i in list(range(self.count() - 1, index, -1)) + \ + list(range(index - 1, -1, -1)): + editor = self.widget(i).getEditor() + self.vm.closeEditorWindow(editor) + + def __contextMenuCloseAll(self): + """ + Private method to close all tabs. + """ + savedEditors = self.editors[:] + for editor in savedEditors: + self.vm.closeEditorWindow(editor) + + def __contextMenuSave(self): + """ + Private method to save the selected tab. + """ + if self.contextMenuEditor: + self.vm.saveEditorEd(self.contextMenuEditor) + + def __contextMenuSaveAs(self): + """ + Private method to save the selected tab to a new file. + """ + if self.contextMenuEditor: + self.vm.saveAsEditorEd(self.contextMenuEditor) + + def __contextMenuSaveAll(self): + """ + Private method to save all tabs. + """ + self.vm.saveEditorsList(self.editors) + + def __contextMenuOpenRejections(self): + """ + Private slot to open a rejections file associated with the selected + tab. + """ + if self.contextMenuEditor: + fileName = self.contextMenuEditor.getFileName() + if fileName: + rej = "{0}.rej".format(fileName) + if os.path.exists(rej): + self.vm.openSourceFile(rej) + + def __contextMenuPrintFile(self): + """ + Private method to print the selected tab. + """ + if self.contextMenuEditor: + self.vm.printEditor(self.contextMenuEditor) + + def __contextMenuCopyPathToClipboard(self): + """ + Private method to copy the file name of the selected tab to the + clipboard. + """ + if self.contextMenuEditor: + fn = self.contextMenuEditor.getFileName() + if fn: + cb = QApplication.clipboard() + cb.setText(fn) + + def __contextMenuMoveLeft(self): + """ + Private method to move a tab one position to the left. + """ + self.moveTab(self.contextMenuIndex, self.contextMenuIndex - 1) + + def __contextMenuMoveRight(self): + """ + Private method to move a tab one position to the right. + """ + self.moveTab(self.contextMenuIndex, self.contextMenuIndex + 1) + + def __contextMenuMoveFirst(self): + """ + Private method to move a tab to the first position. + """ + self.moveTab(self.contextMenuIndex, 0) + + def __contextMenuMoveLast(self): + """ + Private method to move a tab to the last position. + """ + self.moveTab(self.contextMenuIndex, self.count() - 1) + + def __closeButtonClicked(self): + """ + Private method to handle the press of the close button. + """ + self.vm.closeEditorWindow(self.currentWidget().getEditor()) + + def __closeRequested(self, index): + """ + Private method to handle the press of the individual tab close button. + + @param index index of the tab (integer) + """ + if index >= 0: + self.vm.closeEditorWindow(self.widget(index).getEditor()) + + def mouseDoubleClickEvent(self, event): + """ + Protected method handling double click events. + + @param event reference to the event object (QMouseEvent) + """ + self.vm.newEditor() + + +class Tabview(ViewManager): + """ + Class implementing a tabbed viewmanager class embedded in a splitter. + + @signal changeCaption(str) emitted if a change of the caption is necessary + @signal editorChanged(str) emitted when the current editor has changed + @signal editorChangedEd(Editor) emitted when the current editor has changed + @signal lastEditorClosed() emitted after the last editor window was closed + @signal editorOpened(str) emitted after an editor window was opened + @signal editorOpenedEd(Editor) emitted after an editor window was opened + @signal editorClosed(str) emitted just before an editor window gets closed + @signal editorClosedEd(Editor) emitted just before an editor window gets + closed + @signal editorRenamed(str) emitted after an editor was renamed + @signal editorRenamedEd(Editor) emitted after an editor was renamed + @signal editorSaved(str) emitted after an editor window was saved + @signal editorSavedEd(Editor) emitted after an editor window was saved + @signal checkActions(Editor) emitted when some actions should be checked + for their status + @signal cursorChanged(Editor) emitted after the cursor position of the + active window has changed + @signal breakpointToggled(Editor) emitted when a breakpoint is toggled. + @signal bookmarkToggled(Editor) emitted when a bookmark is toggled. + @signal syntaxerrorToggled(Editor) emitted when a syntax error is toggled. + @signal previewStateChanged(bool) emitted to signal a change in the + preview state + @signal previewStateChanged(bool) emitted to signal a change in the + preview state + @signal astViewerStateChanged(bool) emitted to signal a change in the + AST viewer state + @signal editorLanguageChanged(Editor) emitted to signal a change of an + editors language + @signal editorTextChanged(Editor) emitted to signal a change of an + editor's text + @signal editorLineChanged(str,int) emitted to signal a change of an + editor's current line (line is given one based) + """ + changeCaption = pyqtSignal(str) + editorChanged = pyqtSignal(str) + editorChangedEd = pyqtSignal(Editor) + lastEditorClosed = pyqtSignal() + editorOpened = pyqtSignal(str) + editorOpenedEd = pyqtSignal(Editor) + editorClosed = pyqtSignal(str) + editorClosedEd = pyqtSignal(Editor) + editorRenamed = pyqtSignal(str) + editorRenamedEd = pyqtSignal(Editor) + editorSaved = pyqtSignal(str) + editorSavedEd = pyqtSignal(Editor) + checkActions = pyqtSignal(Editor) + cursorChanged = pyqtSignal(Editor) + breakpointToggled = pyqtSignal(Editor) + bookmarkToggled = pyqtSignal(Editor) + syntaxerrorToggled = pyqtSignal(Editor) + previewStateChanged = pyqtSignal(bool) + astViewerStateChanged = pyqtSignal(bool) + editorLanguageChanged = pyqtSignal(Editor) + editorTextChanged = pyqtSignal(Editor) + editorLineChanged = pyqtSignal(str, int) + + def __init__(self, parent): + """ + Constructor + + @param parent parent widget + @type QWidget + """ + self.tabWidgets = [] + + self.__splitter = QSplitter(parent) + ViewManager.__init__(self) + self.__splitter.setChildrenCollapsible(False) + + tw = TabWidget(self) + self.__splitter.addWidget(tw) + self.tabWidgets.append(tw) + self.currentTabWidget = tw + self.currentTabWidget.showIndicator(True) + tw.currentChanged.connect(self.__currentChanged) + tw.installEventFilter(self) + tw.tabBar().installEventFilter(self) + self.__splitter.setOrientation(Qt.Vertical) + self.__inRemoveView = False + + self.maxFileNameChars = Preferences.getUI( + "TabViewManagerFilenameLength") + self.filenameOnly = Preferences.getUI("TabViewManagerFilenameOnly") + + def mainWidget(self): + """ + Public method to return a reference to the main Widget of a + specific view manager subclass. + + @return reference to the main widget + @rtype QWidget + """ + return self.__splitter + + def canCascade(self): + """ + Public method to signal if cascading of managed windows is available. + + @return flag indicating cascading of windows is available + @rtype bool + """ + return False + + def canTile(self): + """ + Public method to signal if tiling of managed windows is available. + + @return flag indicating tiling of windows is available + @rtype bool + """ + return False + + def canSplit(self): + """ + public method to signal if splitting of the view is available. + + @return flag indicating splitting of the view is available. + @rtype bool + """ + return True + + def tile(self): + """ + Public method to tile the managed windows. + """ + pass + + def cascade(self): + """ + Public method to cascade the managed windows. + """ + pass + + def _removeAllViews(self): + """ + Protected method to remove all views (i.e. windows). + """ + for win in self.editors: + self._removeView(win) + + def _removeView(self, win): + """ + Protected method to remove a view (i.e. window). + + @param win editor window to be removed + @type Editor + """ + self.__inRemoveView = True + for tw in self.tabWidgets: + if tw.hasEditor(win): + tw.removeWidget(win) + break + win.closeIt() + self.__inRemoveView = False + + # if this was the last editor in this view, switch to the next, that + # still has open editors + for i in list(range(self.tabWidgets.index(tw), -1, -1)) + \ + list(range(self.tabWidgets.index(tw) + 1, + len(self.tabWidgets))): + if self.tabWidgets[i].hasEditors(): + self.currentTabWidget.showIndicator(False) + self.currentTabWidget = self.tabWidgets[i] + self.currentTabWidget.showIndicator(True) + self.activeWindow().setFocus() + break + + aw = self.activeWindow() + fn = aw and aw.getFileName() or None + if fn: + self.changeCaption.emit(fn) + self.editorChanged.emit(fn) + self.editorLineChanged.emit(fn, aw.getCursorPosition()[0] + 1) + else: + self.changeCaption.emit("") + self.editorChangedEd.emit(aw) + + def _addView(self, win, fn=None, noName="", addNext=False, indexes=None): + """ + Protected method to add a view (i.e. window). + + @param win editor assembly to be added + @type EditorAssembly + @param fn filename of this editor + @type str + @param noName name to be used for an unnamed editor + @type str + @param addNext flag indicating to add the view next to the current + view + @type bool + @param indexes of the editor, first the split view index, second the + index within the view + @type tuple of two int + """ + editor = win.getEditor() + if not fn: + if not noName: + self.untitledCount += 1 + noName = self.tr("Untitled {0}").format(self.untitledCount) + if addNext: + index = self.currentTabWidget.currentIndex() + 1 + self.currentTabWidget.insertWidget(index, win, noName) + elif indexes: + if indexes[0] < len(self.tabWidgets): + tw = self.tabWidgets[indexes[0]] + else: + tw = self.tabWidgets[-1] + tw.insertWidget(indexes[1], win, noName) + else: + self.currentTabWidget.addTab(win, noName) + editor.setNoName(noName) + else: + if self.filenameOnly: + txt = os.path.basename(fn) + else: + txt = e5App().getObject("Project").getRelativePath(fn) + if len(txt) > self.maxFileNameChars: + txt = "...{0}".format(txt[-self.maxFileNameChars:]) + if not QFileInfo(fn).isWritable(): + txt = self.tr("{0} (ro)").format(txt) + if addNext: + index = self.currentTabWidget.currentIndex() + 1 + self.currentTabWidget.insertWidget(index, win, txt) + elif indexes: + if indexes[0] < len(self.tabWidgets): + tw = self.tabWidgets[indexes[0]] + else: + tw = self.tabWidgets[-1] + tw.insertWidget(indexes[1], win, txt) + else: + self.currentTabWidget.addTab(win, txt) + index = self.currentTabWidget.indexOf(win) + self.currentTabWidget.setTabToolTip(index, fn) + self.currentTabWidget.setCurrentWidget(win) + win.show() + editor.setFocus() + if fn: + self.changeCaption.emit(fn) + self.editorChanged.emit(fn) + self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) + else: + self.changeCaption.emit("") + self.editorChangedEd.emit(editor) + + def insertView(self, win, tabWidget, index, fn=None, noName=""): + """ + Public method to add a view (i.e. window). + + @param win editor assembly to be inserted + @type EditorAssembly + @param tabWidget reference to the tab widget to insert the editor into + @type TabWidget + @param index index position to insert at + @type int + @param fn filename of this editor + @type str + @param noName name to be used for an unnamed editor + @type str + """ + editor = win.getEditor() + if fn is None: + if not noName: + self.untitledCount += 1 + noName = self.tr("Untitled {0}").format(self.untitledCount) + tabWidget.insertWidget(index, win, noName) + editor.setNoName(noName) + else: + if self.filenameOnly: + txt = os.path.basename(fn) + else: + txt = e5App().getObject("Project").getRelativePath(fn) + if len(txt) > self.maxFileNameChars: + txt = "...{0}".format(txt[-self.maxFileNameChars:]) + if not QFileInfo(fn).isWritable(): + txt = self.tr("{0} (ro)").format(txt) + nindex = tabWidget.insertWidget(index, win, txt) + tabWidget.setTabToolTip(nindex, fn) + tabWidget.setCurrentWidget(win) + win.show() + editor.setFocus() + if fn: + self.changeCaption.emit(fn) + self.editorChanged.emit(fn) + self.editorLineChanged.emit(fn, editor.getCursorPosition()[0] + 1) + else: + self.changeCaption.emit("") + self.editorChangedEd.emit(editor) + + self._modificationStatusChanged(editor.isModified(), editor) + self._checkActions(editor) + + def _showView(self, win, fn=None): + """ + Protected method to show a view (i.e. window). + + @param win editor assembly to be shown + @type EditorAssembly + @param fn filename of this editor + @type str + """ + win.show() + editor = win.getEditor() + for tw in self.tabWidgets: + if tw.hasEditor(editor): + tw.setCurrentWidget(win) + self.currentTabWidget.showIndicator(False) + self.currentTabWidget = tw + self.currentTabWidget.showIndicator(True) + break + editor.setFocus() + + def activeWindow(self): + """ + Public method to return the active (i.e. current) window. + + @return reference to the active editor + @rtype Editor + """ + cw = self.currentTabWidget.currentWidget() + if cw: + return cw.getEditor() + else: + return None + + def showWindowMenu(self, windowMenu): + """ + Public method to set up the viewmanager part of the Window menu. + + @param windowMenu reference to the window menu + @type QMenu + """ + pass + + def _initWindowActions(self): + """ + Protected method to define the user interface actions for window + handling. + """ + pass + + def setEditorName(self, editor, newName): + """ + Public method to change the displayed name of the editor. + + @param editor editor window to be changed + @type Editor + @param newName new name to be shown + @type str + """ + if newName: + if self.filenameOnly: + tabName = os.path.basename(newName) + else: + tabName = e5App().getObject("Project").getRelativePath(newName) + if len(tabName) > self.maxFileNameChars: + tabName = "...{0}".format(tabName[-self.maxFileNameChars:]) + index = self.currentTabWidget.indexOf(editor) + self.currentTabWidget.setTabText(index, tabName) + self.currentTabWidget.setTabToolTip(index, newName) + self.changeCaption.emit(newName) + + def _modificationStatusChanged(self, m, editor): + """ + Protected slot to handle the modificationStatusChanged signal. + + @param m flag indicating the modification status + @type bool + @param editor editor window changed + @type Editor + """ + for tw in self.tabWidgets: + if tw.hasEditor(editor): + break + index = tw.indexOf(editor) + keys = [] + if m: + keys.append("fileModified.png") + if editor.hasSyntaxErrors(): + keys.append("syntaxError22.png") + elif editor.hasWarnings(): + keys.append("warning22.png") + if not keys: + keys.append("empty.png") + tw.setTabIcon(index, UI.PixmapCache.getCombinedIcon(keys)) + self._checkActions(editor) + + def _syntaxErrorToggled(self, editor): + """ + Protected slot to handle the syntaxerrorToggled signal. + + @param editor editor that sent the signal + @type Editor + """ + for tw in self.tabWidgets: + if tw.hasEditor(editor): + break + index = tw.indexOf(editor) + keys = [] + if editor.isModified(): + keys.append("fileModified.png") + if editor.hasSyntaxErrors(): + keys.append("syntaxError22.png") + elif editor.hasWarnings(): + keys.append("warning22.png") + if not keys: + keys.append("empty.png") + tw.setTabIcon(index, UI.PixmapCache.getCombinedIcon(keys)) + + ViewManager._syntaxErrorToggled(self, editor) + + def addSplit(self): + """ + Public method used to split the current view. + """ + tw = TabWidget(self) + tw.show() + self.__splitter.addWidget(tw) + self.tabWidgets.append(tw) + self.currentTabWidget.showIndicator(False) + self.currentTabWidget = self.tabWidgets[-1] + self.currentTabWidget.showIndicator(True) + tw.currentChanged.connect(self.__currentChanged) + tw.installEventFilter(self) + tw.tabBar().installEventFilter(self) + if self.__splitter.orientation() == Qt.Horizontal: + size = self.width() + else: + size = self.height() + self.__splitter.setSizes( + [int(size / len(self.tabWidgets))] * len(self.tabWidgets)) + self.splitRemoveAct.setEnabled(True) + self.nextSplitAct.setEnabled(True) + self.prevSplitAct.setEnabled(True) + + @pyqtSlot() + def removeSplit(self, index=-1): + """ + Public method used to remove the current split view or a split view + by index. + + @param index index of the split to be removed (-1 means to + delete the current split) + @type int + @return flag indicating successful deletion + @rtype bool + """ + if len(self.tabWidgets) > 1: + if index == -1: + tw = self.currentTabWidget + else: + if index < len(self.tabWidgets): + tw = self.tabWidgets[index] + else: + tw = self.tabWidgets[-1] + res = True + savedEditors = tw.editors[:] + for editor in savedEditors: + res &= self.closeEditor(editor) + if res: + try: + i = self.tabWidgets.index(tw) + except ValueError: + return True + if i == len(self.tabWidgets) - 1: + i -= 1 + self.tabWidgets.remove(tw) + tw.close() + self.currentTabWidget = self.tabWidgets[i] + for tw in self.tabWidgets: + tw.showIndicator(tw == self.currentTabWidget) + if self.currentTabWidget is not None: + assembly = self.currentTabWidget.currentWidget() + if assembly is not None: + editor = assembly.getEditor() + if editor is not None: + editor.setFocus(Qt.OtherFocusReason) + if len(self.tabWidgets) == 1: + self.splitRemoveAct.setEnabled(False) + self.nextSplitAct.setEnabled(False) + self.prevSplitAct.setEnabled(False) + return True + + return False + + def splitCount(self): + """ + Public method to get the number of splitted views. + + @return number of splitted views + @rtype int + """ + return len(self.tabWidgets) + + def setSplitCount(self, count): + """ + Public method to set the number of split views. + + @param count number of split views + @type int + """ + if count > self.splitCount(): + while self.splitCount() < count: + self.addSplit() + elif count < self.splitCount(): + while self.splitCount() > count: + # use an arbitrarily large index to remove the last one + self.removeSplit(index=100) + + def getSplitOrientation(self): + """ + Public method to get the orientation of the split view. + + @return orientation of the split + @rtype Qt.Horizontal or Qt.Vertical + """ + return self.__splitter.orientation() + + def setSplitOrientation(self, orientation): + """ + Public method used to set the orientation of the split view. + + @param orientation orientation of the split + @type Qt.Horizontal or Qt.Vertical + """ + self.__splitter.setOrientation(orientation) + + def nextSplit(self): + """ + Public slot used to move to the next split. + """ + aw = self.activeWindow() + _hasFocus = aw and aw.hasFocus() + ind = self.tabWidgets.index(self.currentTabWidget) + 1 + if ind == len(self.tabWidgets): + ind = 0 + + self.currentTabWidget.showIndicator(False) + self.currentTabWidget = self.tabWidgets[ind] + self.currentTabWidget.showIndicator(True) + if _hasFocus: + aw = self.activeWindow() + if aw: + aw.setFocus() + + def prevSplit(self): + """ + Public slot used to move to the previous split. + """ + aw = self.activeWindow() + _hasFocus = aw and aw.hasFocus() + ind = self.tabWidgets.index(self.currentTabWidget) - 1 + if ind == -1: + ind = len(self.tabWidgets) - 1 + + self.currentTabWidget.showIndicator(False) + self.currentTabWidget = self.tabWidgets[ind] + self.currentTabWidget.showIndicator(True) + if _hasFocus: + aw = self.activeWindow() + if aw: + aw.setFocus() + + def __currentChanged(self, index): + """ + Private slot to handle the currentChanged signal. + + @param index index of the current tab + @type int + """ + if index == -1 or not self.editors: + return + + editor = self.activeWindow() + if editor is None: + return + + self._checkActions(editor) + editor.setFocus() + fn = editor.getFileName() + if fn: + self.changeCaption.emit(fn) + if not self.__inRemoveView: + self.editorChanged.emit(fn) + self.editorLineChanged.emit( + fn, editor.getCursorPosition()[0] + 1) + else: + self.changeCaption.emit("") + self.editorChangedEd.emit(editor) + + def eventFilter(self, watched, event): + """ + Public method called to filter the event queue. + + @param watched the QObject being watched + @type QObject + @param event the event that occurred + @type QEvent + @return always False + @rtype bool + """ + if event.type() == QEvent.MouseButtonPress and \ + not event.button() == Qt.RightButton: + switched = True + self.currentTabWidget.showIndicator(False) + if isinstance(watched, E5TabWidget): + switched = watched is not self.currentTabWidget + self.currentTabWidget = watched + elif isinstance(watched, QTabBar): + switched = watched.parent() is not self.currentTabWidget + self.currentTabWidget = watched.parent() + if switched: + index = self.currentTabWidget.selectTab(event.pos()) + switched = self.currentTabWidget.widget(index) is \ + self.activeWindow() + elif isinstance(watched, QScintilla.Editor.Editor): + for tw in self.tabWidgets: + if tw.hasEditor(watched): + switched = tw is not self.currentTabWidget + self.currentTabWidget = tw + break + self.currentTabWidget.showIndicator(True) + + aw = self.activeWindow() + if aw is not None: + self._checkActions(aw) + aw.setFocus() + fn = aw.getFileName() + if fn: + self.changeCaption.emit(fn) + if switched: + self.editorChanged.emit(fn) + self.editorLineChanged.emit( + fn, aw.getCursorPosition()[0] + 1) + else: + self.changeCaption.emit("") + self.editorChangedEd.emit(aw) + + return False + + def preferencesChanged(self): + """ + Public slot to handle the preferencesChanged signal. + """ + ViewManager.preferencesChanged(self) + + self.maxFileNameChars = Preferences.getUI( + "TabViewManagerFilenameLength") + self.filenameOnly = Preferences.getUI("TabViewManagerFilenameOnly") + + for tabWidget in self.tabWidgets: + for index in range(tabWidget.count()): + editor = tabWidget.widget(index) + if isinstance(editor, QScintilla.Editor.Editor): + fn = editor.getFileName() + if fn: + if self.filenameOnly: + txt = os.path.basename(fn) + else: + txt = e5App().getObject("Project")\ + .getRelativePath(fn) + if len(txt) > self.maxFileNameChars: + txt = "...{0}".format(txt[-self.maxFileNameChars:]) + if not QFileInfo(fn).isWritable(): + txt = self.tr("{0} (ro)").format(txt) + tabWidget.setTabText(index, txt) + + def getTabWidgetById(self, id_): + """ + Public method to get a reference to a tab widget knowing its ID. + + @param id_ id of the tab widget + @type int + @return reference to the tab widget + @rtype TabWidget + """ + for tw in self.tabWidgets: + if id(tw) == id_: + return tw + return None + + def getOpenEditorsForSession(self): + """ + Public method to get a lists of all open editors. + + The returned list contains one list per split view. If the view manager + cannot split the view, only one list of editors is returned. + + @return list of list of editor references + @rtype list of list of Editor + """ + editorLists = [] + for tabWidget in self.tabWidgets: + editors = [] + for index in range(tabWidget.count()): + widget = tabWidget.widget(index) + if isinstance(widget, EditorAssembly): + editor = widget.getEditor() + editors.append(editor) + editorLists.append(editors) + return editorLists