def createIconFromSvg(self, svg, color=None, colorsToBeReplaced=None): """ Creates a QIcon given an SVG string. Optionally replaces the colors in colorsToBeReplaced by color. :param svg: string containing Scalable Vector Graphics XML :param color: '#RRGGBB' string (e.g. '#FF0000' for red) :param colorsToBeReplaced: optional list of colors to be replaced by color If None, it will be set to the fill colors of the snip-icon libary :return: QtGui.QIcon """ if colorsToBeReplaced is None: colorsToBeReplaced = self.colorsToBeReplaced if color: for oldColor in colorsToBeReplaced: svg = svg.replace(oldColor, color) # From http://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt qByteArray = QtCore.QByteArray() qByteArray.append(svg) svgRenderer = QtSvg.QSvgRenderer(qByteArray) icon = QtGui.QIcon() for size in self.renderSizes: pixMap = QtGui.QPixmap(QtCore.QSize(size, size)) pixMap.fill(Qt.transparent) pixPainter = QtGui.QPainter(pixMap) pixPainter.setRenderHint(QtGui.QPainter.TextAntialiasing, True) pixPainter.setRenderHint(QtGui.QPainter.Antialiasing, True) svgRenderer.render(pixPainter) pixPainter.end() icon.addPixmap(pixMap) return icon
def __init__(self, settingsFile=None, setExceptHook=True): """ Constructor :param settingsFile: Config file from which the persistent settings are loaded. :param setExceptHook: Sets the global sys.except hook so that Qt shows a dialog box when an exception is raised. In debugging mode, the program will just quit in case of an exception. This is standard Python behavior but PyQt and PySide swallow exceptions by default (only a log message is displayed). The practice of swallowing exceptions fosters bad programming IHMO as it is easy to miss errors. I strongly recommend that you set the setExceptHook to True. """ super(ArgosApplication, self).__init__() if not settingsFile: settingsFile = ArgosApplication.defaultSettingsFile() logger.debug("No config file specified. Using default: {}".format( settingsFile)) self._settingsFile = ArgosApplication.userConfirmedSettingsFile( settingsFile, createWithoutConfirm=ArgosApplication.defaultSettingsFile()) if setExceptHook: logger.debug("Setting sys.excepthook to Argos exception handling") sys.excepthook = handleException QtCore.qInstallMessageHandler(self.handleQtLogMessages) if DEBUGGING: self.qApplication.focusChanged.connect( self.focusChanged) # for debugging self._repo = RepoTreeModel() self._rtiRegistry = globalRtiRegistry() self._inspectorRegistry = InspectorRegistry() self._mainWindows = [] self._settingsSaved = False # boolean to prevent saving settings twice self._recentFiles = [ ] # list of recently opened files ([timeStampe, fileName] per file). self._maxRecentFiles = 10 # Maximum size of recent file #self.qApplication.lastWindowClosed.connect(self.quit) self.qApplication.aboutToQuit.connect(self.aboutToQuitHandler) # Activate-actions for all windows self.windowActionGroup = QtWidgets.QActionGroup(self) self.windowActionGroup.setExclusive(True) # Call setup when the event loop starts. QtCore.QTimer.singleShot(0, self.setup)
def insertItem(self, childItem, position=None, parentIndex=None): """ Inserts a childItem before row 'position' under the parent index. If position is None the child will be appended as the last child of the parent. Returns the index of the new inserted child. """ if parentIndex is None: parentIndex = QtCore.QModelIndex() parentItem = self.getItem(parentIndex, altItem=self.invisibleRootItem) nChildren = parentItem.nChildren() if position is None: position = nChildren assert 0 <= position <= nChildren, \ "position should be 0 < {} <= {}".format(position, nChildren) self.beginInsertRows(parentIndex, position, position) try: parentItem.insertChild(childItem, position) finally: self.endInsertRows() childIndex = self.index(position, 0, parentIndex) assert childIndex.isValid( ), "Sanity check failed: childIndex not valid" return childIndex
def indexTupleFromItem(self, treeItem): # TODO: move to BaseTreeItem? """ Return (first column model index, last column model index) tuple for a configTreeItem """ if not treeItem: return (QtCore.QModelIndex(), QtCore.QModelIndex()) if not treeItem.parentItem: # TODO: only necessary because of childNumber? return (QtCore.QModelIndex(), QtCore.QModelIndex()) # Is there a bug in Qt in QStandardItemModel::indexFromItem? # It passes the parent in createIndex. TODO: investigate row = treeItem.childNumber() return (self.createIndex(row, 0, treeItem), self.createIndex(row, self.columnCount() - 1, treeItem))
def deleteSettings(self, groupName=None): """ Deletes registry items from the persistent store. """ groupName = groupName if groupName else self.settingsGroupName settings = QtCore.QSettings() logger.info("Deleting {} from: {}".format(groupName, settings.fileName())) removeSettingsGroup(groupName)
def __init__(self, registry, onlyShowImported=False, parent=None): """ Constructor """ super(OpenInspectorDialog, self).__init__(parent=parent) self._registry = registry self.setWindowTitle('Open Inspector') self.setModal(True) layout = QtWidgets.QVBoxLayout(self) attrNames = ['name', 'library', 'nDims'] headerSizes = [250, 250, None] self.inspectorTab = RegistryTab(registry, attrNames=attrNames, headerSizes=headerSizes, onlyShowImported=onlyShowImported) self.inspectorTab.tableView.sortByColumn( 1, Qt.AscendingOrder) # sort by library layout.addWidget(self.inspectorTab) # Buttons buttonBox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) layout.addWidget(buttonBox) # Double clicking is equivalent to selecting it and clicking Ok. self.inspectorTab.tableView.doubleClicked.connect(self.accept) self.resize(QtCore.QSize(800, 600))
def cloneWindow(self): """ Opens a new window with the same inspector as the current window. """ # Save current window settings. settings = QtCore.QSettings() settings.beginGroup( self.argosApplication.windowGroupName(self.windowNumber)) try: self.saveProfile(settings) # Create new window with the freshly baked settings of the current window. name = self.inspectorRegItem.fullName newWindow = self.argosApplication.addNewMainWindow( settings=settings, inspectorFullName=name) finally: settings.endGroup() # Select the current item in the new window. currentItem, _currentIndex = self.repoWidget.repoTreeView.getCurrentItem( ) if currentItem: newWindow.trySelectRtiByPath(currentItem.nodePath) # Move the new window 24 pixels to the bottom right and raise it to the front. newGeomRect = newWindow.geometry() logger.debug("newGeomRect: x={}".format(newGeomRect.x())) newGeomRect.moveTo(newGeomRect.x() + 24, newGeomRect.y() + 24) newWindow.setGeometry(newGeomRect) logger.debug("newGeomRect: x={}".format(newGeomRect.x())) newWindow.raise_()
def saveProfile(self, settings=None): """ Writes the view settings to the persistent store """ self._updateNonDefaultsForInspector(self.inspectorRegItem, self.inspector) if settings is None: settings = QtCore.QSettings() logger.debug("Writing settings to: {}".format(settings.group())) settings.beginGroup('cfg_inspectors') try: for key, nonDefaults in self._inspectorsNonDefaults.items(): if nonDefaults: settings.setValue(key, ctiDumps(nonDefaults)) logger.debug("Writing non defaults for {}: {}".format( key, nonDefaults)) finally: settings.endGroup() self.configWidget.configTreeView.saveProfile( "config_tree/header_state", settings) self.repoWidget.repoTreeView.saveProfile("repo_tree/header_state", settings) settings.setValue("geometry", self.saveGeometry()) settings.setValue("state", self.saveState()) identifier = self.inspectorRegItem.identifier if self.inspectorRegItem else '' settings.setValue("inspector", identifier)
def readViewSettings(self, settings=None): # TODO: rename to readProfile? """ Reads the persistent program settings :param settings: optional QSettings object which can have a group already opened. :returns: True if the header state was restored, otherwise returns False """ if settings is None: settings = QtCore.QSettings() logger.debug("Reading settings from: {}".format(settings.group())) self.restoreGeometry(settings.value("geometry")) self.restoreState(settings.value("state")) self.repoWidget.repoTreeView.readViewSettings('repo_tree/header_state', settings) self.configWidget.configTreeView.readViewSettings( 'config_tree/header_state', settings) #self._configTreeModel.readModelSettings('config_model', settings) settings.beginGroup('cfg_inspectors') try: for key in settings.childKeys(): json = settings.value(key) self._inspectorsNonDefaults[key] = ctiLoads(json) finally: settings.endGroup() identifier = settings.value("inspector", None) try: if identifier: self.setInspectorById(identifier) except KeyError as ex: logger.warn("No inspector with ID {!r}.: {}".format( identifier, ex))
def setUp(self): self.app = getQApplicationInstance() self.groupName = '__test__' self.qs = QtCore.QSettings() self.qs.remove(self.groupName) # start with clean slate self.qs.beginGroup(self.groupName)
def hasChildren(self, parentIndex=QtCore.QModelIndex()): """ Returns true if parent has any children; otherwise returns false. Use rowCount() on the parent to find out the number of children. """ parentItem = self.getItem(parentIndex, altItem=self.invisibleRootTreeItem) return parentItem.hasChildren()
def mouseClickEvent(self, mouseClickEvent): """ Handles (PyQtGraph) mouse click events. Overrides the middle mouse click to reset using the settings in the config tree. Opens the context menu if a right mouse button was clicked. (We can't simply use setContextMenuPolicy(Qt.ActionsContextMenu because the PlotItem class does not derive from QWidget). :param mouseClickEvent: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent """ if mouseClickEvent.button() in self.resetRangeMouseButtons: self.emitResetColorScaleSignal() mouseClickEvent.accept() elif mouseClickEvent.button() == QtCore.Qt.RightButton: contextMenu = QtWidgets.QMenu() for action in self.actions(): contextMenu.addAction(action) screenPos = mouseClickEvent.screenPos( ) # Screenpos is a QPointF, convert to QPoint. screenX = round(screenPos.x()) screenY = round(screenPos.y()) contextMenu.exec_(QtCore.QPoint(screenX, screenY)) else: super(ArgosColorLegendItem, self).mouseClickEvent(mouseClickEvent)
def deleteProfile(self, profile): """ Removes a profile from the persistent settings """ profGroupName = self.profileGroupName(profile) logger.debug("Resetting profile settings: {}".format(profGroupName)) settings = QtCore.QSettings() settings.remove(profGroupName)
def __init__(self, tableModel=None, parent=None): """ Constructor. """ super(TableEditWidget, self).__init__(parent=parent) self.setFocusPolicy(Qt.NoFocus) self.mainLayout = QtWidgets.QHBoxLayout() self.mainLayout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.mainLayout) self.tableView = BaseTableView(tableModel) self.mainLayout.addWidget(self.tableView) buttonLayout = QtWidgets.QVBoxLayout() self.mainLayout.addLayout(buttonLayout) iconDir = icons_directory() iconSize = QtCore.QSize(20, 20) self.addButton = QtWidgets.QPushButton() self.addButton.setToolTip("Add new row.") self.addButton.setIcon( QtGui.QIcon(os.path.join(iconDir, 'plus-sign-l.svg'))) self.addButton.setIconSize(iconSize) self.addButton.clicked.connect(self.addRow) buttonLayout.addWidget(self.addButton) self.removeButton = QtWidgets.QPushButton() self.removeButton.setToolTip("Remove row.") self.removeButton.setIcon( QtGui.QIcon(os.path.join(iconDir, 'minus-sign-l.svg'))) self.removeButton.setIconSize(iconSize) self.removeButton.clicked.connect(self.removeRow) buttonLayout.addWidget(self.removeButton) buttonLayout.addSpacing(25) self.moveUpButton = QtWidgets.QPushButton() self.moveUpButton.setToolTip("Move row up") self.moveUpButton.setIcon( QtGui.QIcon(os.path.join(iconDir, 'circle-arrow-up-l.svg'))) self.moveUpButton.setIconSize(iconSize) self.moveUpButton.clicked.connect(lambda: self.moveRow(-1)) buttonLayout.addWidget(self.moveUpButton) self.moveDownButton = QtWidgets.QPushButton() self.moveDownButton.setToolTip("Move row down") self.moveDownButton.setIcon( QtGui.QIcon(os.path.join(iconDir, 'circle-arrow-down-l.svg'))) self.moveDownButton.setIconSize(iconSize) self.moveDownButton.clicked.connect(lambda: self.moveRow(+1)) buttonLayout.addWidget(self.moveDownButton) buttonLayout.addStretch() self.tableView.selectionModel().currentChanged.connect( self.onCurrentChanged) self.tableView.setFocus(Qt.NoFocusReason) self.updateWidgets()
def rowCount(self, parentIndex=QtCore.QModelIndex()): """ Returns the number of rows under the given parent. When the parent is valid it means that rowCount is returning the number of children of parent. Note: When implementing a table based model, rowCount() should return 0 when the parent is valid. """ parentItem = self.getItem(parentIndex, altItem=self.invisibleRootItem) return parentItem.nChildren()
def findItemAndIndexPath(self, path, startIndex=None): """ Searches all the model recursively (starting at startIndex) for an item where item.nodePath == path. Returns list of (item, itemIndex) tuples from the start index to that node. Raises IndexError if the item cannot be found. If startIndex is None, or path starts with a slash, searching begins at the (invisible) root item. """ def _getIndexAndItemByName(nodeName, parentItem, parentIndex): """ Searches the parent for a direct child having the nodeName. Returns (item, itemIndex) tuple. Raises IndexError if the item cannot be found. """ if self.canFetchMore(parentIndex): self.fetchMore(parentIndex) for rowNr, childItem in enumerate(parentItem.childItems): if childItem.nodeName == nodeName: childIndex = self.index(rowNr, 0, parentIndex=parentIndex) return (childItem, childIndex) raise IndexError("Item not found: {!r}".format(path)) def _auxGetByPath(parts, item, index): "Aux function that does the actual recursive search" #logger.debug("_auxGetByPath item={}, parts={}".format(item, parts)) if len(parts) == 0: return [(item, index)] head, tail = parts[0], parts[1:] if head == '': # Two consecutive slashes. Just go one level deeper. return _auxGetByPath(tail, item, index) else: childItem, childIndex = _getIndexAndItemByName( head, item, index) return [(item, index)] + _auxGetByPath(tail, childItem, childIndex) # The actual body of findItemAndIndexPath starts here check_is_a_string(path) if not path: raise IndexError("Item not found: {!r}".format(path)) if startIndex is None or path.startswith('/'): startIndex = QtCore.QModelIndex() startItem = self.invisibleRootItem else: startItem = self.getItem(startIndex, None) if not startItem: raise IndexError( "Item not found: {!r}. No start item!".format(path)) return _auxGetByPath(path.split('/'), startItem, startIndex)
def saveProfile(self, key, settings=None): """ Writes the view settings to the persistent store :param key: key where the setting will be read from :param settings: optional QSettings object which can have a group already opened. """ #logger.debug("Writing view settings for: {}".format(key)) if settings is None: settings = QtCore.QSettings() settings.setValue(key, self.horizontalHeader().saveState())
def insertItem(self, item, row): """ Insert an item in the store at a certain row. """ check_class(item, self.store.ITEM_CLASS) logger.info("Inserting {!r} at row {}".format(item, row, self)) self.beginInsertRows(QtCore.QModelIndex(), row, row) try: self.store.items.insert(row, item) finally: self.endInsertRows()
def popItemAtRow(self, row): """ Removes a store item from the store. Returns the item """ self.beginRemoveRows(QtCore.QModelIndex(), row, row) try: item = self.store.items[row] del self.store.items[row] return item finally: self.endRemoveRows()
def indexFromItem(self, regItem, col=0): """ Gets the index (with column=0) for the row that contains the regItem If col is negative, it is counted from the end """ if col < 0: col = len(self.attrNames) - col try: row = self.registry.items.index(regItem) except ValueError: return QtCore.QModelIndex() else: return self.index(row, col)
def __obsolete__saveProfile(self, key, settings=None): """ Writes the view settings to the persistent store :param key: key where the setting will be read from :param settings: optional QSettings object which can have a group already opened. """ logger.debug("Writing model settings for: {}".format(key)) if settings is None: settings = QtCore.QSettings() values = self.invisibleRootItem.getNonDefaultsDict() values_json = ctiDumps(values) settings.setValue(key, values_json)
def parent(self, index): """ Returns the parent of the model item with the given index. If the item has no parent, an invalid QModelIndex is returned. A common convention used in models that expose tree data structures is that only items in the first column have children. For that case, when reimplementing this function in a subclass the column of the returned QModelIndex would be 0. (This is done here.) When reimplementing this function in a subclass, be careful to avoid calling QModelIndex member functions, such as QModelIndex.parent(), since indexes belonging to your model will simply call your implementation, leading to infinite recursion. """ if not index.isValid(): return QtCore.QModelIndex() childItem = self.getItem(index, altItem=self.invisibleRootItem) parentItem = childItem.parentItem if parentItem == self.invisibleRootItem: return QtCore.QModelIndex() return self.createIndex(parentItem.childNumber(), 0, parentItem)
def index(self, row, column, parentIndex=QtCore.QModelIndex()): """ Returns the index of the item in the model specified by the given row, column and parent index. Since each item contains information for an entire row of data, we create a model index to uniquely identify it by calling createIndex() it with the row and column numbers and a pointer to the item. (In the data() function, we will use the item pointer and column number to access the data associated with the model index; in this model, the row number is not needed to identify data.) When reimplementing this function in a subclass, call createIndex() to generate model indexes that other components can use to refer to items in your model. """ # logger.debug(" called index({}, {}, {}) {}" # .format(parentIndex.row(), parentIndex.column(), parentIndex.isValid(), # parentIndex.isValid() and parentIndex.column() != 0)) parentItem = self.getItem(parentIndex, altItem=self.invisibleRootTreeItem) #logger.debug(" Getting row {} from parentItem: {}".format(row, parentItem)) if not (0 <= row < parentItem.nChildren()): # Can happen when deleting the last child. #logger.warning("Index row {} invalid for parent item: {}".format(row, parentItem)) return QtCore.QModelIndex() if not (0 <= column < self.columnCount()): #logger.warning("Index column {} invalid for parent item: {}".format(column, parentItem)) return QtCore.QModelIndex() childItem = parentItem.child(row) if childItem: return self.createIndex(row, column, childItem) else: logger.warning( "No child item found at row {} for parent item: {}".format( row, parentItem)) return QtCore.QModelIndex()
def saveSettings(self, groupName=None): """ Writes the registry items into the persistent settings store. """ groupName = groupName if groupName else self.settingsGroupName settings = QtCore.QSettings() logger.info("Saving {} to: {}".format(groupName, settings.fileName())) settings.remove(groupName) # start with a clean slate settings.beginGroup(groupName) try: for itemNr, item in enumerate(self.items): key = "item-{:03d}".format(itemNr) value = repr(item.asDict()) settings.setValue(key, value) finally: settings.endGroup()
def loadSettings(self, groupName=None): """ Reads the registry items from the persistent settings store. """ groupName = groupName if groupName else self.settingsGroupName settings = QtCore.QSettings() logger.info("Reading {!r} from: {}".format(groupName, settings.fileName())) settings.beginGroup(groupName) self.clear() try: for key in settings.childKeys(): if key.startswith('item'): dct = ast.literal_eval(settings.value(key)) regItem = self._itemClass.createFromDict(dct) self.registerItem(regItem) finally: settings.endGroup()
def mouseClickEvent(self, mouseClickEvent): """ Handles (PyQtGraph) mouse click events. Opens the context menu if a right mouse button was clicked. (We can't simply use setContextMenuPolicy(Qt.ActionsContextMenu because the PlotItem class does not derive from QWidget). :param mouseClickEvent: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent """ if mouseClickEvent.button() == QtCore.Qt.RightButton: contextMenu = QtWidgets.QMenu() for action in self.actions(): contextMenu.addAction(action) screenPos = mouseClickEvent.screenPos() # Screenpos is a QPointF, convert to QPoint. screenX = round(screenPos.x()) screenY = round(screenPos.y()) contextMenu.exec_(QtCore.QPoint(screenX, screenY))
def expandBranch(self, index=None, expanded=True): """ Expands or collapses the node at the index and all it's descendants. If expanded is True the nodes will be expanded, if False they will be collapsed. If parentIndex is None, the invisible root will be used (i.e. the complete forest will be expanded). """ treeModel = self.model() if index is None: index = QtCore.QModelIndex() if index.isValid(): self.setExpanded(index, expanded) for rowNr in range(treeModel.rowCount(index)): childIndex = treeModel.index(rowNr, 0, parentIndex=index) self.expandBranch(index=childIndex, expanded=expanded)
def __init__(self, cti, delegate, parent=None): """ See the AbstractCtiEditor for more info on the parameters """ super(ColorCtiEditor, self).__init__(cti, delegate, parent=parent) lineEditor = QtWidgets.QLineEdit(parent) regExp = QtCore.QRegExp(r'#?[0-9A-F]{6}', Qt.CaseInsensitive) validator = QtGui.QRegExpValidator(regExp, parent=lineEditor) lineEditor.setValidator(validator) self.lineEditor = self.addSubEditor(lineEditor, isFocusProxy=True) pickButton = QtWidgets.QToolButton() pickButton.setText("...") pickButton.setToolTip("Open color dialog.") pickButton.setFocusPolicy(Qt.NoFocus) pickButton.clicked.connect(self.openColorDialog) self.pickButton = self.addSubEditor(pickButton)
def loadOrInitSettings(self, groupName=None): """ Reads the registry items from the persistent settings store, falls back on the default plugins if there are no settings in the store for this registry. """ groupName = groupName if groupName else self.settingsGroupName settings = QtCore.QSettings() #for key in sorted(settings.allKeys()): # print(key) if containsSettingsGroup(groupName, settings): self.loadSettings(groupName) else: logger.info("Group {!r} not found, falling back on default settings".format(groupName)) for item in self.getDefaultItems(): self.registerItem(item) self.saveSettings(groupName) assert containsSettingsGroup(groupName, settings), \ "Sanity check failed. {} not found".format(groupName)
def readViewSettings(self, key, settings=None): """ Reads the persistent program settings :param key: key where the setting will be read from :param settings: optional QSettings object which can have a group already opened. :returns: True if the header state was restored, otherwise returns False """ #logger.debug("Reading view settings for: {}".format(key)) if settings is None: settings = QtCore.QSettings() horizontal_header = self.horizontalHeader() header_restored = horizontal_header.restoreState(settings.value(key)) # update actions for col, action in enumerate(horizontal_header.actions()): is_checked = not horizontal_header.isSectionHidden(col) action.setChecked(is_checked) return header_restored