def on_tree_file_doubleClicked(self, index: QModelIndex): if index.parent().data() == 'table': sql = f'USE {index.parent().parent().data()};SELECT * FROM {index.data()};' self.run_sql(sql) elif index.parent().data() == 'schema': sql = f'USE {index.parent().parent().data()};DESC {index.data()};' self.run_sql(sql)
def setData(self, idx: QModelIndex, value, role: int = Qt.EditRole): if not idx.isValid() or idx.column() != 1: return False if role == Qt.EditRole: if Id.depth(idx) == Id.Depth.D1 and idx.parent().row( ) == Row.ZEditOptions and isBoolOption(idx.row()): setattr(self.mergeFile, MERGE_OPTIONS[idx.row()][0], value is True) self.dataChanged.emit(idx, idx, [role]) return True if Id.depth(idx) == Id.Depth.D0: if idx.row() == Row.ModName: self.mergeFile.name = value if value is not None else "" self.dataChanged.emit(idx, idx, [role]) return True elif idx.row() == Row.PluginName: self.mergeFile.filename = value if value is not None else "" self.dataChanged.emit(idx, idx, [role]) return True elif Id.depth(idx) == Id.Depth.D1 and idx.parent().row( ) == Row.ZEditOptions: if idx.row() == OptionRow.Method: if value in MergeFile.METHOD_VALUES: self.mergeFile.method = value self.dataChanged.emit(idx, idx, [role]) return True elif idx.row() == OptionRow.ArchiveAction: if value in MergeFile.ARCHIVEACTION_VALUES: self.mergeFile.archiveAction = value self.dataChanged.emit(idx, idx, [role]) return True return False
def data(self, index: QtCore.QModelIndex, role: Qt = Qt.DisplayRole) -> typing.Any: result = None invalid_index = QtCore.QModelIndex() if index.isValid(): node = index.internalPointer() if index.parent() == invalid_index: check_head: ChecklistItemHead = node.ref if role == Qt.DisplayRole: if index.column() == 0: result = check_head.name elif index.column() == 1: pass else: raise RuntimeError(f'Invalid column: {index.column()}') elif role == Qt.CheckStateRole and index.column() == 1: result = check_head.validated elif role == Qt.BackgroundRole and index.column() == 1 and check_head.validated == Qt.Checked: result = QtGui.QColor(Qt.green) else: # it is a checklist property check_head: ChecklistItemHead = index.parent().internalPointer().ref check_property: ChecklistItemProperty = node.ref if role == Qt.DisplayRole: if index.column() == 0: result = check_property.name elif index.column() == 1: if index.row() == ChecklistItemPropertyColumn.AUTOMATION.value: if check_head.automation is None: result = 'Not enabled' else: pass else: result = check_property.value else: raise RuntimeError(f'Invalid column: {index.column()}') return result
def data(self, idx: QModelIndex, role: int = Qt.DisplayRole): depth = Id.depth(idx) if depth == Id.Depth.Invalid: return if depth == Id.Depth.D0: if role == Role.Data: return self.__merges[idx.row()] if role == Qt.DisplayRole: if idx.column() == Column.Name: if idx.row() == 0: return self.tr("-- New Merge --") return self.__merges[idx.row()].name elif idx.column() == Column.PluginCount: return self.rowCount(idx) elif role == Qt.ForegroundRole: if idx.column() == Column.PluginCount: return QColor(Qt.lightGray).darker() elif role == Qt.FontRole: if idx.row() == 0: font = QFont() font.setBold(True) return font elif role == Qt.DecorationRole: if idx.column() == Column.Active and idx.row() > 0: if not self.__merges[idx.row()].modIsActive: return QIcon(Icon.INACTIVE) if depth == Id.Depth.D1 and idx.column() == Column.Name: if role == Role.Data: return self.__merges[idx.parent().row()] if role == Qt.DisplayRole: return self.__merges[idx.parent().row()].plugins[idx.row()].filename if role == Qt.ForegroundRole: return QColor(Qt.lightGray).darker()
def _itemClicked(self, idx: QModelIndex): """ If the user clicks on the plugin, we choose its parent Merge. The view emits a selectionChanged signal. """ if idx.parent().isValid(): self.ui.mergeView.selectionModel().select( idx.parent(), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
def onViewDoubleClicked(self, idx: QModelIndex): idx = self.infoModel().mapToSource(idx) if Id.depth(idx) == Id.Depth.D2: if idx.parent().row() == Row.MergedBy or idx.parent().row() == Row.MergedPlugins: name = self.infoModel().sourceModel().data(idx.siblingAtColumn(1)) if name: self.doubleClicked.emit(name)
def _display_data(self, idx: QModelIndex): depth = Id.depth(idx) if depth == Id.Depth.D0: if idx.column() == Column.Property: if idx.row() == Row.ModName: return self.tr("Mod Name: ") if idx.row() == Row.PluginName: return self.tr("Plugin Name: ") if idx.row() == Row.WhenBuilt: return self.tr("Built On: ") if idx.row() == Row.ZEditOptions: return self.tr("zMerge Options: ") if idx.row() == Row.LoadOrder: return self.tr("Load Order: ") if idx.row() == Row.MergedPlugins: return self.tr("Merged Plugins: ") elif idx.column() == Column.Value: if idx.row() == Row.ModName: return self.mergeFile.name if idx.row() == Row.PluginName: return self.mergeFile.filename if idx.row() == Row.WhenBuilt: return self.mergeFile.dateBuilt if idx.row() == Row.ZEditOptions: return self.tr("({})").format(len(MERGE_OPTIONS)) if idx.row() == Row.LoadOrder: return self.tr("({})").format(len( self.mergeFile.loadOrder)) if idx.row() == Row.MergedPlugins: return self.tr("({})").format(len(self.mergeFile.plugins)) elif depth == Id.Depth.D1: parentRow = idx.parent().row() if parentRow == Row.LoadOrder: if idx.column() == 0: return "{}".format(idx.row() + 1) elif idx.column() == 1: return self.mergeFile.loadOrder[idx.row()] elif parentRow == Row.MergedPlugins: if idx.column() == 0: return "{}".format(idx.row() + 1) elif idx.column() == 1: return self.mergeFile.plugins[idx.row()].filename elif idx.column() == 2: return self.mergeFile.plugins[idx.row()].dataFolder if idx.parent().row() == Row.ZEditOptions: if idx.column() == 0: return MERGE_OPTIONS[idx.row()][1] elif idx.column() == 1: value = getattr(self.mergeFile, MERGE_OPTIONS[idx.row()][0], None) if isinstance(value, bool): return "true" if value else "false" return value
def _display_data(self, idx: QModelIndex): depth = Id.depth(idx) if depth == Id.Depth.D0: if idx.column() == Column.Property: return self.tr("Plugin Name: ") if idx.column() == Column.Value: return self.data(idx, Role.Data).pluginName if depth == Id.Depth.D1: if idx.column() == Column.Property: if idx.row() == Row.MergedBy: return self.tr("Merged By: ") if idx.row() == Row.MergedPlugins: return self.tr("Merged Plugins: ") if idx.row() == Row.WhenBuilt: return self.tr("Built On: ") if idx.row() == Row.ZEditOptions: return self.tr("zMerge Options: ") if idx.column() == Column.Value: plugin = self.data(idx, Role.Data) plugins, merges = self.data(idx, Role.MergeAssociations) if idx.row() == Row.MergedBy and merges: return self.tr("({})").format(len(merges)) if idx.row() == Row.MergedPlugins and plugins: return self.tr("({})").format(len(plugins)) if idx.row() == Row.ZEditOptions and plugin.isMerge: return self.tr("({})").format(len(MERGE_OPTIONS)) if idx.row( ) == Row.WhenBuilt and plugin.isMerge and plugin.mergeFile: return plugin.mergeFile.dateBuilt if depth == Id.Depth.D2: if idx.parent().row() == Row.MergedBy: if idx.column() == 0: return "{}".format(idx.row() + 1) if idx.column() == 1: plugins, merges = self.data(idx, Role.MergeAssociations) return merges[idx.row()].pluginName if idx.parent().row() == Row.MergedPlugins: if idx.column() == 0: return "{}".format(idx.row() + 1) if idx.column() == 1: plugins, merges = self.data(idx, Role.MergeAssociations) return plugins[idx.row()].pluginName if idx.parent().row() == Row.ZEditOptions: if idx.column() == 0: return MERGE_OPTIONS[idx.row()][1] if idx.column() == 1: plugin = self.data(idx, Role.Data) return getattr(plugin.mergeFile, MERGE_OPTIONS[idx.row()][0], None)
def get_file_path(self, index: QModelIndex): """ From index to get real file path """ if not index.isValid(): return None path = list() path.append(index.data()) while index.parent().isValid(): index = index.parent() path.append(index.data()) row = index.row() path[-1] = self.base_dirs[row] path.reverse() return '/'.join(path)
def on_clicked(self, index: QModelIndex): widgets_map['create_vault_button'].set_enabled(False) if index.parent().isValid(): self.on_clicked_vault() else: self.on_clicked_region()
def background(self, index: QModelIndex): sourceParent = index.parent() dataRow = index.row() dataIndex = self.parent.index(dataRow, 1, sourceParent) ## get ticker ticker = dataIndex.data() col = marker_background_color(self.dataObject, ticker) return col
def background(self, index: QModelIndex): sourceParent = index.parent() dataRow = index.row() dataIndex = self.parent.index(dataRow, 0, sourceParent) ## get name name = dataIndex.data() ticker = self.dataObject.getTickerFromName(name) return stock_background_color(self.dataObject, ticker)
def data(self, idx: QModelIndex, role: int = Qt.DisplayRole): depth = Id.depth(idx) if depth == Id.Depth.Invalid: return if role == Role.Data: # return the top level plugin if depth == Id.Depth.D0: return self.sourcePlugin(idx.row()) if depth == Id.Depth.D1: return self.sourcePlugin(Id.row(idx.internalId())) if depth == Id.Depth.D2: return self.sourcePlugin(Id.row(Id.parentId(idx.internalId()))) if role == Role.MergeAssociations: if depth == Id.Depth.D0: return self.mergeAssociations(idx.row()) if depth == Id.Depth.D1: return self.mergeAssociations(Id.row(idx.internalId())) if depth == Id.Depth.D2: return self.mergeAssociations( Id.row(Id.parentId(idx.internalId()))) if role == Role.Cell: return self._display_data(idx) if not idx.parent().isValid(): return if role == Qt.DisplayRole: return self._display_data(idx) return self._style_data(idx, role)
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool: accept = super(CustomFilterModel, self).filterAcceptsRow(source_row, source_parent) if source_parent.row() == -1: return accept if accept: return True return self.filterAcceptsRow(source_parent.row(), source_parent.parent())
def onDataChanged(self, topLeft: QtCore.QModelIndex, bottomRight: QtCore.QModelIndex): """Handle row insertions. This method is actually called for every data change which is frustratingly inefficient. However insertRows doesn't work for this purpose as, as far as I can tell, before it returns rows are not actually added to the model making calling updateAccountSummary moot.""" parent = topLeft.parent() if parent.isValid(): self.updateAccountSummary(self.itemFromIndex(parent))
def visualRect(self, index: qtcore.QModelIndex) -> qtcore.QRect: rect = super(QPortfolioTableView, self).visualRect(index) if index.parent().isValid(): # child level if index.column() == 0: # properties sectionStart = self.header().logicalIndex(0) sectionEnd = self.header().logicalIndex(2) left = self.header().sectionViewportPosition(sectionStart) right = self.header().sectionViewportPosition(sectionEnd) + self.header().sectionSize(sectionEnd) indent = 1 * self.indentation() left += indent # update visual rect rect.setX(int(left + 1)) rect.setY(int(rect.y() + 1)) rect.setWidth(right - left - 2) rect.setHeight(rect.height() - 2) if index.column() == 3: # chart sectionStart = self.header().logicalIndex(3) sectionEnd = self.header().logicalIndex(self.header().count() - 1) left = self.header().sectionViewportPosition(sectionStart) right = self.header().sectionViewportPosition(sectionEnd) + self.header().sectionSize(sectionEnd) # update visual rect rect.setX(int(left + 1)) rect.setY(int(rect.y() + 1)) rect.setWidth(right - left - 2) rect.setHeight(rect.height() - 2) return rect
def __newFolder(self): """ Private slot to add a new bookmarks folder. """ from .BookmarkNode import BookmarkNode currentIndex = self.bookmarksTree.currentIndex() idx = QModelIndex(currentIndex) sourceIndex = self.__proxyModel.mapToSource(idx) sourceNode = self.__bookmarksModel.node(sourceIndex) row = -1 # append new folder as the last item per default if sourceNode is not None and \ sourceNode.type() != BookmarkNode.Folder: # If the selected item is not a folder, add a new folder to the # parent folder, but directly below the selected item. idx = idx.parent() row = currentIndex.row() + 1 if not idx.isValid(): # Select bookmarks menu as default. idx = self.__proxyModel.index(1, 0) idx = self.__proxyModel.mapToSource(idx) parent = self.__bookmarksModel.node(idx) node = BookmarkNode(BookmarkNode.Folder) node.title = self.tr("New Folder") self.__bookmarksManager.addBookmark(parent, node, row)
def __newFolder(self): """ Private slot to add a new bookmarks folder. """ from .BookmarkNode import BookmarkNode currentIndex = self.bookmarksTree.currentIndex() idx = QModelIndex(currentIndex) sourceIndex = self.__proxyModel.mapToSource(idx) sourceNode = self.__bookmarksModel.node(sourceIndex) row = -1 # append new folder as the last item per default if ( sourceNode is not None and sourceNode.type() != BookmarkNode.Folder ): # If the selected item is not a folder, add a new folder to the # parent folder, but directly below the selected item. idx = idx.parent() row = currentIndex.row() + 1 if not idx.isValid(): # Select bookmarks menu as default. idx = self.__proxyModel.index(1, 0) idx = self.__proxyModel.mapToSource(idx) parent = self.__bookmarksModel.node(idx) node = BookmarkNode(BookmarkNode.Folder) node.title = self.tr("New Folder") self.__bookmarksManager.addBookmark(parent, node, row)
def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: is_first_level = index.parent() == QtCore.QModelIndex() multiline_columns = ( ChecklistItemPropertyColumn.DESCRIPTION.value, ChecklistItemPropertyColumn.GUIDE.value, ChecklistItemPropertyColumn.VALIDATION_NOTES.value, ) is_multiline = index.row() in multiline_columns if not is_first_level and is_multiline and index.column() == 1: check_property: ChecklistItemProperty = index.internalPointer().ref text_to_draw = check_property.value base_width = self.gui_view.columnWidth(1) base_height = 10000 # some ridiculous high value just for initialization base_size = QtCore.QSize(base_width, base_height) metrics = QtGui.QFontMetrics(option.font) out_rect: QtCore.QRect = metrics.boundingRect( QtCore.QRect(QtCore.QPoint(0, 0), base_size), Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, text_to_draw, ) base_size.setHeight(out_rect.height()) result = base_size else: result = super().sizeHint(option, index) return result
def _tooltip_data(self, idx: QModelIndex): depth = Id.depth(idx) if depth == Id.Depth.D1 and idx.parent().row() == Row.MergedPlugins: dataFolder = self.mergeFile.plugins[idx.row()].dataFolder return '"{}"'.format(dataFolder) hash = getattr(self.mergeFile.plugins[idx.row()], "hash", "") return "<html><head/><body><p>folder: " + dataFolder + "</p><p>hash: " + hash + "</p></body></html>"
def insertRows(self, node_type, position: int, rows: int, parent: QModelIndex = QModelIndex()) -> bool: """ :param parent: QModelIndex """ parent_node = self.getNode(parent) parent_num_children = parent_node.childCount() parent = self.index(parent.row(), 0, parent.parent()) if (position < 0 or parent_node.childCount() < position or rows < 0): return False self.beginInsertRows(parent, parent_num_children, parent_num_children + rows - 1) for _ in range(rows): child_node = node_type(parent_node) success = parent_node.insertChild(position, child_node) self.endInsertRows() return success
def handle_parts_tree_activated(self, qindex: QModelIndex): if isinstance(qindex.internalPointer(), ArmorSetNode): return self.armor_item_mapper.setRootIndex(qindex.parent()) self.armor_item_mapper.setCurrentModelIndex(qindex) entry = qindex.internalPointer().ref self.crafting_requirements_editor.set_current(entry.id)
def delete_plot(self, index: QModelIndex): logging.debug('Delete plot with index %s', index) if not index.isValid(): return data = _get_data(index) parent = index.parent() if isinstance(data, _ParentData): # Notify view to remove all children plots self.beginRemoveRows(parent, index.row(), index.row()) for child in data.children: self.visibility_changed(False, child.plot) self._files.remove(data) self.endRemoveRows() elif isinstance(data, _ChildData): # Get parent and remove parent from childre parent_data = _get_data(parent) if not isinstance(parent_data, _ParentData): _raise_data_err(parent_data) if index.row() >= len(parent_data.children): logging.warn('Selected index is out of range') return self.beginRemoveRows(parent, index.row(), index.row()) self.visibility_changed(False, data.plot) parent_data.children.remove(data) self.endRemoveRows() else: _raise_data_err(data)
def _setData(self, index: QModelIndex, value: Any, role: int = Qt.EditRole) -> bool: """Устанавливает локальные данные. _setData(self, index: QModelIndex, value: Any, role: int = Qt.EditRole) -> bool """ assert self.checkIndex(index) # if role is None: # role = Qt.EditRole if role == Vns.ItemDataRole.ItemDict: assert index.isValid() assert isinstance(value, dict) indexWithZeroColumn = self.sibling(index.row(), self.ZERO_COLUMN, index) if self.__localDataModel.setData( self._mapToLocal(indexWithZeroColumn), value, role): indexWithLastColumn = self.sibling( index.row(), self.columnCount(index.parent()) - 1, index) self.dataChanged.emit(indexWithZeroColumn, indexWithLastColumn, []) return True return False elif role == Vns.ItemDataRole._ChildrenLoadingInfo: return False elif role == Vns.ItemDataRole.ChildrenAreLoadedSeparately: return False elif role == Vns.ItemDataRole.ChildrenLoadingPolicy: assert isinstance(value, Vns.LoadingPolicy) # TODO: Для какого индекса надо устанавливать политику: для index или для indexWithZeroColumn? if self.setChildrenLoadingPolicy(value, index): # TODO: Какие индексы должны быть в сигнале? self.dataChanged.emit(index, index, [role]) return True return False elif role == Vns.ItemDataRole.ChildrenLoadingState: return False elif role == Vns.ItemDataRole.ChildrenPagination: return False elif role == Vns.ItemDataRole._DetailsLoadingInfo: return False elif role == Vns.ItemDataRole.DetailsAreLoadedSeparately: return False elif role == Vns.ItemDataRole.DetailsAreLoaded: return False elif role == Vns.ItemDataRole.DetailsLoadingState: return False assert not (Vns.ItemDataRole.First <= role < Vns.ItemDataRole.Custom) if role == Qt.DisplayRole or role == Qt.EditRole: return False return self.__localDataModel.setData(self._mapToLocal(index), value, role)
def _getBgColor(self, index: QModelIndex): bg = super(DetailedInfoTable, self)._getBgColor(index) if isinstance(bg, QBrush) and bg.color().alpha(): return bg.color() color = LIGHT_BLUE if index.parent().isValid(): if index.row() == 0: color.setAlpha(260 - index.parent().data(Qt.BackgroundRole).alpha()) else: color.setAlpha( 260 - index.siblingAtRow(index.row() - 1).data(Qt.BackgroundRole).alpha()) else: if index.row() % 2: color.setAlpha(150) else: color.setAlpha(110) return color
def background(self, index: QModelIndex ): sourceParent = index.parent() dataRow = index.row() dataIndex = self.parent.index( dataRow, 3, sourceParent ) ## get ticker ticker = dataIndex.data() markerColor = marker_background_color( self.dataObject, ticker ) if markerColor is not None: return markerColor return wallet_background_color( self.dataObject, ticker )
def sectionSizeFromContents(self, logicalIndex: int) -> QSize: if self._pd.headerModel: curLeafIndex = QModelIndex(self._pd.leafIndex(logicalIndex)) if curLeafIndex.isValid(): styleOption = QStyleOptionHeader( self.styleOptionForCell(logicalIndex)) s = QSize(self._pd.cellSize(curLeafIndex, self, styleOption)) curLeafIndex = curLeafIndex.parent() while curLeafIndex.isValid(): if self.orientation() == Qt.Horizontal: s.setHeight(s.height() + self._pd.cellSize( curLeafIndex, self, styleOption).height()) else: s.setWidth(s.width() + self._pd.cellSize( curLeafIndex, self, styleOption).width()) curLeafIndex = curLeafIndex.parent() return s return super().sectionSizeFromContents(logicalIndex)
def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags: result = super().flags(index) if index.isValid(): if index.parent() == QtCore.QModelIndex(): if index.column() == 1: result = result | Qt.ItemIsEditable | Qt.ItemIsUserCheckable else: if index.row() == ChecklistItemPropertyColumn.VALIDATION_NOTES.value and index.column() == 1: result = result | Qt.ItemIsEditable return result
def flags(self, idx: QModelIndex): if self._displayCheckbox and idx.isValid( ) and not idx.parent().isValid(): sourceIdx = self.mapToSource(idx) flags = self.sourceModel().flags(sourceIdx) if sourceIdx.column() == Column.PluginName: return flags | Qt.ItemIsUserCheckable if self._readonly: return super().flags(idx) & ~Qt.ItemIsEditable return super().flags(idx)
def clvHooksClicked(self, index: QModelIndex): if index.parent().column() != 0: return data = index.data() model = mainWindowHelper.tbvAllHooks.model() match = model.match(model.index(0, 0), Qt.DisplayRole, data, 1, Qt.MatchContains) if len(match) > 0: mainWindowHelper.tbvAllHooks.selectRow(match[0].row())
def _checkstate_data(self, idx: QModelIndex): return depth = Id.depth(idx) if (depth == Id.Depth.D1 and idx.column() == 1 and idx.parent().row() == Row.ZEditOptions and isBoolOption(idx.row())): if getattr(self.mergeFile, MERGE_OPTIONS[idx.row()][0], False): return Qt.Checked else: return Qt.Unchecked
def expand_item(self, index: QModelIndex) -> None: index = self.model().index(index.row(), 0, index.parent()) item = self.model().itemFromIndex(index) self.load_children(item)
class textEditView(QTextEdit): def __init__(self, parent=None, index=None, html=None, spellcheck=True, highlighting=False, dict="", autoResize=False): QTextEdit.__init__(self, parent) self._column = Outline.text.value self._index = None self._indexes = None self._model = None self._placeholderText = self.placeholderText() self._updating = False self._item = None self._highlighting = highlighting self._textFormat = "text" self.setAcceptRichText(False) # When setting up a theme, this becomes true. self._fromTheme = False self.spellcheck = spellcheck self.currentDict = dict if dict else settings.dict self.highlighter = None self.setAutoResize(autoResize) self._defaultBlockFormat = QTextBlockFormat() self._defaultCharFormat = QTextCharFormat() self.highlightWord = "" self.highligtCS = False self.defaultFontPointSize = qApp.font().pointSize() self._dict = None # self.document().contentsChanged.connect(self.submit, AUC) # Submit text changed only after 500ms without modifications self.updateTimer = QTimer() self.updateTimer.setInterval(500) self.updateTimer.setSingleShot(True) self.updateTimer.timeout.connect(self.submit) # self.updateTimer.timeout.connect(lambda: print("Timeout")) self.updateTimer.stop() self.document().contentsChanged.connect(self.updateTimer.start, AUC) # self.document().contentsChanged.connect(lambda: print("Document changed")) # self.document().contentsChanged.connect(lambda: print(self.objectName(), "Contents changed")) self.setEnabled(False) if index: self.setCurrentModelIndex(index) elif html: self.document().setHtml(html) self.setReadOnly(True) # Spellchecking if enchant and self.spellcheck: try: self._dict = enchant.Dict(self.currentDict if self.currentDict else enchant.get_default_language()) except enchant.errors.DictNotFoundError: self.spellcheck = False else: self.spellcheck = False if self._highlighting and not self.highlighter: self.highlighter = basicHighlighter(self) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) def setModel(self, model): self._model = model try: self._model.dataChanged.connect(self.update, AUC) except TypeError: pass try: self._model.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved, AUC) except TypeError: pass def setColumn(self, col): self._column = col def setHighlighting(self, val): self._highlighting = val def setDefaultBlockFormat(self, bf): self._defaultBlockFormat = bf if self.highlighter: self.highlighter.setDefaultBlockFormat(bf) def setCurrentModelIndex(self, index): self._indexes = None if index.isValid(): self.setEnabled(True) if index.column() != self._column: index = index.sibling(index.row(), self._column) self._index = index self.setPlaceholderText(self._placeholderText) if not self._model: self.setModel(index.model()) self.setupEditorForIndex(self._index) self.loadFontSettings() self.updateText() else: self._index = QModelIndex() self.setPlainText("") self.setEnabled(False) def setCurrentModelIndexes(self, indexes): self._index = None self._indexes = [] for i in indexes: if i.isValid(): self.setEnabled(True) if i.column() != self._column: i = i.sibling(i.row(), self._column) self._indexes.append(i) if not self._model: self.setModel(i.model()) self.updateText() def setupEditorForIndex(self, index): # In which model are we editing? if type(index.model()) != outlineModel: self._textFormat = "text" return # what type of text are we editing? if self._column not in [Outline.text.value, Outline.notes.value]: self._textFormat = "text" else: self._textFormat = "md" # Setting highlighter if self._highlighting: item = index.internalPointer() if self._column in [Outline.text.value, Outline.notes.value]: self.highlighter = MMDHighlighter(self) else: self.highlighter = basicHighlighter(self) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) def loadFontSettings(self): if self._fromTheme or \ not self._index or \ type(self._index.model()) != outlineModel or \ self._column != Outline.text.value: return opt = settings.textEditor f = QFont() f.fromString(opt["font"]) # self.setFont(f) self.setStyleSheet("""QTextEdit{{ background: {bg}; color: {foreground}; font-family: {ff}; font-size: {fs}; }} """.format( bg=opt["background"], foreground=opt["fontColor"], ff=f.family(), fs="{}pt".format(str(f.pointSize())))) cf = QTextCharFormat() # cf.setFont(f) # cf.setForeground(QColor(opt["fontColor"])) bf = QTextBlockFormat() bf.setLineHeight(opt["lineSpacing"], bf.ProportionalHeight) bf.setTextIndent(opt["tabWidth"] * 1 if opt["indent"] else 0) bf.setTopMargin(opt["spacingAbove"]) bf.setBottomMargin(opt["spacingBelow"]) self._defaultCharFormat = cf self._defaultBlockFormat = bf if self.highlighter: self.highlighter.setMisspelledColor(QColor(opt["misspelled"])) self.highlighter.setDefaultCharFormat(self._defaultCharFormat) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) def update(self, topLeft, bottomRight): if self._updating: return elif self._index: if topLeft.parent() != self._index.parent(): return # print("Model changed: ({}:{}), ({}:{}/{}), ({}:{}) for {} of {}".format( # topLeft.row(), topLeft.column(), # self._index.row(), self._index.row(), self._column, # bottomRight.row(), bottomRight.column(), # self.objectName(), self.parent().objectName())) if topLeft.row() <= self._index.row() <= bottomRight.row(): if topLeft.column() <= self._column <= bottomRight.column(): self.updateText() elif self._indexes: update = False for i in self._indexes: if topLeft.row() <= i.row() <= bottomRight.row(): update = True if update: self.updateText() def rowsAboutToBeRemoved(self, parent, first, last): if self._index: if self._index.parent() == parent and \ first <= self._index.row() <= last: self._index = None self.setEnabled(False) # FIXME: self._indexes def disconnectDocument(self): try: self.document().contentsChanged.disconnect(self.updateTimer.start) except: pass def reconnectDocument(self): self.document().contentsChanged.connect(self.updateTimer.start, AUC) def updateText(self): if self._updating: return # print("Updating", self.objectName()) self._updating = True if self._index: self.disconnectDocument() if self.toPlainText() != toString(self._model.data(self._index)): # print(" Updating plaintext") self.document().setPlainText(toString(self._model.data(self._index))) self.reconnectDocument() elif self._indexes: self.disconnectDocument() t = [] same = True for i in self._indexes: item = i.internalPointer() t.append(toString(item.data(self._column))) for t2 in t[1:]: if t2 != t[0]: same = False break if same: self.document().setPlainText(t[0]) else: self.document().setPlainText("") if not self._placeholderText: self._placeholderText = self.placeholderText() self.setPlaceholderText(self.tr("Various")) self.reconnectDocument() self._updating = False def submit(self): self.updateTimer.stop() if self._updating: return # print("Submitting", self.objectName()) if self._index: # item = self._index.internalPointer() if self.toPlainText() != self._model.data(self._index): # print(" Submitting plain text") self._updating = True self._model.setData(self._index, self.toPlainText()) self._updating = False elif self._indexes: self._updating = True for i in self._indexes: item = i.internalPointer() if self.toPlainText() != toString(item.data(self._column)): print("Submitting many indexes") self._model.setData(i, self.toPlainText()) self._updating = False def keyPressEvent(self, event): QTextEdit.keyPressEvent(self, event) if event.key() == Qt.Key_Space: self.submit() # ----------------------------------------------------------------------------------------------------- # Resize stuff def resizeEvent(self, e): QTextEdit.resizeEvent(self, e) if self._autoResize: self.sizeChange() def sizeChange(self): docHeight = self.document().size().height() if self.heightMin <= docHeight <= self.heightMax: self.setMinimumHeight(docHeight) def setAutoResize(self, val): self._autoResize = val if self._autoResize: self.document().contentsChanged.connect(self.sizeChange) self.heightMin = 0 self.heightMax = 65000 self.sizeChange() ############################################################################### # SPELLCHECKING ############################################################################### # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ def setDict(self, d): self.currentDict = d self._dict = enchant.Dict(d) if self.highlighter: self.highlighter.rehighlight() def toggleSpellcheck(self, v): self.spellcheck = v if enchant and self.spellcheck and not self._dict: if self.currentDict: self._dict = enchant.Dict(self.currentDict) elif enchant.dict_exists(enchant.get_default_language()): self._dict = enchant.Dict(enchant.get_default_language()) else: self.spellcheck = False if self.highlighter: self.highlighter.rehighlight() else: self.spellcheck = False def mousePressEvent(self, event): if event.button() == Qt.RightButton: # Rewrite the mouse event to a left button event so the cursor is # moved to the location of the pointer. event = QMouseEvent(QEvent.MouseButtonPress, event.pos(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QTextEdit.mousePressEvent(self, event) class SpellAction(QAction): """A special QAction that returns the text in a signal. Used for spellckech.""" correct = pyqtSignal(str) def __init__(self, *args): QAction.__init__(self, *args) self.triggered.connect(lambda x: self.correct.emit( str(self.text()))) def contextMenuEvent(self, event): # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ popup_menu = self.createStandardContextMenu() popup_menu.exec_(event.globalPos()) def createStandardContextMenu(self): popup_menu = QTextEdit.createStandardContextMenu(self) if not self.spellcheck: return popup_menu # Select the word under the cursor. # But only if there is no selection (otherwise it's impossible to select more text to copy/cut) cursor = self.textCursor() if not cursor.hasSelection(): cursor.select(QTextCursor.WordUnderCursor) self.setTextCursor(cursor) # Check if the selected word is misspelled and offer spelling # suggestions if it is. if cursor.hasSelection(): text = str(cursor.selectedText()) valid = self._dict.check(text) selectedWord = cursor.selectedText() if not valid: spell_menu = QMenu(self.tr('Spelling Suggestions'), self) for word in self._dict.suggest(text): action = self.SpellAction(word, spell_menu) action.correct.connect(self.correctWord) spell_menu.addAction(action) # Only add the spelling suggests to the menu if there are # suggestions. if len(spell_menu.actions()) != 0: popup_menu.insertSeparator(popup_menu.actions()[0]) # Adds: add to dictionary addAction = QAction(self.tr("&Add to dictionary"), popup_menu) addAction.triggered.connect(self.addWordToDict) addAction.setData(selectedWord) popup_menu.insertAction(popup_menu.actions()[0], addAction) # Adds: suggestions popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) # popup_menu.insertSeparator(popup_menu.actions()[0]) # If word was added to custom dict, give the possibility to remove it elif valid and self._dict.is_added(selectedWord): popup_menu.insertSeparator(popup_menu.actions()[0]) # Adds: remove from dictionary rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu) rmAction.triggered.connect(self.rmWordFromDict) rmAction.setData(selectedWord) popup_menu.insertAction(popup_menu.actions()[0], rmAction) return popup_menu def correctWord(self, word): """ Replaces the selected text with word. """ cursor = self.textCursor() cursor.beginEditBlock() cursor.removeSelectedText() cursor.insertText(word) cursor.endEditBlock() def addWordToDict(self): word = self.sender().data() self._dict.add(word) self.highlighter.rehighlight() def rmWordFromDict(self): word = self.sender().data() self._dict.remove(word) self.highlighter.rehighlight() ############################################################################### # FORMATTING ############################################################################### def focusOutEvent(self, event): """Submit changes just before focusing out.""" QTextEdit.focusOutEvent(self, event) self.submit() def focusInEvent(self, event): """Finds textFormatter and attach them to that view.""" QTextEdit.focusInEvent(self, event) p = self.parent() while p.parent(): p = p.parent() if self._index: for tF in p.findChildren(textFormat, QRegExp(".*"), Qt.FindChildrenRecursively): tF.updateFromIndex(self._index) tF.setTextEdit(self) def applyFormat(self, _format): if self._textFormat == "md": if _format == "Bold": MDFormatSelection(self, 0) elif _format == "Italic": MDFormatSelection(self, 1) elif _format == "Code": MDFormatSelection(self, 2) elif _format == "Clear": MDFormatSelection(self)
class TreeModel(QAbstractItemModel): # Funktion hasChildren? # signals statusChanged = pyqtSignal(QModelIndex) speciesChanged = pyqtSignal(QModelIndex, int, int) calculated = pyqtSignal() itemsInserted = pyqtSignal(bool) itemsAboutToBeCalculated = pyqtSignal(bool) allItemsRemoved = pyqtSignal(bool) # class constants ItemRole = Qt.UserRole + 1 StatusRole = Qt.UserRole + 2 ColorRole = Qt.UserRole + 3 TypeRole = Qt.UserRole + 4 NameRole = Qt.UserRole + 5 ResultRole = Qt.UserRole + 6 PlantRole = Qt.UserRole + 7 ProtectionRole = Qt.UserRole + 8 SpeciesRole = Qt.UserRole + 9 TypeRole = Qt.UserRole + 10 IdentificationRole = Qt.UserRole + 11 LengthRole = Qt.UserRole + 12 CountRole = Qt.UserRole + 13 _roles = {ItemRole : "item", StatusRole : "status", ColorRole : "color", TypeRole : "type", NameRole : "name", ResultRole : "result", PlantRole : "plant", ProtectionRole : "protection", IdentificationRole : "identification", LengthRole : "length", CountRole : "count"} TYPE, IDENTIFICATION, SPECIES, NAME = range(4) def __init__(self, parent=None): super().__init__(parent) self.root = VariantItem("root item") # species registry self.species = {} self.variants = 0 # initialize class attributes self._TABLE_HEADER_LABELS = (QtCore.QT_TRANSLATE_NOOP("TreeModel", "Protection type"), QtCore.QT_TRANSLATE_NOOP("TreeModel", "Name"), QtCore.QT_TRANSLATE_NOOP("TreeModel", "Tree species"), QtCore.QT_TRANSLATE_NOOP("TreeModel", "Description of protection")) # project settings self.project = Project() self.rowsInserted.connect(self.updateSpecies) self.speciesChanged.connect(self.moveItem) self.statusChanged.connect(self.updateStatus) self.new = True self.changed = False self.filename = "" self.file = False self.read = False self.last = QModelIndex() self.current = -1 self.count = 0 # temoporary plant count for calculation help self.length = 0 # temoporary fence length for calculation help def columnCount(self, parent): return len(self._TABLE_HEADER_LABELS) def roleNames(self): return self._roles def projectData(self, key): return getattr(self.project, key) def setProjectData(self, key, value): setattr(self.project, key, value) self.changed = True def itemData(self, index): if not index.isValid(): return None item = self.getItem(index) # create the QMap as python dict data = { self.NameRole : item.name, self.ColorRole : item.color, self.StatusRole : item.status, self.PlantRole : item.plant, self.ProtectionRole : item.protection } return data def setItemData(self, index, roles): if not index.isValid(): return False item = self.getItem(index) oldSpecies = item.plant.species newSpecies = roles[self.PlantRole].species # if the species has changed, the item have to move # a fence item doesn't have already known species if not newSpecies == oldSpecies: if item.type == Fence.TYPE and newSpecies in self.species: return False self.speciesChanged.emit(index, oldSpecies, newSpecies) # update the item for role in roles: if not role in self._roles: return False attribute = self._roles[role] setattr(item, attribute, roles[role]) # update model's change status self.dataChanged.emit(index, index) self.changed = True return True def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None item = index.internalPointer() column = index.column() if role == Qt.DisplayRole: if column == TreeModel.TYPE: return QApplication.translate("VariantItem", item.protection.TYPE_DESCRIPTION) elif column == TreeModel.NAME: return item.name elif column == TreeModel.SPECIES: return library.TREESPECIES_ABBREVIATION[item.plant.species] elif column == TreeModel.IDENTIFICATION: return "{protection}{identification}".format( protection=QApplication.translate("VariantItem", item.protection.TYPE_SHORT), identification=item.identification + 1) elif role == Qt.CheckStateRole: if column == TreeModel.TYPE: if item.status: if item.type == Fence.TYPE: checked = True for child in item.children: if not child.status: checked = False if checked: return Qt.Checked else: return Qt.PartiallyChecked else: return Qt.Checked else: return Qt.Unchecked elif role == self.IdentificationRole: return "{protection}{identification}".format( protection=QApplication.translate("VariantItem", item.protection.TYPE_SHORT), identification=item.identification + 1) elif role == self.LengthRole: return self.project.length elif role == self.CountRole: return self.project.count elif role == self.TypeRole: return item.type elif role == self.NameRole: return item.name elif role == self.SpeciesRole: return item.plant.species elif role == self.ColorRole: return QColor(item.color) elif role == self.StatusRole: return item.status elif role == self.ResultRole: return item.result else: return None def setData(self, index, value, role=Qt.EditRole): item = index.internalPointer() if role == Qt.CheckStateRole: if value in (Qt.Checked, Qt.PartiallyChecked): item.status = True else: item.status = False self.dataChanged.emit(index, index) #self.statusChanged.emit(index) if item.type == Fence.TYPE: for child in item.children: child.status = item.status childIndex = self.createIndex(child.childNumber(), self.TYPE, child) self.dataChanged.emit(childIndex, childIndex) if item.type > Fence.TYPE: parent = item.parent status = False for child in parent.children: if child.status: status = True parent.status = status self.dataChanged.emit(index.parent(), index.parent()) return True return True elif role == self.ResultRole: item.result = value self.dataChanged.emit(index, index) return True elif role == self.ColorRole: item.color = value self.dataChanged.emit(index, index) return True else: return False def flags(self, index): if not index.isValid(): return Qt.NoItemFlags flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable column = index.column() if column == TreeModel.TYPE: if index.data() == Fence.TYPE_DESCRIPTION: return flags | Qt.ItemIsUserCheckable | 64 else: return flags | Qt.ItemIsUserCheckable else: return flags def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return QApplication.translate("TreeModel", self._TABLE_HEADER_LABELS[section]) return None def index(self, row, column, parent=QModelIndex()): # kann Fehler verursachen! if not self.hasIndex(row, column, parent): return QModelIndex() if not parent.isValid(): parentItem = self.root else: parentItem = parent.internalPointer() childItem = parentItem.child(row) if childItem: return self.createIndex(row, column, childItem) else: return QModelIndex() def parent(self, index): if not index.isValid(): return QModelIndex() child = self.getItem(index) parent = child.parent if parent == self.root: return QModelIndex() return self.createIndex(parent.childNumber(), 0, parent) def rowCount(self, parent=QModelIndex()): #if parent.column() > 0: # return 0 #if not parent.isValid(): # parent_item = self.root #else: # parent_item = parent.internalPointer() #return parent_item.childCount() item = self.getItem(parent) return item.childCount() def evaluate(self): evaluation = False # if there is a fence item with at least # one children, enable the calculation button items = self.match(self.index(0, 0), self.TypeRole, Tube.TYPE, -1, Qt.MatchRecursive) for item in items: if item.parent().isValid(): # the calculation button should be enabled evaluation = True break self.itemsAboutToBeCalculated.emit(evaluation) # automatic protection type # self.last contains an index value if not self.last.parent().isValid() and self.current == -1: if self.last.data(self.TypeRole) == Fence.TYPE: self.current = Tube.TYPE elif self.last.data(self.TypeRole) == Tube.TYPE: self.current = Fence.TYPE else: self.current = -1 else: self.current = -1 return evaluation def getItem(self, index): if index.isValid(): item = index.internalPointer() if item: return item return self.root def insertItem(self, item): return self.insertItems([item]) def insertItems(self, items): if not items: return False # extract items with type = 0 as top level items toplevel = [] species = [] for item in items[:]: # update the identification item.identification = self.variants self.variants = self.variants + 1 if item.type == 0: if not item.plant.species in self.species: toplevel.append(item) species.append(item.plant.species) items.remove(item) else: return False elif item.type > 0: if item.plant.species in self.species: parent = self.root.child(self.species[item.plant.species]) index = self.createIndex(parent.childCount(), 0, parent) self.insertRow(parent.childCount(), item, index) items.remove(item) else: return False # extract current top level items with type > 0 from root if toplevel: current = [] for child in self.root.children[:]: if child.plant.species in species: current.append(child) self.removeRow(child.childNumber()) items.extend(current) for item in toplevel: for child in items[:]: if child.plant.species == item.plant.species: item.insertChildren(item.childCount(), [child]) items.remove(child) toplevel.extend(items) else: toplevel = items # insert the new rows and emit the signal to enable the view # also do an evaluation if toplevel: self.insertRows(self.root.childCount(), toplevel) # always evaluate self.itemsInserted.emit(True) self.evaluate() return True def insertRow(self, row, item, parent=QModelIndex()): return self.insertRows(row, [item], parent) def insertRows(self, row, items, parent=QModelIndex()): item = self.getItem(parent) self.beginInsertRows(parent, row, row + len(items) - 1) item.insertChildren(row, items) self.endInsertRows() # get the index of the last item if not self.read: self.last = self.index(row + len(items) -1, 0, parent) # update model's change status self.new = False self.changed = True return True def updateStatus(self, index): # möglicherweise sollte diese Funktion in die Delegate #if not index.parent().isValid(): # item.type = Fence.TYPE item = self.getItem(index) if item.type == Fence.TYPE: for child in item.children: child.status = item.status childIndex = self.createIndex(child.childNumber(), self.TYPE, child) self.dataChanged.emit(childIndex, childIndex) if item.type > Fence.TYPE: parent = item.parent status = False for child in parent.children: if child.status: status = True parent.status = status self.dataChanged.emit(index.parent(), index.parent()) def updateSpecies(self, parent, first, last): if not parent.isValid(): item = self.getItem(parent) for row in range(first, last + 1): child = item.child(row) if child.type == Fence.TYPE: self.species[child.plant.species] = row def removeItem(self, position, parent=QModelIndex()): return self.removeItems(position, 1, parent) def removeItems(self, position, rows, parent=QModelIndex()): item = self.getItem(parent) if position < 0 or position + rows - 1 > item.childCount(): return False # first it's necessary to remove the species entry for row in range(position, position + rows): child = item.child(row) if child.type == Fence.TYPE: del self.species[child.plant.species] # now the child items can be removed # and do an evaluation self.removeRows(position, rows, parent) self.evaluate() # if the model is empty, emit the signal # to disable the view if not self.rowCount(): self.allItemsRemoved.emit(True) return True def removeRow(self, row, parent=QModelIndex()): return self.removeRows(row, 1, parent) def removeRows(self, row, count, parent=QModelIndex()): item = self.getItem(parent) self.beginRemoveRows(parent, row, row + count -1) item.removeChildren(row, count) self.endRemoveRows() # get the index of the last item self.last = QModelIndex() # update model's change status self.changed = True return True def moveItem(self, index, oldSpecies, newSpecies): # item.type durch index.parent is valid ersetzen?! item = self.getItem(index) parent = item.parent if item.type == Fence.TYPE: # first update the species registry del self.species[oldSpecies] self.species[newSpecies] = index.row() # simply clear item's internal list and # move child item's to tree root if item.hasChildren(): count = item.childCount() child = parent.childCount() self.moveRows(index, 0, count, index.parent(), child) # if there are any child items with the same species # within tree root, add them to the current item for child in parent.children[:]: if child.type > Fence.TYPE and child.plant.species == newSpecies: count = item.childCount() row = child.childNumber() self.moveRow(index.parent(), row, index, count) elif item.type > Fence.TYPE: if newSpecies in self.species: newParent = self.root.child(self.species[newSpecies]) newIndex = self.createIndex(newParent.childNumber(), 0, newParent) child = newParent.childCount() self.moveRow(index.parent(), index.row(), newIndex, child) elif index.parent().isValid(): # only if the parent is valid, the item # can be moved to the tree root child = self.root.childCount() self.moveRow(index.parent(), index.row(), QModelIndex(), child) # after moving items do an evaluation #self.evaluate() def moveRow(self, source, row, destination, child): return self.moveRows(source, row, 1, destination, child) def moveRows(self, source, row, count, destination, child): sourceParent = self.getItem(source) destinationParent = self.getItem(destination) self.beginMoveRows(source, row, row + count - 1, destination, child) destinationParent.insertChildren(child, sourceParent.children[row:row+count]) sourceParent.removeChildren(row, count) self.endMoveRows() # update model's change status self.changed = True return True def saveFile(self, xmlfile=""): if xmlfile: self.filename = xmlfile self.file = True elif self.file: xmlfile = self.filename else: return False writer = XMLFileWriter() success = writer.writeXMLFile(self.project.__dict__, self.root, xmlfile) # update model's change status if success: self.changed = False return success def readFile(self, xmlfile): if xmlfile: self.filename = xmlfile self.file = True else: return False # wichtig, um keine Vorauswahl zu haben self.read = True reader = XMLFileReader() success = reader.readXMLFile(xmlfile) if success: if not self.project.update(reader.getProject()): success = False if not self.insertItems(reader.getItems()): success = False # TODO self.dataChanged.emit(QModelIndex(), QModelIndex()) # update model's change status and clear model if necessary self.new = False self.changed = success self.read = False if not success: self.clear() return success def clear(self): # first call beginResetModel() self.beginResetModel() # now clear all model data self.project = Project() self.root = VariantItem("root item") self.species = {} self.variants = 0 self.new = True self.changed = False self.filename = "" self.file = False self.read = False self.last = QModelIndex() self.current = -1 self.count = 0 self.length = 0 # it's necessary to call endResetModel() self.endResetModel() def calculate(self): shelters = [] for child in self.root.children: if child.type == Fence.TYPE and child.hasChildren(): # fence result is set to fence length #child.result = child.protection.length for shelter in child.children: # the following operations are based on the example calculation # of Dr. Anton Hammer <*****@*****.**> slope = shelter.sumCosts() * (1 - shelter.plant.mortality) - child.sumCosts() result = child.protection.installation / slope # update result in model's item index = self.createIndex(shelter.childNumber(), 0, shelter) self.setData(index, result, TreeModel.ResultRole) else: shelters.append(child.name) # an empty list means that the calculation was successful if not shelters: self.calculated.emit() return shelters
class editorWidget(QWidget, Ui_editorWidget_ui): toggledSpellcheck = pyqtSignal(bool) dictChanged = pyqtSignal(str) def __init__(self, parent): QWidget.__init__(self, parent) self.setupUi(self) self.currentIndex = QModelIndex() self.currentID = None self.txtEdits = [] self.scroll.setBackgroundRole(QPalette.Base) self.toggledSpellcheck.connect(self.txtRedacText.toggleSpellcheck, AUC) self.dictChanged.connect(self.txtRedacText.setDict, AUC) self.txtRedacText.setHighlighting(True) self.currentDict = "" self.spellcheck = True self.folderView = "cork" self.mw = mainWindow() # def setModel(self, model): # self._model = model # self.setView() def setFolderView(self, v): oldV = self.folderView if v == "cork": self.folderView = "cork" elif v == "outline": self.folderView = "outline" else: self.folderView = "text" # Saving value settings.folderView = self.folderView if oldV != self.folderView and self.currentIndex: self.setCurrentModelIndex(self.currentIndex) def setCorkSizeFactor(self, v): self.corkView.itemDelegate().setCorkSizeFactor(v) self.redrawCorkItems() def redrawCorkItems(self): r = self.corkView.rootIndex() if r.isValid(): count = r.internalPointer().childCount() else: count = self.mw.mdlOutline.rootItem.childCount() for c in range(count): self.corkView.itemDelegate().sizeHintChanged.emit(r.child(c, 0)) def setView(self): # index = mainWindow().treeRedacOutline.currentIndex() # Couting the number of other selected items # sel = [] # for i in mainWindow().treeRedacOutline.selectionModel().selection().indexes(): # if i.column() != 0: continue # if i not in sel: sel.append(i) # if len(sel) != 0: # item = index.internalPointer() # else: # index = QModelIndex() # item = self.mw.mdlOutline.rootItem # self.currentIndex = index if self.currentIndex.isValid(): item = self.currentIndex.internalPointer() else: item = self.mw.mdlOutline.rootItem def addTitle(itm): edt = textEditView(self, html="<h{l}>{t}</h{l}>".format(l=min(itm.level() + 1, 5), t=itm.title()), autoResize=True) edt.setFrameShape(QFrame.NoFrame) self.txtEdits.append(edt) l.addWidget(edt) def addLine(): line = QFrame(self.text) line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) l.addWidget(line) def addText(itm): edt = textEditView(self, index=itm.index(), spellcheck=self.spellcheck, dict=settings.dict, highlighting=True, autoResize=True) edt.setFrameShape(QFrame.NoFrame) edt.setStyleSheet("background: {};".format(settings.textEditor["background"])) edt.setStatusTip("{} ({})".format(itm.path(), itm.type())) self.toggledSpellcheck.connect(edt.toggleSpellcheck, AUC) self.dictChanged.connect(edt.setDict, AUC) # edt.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.txtEdits.append(edt) l.addWidget(edt) def addChildren(itm): for c in range(itm.childCount()): child = itm.child(c) if child.isFolder(): addTitle(child) addChildren(child) else: addText(child) addLine() def addSpacer(): l.addItem(QSpacerItem(10, 1000, QSizePolicy.Minimum, QSizePolicy.Expanding)) # Display multiple selected items # if len(sel) > 1 and False: # Buggy and not very useful, skip # self.stack.setCurrentIndex(1) # w = QWidget() # l = QVBoxLayout(w) # self.txtEdits = [] # for idx in sel: # sItem = idx.internalPointer() # addTitle(sItem) # if sItem.isFolder(): # addChildren(sItem) # else: # addText(sItem) # addLine() # addSpacer() # self.scroll.setWidget(w) if item and item.isFolder() and self.folderView == "text": self.stack.setCurrentIndex(1) w = QWidget() l = QVBoxLayout(w) w.setStyleSheet("background: {};".format(settings.textEditor["background"])) # self.scroll.setWidgetResizable(False) self.txtEdits = [] if item != self.mw.mdlOutline.rootItem: addTitle(item) addChildren(item) addSpacer() self.scroll.setWidget(w) elif item and item.isFolder() and self.folderView == "cork": self.stack.setCurrentIndex(2) self.corkView.setModel(self.mw.mdlOutline) self.corkView.setRootIndex(self.currentIndex) self.corkView.selectionModel().selectionChanged.connect( lambda: mainWindow().redacMetadata.selectionChanged(self.corkView), AUC) self.corkView.clicked.connect( lambda: mainWindow().redacMetadata.selectionChanged(self.corkView), AUC) elif item and item.isFolder() and self.folderView == "outline": self.stack.setCurrentIndex(3) self.outlineView.setModelPersos(mainWindow().mdlPersos) self.outlineView.setModelLabels(mainWindow().mdlLabels) self.outlineView.setModelStatus(mainWindow().mdlStatus) self.outlineView.setModel(self.mw.mdlOutline) self.outlineView.setRootIndex(self.currentIndex) self.outlineView.selectionModel().selectionChanged.connect( lambda: mainWindow().redacMetadata.selectionChanged(self.outlineView), AUC) self.outlineView.clicked.connect( lambda: mainWindow().redacMetadata.selectionChanged(self.outlineView), AUC) else: self.txtRedacText.setCurrentModelIndex(self.currentIndex) self.stack.setCurrentIndex(0) # Single text item try: self.mw.mdlOutline.dataChanged.connect(self.modelDataChanged, AUC) self.mw.mdlOutline.rowsInserted.connect(self.updateIndexFromID, AUC) self.mw.mdlOutline.rowsRemoved.connect(self.updateIndexFromID, AUC) self.mw.mdlOutline.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved, AUC) except TypeError: pass self.updateStatusBar() def setCurrentModelIndex(self, index=None): if index.isValid(): self.currentIndex = index self.currentID = self.mw.mdlOutline.ID(index) # self._model = index.model() else: self.currentIndex = QModelIndex() self.setView() def updateIndexFromID(self): idx = self.mw.mdlOutline.getIndexByID(self.currentID) if idx != self.currentIndex: self.currentIndex = idx self.setView() def modelDataChanged(self, topLeft, bottomRight): # if self.currentID: # self.updateIndexFromID() if not self.currentIndex: return if topLeft.row() <= self.currentIndex.row() <= bottomRight.row(): self.updateStatusBar() def rowsAboutToBeRemoved(self, parent, first, last): if self.currentIndex: if self.currentIndex.parent() == parent and \ first <= self.currentIndex.row() <= last: # Item deleted, close tab self.mw.mainEditor.tab.removeTab(self.mw.mainEditor.tab.indexOf(self)) def updateStatusBar(self): # Update progress # if self.currentIndex and self.currentIndex.isValid(): # if self._model: mw = mainWindow() if not mw: return mw.mainEditor.updateStats() def toggleSpellcheck(self, v): self.spellcheck = v self.toggledSpellcheck.emit(v) def setDict(self, dct): self.currentDict = dct self.dictChanged.emit(dct)
class textEditView(QTextEdit): def __init__(self, parent=None, index=None, html=None, spellcheck=None, highlighting=False, dict="", autoResize=False): QTextEdit.__init__(self, parent) self._column = Outline.text self._index = None self._indexes = None self._model = None self._placeholderText = self.placeholderText() self._updating = False self._item = None self._highlighting = highlighting self._textFormat = "text" self.setAcceptRichText(False) # When setting up a theme, this becomes true. self._fromTheme = False self._themeData = None self._highlighterClass = BasicHighlighter if spellcheck is None: spellcheck = settings.spellcheck self.spellcheck = spellcheck self.currentDict = dict if dict else settings.dict self._defaultFontSize = qApp.font().pointSize() self.highlighter = None self.setAutoResize(autoResize) self._defaultBlockFormat = QTextBlockFormat() self._defaultCharFormat = QTextCharFormat() self.highlightWord = "" self.highligtCS = False self._dict = None # self.document().contentsChanged.connect(self.submit, F.AUC) # Submit text changed only after 500ms without modifications self.updateTimer = QTimer() self.updateTimer.setInterval(500) self.updateTimer.setSingleShot(True) self.updateTimer.timeout.connect(self.submit) # self.updateTimer.timeout.connect(lambda: print("Timeout")) self.updateTimer.stop() self.document().contentsChanged.connect(self.updateTimer.start, F.AUC) # self.document().contentsChanged.connect(lambda: print("Document changed")) # self.document().contentsChanged.connect(lambda: print(self.objectName(), "Contents changed")) self.setEnabled(False) if index: self.setCurrentModelIndex(index) elif html: self.document().setHtml(html) self.setReadOnly(True) # Spellchecking if enchant and self.spellcheck: try: self._dict = enchant.Dict(self.currentDict if self.currentDict else self.getDefaultLocale()) except enchant.errors.DictNotFoundError: self.spellcheck = False else: self.spellcheck = False if self._highlighting and not self.highlighter: self.highlighter = self._highlighterClass(self) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) def getDefaultLocale(self): default_locale = enchant.get_default_language() if default_locale is None: default_locale = QLocale.system().name() if default_locale is None: default_locale = enchant.list_dicts()[0][0] return default_locale def setModel(self, model): self._model = model try: self._model.dataChanged.connect(self.update, F.AUC) except TypeError: pass def setColumn(self, col): self._column = col def setHighlighting(self, val): self._highlighting = val def setDefaultBlockFormat(self, bf): self._defaultBlockFormat = bf if self.highlighter: self.highlighter.setDefaultBlockFormat(bf) def setCurrentModelIndex(self, index): self._indexes = None if index.isValid(): self.setEnabled(True) if index.column() != self._column: index = index.sibling(index.row(), self._column) self._index = QPersistentModelIndex(index) self.setPlaceholderText(self._placeholderText) if not self._model: self.setModel(index.model()) self.setupEditorForIndex(self._index) self.loadFontSettings() self.updateText() else: self._index = QModelIndex() self.setPlainText("") self.setEnabled(False) def currentIndex(self): """ Getter function used to normalized views access with QAbstractItemViews. """ if self._index: return self._index else: return QModelIndex() def getSelection(self): """ Getter function used to normalized views access with QAbstractItemViews. """ return [self.currentIndex()] def setCurrentModelIndexes(self, indexes): self._index = None self._indexes = [] for i in indexes: if i.isValid(): self.setEnabled(True) if i.column() != self._column: i = i.sibling(i.row(), self._column) self._indexes.append(QModelIndex(i)) if not self._model: self.setModel(i.model()) self.updateText() def setupEditorForIndex(self, index): # Setting highlighter if self._highlighting: self.highlighter = self._highlighterClass(self) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) self.highlighter.updateColorScheme() def loadFontSettings(self): if self._fromTheme or \ not self._index or \ type(self._index.model()) != outlineModel or \ self._column != Outline.text: return opt = settings.textEditor f = QFont() f.fromString(opt["font"]) background = (opt["background"] if not opt["backgroundTransparent"] else "transparent") foreground = opt["fontColor"] # if not opt["backgroundTransparent"] # else S.text # self.setFont(f) self.setStyleSheet("""QTextEdit{{ background: {bg}; color: {foreground}; font-family: {ff}; font-size: {fs}; margin: {mTB}px {mLR}px; {maxWidth} }} """.format( bg=background, foreground=foreground, ff=f.family(), fs="{}pt".format(str(f.pointSize())), mTB = opt["marginsTB"], mLR = opt["marginsLR"], maxWidth = "max-width: {}px;".format(opt["maxWidth"]) if opt["maxWidth"] else "", ) ) self._defaultFontSize = f.pointSize() # We set the parent background to the editor's background in case # there are margins. We check that the parent class is a QWidget because # if textEditView is used in fullScreenEditor, then we don't want to # set the background. if self.parent().__class__ == QWidget: self.parent().setStyleSheet(""" QWidget#{name}{{ background: {bg}; }}""".format( # We style by name, otherwise all inheriting widgets get the same # colored background, for example context menu. name=self.parent().objectName(), bg=background, )) cf = QTextCharFormat() # cf.setFont(f) # cf.setForeground(QColor(opt["fontColor"])) self.setCursorWidth(opt["cursorWidth"]) bf = QTextBlockFormat() bf.setLineHeight(opt["lineSpacing"], bf.ProportionalHeight) bf.setTextIndent(opt["tabWidth"] * 1 if opt["indent"] else 0) bf.setTopMargin(opt["spacingAbove"]) bf.setBottomMargin(opt["spacingBelow"]) bf.setAlignment(Qt.AlignLeft if opt["textAlignment"] == 0 else Qt.AlignCenter if opt["textAlignment"] == 1 else Qt.AlignRight if opt["textAlignment"] == 2 else Qt.AlignJustify) self._defaultCharFormat = cf self._defaultBlockFormat = bf if self.highlighter: self.highlighter.updateColorScheme() self.highlighter.setMisspelledColor(QColor(opt["misspelled"])) self.highlighter.setDefaultCharFormat(self._defaultCharFormat) self.highlighter.setDefaultBlockFormat(self._defaultBlockFormat) def update(self, topLeft, bottomRight): if self._updating: return if self._index and self._index.isValid(): if topLeft.parent() != self._index.parent(): return # print("Model changed: ({}:{}), ({}:{}/{}), ({}:{}) for {} of {}".format( # topLeft.row(), topLeft.column(), # self._index.row(), self._index.row(), self._column, # bottomRight.row(), bottomRight.column(), # self.objectName(), self.parent().objectName())) if topLeft.row() <= self._index.row() <= bottomRight.row(): if topLeft.column() <= self._column <= bottomRight.column(): self.updateText() elif self._indexes: update = False for i in self._indexes: if topLeft.row() <= i.row() <= bottomRight.row(): update = True if update: self.updateText() def disconnectDocument(self): try: self.document().contentsChanged.disconnect(self.updateTimer.start) except: pass def reconnectDocument(self): self.document().contentsChanged.connect(self.updateTimer.start, F.AUC) def updateText(self): if self._updating: return # print("Updating", self.objectName()) self._updating = True if self._index: self.disconnectDocument() if self.toPlainText() != F.toString(self._index.data()): # print(" Updating plaintext") self.document().setPlainText(F.toString(self._index.data())) self.reconnectDocument() elif self._indexes: self.disconnectDocument() t = [] same = True for i in self._indexes: item = i.internalPointer() t.append(F.toString(item.data(self._column))) for t2 in t[1:]: if t2 != t[0]: same = False break if same: self.document().setPlainText(t[0]) else: self.document().setPlainText("") if not self._placeholderText: self._placeholderText = self.placeholderText() self.setPlaceholderText(self.tr("Various")) self.reconnectDocument() self._updating = False def submit(self): self.updateTimer.stop() if self._updating: return # print("Submitting", self.objectName()) if self._index and self._index.isValid(): # item = self._index.internalPointer() if self.toPlainText() != self._index.data(): # print(" Submitting plain text") self._updating = True self._model.setData(QModelIndex(self._index), self.toPlainText()) self._updating = False elif self._indexes: self._updating = True for i in self._indexes: item = i.internalPointer() if self.toPlainText() != F.toString(item.data(self._column)): print("Submitting many indexes") self._model.setData(i, self.toPlainText()) self._updating = False def keyPressEvent(self, event): if event.key() == Qt.Key_V and event.modifiers() & Qt.ControlModifier: text = QApplication.clipboard().text() self.insertPlainText(text) else: QTextEdit.keyPressEvent(self, event) if event.key() == Qt.Key_Space: self.submit() # ----------------------------------------------------------------------------------------------------- # Resize stuff def resizeEvent(self, e): QTextEdit.resizeEvent(self, e) if self._autoResize: self.sizeChange() def sizeChange(self): opt = settings.textEditor docHeight = self.document().size().height() + 2 * opt["marginsTB"] if self.heightMin <= docHeight <= self.heightMax: self.setMinimumHeight(docHeight) def setAutoResize(self, val): self._autoResize = val if self._autoResize: self.document().contentsChanged.connect(self.sizeChange) self.heightMin = 0 self.heightMax = 65000 self.sizeChange() ############################################################################### # SPELLCHECKING ############################################################################### # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ def setDict(self, d): self.currentDict = d if d and enchant.dict_exists(d): self._dict = enchant.Dict(d) if self.highlighter: self.highlighter.rehighlight() def toggleSpellcheck(self, v): self.spellcheck = v if enchant and self.spellcheck and not self._dict: if self.currentDict and enchant.dict_exists(self.currentDict): self._dict = enchant.Dict(self.currentDict) elif enchant.get_default_language() and enchant.dict_exists(enchant.get_default_language()): self._dict = enchant.Dict(enchant.get_default_language()) else: self.spellcheck = False if self.highlighter: self.highlighter.rehighlight() else: self.spellcheck = False def mousePressEvent(self, event): if event.button() == Qt.RightButton: # Rewrite the mouse event to a left button event so the cursor is # moved to the location of the pointer. event = QMouseEvent(QEvent.MouseButtonPress, event.pos(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier) QTextEdit.mousePressEvent(self, event) def wheelEvent(self, event): """ We catch wheelEvent if key modifier is CTRL to change font size. Note: this should be in a class specific for main textEditView (#TODO). """ if event.modifiers() & Qt.ControlModifier: # Get the wheel angle. d = event.angleDelta().y() / 120 # Update settings f = QFont() f.fromString(settings.textEditor["font"]) f.setPointSizeF(f.pointSizeF() + d) settings.textEditor["font"] = f.toString() # Update font to all textEditView. Drastically. for w in F.mainWindow().findChildren(textEditView, QRegExp(".*")): w.loadFontSettings() # We tell the world that we accepted this event event.accept() return QTextEdit.wheelEvent(self, event) class SpellAction(QAction): """A special QAction that returns the text in a signal. Used for spellcheck.""" correct = pyqtSignal(str) def __init__(self, *args): QAction.__init__(self, *args) self.triggered.connect(lambda x: self.correct.emit( str(self.text()))) def contextMenuEvent(self, event): # Based on http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check/ popup_menu = self.createStandardContextMenu() popup_menu.exec_(event.globalPos()) def createStandardContextMenu(self): popup_menu = QTextEdit.createStandardContextMenu(self) if not self.spellcheck: return popup_menu # Select the word under the cursor. # But only if there is no selection (otherwise it's impossible to select more text to copy/cut) cursor = self.textCursor() if not cursor.hasSelection(): cursor.select(QTextCursor.WordUnderCursor) self.setTextCursor(cursor) # Check if the selected word is misspelled and offer spelling # suggestions if it is. if self._dict and cursor.hasSelection(): text = str(cursor.selectedText()) valid = self._dict.check(text) selectedWord = cursor.selectedText() if not valid: spell_menu = QMenu(self.tr('Spelling Suggestions'), self) spell_menu.setIcon(F.themeIcon("spelling")) for word in self._dict.suggest(text): action = self.SpellAction(word, spell_menu) action.correct.connect(self.correctWord) spell_menu.addAction(action) # Only add the spelling suggests to the menu if there are # suggestions. if len(spell_menu.actions()) != 0: popup_menu.insertSeparator(popup_menu.actions()[0]) # Adds: add to dictionary addAction = QAction(self.tr("&Add to dictionary"), popup_menu) addAction.setIcon(QIcon.fromTheme("list-add")) addAction.triggered.connect(self.addWordToDict) addAction.setData(selectedWord) popup_menu.insertAction(popup_menu.actions()[0], addAction) # Adds: suggestions popup_menu.insertMenu(popup_menu.actions()[0], spell_menu) # popup_menu.insertSeparator(popup_menu.actions()[0]) # If word was added to custom dict, give the possibility to remove it elif valid and self._dict.is_added(selectedWord): popup_menu.insertSeparator(popup_menu.actions()[0]) # Adds: remove from dictionary rmAction = QAction(self.tr("&Remove from custom dictionary"), popup_menu) rmAction.setIcon(QIcon.fromTheme("list-remove")) rmAction.triggered.connect(self.rmWordFromDict) rmAction.setData(selectedWord) popup_menu.insertAction(popup_menu.actions()[0], rmAction) return popup_menu def correctWord(self, word): """ Replaces the selected text with word. """ cursor = self.textCursor() cursor.beginEditBlock() cursor.removeSelectedText() cursor.insertText(word) cursor.endEditBlock() def addWordToDict(self): word = self.sender().data() self._dict.add(word) self.highlighter.rehighlight() def rmWordFromDict(self): word = self.sender().data() self._dict.remove(word) self.highlighter.rehighlight() ############################################################################### # FORMATTING ############################################################################### def focusOutEvent(self, event): """Submit changes just before focusing out.""" QTextEdit.focusOutEvent(self, event) self.submit() ############################################################################### # KEYBOARD SHORTCUTS ############################################################################### def callMainTreeView(self, functionName): """ The tree view in main window must have same index as the text edit that has focus. So we can pass it the call for documents edits like: duplicate, move up, etc. """ if self._index and self._column == Outline.text: function = getattr(F.mainWindow().treeRedacOutline, functionName) function() def rename(self): self.callMainTreeView("rename") def duplicate(self): self.callMainTreeView("duplicate") def moveUp(self): self.callMainTreeView("moveUp") def moveDown(self): self.callMainTreeView("moveDown")
class editorWidget(QWidget, Ui_editorWidget_ui): """ `editorWidget` is a class responsible for displaying and editing one `outlineItem`. This item can be a folder or a text. It has four views (see `self.setView`) - For folders: "text", "outline" or "cork" (set in `self.folderView`) Text: displays a list of `textEditView` in a scroll area Outline: displays an outline, using an `outlineView` Cork: displays flash cards, using a `corkView` - For text: item is simply displayed in a `textEditView` All those views are contained in `editorWidget` single widget: `self.stack`. `editorWidget` are managed in `tabSplitted` (that allow to open several `outlineItem`s, either in Tabs or in split views. `tabSplitted` are in turn managed by the `mainEditor`, which is unique and gives UI buttons to manage all those views. """ toggledSpellcheck = pyqtSignal(bool) dictChanged = pyqtSignal(str) _maxTabTitleLength = 24 def __init__(self, parent): QWidget.__init__(self, parent) self.setupUi(self) self.currentIndex = QModelIndex() self.currentID = None self.txtEdits = [] self.scroll.setBackgroundRole(QPalette.Base) self.toggledSpellcheck.connect(self.txtRedacText.toggleSpellcheck, AUC) self.dictChanged.connect(self.txtRedacText.setDict, AUC) self.txtRedacText.setHighlighting(True) self.currentDict = "" self.spellcheck = settings.spellcheck self.folderView = "cork" self.mw = mainWindow() self._tabWidget = None # set by mainEditor on creation self._model = None # Capture textEdit scrollbar, so that we can put it outside the margins. self.txtEditScrollBar = self.txtRedacText.verticalScrollBar() self.txtEditScrollBar.setParent(self) self.stack.currentChanged.connect(self.setScrollBarVisibility) # def setModel(self, model): # self._model = model # self.setView() def resizeEvent(self, event): """ textEdit's scrollBar has been reparented to self. So we need to update it's geomtry when self is resized, and put it where we want it to be. """ # Update scrollbar geometry r = self.geometry() w = 10 # Cf. style.mainEditorTabSS r.setWidth(w) r.moveRight(self.geometry().width()) self.txtEditScrollBar.setGeometry(r) QWidget.resizeEvent(self, event) def setScrollBarVisibility(self): """ Since the texteEdit scrollBar has been reparented to self, it is not hidden when stack changes. We have to do it manually. """ self.txtEditScrollBar.setVisible(self.stack.currentIndex() == 0) def setFolderView(self, v): oldV = self.folderView if v == "cork": self.folderView = "cork" elif v == "outline": self.folderView = "outline" else: self.folderView = "text" # Saving value settings.folderView = self.folderView if oldV != self.folderView and self.currentIndex: self.setCurrentModelIndex(self.currentIndex) def setCorkSizeFactor(self, v): self.corkView.itemDelegate().setCorkSizeFactor(v) self.redrawCorkItems() def redrawCorkItems(self): r = self.corkView.rootIndex() if r.isValid(): count = r.internalPointer().childCount() elif self._model: count = self._model.rootItem.childCount() else: count = 0 for c in range(count): self.corkView.itemDelegate().sizeHintChanged.emit(r.child(c, 0)) def updateTabTitle(self): """ `editorWidget` belongs to a `QTabWidget` in a `tabSplitter`. We update the tab title to reflect that of current item. """ # `self._tabWidget` is set by mainEditor when creating tab and `editorWidget`. # if `editorWidget` is ever used out of `mainEditor`, this could throw # an error. if not self._tabWidget: return if self.currentIndex.isValid(): item = self.currentIndex.internalPointer() elif self._model: item = self._model.rootItem else: return i = self._tabWidget.indexOf(self) self._tabWidget.setTabText(i, self.ellidedTitle(item.title())) self._tabWidget.setTabToolTip(i, item.title()) def ellidedTitle(self, title): if len(title) > self._maxTabTitleLength: return "{}…".format(title[:self._maxTabTitleLength]) else: return title def setView(self): # index = mainWindow().treeRedacOutline.currentIndex() # Counting the number of other selected items # sel = [] # for i in mainWindow().treeRedacOutline.selectionModel().selection().indexes(): # if i.column() != 0: continue # if i not in sel: sel.append(i) # if len(sel) != 0: # item = index.internalPointer() # else: # index = QModelIndex() # item = self.mw.mdlOutline.rootItem # self.currentIndex = index if self.currentIndex.isValid(): item = self.currentIndex.internalPointer() else: item = self.mw.mdlOutline.rootItem self.updateTabTitle() def addTitle(itm): edt = MDEditView(self, html="<h{l}>{t}</h{l}>".format(l=min(itm.level() + 1, 5), t=itm.title()), autoResize=True) edt.setFrameShape(QFrame.NoFrame) self.txtEdits.append(edt) l.addWidget(edt) def addLine(): line = QFrame(self.text) line.setFrameShape(QFrame.HLine) line.setFrameShadow(QFrame.Sunken) l.addWidget(line) def addText(itm): edt = MDEditView(self, index=itm.index(), spellcheck=self.spellcheck, dict=settings.dict, highlighting=True, autoResize=True) edt.setFrameShape(QFrame.NoFrame) edt.setStatusTip("{}".format(itm.path())) self.toggledSpellcheck.connect(edt.toggleSpellcheck, AUC) self.dictChanged.connect(edt.setDict, AUC) # edt.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.txtEdits.append(edt) l.addWidget(edt) def addChildren(itm): for c in range(itm.childCount()): child = itm.child(c) if child.isFolder(): addTitle(child) addChildren(child) else: addText(child) addLine() def addSpacer(): l.addItem(QSpacerItem(10, 1000, QSizePolicy.Minimum, QSizePolicy.Expanding)) # Display multiple selected items # if len(sel) > 1 and False: # Buggy and not very useful, skip # self.stack.setCurrentIndex(1) # w = QWidget() # l = QVBoxLayout(w) # self.txtEdits = [] # for idx in sel: # sItem = idx.internalPointer() # addTitle(sItem) # if sItem.isFolder(): # addChildren(sItem) # else: # addText(sItem) # addLine() # addSpacer() # self.scroll.setWidget(w) if item and item.isFolder() and self.folderView == "text": self.stack.setCurrentIndex(1) w = QWidget() w.setObjectName("editorWidgetFolderText") l = QVBoxLayout(w) opt = settings.textEditor background = (opt["background"] if not opt["backgroundTransparent"] else "transparent") w.setStyleSheet("background: {};".format(background)) self.stack.widget(1).setStyleSheet("background: {}" .format(background)) # self.scroll.setWidgetResizable(False) self.txtEdits = [] if item != self._model.rootItem: addTitle(item) addChildren(item) addSpacer() self.scroll.setWidget(w) elif item and item.isFolder() and self.folderView == "cork": self.stack.setCurrentIndex(2) self.corkView.setModel(self._model) self.corkView.setRootIndex(self.currentIndex) try: self.corkView.selectionModel().selectionChanged.connect(mainWindow().redacMetadata.selectionChanged, AUC) self.corkView.clicked.connect(mainWindow().redacMetadata.selectionChanged, AUC) self.corkView.clicked.connect(mainWindow().mainEditor.updateTargets, AUC) except TypeError: pass elif item and item.isFolder() and self.folderView == "outline": self.stack.setCurrentIndex(3) self.outlineView.setModelCharacters(mainWindow().mdlCharacter) self.outlineView.setModelLabels(mainWindow().mdlLabels) self.outlineView.setModelStatus(mainWindow().mdlStatus) self.outlineView.setModel(self._model) self.outlineView.setRootIndex(self.currentIndex) try: self.outlineView.selectionModel().selectionChanged.connect(mainWindow().redacMetadata.selectionChanged, AUC) self.outlineView.clicked.connect(mainWindow().redacMetadata.selectionChanged, AUC) self.outlineView.clicked.connect(mainWindow().mainEditor.updateTargets, AUC) except TypeError: pass if item and item.isText(): self.txtRedacText.setCurrentModelIndex(self.currentIndex) self.stack.setCurrentIndex(0) # Single text item else: self.txtRedacText.setCurrentModelIndex(QModelIndex()) try: self._model.dataChanged.connect(self.modelDataChanged, AUC) self._model.rowsInserted.connect(self.updateIndexFromID, AUC) self._model.rowsRemoved.connect(self.updateIndexFromID, AUC) #self.mw.mdlOutline.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved, AUC) except TypeError: pass self.updateStatusBar() def setCurrentModelIndex(self, index=None): if index.isValid(): self.currentIndex = index self._model = index.model() self.currentID = self._model.ID(index) else: self.currentIndex = QModelIndex() self.currentID = None if self._model: self.setView() def updateIndexFromID(self): """ Index might have changed (through drag an drop), so we keep current item's ID and update index. Item might have been deleted too. """ idx = self._model.getIndexByID(self.currentID) # If we have an ID but the ID does not exist, it has been deleted if self.currentID and idx == QModelIndex(): # Item has been deleted, we open the parent instead self.setCurrentModelIndex(self.currentIndex.parent()) # FIXME: selection in self.mw.treeRedacOutline is not updated # but we cannot simply setCurrentIndex through treeRedacOutline # because this might be a tab in the background / out of focus # Also the UI of mainEditor is not updated (so the folder icons # are not display, button "up" doesn't work, etc.). # Item has been moved elif idx != self.currentIndex: # We update the index self.currentIndex = idx self.setView() def modelDataChanged(self, topLeft, bottomRight): # if self.currentID: # self.updateIndexFromID() if not self.currentIndex: return if topLeft.row() <= self.currentIndex.row() <= bottomRight.row(): self.updateStatusBar() #def rowsAboutToBeRemoved(self, parent, first, last): #if self.currentIndex: #if self.currentIndex.parent() == parent and \ #first <= self.currentIndex.row() <= last: ## Item deleted, close tab #self.mw.mainEditor.tab.removeTab(self.mw.mainEditor.tab.indexOf(self)) def updateStatusBar(self): # Update progress # if self.currentIndex and self.currentIndex.isValid(): # if self._model: mw = mainWindow() if not mw: return mw.mainEditor.updateStats() def toggleSpellcheck(self, v): self.spellcheck = v self.toggledSpellcheck.emit(v) def setDict(self, dct): self.currentDict = dct self.dictChanged.emit(dct) ############################################################################### # FUNCTIONS FOR MENU ACCESS ############################################################################### def getCurrentItemView(self): """ Returns the current item view, between txtRedacText, outlineView and corkView. If folder/text view, returns None. (Because handled differently) """ if self.stack.currentIndex() == 0: return self.txtRedacText elif self.folderView == "outline": return self.outlineView elif self.folderView == "cork": return self.corkView else: return None def copy(self): if self.getCurrentItemView(): self.getCurrentItemView().copy() def cut(self): if self.getCurrentItemView(): self.getCurrentItemView().cut() def paste(self): if self.getCurrentItemView(): self.getCurrentItemView().paste() def rename(self): if self.getCurrentItemView(): self.getCurrentItemView().rename() def duplicate(self): if self.getCurrentItemView(): self.getCurrentItemView().duplicate() def delete(self): if self.getCurrentItemView(): self.getCurrentItemView().delete() def moveUp(self): if self.getCurrentItemView(): self.getCurrentItemView().moveUp() def moveDown(self): if self.getCurrentItemView(): self.getCurrentItemView().moveDown() def splitDialog(self): """ Opens a dialog to split selected items. """ if self.getCurrentItemView() == self.txtRedacText: # Text editor if not self.currentIndex.isValid(): return sel = self.txtRedacText.textCursor().selectedText() # selectedText uses \u2029 instead of \n, no idea why. sel = sel.replace("\u2029", "\n") splitDialog(self, [self.currentIndex], mark=sel) elif self.getCurrentItemView(): # One of the views self.getCurrentItemView().splitDialog() def splitCursor(self): """ Splits items at cursor position. If there is a selection, that selection becomes the new item's title. Call context: Only works when editing a file. """ if not self.currentIndex.isValid(): return if self.getCurrentItemView() == self.txtRedacText: c = self.txtRedacText.textCursor() title = c.selectedText() # selection can be backward pos = min(c.selectionStart(), c.selectionEnd()) item = self.currentIndex.internalPointer() item.splitAt(pos, len(title)) def merge(self): """ Merges selected items together. Call context: Multiple selection, same parent. """ if self.getCurrentItemView() == self.txtRedacText: # Text editor, nothing to merge pass elif self.getCurrentItemView(): # One of the views self.getCurrentItemView().merge()