def main(argv): app = QApplication(argv) main_window = uic.loadUi('mainwindow.ui') table_view = main_window.findChild(QTableView, 'tableView') model = QStandardItemModel() root_item = Root() model.appendRow(root_item) table_model = QSortFilterProxyModel() table_model.setSourceModel(model) table_view.setModel(table_model) table_view.setRootIndex(table_model.mapFromSource(root_item.index())) main_window.show() sys.exit(app.exec_())
class BreakPointViewer(QTreeView): """ Class implementing the Breakpoint viewer widget. Breakpoints will be shown with all their details. They can be modified through the context menu of this widget. @signal sourceFile(str, int) emitted to show the source of a breakpoint """ sourceFile = pyqtSignal(str, int) def __init__(self, parent=None): """ Constructor @param parent the parent (QWidget) """ super(BreakPointViewer, self).__init__(parent) self.setObjectName("BreakPointViewer") self.__model = None self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setWindowTitle(self.tr("Breakpoints")) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() self.condHistory = [] self.fnHistory = [] self.fnHistory.append('') def setModel(self, model): """ Public slot to set the breakpoint model. @param model reference to the breakpoint model (BreakPointModel) """ self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setDynamicSortFilter(True) self.sortingModel.setSourceModel(self.__model) super(BreakPointViewer, self).setModel(self.sortingModel) header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) if qVersion() >= "5.0.0": header.setSectionsClickable(True) else: header.setClickable(True) self.setSortingEnabled(True) self.__layoutDisplay() def __layoutDisplay(self): """ Private slot to perform a layout operation. """ self.__resizeColumns() self.__resort() def __resizeColumns(self): """ Private slot to resize the view when items get added, edited or deleted. """ self.header().resizeSections(QHeaderView.ResizeToContents) self.header().setStretchLastSection(True) def __resort(self): """ Private slot to resort the tree. """ self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def __toSourceIndex(self, index): """ Private slot to convert an index to a source index. @param index index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """ Private slot to convert a source index to an index. @param sindex source index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """ Private slot to select a complete row. @param index index determining the row to be selected (QModelIndex) @param selected flag indicating the action (bool) """ if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """ Private method to generate the popup menus. """ self.menu = QMenu() self.menu.addAction(self.tr("Add"), self.__addBreak) self.menu.addAction(self.tr("Edit..."), self.__editBreak) self.menu.addSeparator() self.menu.addAction(self.tr("Enable"), self.__enableBreak) self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Disable"), self.__disableBreak) self.menu.addAction(self.tr("Disable all"), self.__disableAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Delete"), self.__deleteBreak) self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Goto"), self.__showSource) self.menu.addSeparator() self.menu.addAction(self.tr("Configure..."), self.__configure) self.backMenuActions = {} self.backMenu = QMenu() self.backMenu.addAction(self.tr("Add"), self.__addBreak) self.backMenuActions["EnableAll"] = \ self.backMenu.addAction(self.tr("Enable all"), self.__enableAllBreaks) self.backMenuActions["DisableAll"] = \ self.backMenu.addAction(self.tr("Disable all"), self.__disableAllBreaks) self.backMenuActions["DeleteAll"] = \ self.backMenu.addAction(self.tr("Delete all"), self.__deleteAllBreaks) self.backMenu.aboutToShow.connect(self.__showBackMenu) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Configure..."), self.__configure) self.multiMenu = QMenu() self.multiMenu.addAction(self.tr("Add"), self.__addBreak) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Enable selected"), self.__enableSelectedBreaks) self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Disable selected"), self.__disableSelectedBreaks) self.multiMenu.addAction(self.tr("Disable all"), self.__disableAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Delete selected"), self.__deleteSelectedBreaks) self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Configure..."), self.__configure) def __showContextMenu(self, coord): """ Private slot to show the context menu. @param coord the position of the mouse pointer (QPoint) """ cnt = self.__getSelectedItemsCount() if cnt <= 1: index = self.indexAt(coord) if index.isValid(): cnt = 1 self.__setRowSelected(index) coord = self.mapToGlobal(coord) if cnt > 1: self.multiMenu.popup(coord) elif cnt == 1: self.menu.popup(coord) else: self.backMenu.popup(coord) def __clearSelection(self): """ Private slot to clear the selection. """ for index in self.selectedIndexes(): self.__setRowSelected(index, False) def __addBreak(self): """ Private slot to handle the add breakpoint context menu entry. """ from .EditBreakpointDialog import EditBreakpointDialog dlg = EditBreakpointDialog((self.fnHistory[0], None), None, self.condHistory, self, modal=1, addMode=1, filenameHistory=self.fnHistory) if dlg.exec_() == QDialog.Accepted: fn, line, cond, temp, enabled, count = dlg.getAddData() if fn is not None: if fn in self.fnHistory: self.fnHistory.remove(fn) self.fnHistory.insert(0, fn) if cond: if cond in self.condHistory: self.condHistory.remove(cond) self.condHistory.insert(0, cond) self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count)) self.__resizeColumns() self.__resort() def __doubleClicked(self, index): """ Private slot to handle the double clicked signal. @param index index of the entry that was double clicked (QModelIndex) """ if index.isValid(): self.__editBreakpoint(index) def __editBreak(self): """ Private slot to handle the edit breakpoint context menu entry. """ index = self.currentIndex() if index.isValid(): self.__editBreakpoint(index) def __editBreakpoint(self, index): """ Private slot to edit a breakpoint. @param index index of breakpoint to be edited (QModelIndex) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): bp = self.__model.getBreakPointByIndex(sindex) if not bp: return fn, line, cond, temp, enabled, count = bp[:6] from .EditBreakpointDialog import EditBreakpointDialog dlg = EditBreakpointDialog( (fn, line), (cond, temp, enabled, count), self.condHistory, self, modal=True) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, count = dlg.getData() if cond: if cond in self.condHistory: self.condHistory.remove(cond) self.condHistory.insert(0, cond) self.__model.setBreakPointByIndex( sindex, fn, line, (cond, temp, enabled, count)) self.__resizeColumns() self.__resort() def __setBpEnabled(self, index, enabled): """ Private method to set the enabled status of a breakpoint. @param index index of breakpoint to be enabled/disabled (QModelIndex) @param enabled flag indicating the enabled status to be set (boolean) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.setBreakPointEnabledByIndex(sindex, enabled) def __enableBreak(self): """ Private slot to handle the enable breakpoint context menu entry. """ index = self.currentIndex() self.__setBpEnabled(index, True) self.__resizeColumns() self.__resort() def __enableAllBreaks(self): """ Private slot to handle the enable all breakpoints context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __enableSelectedBreaks(self): """ Private slot to handle the enable selected breakpoints context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setBpEnabled(index, True) self.__resizeColumns() self.__resort() def __disableBreak(self): """ Private slot to handle the disable breakpoint context menu entry. """ index = self.currentIndex() self.__setBpEnabled(index, False) self.__resizeColumns() self.__resort() def __disableAllBreaks(self): """ Private slot to handle the disable all breakpoints context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __disableSelectedBreaks(self): """ Private slot to handle the disable selected breakpoints context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setBpEnabled(index, False) self.__resizeColumns() self.__resort() def __deleteBreak(self): """ Private slot to handle the delete breakpoint context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.deleteBreakPointByIndex(sindex) def __deleteAllBreaks(self): """ Private slot to handle the delete all breakpoints context menu entry. """ self.__model.deleteAll() def __deleteSelectedBreaks(self): """ Private slot to handle the delete selected breakpoints context menu entry. """ idxList = [] for index in self.selectedIndexes(): sindex = self.__toSourceIndex(index) if sindex.isValid() and index.column() == 0: idxList.append(sindex) self.__model.deleteBreakPoints(idxList) def __showSource(self): """ Private slot to handle the goto context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) bp = self.__model.getBreakPointByIndex(sindex) if not bp: return fn, line = bp[:2] self.sourceFile.emit(fn, line) def highlightBreakpoint(self, fn, lineno): """ Public slot to handle the clientLine signal. @param fn filename of the breakpoint (string) @param lineno line number of the breakpoint (integer) """ sindex = self.__model.getBreakPointIndex(fn, lineno) if sindex.isValid(): return index = self.__fromSourceIndex(sindex) if index.isValid(): self.__clearSelection() self.__setRowSelected(index, True) def handleResetUI(self): """ Public slot to reset the breakpoint viewer. """ self.__clearSelection() def __showBackMenu(self): """ Private slot to handle the aboutToShow signal of the background menu. """ if self.model().rowCount() == 0: self.backMenuActions["EnableAll"].setEnabled(False) self.backMenuActions["DisableAll"].setEnabled(False) self.backMenuActions["DeleteAll"].setEnabled(False) else: self.backMenuActions["EnableAll"].setEnabled(True) self.backMenuActions["DisableAll"].setEnabled(True) self.backMenuActions["DeleteAll"].setEnabled(True) def __getSelectedItemsCount(self): """ Private method to get the count of items selected. @return count of items selected (integer) """ count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface").showPreferences( "debuggerGeneralPage")
class Explorer(QWidget): """ This class implements the diagram predicate node explorer. """ def __init__(self, mainwindow): """ Initialize the Explorer. :type mainwindow: MainWindow """ super().__init__(mainwindow) self.expanded = {} self.searched = {} self.scrolled = {} self.mainview = None self.iconA = QIcon(':/icons/treeview-icon-attribute') self.iconC = QIcon(':/icons/treeview-icon-concept') self.iconD = QIcon(':/icons/treeview-icon-datarange') self.iconI = QIcon(':/icons/treeview-icon-instance') self.iconR = QIcon(':/icons/treeview-icon-role') self.iconV = QIcon(':/icons/treeview-icon-value') self.search = StringField(self) self.search.setAcceptDrops(False) self.search.setClearButtonEnabled(True) self.search.setPlaceholderText('Search...') self.search.setFixedHeight(30) self.model = QStandardItemModel(self) self.proxy = QSortFilterProxyModel(self) self.proxy.setDynamicSortFilter(False) self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive) self.proxy.setSortCaseSensitivity(Qt.CaseSensitive) self.proxy.setSourceModel(self.model) self.view = ExplorerView(mainwindow, self) self.view.setModel(self.proxy) self.mainLayout = QVBoxLayout(self) self.mainLayout.setContentsMargins(0, 0, 0, 0) self.mainLayout.addWidget(self.search) self.mainLayout.addWidget(self.view) self.setContentsMargins(0, 0, 0, 0) self.setMinimumWidth(216) self.setMinimumHeight(160) connect(self.view.doubleClicked, self.itemDoubleClicked) connect(self.view.pressed, self.itemPressed) connect(self.view.collapsed, self.itemCollapsed) connect(self.view.expanded, self.itemExpanded) connect(self.search.textChanged, self.filterItem) #################################################################################################################### # # # EVENTS # # # #################################################################################################################### def paintEvent(self, paintEvent): """ This is needed for the widget to pick the stylesheet. :type paintEvent: QPaintEvent """ option = QStyleOption() option.initFrom(self) painter = QPainter(self) style = self.style() style.drawPrimitive(QStyle.PE_Widget, option, painter, self) #################################################################################################################### # # # SLOTS # # # #################################################################################################################### @pyqtSlot('QGraphicsItem') def add(self, item): """ Add a node in the tree view. :type item: AbstractItem """ if item.node and item.predicate: parent = self.parentFor(item) if not parent: parent = ParentItem(item) parent.setIcon(self.iconFor(item)) self.model.appendRow(parent) self.proxy.sort(0, Qt.AscendingOrder) child = ChildItem(item) child.setData(item) parent.appendRow(child) self.proxy.sort(0, Qt.AscendingOrder) @pyqtSlot(str) def filterItem(self, key): """ Executed when the search box is filled with data. :type key: str """ if self.mainview: self.proxy.setFilterFixedString(key) self.proxy.sort(Qt.AscendingOrder) self.searched[self.mainview] = key @pyqtSlot('QModelIndex') def itemCollapsed(self, index): """ Executed when an item in the tree view is collapsed. :type index: QModelIndex """ if self.mainview: if self.mainview in self.expanded: item = self.model.itemFromIndex(self.proxy.mapToSource(index)) expanded = self.expanded[self.mainview] expanded.remove(item.text()) @pyqtSlot('QModelIndex') def itemDoubleClicked(self, index): """ Executed when an item in the tree view is double clicked. :type index: QModelIndex """ item = self.model.itemFromIndex(self.proxy.mapToSource(index)) node = item.data() if node: self.selectNode(node) self.focusNode(node) @pyqtSlot('QModelIndex') def itemExpanded(self, index): """ Executed when an item in the tree view is expanded. :type index: QModelIndex """ if self.mainview: item = self.model.itemFromIndex(self.proxy.mapToSource(index)) if self.mainview not in self.expanded: self.expanded[self.mainview] = set() expanded = self.expanded[self.mainview] expanded.add(item.text()) @pyqtSlot('QModelIndex') def itemPressed(self, index): """ Executed when an item in the tree view is clicked. :type index: QModelIndex """ item = self.model.itemFromIndex(self.proxy.mapToSource(index)) node = item.data() if node: self.selectNode(node) @pyqtSlot('QGraphicsItem') def remove(self, item): """ Remove a node from the tree view. :type item: AbstractItem """ if item.node and item.predicate: parent = self.parentFor(item) if parent: child = self.childFor(parent, item) if child: parent.removeRow(child.index().row()) if not parent.rowCount(): self.model.removeRow(parent.index().row()) #################################################################################################################### # # # AUXILIARY METHODS # # # #################################################################################################################### @staticmethod def childFor(parent, node): """ Search the item representing this node among parent children. :type parent: QStandardItem :type node: AbstractNode """ key = ChildItem.key(node) for i in range(parent.rowCount()): child = parent.child(i) if child.text() == key: return child return None def parentFor(self, node): """ Search the parent element of the given node. :type node: AbstractNode :rtype: QStandardItem """ key = ParentItem.key(node) for i in self.model.findItems(key, Qt.MatchExactly): n = i.child(0).data() if node.item is n.item: return i return None #################################################################################################################### # # # INTERFACE # # # #################################################################################################################### def browse(self, view): """ Set the widget to inspect the given view. :type view: MainView """ self.reset() self.mainview = view if self.mainview: scene = self.mainview.scene() connect(scene.index.sgnItemAdded, self.add) connect(scene.index.sgnItemRemoved, self.remove) for item in scene.index.nodes(): self.add(item) if self.mainview in self.expanded: expanded = self.expanded[self.mainview] for i in range(self.model.rowCount()): item = self.model.item(i) index = self.proxy.mapFromSource( self.model.indexFromItem(item)) self.view.setExpanded(index, item.text() in expanded) key = '' if self.mainview in self.searched: key = self.searched[self.mainview] self.search.setText(key) if self.mainview in self.scrolled: rect = self.rect() item = first(self.model.findItems( self.scrolled[self.mainview])) for i in range(self.model.rowCount()): self.view.scrollTo( self.proxy.mapFromSource( self.model.indexFromItem(self.model.item(i)))) index = self.proxy.mapToSource( self.view.indexAt(rect.topLeft())) if self.model.itemFromIndex(index) is item: break def reset(self): """ Clear the widget from inspecting the current view. """ if self.mainview: rect = self.rect() item = self.model.itemFromIndex( self.proxy.mapToSource(self.view.indexAt(rect.topLeft()))) if item: node = item.data() key = ParentItem.key(node) if node else item.text() self.scrolled[self.mainview] = key else: self.scrolled.pop(self.mainview, None) try: scene = self.mainview.scene() disconnect(scene.index.sgnItemAdded, self.add) disconnect(scene.index.sgnItemRemoved, self.remove) except RuntimeError: pass finally: self.mainview = None self.model.clear() def flush(self, view): """ Flush the cache of the given mainview. :type view: MainView """ self.expanded.pop(view, None) self.searched.pop(view, None) self.scrolled.pop(view, None) def iconFor(self, node): """ Returns the icon for the given node. :type node: """ if node.item is Item.AttributeNode: return self.iconA if node.item is Item.ConceptNode: return self.iconC if node.item is Item.ValueDomainNode: return self.iconD if node.item is Item.ValueRestrictionNode: return self.iconD if node.item is Item.IndividualNode: if node.identity is Identity.Instance: return self.iconI if node.identity is Identity.Value: return self.iconV if node.item is Item.RoleNode: return self.iconR def focusNode(self, node): """ Focus the given node in the main view. :type node: AbstractNode """ if self.mainview: self.mainview.centerOn(node) def selectNode(self, node): """ Select the given node in the main view. :type node: AbstractNode """ if self.mainview: scene = self.mainview.scene() scene.clearSelection() node.setSelected(True)
class WatchPointViewer(QTreeView): """ Class implementing the watch expression viewer widget. Watch expressions will be shown with all their details. They can be modified through the context menu of this widget. """ def __init__(self, parent=None): """ Constructor @param parent the parent (QWidget) """ super(WatchPointViewer, self).__init__(parent) self.setObjectName("WatchExpressionViewer") self.__model = None self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setWindowTitle(self.tr("Watchpoints")) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() def setModel(self, model): """ Public slot to set the watch expression model. @param model reference to the watch expression model (WatchPointModel) """ self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setDynamicSortFilter(True) self.sortingModel.setSourceModel(self.__model) super(WatchPointViewer, self).setModel(self.sortingModel) header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) if qVersion() >= "5.0.0": header.setSectionsClickable(True) else: header.setClickable(True) self.setSortingEnabled(True) self.__layoutDisplay() def __layoutDisplay(self): """ Private slot to perform a layout operation. """ self.__resizeColumns() self.__resort() def __resizeColumns(self): """ Private slot to resize the view when items get added, edited or deleted. """ self.header().resizeSections(QHeaderView.ResizeToContents) self.header().setStretchLastSection(True) def __resort(self): """ Private slot to resort the tree. """ self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def __toSourceIndex(self, index): """ Private slot to convert an index to a source index. @param index index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """ Private slot to convert a source index to an index. @param sindex source index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """ Private slot to select a complete row. @param index index determining the row to be selected (QModelIndex) @param selected flag indicating the action (bool) """ if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """ Private method to generate the popup menus. """ self.menu = QMenu() self.menu.addAction(self.tr("Add"), self.__addWatchPoint) self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint) self.menu.addSeparator() self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint) self.menu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint) self.menu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint) self.menu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Configure..."), self.__configure) self.backMenuActions = {} self.backMenu = QMenu() self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint) self.backMenuActions["EnableAll"] = \ self.backMenu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.backMenuActions["DisableAll"] = \ self.backMenu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.backMenuActions["DeleteAll"] = \ self.backMenu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Configure..."), self.__configure) self.backMenu.aboutToShow.connect(self.__showBackMenu) self.multiMenu = QMenu() self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Enable selected"), self.__enableSelectedWatchPoints) self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Disable selected"), self.__disableSelectedWatchPoints) self.multiMenu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Delete selected"), self.__deleteSelectedWatchPoints) self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Configure..."), self.__configure) def __showContextMenu(self, coord): """ Private slot to show the context menu. @param coord the position of the mouse pointer (QPoint) """ cnt = self.__getSelectedItemsCount() if cnt <= 1: index = self.indexAt(coord) if index.isValid(): cnt = 1 self.__setRowSelected(index) coord = self.mapToGlobal(coord) if cnt > 1: self.multiMenu.popup(coord) elif cnt == 1: self.menu.popup(coord) else: self.backMenu.popup(coord) def __clearSelection(self): """ Private slot to clear the selection. """ for index in self.selectedIndexes(): self.__setRowSelected(index, False) def __findDuplicates(self, cond, special, showMessage=False, index=QModelIndex()): """ Private method to check, if an entry already exists. @param cond condition to check (string) @param special special condition to check (string) @param showMessage flag indicating a message should be shown, if a duplicate entry is found (boolean) @param index index that should not be considered duplicate (QModelIndex) @return flag indicating a duplicate entry (boolean) """ idx = self.__model.getWatchPointIndex(cond, special) duplicate = idx.isValid() and \ idx.internalPointer() != index.internalPointer() if showMessage and duplicate: if not special: msg = self.tr("""<p>A watch expression '<b>{0}</b>'""" """ already exists.</p>""")\ .format(Utilities.html_encode(cond)) else: msg = self.tr( """<p>A watch expression '<b>{0}</b>'""" """ for the variable <b>{1}</b> already exists.</p>""")\ .format(special, Utilities.html_encode(cond)) E5MessageBox.warning(self, self.tr("Watch expression already exists"), msg) return duplicate def __addWatchPoint(self): """ Private slot to handle the add watch expression context menu entry. """ from .EditWatchpointDialog import EditWatchpointDialog dlg = EditWatchpointDialog(("", False, True, 0, ""), self) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, ignorecount, special = dlg.getData() if not self.__findDuplicates(cond, special, True): self.__model.addWatchPoint(cond, special, (temp, enabled, ignorecount)) self.__resizeColumns() self.__resort() def __doubleClicked(self, index): """ Private slot to handle the double clicked signal. @param index index of the entry that was double clicked (QModelIndex) """ if index.isValid(): self.__doEditWatchPoint(index) def __editWatchPoint(self): """ Private slot to handle the edit watch expression context menu entry. """ index = self.currentIndex() if index.isValid(): self.__doEditWatchPoint(index) def __doEditWatchPoint(self, index): """ Private slot to edit a watch expression. @param index index of watch expression to be edited (QModelIndex) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): wp = self.__model.getWatchPointByIndex(sindex) if not wp: return cond, special, temp, enabled, count = wp[:5] from .EditWatchpointDialog import EditWatchpointDialog dlg = EditWatchpointDialog((cond, temp, enabled, count, special), self) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, count, special = dlg.getData() if not self.__findDuplicates(cond, special, True, sindex): self.__model.setWatchPointByIndex(sindex, cond, special, (temp, enabled, count)) self.__resizeColumns() self.__resort() def __setWpEnabled(self, index, enabled): """ Private method to set the enabled status of a watch expression. @param index index of watch expression to be enabled/disabled (QModelIndex) @param enabled flag indicating the enabled status to be set (boolean) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.setWatchPointEnabledByIndex(sindex, enabled) def __enableWatchPoint(self): """ Private slot to handle the enable watch expression context menu entry. """ index = self.currentIndex() self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __enableAllWatchPoints(self): """ Private slot to handle the enable all watch expressions context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __enableSelectedWatchPoints(self): """ Private slot to handle the enable selected watch expressions context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __disableWatchPoint(self): """ Private slot to handle the disable watch expression context menu entry. """ index = self.currentIndex() self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __disableAllWatchPoints(self): """ Private slot to handle the disable all watch expressions context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __disableSelectedWatchPoints(self): """ Private slot to handle the disable selected watch expressions context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __deleteWatchPoint(self): """ Private slot to handle the delete watch expression context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.deleteWatchPointByIndex(sindex) def __deleteAllWatchPoints(self): """ Private slot to handle the delete all watch expressions context menu entry. """ self.__model.deleteAll() def __deleteSelectedWatchPoints(self): """ Private slot to handle the delete selected watch expressions context menu entry. """ idxList = [] for index in self.selectedIndexes(): sindex = self.__toSourceIndex(index) if sindex.isValid() and index.column() == 0: idxList.append(sindex) self.__model.deleteWatchPoints(idxList) def __showBackMenu(self): """ Private slot to handle the aboutToShow signal of the background menu. """ if self.model().rowCount() == 0: self.backMenuActions["EnableAll"].setEnabled(False) self.backMenuActions["DisableAll"].setEnabled(False) self.backMenuActions["DeleteAll"].setEnabled(False) else: self.backMenuActions["EnableAll"].setEnabled(True) self.backMenuActions["DisableAll"].setEnabled(True) self.backMenuActions["DeleteAll"].setEnabled(True) def __getSelectedItemsCount(self): """ Private method to get the count of items selected. @return count of items selected (integer) """ count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface")\ .showPreferences("debuggerGeneralPage")
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent=None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode( 0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect( self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect( self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x == QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x == Qt.Key_Delete or x == Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else: removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory( self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo( self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
class BreakPointViewer(QTreeView): """ Class implementing the Breakpoint viewer widget. Breakpoints will be shown with all their details. They can be modified through the context menu of this widget. @signal sourceFile(str, int) emitted to show the source of a breakpoint """ sourceFile = pyqtSignal(str, int) def __init__(self, parent=None): """ Constructor @param parent the parent (QWidget) """ super(BreakPointViewer, self).__init__(parent) self.setObjectName("BreakPointViewer") self.__model = None self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setWindowTitle(self.tr("Breakpoints")) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() self.condHistory = [] self.fnHistory = [] self.fnHistory.append('') self.__loadRecent() def setModel(self, model): """ Public slot to set the breakpoint model. @param model reference to the breakpoint model (BreakPointModel) """ self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setDynamicSortFilter(True) self.sortingModel.setSourceModel(self.__model) super(BreakPointViewer, self).setModel(self.sortingModel) header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) header.setSectionsClickable(True) self.setSortingEnabled(True) self.__layoutDisplay() def __layoutDisplay(self): """ Private slot to perform a layout operation. """ self.__resizeColumns() self.__resort() def __resizeColumns(self): """ Private slot to resize the view when items get added, edited or deleted. """ self.header().resizeSections(QHeaderView.ResizeToContents) self.header().setStretchLastSection(True) def __resort(self): """ Private slot to resort the tree. """ self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def __toSourceIndex(self, index): """ Private slot to convert an index to a source index. @param index index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """ Private slot to convert a source index to an index. @param sindex source index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """ Private slot to select a complete row. @param index index determining the row to be selected (QModelIndex) @param selected flag indicating the action (bool) """ if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """ Private method to generate the popup menus. """ self.menu = QMenu() self.menu.addAction(self.tr("Add"), self.__addBreak) self.menu.addAction(self.tr("Edit..."), self.__editBreak) self.menu.addSeparator() self.menu.addAction(self.tr("Enable"), self.__enableBreak) self.menu.addAction(self.tr("Enable all"), self.__enableAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Disable"), self.__disableBreak) self.menu.addAction(self.tr("Disable all"), self.__disableAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Delete"), self.__deleteBreak) self.menu.addAction(self.tr("Delete all"), self.__deleteAllBreaks) self.menu.addSeparator() self.menu.addAction(self.tr("Goto"), self.__showSource) self.menu.addSeparator() self.menu.addAction(self.tr("Configure..."), self.__configure) self.backMenuActions = {} self.backMenu = QMenu() self.backMenu.addAction(self.tr("Add"), self.__addBreak) self.backMenuActions["EnableAll"] = self.backMenu.addAction( self.tr("Enable all"), self.__enableAllBreaks) self.backMenuActions["DisableAll"] = self.backMenu.addAction( self.tr("Disable all"), self.__disableAllBreaks) self.backMenuActions["DeleteAll"] = self.backMenu.addAction( self.tr("Delete all"), self.__deleteAllBreaks) self.backMenu.aboutToShow.connect(self.__showBackMenu) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Configure..."), self.__configure) self.multiMenu = QMenu() self.multiMenu.addAction(self.tr("Add"), self.__addBreak) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Enable selected"), self.__enableSelectedBreaks) self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Disable selected"), self.__disableSelectedBreaks) self.multiMenu.addAction(self.tr("Disable all"), self.__disableAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Delete selected"), self.__deleteSelectedBreaks) self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllBreaks) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Configure..."), self.__configure) def __showContextMenu(self, coord): """ Private slot to show the context menu. @param coord the position of the mouse pointer (QPoint) """ cnt = self.__getSelectedItemsCount() if cnt <= 1: index = self.indexAt(coord) if index.isValid(): cnt = 1 self.__setRowSelected(index) coord = self.mapToGlobal(coord) if cnt > 1: self.multiMenu.popup(coord) elif cnt == 1: self.menu.popup(coord) else: self.backMenu.popup(coord) def __clearSelection(self): """ Private slot to clear the selection. """ for index in self.selectedIndexes(): self.__setRowSelected(index, False) def __addBreak(self): """ Private slot to handle the add breakpoint context menu entry. """ from .EditBreakpointDialog import EditBreakpointDialog dlg = EditBreakpointDialog((self.fnHistory[0], None), None, self.condHistory, self, modal=1, addMode=1, filenameHistory=self.fnHistory) if dlg.exec_() == QDialog.Accepted: fn, line, cond, temp, enabled, count = dlg.getAddData() if fn is not None: if fn in self.fnHistory: self.fnHistory.remove(fn) self.fnHistory.insert(0, fn) if cond: if cond in self.condHistory: self.condHistory.remove(cond) self.condHistory.insert(0, cond) self.__saveRecent() self.__model.addBreakPoint(fn, line, (cond, temp, enabled, count)) self.__resizeColumns() self.__resort() def __doubleClicked(self, index): """ Private slot to handle the double clicked signal. @param index index of the entry that was double clicked (QModelIndex) """ if index.isValid(): self.__editBreakpoint(index) def __editBreak(self): """ Private slot to handle the edit breakpoint context menu entry. """ index = self.currentIndex() if index.isValid(): self.__editBreakpoint(index) def __editBreakpoint(self, index): """ Private slot to edit a breakpoint. @param index index of breakpoint to be edited (QModelIndex) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): bp = self.__model.getBreakPointByIndex(sindex) if not bp: return fn, line, cond, temp, enabled, count = bp[:6] from .EditBreakpointDialog import EditBreakpointDialog dlg = EditBreakpointDialog((fn, line), (cond, temp, enabled, count), self.condHistory, self, modal=True) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, count = dlg.getData() if cond: if cond in self.condHistory: self.condHistory.remove(cond) self.condHistory.insert(0, cond) self.__saveRecent() self.__model.setBreakPointByIndex(sindex, fn, line, (cond, temp, enabled, count)) self.__resizeColumns() self.__resort() def __setBpEnabled(self, index, enabled): """ Private method to set the enabled status of a breakpoint. @param index index of breakpoint to be enabled/disabled (QModelIndex) @param enabled flag indicating the enabled status to be set (boolean) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.setBreakPointEnabledByIndex(sindex, enabled) def __enableBreak(self): """ Private slot to handle the enable breakpoint context menu entry. """ index = self.currentIndex() self.__setBpEnabled(index, True) self.__resizeColumns() self.__resort() def __enableAllBreaks(self): """ Private slot to handle the enable all breakpoints context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __enableSelectedBreaks(self): """ Private slot to handle the enable selected breakpoints context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setBpEnabled(index, True) self.__resizeColumns() self.__resort() def __disableBreak(self): """ Private slot to handle the disable breakpoint context menu entry. """ index = self.currentIndex() self.__setBpEnabled(index, False) self.__resizeColumns() self.__resort() def __disableAllBreaks(self): """ Private slot to handle the disable all breakpoints context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __disableSelectedBreaks(self): """ Private slot to handle the disable selected breakpoints context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setBpEnabled(index, False) self.__resizeColumns() self.__resort() def __deleteBreak(self): """ Private slot to handle the delete breakpoint context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.deleteBreakPointByIndex(sindex) def __deleteAllBreaks(self): """ Private slot to handle the delete all breakpoints context menu entry. """ self.__model.deleteAll() def __deleteSelectedBreaks(self): """ Private slot to handle the delete selected breakpoints context menu entry. """ idxList = [] for index in self.selectedIndexes(): sindex = self.__toSourceIndex(index) if sindex.isValid() and index.column() == 0: idxList.append(sindex) self.__model.deleteBreakPoints(idxList) def __showSource(self): """ Private slot to handle the goto context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) bp = self.__model.getBreakPointByIndex(sindex) if not bp: return fn, line = bp[:2] self.sourceFile.emit(fn, line) def highlightBreakpoint(self, fn, lineno): """ Public slot to handle the clientLine signal. @param fn filename of the breakpoint (string) @param lineno line number of the breakpoint (integer) """ sindex = self.__model.getBreakPointIndex(fn, lineno) if sindex.isValid(): return index = self.__fromSourceIndex(sindex) if index.isValid(): self.__clearSelection() self.__setRowSelected(index, True) def handleResetUI(self): """ Public slot to reset the breakpoint viewer. """ self.__clearSelection() def __showBackMenu(self): """ Private slot to handle the aboutToShow signal of the background menu. """ if self.model().rowCount() == 0: self.backMenuActions["EnableAll"].setEnabled(False) self.backMenuActions["DisableAll"].setEnabled(False) self.backMenuActions["DeleteAll"].setEnabled(False) else: self.backMenuActions["EnableAll"].setEnabled(True) self.backMenuActions["DisableAll"].setEnabled(True) self.backMenuActions["DeleteAll"].setEnabled(True) def __getSelectedItemsCount(self): """ Private method to get the count of items selected. @return count of items selected (integer) """ count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface").showPreferences( "debuggerGeneralPage") def __loadRecent(self): """ Private method to load the recently used file names. """ Preferences.Prefs.rsettings.sync() # load recently used file names self.fnHistory = [] self.fnHistory.append('') rs = Preferences.Prefs.rsettings.value(recentNameBreakpointFiles) if rs is not None: recent = [ f for f in Preferences.toList(rs) if QFileInfo(f).exists() ] self.fnHistory.extend( recent[:Preferences.getDebugger("RecentNumber")]) # load recently entered condition expressions self.condHistory = [] rs = Preferences.Prefs.rsettings.value(recentNameBreakpointConditions) if rs is not None: self.condHistory = Preferences.toList( rs)[:Preferences.getDebugger("RecentNumber")] def __saveRecent(self): """ Private method to save the list of recently used file names. """ recent = [f for f in self.fnHistory if f] Preferences.Prefs.rsettings.setValue(recentNameBreakpointFiles, recent) Preferences.Prefs.rsettings.setValue(recentNameBreakpointConditions, self.condHistory) Preferences.Prefs.rsettings.sync()
class MainWindow(QMainWindow): """Controller class for VmsParser ### Args: QMainWindow ([class]): Create the controller class """ def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) #Load VIEW from resource file uic.loadUi(self.resourcePath('gui.ui'), self) #Create first data for the model self.data = [VAMAS_File()] #Create the MODEL self.model = ParameterModel(self.data) #Selected first model column self.selectedModelColumn = 1 #Create the maptlotlib FigureCanvas object, #which defines a single set of axes as self.axes. self.spectralPlot = MplCanvas(self, width=8, height=4, dpi=100) #self.spectralPlot.axes.plot([0,1,2,3,4], [10,1,20,3,40]) toolbar = NavigationToolbar(self.spectralPlot, self) self.plotToolBar.addWidget(toolbar) self.plotWidget.addWidget(self.spectralPlot) #Popup menu event self.dataSelector.currentIndexChanged.connect(lambda index: (self.selectModelColumn(index+1))) self.dataSelector.setCurrentIndex(0) #Display model as table self.vmsTable.setModel(self.model) self.vmsTable.verticalHeader().setMaximumWidth(160) self.vmsTable.setColumnWidth(0, 30) #Hide the first row with model-column selector checkbox self.vmsTable.hideRow(0) self.model.dataChanged.connect(lambda index: (self.modelEditedEvent(index))) #Proxy model with swapped rows/columns for paramTabel self.proxy = QTransposeProxyModel() self.proxy.setSourceModel(self.model) #Sorting model self.proxy2 = QSortFilterProxyModel() self.proxy2.setSourceModel(self.proxy) #Display sorted proxy model for param table self.paramTable.setModel(self.proxy2) #Show only selected columns self.initSelectedFields() #Movable columns self.paramTable.horizontalHeader().setSectionsMovable(True) self.paramTable.setColumnWidth(0, 30) #Double click event selects row from model and updates view self.paramTable.doubleClicked.connect(lambda index: (self.selectModelColumn(self.proxy2.mapToSource(index).row()))) #Hide first row with model-row selector checkboxes self.paramTable.hideRow(0) #Menu Bar events self.actionSave.triggered.connect(self.saveModel) self.actionLoad.triggered.connect(self.loadModel) self.actionAppend_Files.triggered.connect(self.appendData) self.actionQuit.triggered.connect(self.close) #Button events self.buttonPrevArea.clicked.connect(self.goToPreviousColumn) self.buttonNextArea.clicked.connect(self.goToNextColumn) self.colNumber.setText("0/0") self.show() def modelEditedEvent(self, index): """Event triggered when model data changed by clicking a checkbox ### Arguments: index {QModelIndex} -- row/colum of changed data """ print("Model edited event row {0}, column {1} changed".format(index.row(), index.column())) #First column changes selected fields which are displayed in paramTable if index.column() == 0: if self.model.selectedRows[index.row()-1] == True: self.paramTable.showColumn(index.row()) else: self.paramTable.hideColumn(index.row()) #First row changes data columns to be plotted if index.row() == 0: self.updatePlot() def initSelectedFields(self): """Init selected dataclass fields = columns in param Table with default values """ #Show only selected columns visibleColumns = ["sampleName", "blockName", "posName", "date", "technique", "analyserSettingStr", "analyzerPEorRR", "dwellTime"] for r in range(self.proxy.columnCount()): if not self.proxy.headerData(r, Qt.Horizontal) in visibleColumns: self.paramTable.hideColumn(r) else: self.model.selectedRows[r-1] = True self.paramTable.showColumn(0) def goToNextColumn(self): """Select next column from model """ if self.selectedModelColumn < self.model.columnCount()-1: self.selectedModelColumn+=1 self.updateSelectedData() def goToPreviousColumn(self): """Select previous column from model """ if self.selectedModelColumn > 1: self.selectedModelColumn-=1 self.updateSelectedData() def selectModelColumn(self, index): """Select indexed data column from the model, update the datamapper and view ### Arguments: index {int} -- Model column index """ # self.dataMapper.submit() #self.dataMapper.setCurrentIndex(index) print("Selecting model column " + str(index)) self.selectedModelColumn = index self.updateSelectedData() def updateSelectedData(self): """update dataSelector PopupMenu, Table and plot after a different data column selected from the model """ #Update colNumber label self.colNumber.setText(str(self.selectedModelColumn) + "/" + str(self.model.columnCount()-1)) #Select corresponding line in popupmenu without triggering event self.dataSelector.blockSignals(True) self.dataSelector.setCurrentIndex(self.selectedModelColumn-1) self.dataSelector.blockSignals(False) #Show only only the selected column (=dataclass) from the model in the vmsTable for column in range(self.model.columnCount()): if column == self.selectedModelColumn or column == 0: self.vmsTable.showColumn(column) else: self.vmsTable.hideColumn(column) #print ("Combobox currentIndex", self.dataSelector.currentIndex(), " from ", self.dataSelector.count()) #Select line in inverse paramTable index = self.proxy.index(self.selectedModelColumn, 0) #Get column = row index from Transpose Proxy model print("Param table selecting row {0}".format(self.proxy2.mapFromSource(index).row())) #Deselect all self.paramTable.clearSelection() self.paramTable.selectRow(self.proxy2.mapFromSource(index).row()) #Convert to row index in sorted Proxy model #Plot data #Get current data class object self.updatePlot() def updatePlot(self): """update the plot with the selected data """ #Get current data class object data = self.model.getObject(self.selectedModelColumn) #print("plotting column " + str(self.selectedModelColumn)) if len(data.yAxisValuesList) > 0: self.spectralPlot.axes.cla() self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0]) #Create axis labels from data self.spectralPlot.axes.set_xlabel("{0} [{1}]".format(data.xAxisLabel, data.xAxisUnit)) self.spectralPlot.axes.set_ylabel("{0} [{1}]".format(data.yAxisVarsLabelList[0], data.yAxisVarsUnitList[0])) #Plot also selected data columns for colIndex, checked in enumerate(self.model.selectedColumns): if checked and colIndex+1 != self.selectedModelColumn: #Starts from 0 #print("plotting also column " + str(colIndex+1)) #Get current data class object data = self.model.getObject(colIndex+1) #Starts from 0 self.spectralPlot.axes.plot(data.xAxisValuesList, data.yAxisValuesList[0]) #redraw self.spectralPlot.draw() def resourcePath(self, relPath): """To access resources when bundled as an executable using PyInstaller relative paths are redirected to temporary _MEIPASS folder Ref.: https://blog.aaronhktan.com/posts/2018/05/14/pyqt5-pyinstaller-executable ### Args: relPath {str}: Relative path to resource file ### Returns: {str}: Converted path """ if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relPath) return os.path.join(os.path.abspath('.'), relPath) def saveModel(self): """Present file dialog to save model data to config (.ini) file """ #Present file dialog using last saved folder lastFolder = self.getLastSaveFolder() dialog = QFileDialog(self) dialog.setWindowTitle("Save settings") dialog.setNameFilter("Ini files (*.ini)") dialog.setFileMode(QFileDialog.AnyFile) dialog.setDirectory(lastFolder) dialog.setAcceptMode(QFileDialog.AcceptSave) if dialog.exec_() == QDialog.Accepted: fileName = dialog.selectedFiles()[0] self.saveLastFolder(fileName) #Save the model to config file self.model.saveModelToConfigFile(fileName) print ("Saved to " + fileName) def getLastSaveFolder(self): """ Try to get a selected folder from QSettings file (Mac: ~\library\preferences\) Defaults to userfolder ~ ### Returns: {str}: folder path """ try: settings = QSettings('vmsParser', 'vmsParser') print(settings.fileName()) lastFolder = settings.value('saveFolder', type=str) except: lastFolder = os.path.expanduser('~') return lastFolder def saveLastFolder(self, foldername): """ Saves the last visited folder as QSettings (Mac: ~\library\preferences\) ### Args: foldername {str}: foldername """ settings = QSettings('vmsParser', 'vmsParser') settings.setValue('saveFolder', os.path.dirname(foldername)) def loadModel(self): """Present file dialog to load model data from .vms files """ print("model column count before load " + str(self.model.columnCount())) #Present file dialog using last saved folder fileNames = self.vmsFileSelectorDialog() if fileNames: #Continue if files selected #Clear popup menu before loading new data self.dataSelector.clear() dataList = self.loadfilesIntoList(fileNames) #Supply the new datalist to the model to replace its data self.model.loadData(dataList) #Select first model column self.selectedModelColumn = 1 #Update the column number self.colNumber.setText("1/" + str(self.model.columnCount()-1)) #Refill popup menu with filenames for data in self.model.getData(): self.dataSelector.addItem(os.path.basename(data.fileName)) self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments def appendData(self): """Present file dialog to append files """ if self.dataSelector.currentText() == "": self.loadModel() else: #Present file dialog using last saved folder print("model column count before append " + str(self.model.columnCount())) print("Selected model column " + str(self.selectedModelColumn)) fileNames = self.vmsFileSelectorDialog() if fileNames: #Continue if files selected #Deselect all self.paramTable.clearSelection() dataList = self.loadfilesIntoList(fileNames) #Get the number of columns before insert oldColumnNum = self.model.columnCount() for data in dataList: self.dataSelector.addItem(os.path.basename(data.fileName)) self.model.appendData(data) self.selectModelColumn(oldColumnNum) self.vmsTable.resizeRowsToContents() #Resize rows in table view to make space for multiline comments def loadfilesIntoList(self, fileNames): """Load a list of vamas files into list of dataclasses to use as new model data ### Arguments: fileNames {list} -- List of filenames """ data = list() for filename in fileNames: vms = VAMAS_File(fileName=filename) vms.readVamasFile() data.append(vms) return data def vmsFileSelectorDialog(self): """ Present file dialog to select one or more vamas files Starting with last used folder from App preferences ### Returns: {list} -- list of selected filePaths """ lastFolder = self.getLastSaveFolder() dialog = QFileDialog(self) dialog.setWindowTitle("Open vms files") dialog.setNameFilter("vms files (*.vms)") dialog.setFileMode(QFileDialog.ExistingFiles) #Select multiple existing files dialog.setDirectory(lastFolder) dialog.setAcceptMode(QFileDialog.AcceptOpen) if dialog.exec_() == QDialog.Accepted: fileNames = dialog.selectedFiles() #Save selected foldername for next time self.saveLastFolder(fileNames[0]) return fileNames else: return None
class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setAttribute(Qt.WA_QuitOnClose) self.setAttribute(Qt.WA_DeleteOnClose) # create instance variables # ui self.ui = uic.loadUi('mainwindow.ui', self) # report manager self._reportManager = ReportManager(parent=self) self._archiveManager = ArchiveManager(parent=self, arcPath=arc_path) # report engines self._xlsxEngine = XlsxEngine(parent=self) self._reportManager.setEngine(self._xlsxEngine) # self._printEngine = PrintEngine(parent=self) # self._reportManager.setEngine(self._printEngine) # persistence engine # self._persistenceEngine = CsvEngine(parent=self) # self._persistenceEngine = SqliteEngine(parent=self) self._persistenceEngine = MysqlEngine(parent=self, dbItemClass=BillItem) # facades self._persistenceFacade = PersistenceFacade( parent=self, persistenceEngine=self._persistenceEngine) self._uiFacade = UiFacade(parent=self, reportManager=self._reportManager, archiveManager=self._archiveManager) # models # domain self._modelDomain = DomainModel( parent=self, persistenceFacade=self._persistenceFacade) # bill list + search proxy # TODO: use settings to set icon self._modelBillList = BillTableModel( parent=self, domainModel=self._modelDomain, docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22), rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22)) self._modelBillSearchProxy = BillSearchProxyModel(parent=self) self._modelBillSearchProxy.setSourceModel(self._modelBillList) # bill plan + search proxy self._modelBillPlan = BillPlanModel(parent=self, domainModel=self._modelDomain) self._modelPlanSearchProxy = QSortFilterProxyModel(parent=self) self._modelPlanSearchProxy.setSourceModel(self._modelBillPlan) # orders + search proxy self._modelOrderList = OrderTableModel( parent=self, domainModel=self._modelDomain, docIcon=QPixmap('./icons/doc.png', 'PNG').scaled(22, 22), rightIcon=QPixmap('./icons/right.png', 'PNG').scaled(22, 22)) self._modelOrderSearchProxy = QSortFilterProxyModel(parent=self) self._modelOrderSearchProxy.setSourceModel(self._modelOrderList) # connect ui facade to models self._uiFacade.setDomainModel(self._modelDomain) self._uiFacade.setBillModel(self._modelBillSearchProxy) self._uiFacade.setPlanModel(self._modelPlanSearchProxy) self._uiFacade.setOrderModel(self._modelOrderSearchProxy) # actions self.actRefresh = QAction('Обновить', self) self.actAddBillRecord = QAction('Добавить счёт...', self) self.actEditBillRecord = QAction('Изменить счёт...', self) self.actDeleteBillRecord = QAction('Удалить счёт...', self) self.actPrint = QAction('Распечатать...', self) self.actOpenDictEditor = QAction('Словари', self) self.actMakeBillFromOrder = QAction('Создать счёт...', self) self.actAddOrderRecord = QAction('Добавить заказ...', self) self.actEditOrderRecord = QAction('Изменить заказ...', self) self.actViewBillStats = QAction('Статистика по счетам...', self) self.actDeleteOrderRecord = QAction('Удалить заказ...', self) def buildWeekSelectionCombo(self): # TODO if more settings is needed, move all settings-related code to a separate class year, week, day = datetime.datetime.now().isocalendar() week_list = list() for i in range(1, isoweek.Week.last_week_of_year(year).week + 1): w = isoweek.Week(year, i) week_list.append( str(i) + ': ' + str(w.monday().strftime('%d.%m')) + '-' + str(w.friday().strftime('%d.%m'))) self.ui.comboWeek.addItems(week_list) # TODO read settings if os.path.isfile('settings.ini'): with open('settings.ini', mode='tr') as f: line = f.readline() index = int(line.split('=')[1]) else: index = week self.ui.comboWeek.setCurrentIndex(index - 1) self._modelBillPlan.updateHeader(index) def initApp(self): # TODO: extract methods # init instances # self._persistenceEngine.initEngine(fileName='ref/1.csv') # self._persistenceEngine.initEngine(fileName='sqlite3.db') self._persistenceEngine.initEngine() self._persistenceFacade.initFacade() # self._uiFacade.initFacade() self._modelDomain.initModel() self.show() if not self._uiFacade.requestLogin(): sys.exit(5) self.setWindowTitle('Планировщик счетов, пользователь: ' + self._modelDomain.getLoggedUserName()) self._modelDomain.buildPlanData() self._modelBillList.initModel() self._modelBillPlan.initModel() self._modelOrderList.initModel() # init UI # bill list table self.ui.tableBill: QTableView self.ui.tableBill.setModel(self._modelBillSearchProxy) self.ui.tableBill.setSelectionMode(QAbstractItemView.SingleSelection) self.ui.tableBill.setSelectionBehavior(QAbstractItemView.SelectRows) self.ui.tableBill.setEditTriggers(QAbstractItemView.NoEditTriggers) # draw delegates # self.ui.tableBill.setItemDelegateForRow(0, TableRowDelegate(self.ui.tableBill)) # self.ui.tableBill.setHorizontalHeader(SectionHeaderView(Qt.Horizontal, parent=self.ui.tableBill)) # formatting self.ui.tableBill.horizontalHeader().setDefaultAlignment( Qt.AlignCenter) self.ui.tableBill.horizontalHeader().setHighlightSections(False) self.ui.tableBill.horizontalHeader().setFixedHeight(24) self.ui.tableBill.horizontalHeader().setStretchLastSection(True) self.ui.tableBill.horizontalHeader().setStyleSheet( 'QHeaderView::section {' ' padding: 4px;' ' border-style: none;' ' border-color: #000000;' ' border-bottom: 1px solid #000000;' ' border-right: 1px solid #000000;' '}' 'QHeaderView::section:horizontal {' ' border-right: 1px solid #000000' '}') # self.ui.tableBill.horizontalHeader().setAutoFillBackground(False) self.ui.tableBill.verticalHeader().setVisible(False) # self.ui.tableBill.verticalHeader().setDefaultSectionSize(40) self.ui.tableBill.setWordWrap(True) self.ui.tableBill.resizeRowsToContents() self.ui.tableBill.setStyleSheet('QTableView { gridline-color : black}') self.hideBillTableColumns() # self.ui.tableBill.setSpan(0, 0, 1, 3) # bill plan table self.ui.tablePlan.setModel(self._modelPlanSearchProxy) self.ui.tablePlan.setSelectionMode(QAbstractItemView.NoSelection) self.ui.tablePlan.setSelectionBehavior(QAbstractItemView.SelectRows) self.ui.tablePlan.setEditTriggers(QAbstractItemView.SelectedClicked) self.ui.tablePlan.horizontalHeader().setDefaultAlignment( Qt.AlignCenter) self.ui.tablePlan.horizontalHeader().setHighlightSections(False) self.ui.tablePlan.horizontalHeader().setFixedHeight(24) self.ui.tablePlan.horizontalHeader().setStretchLastSection(True) self.ui.tablePlan.horizontalHeader().setStyleSheet( 'QHeaderView::section {' ' padding: 4px;' ' border-style: none;' ' border-color: #000000;' ' border-bottom: 1px solid #000000;' ' border-right: 1px solid #000000;' '}' 'QHeaderView::section:horizontal {' ' border-right: 1px solid #000000' '}') self.ui.tablePlan.verticalHeader().setVisible(False) self.ui.tablePlan.hideColumn(0) # self.ui.tablePlan.hideColumn(3) self.ui.tablePlan.hideColumn(4) # self.ui.tablePlan.verticalHeader().setDefaultSectionSize(40) self.ui.tablePlan.setWordWrap(True) self.ui.tablePlan.resizeRowsToContents() # self.ui.tablePlan.setSpan(0, 0, 1, 3) self.ui.tablePlan.setStyleSheet( 'QTableView { gridline-color : black }') # self.ui.tablePlan.setItemDelegateForRow(0, TableRowDelegate(self.ui.tablePlan)) # bill order table self.ui.tableOrder: QTableView self.ui.tableOrder.setModel(self._modelOrderSearchProxy) self.ui.tableOrder.setSelectionMode(QAbstractItemView.SingleSelection) self.ui.tableOrder.setSelectionBehavior(QAbstractItemView.SelectRows) self.ui.tableOrder.setEditTriggers(QAbstractItemView.NoEditTriggers) # formatting self.ui.tableOrder.horizontalHeader().setDefaultAlignment( Qt.AlignCenter) self.ui.tableOrder.horizontalHeader().setHighlightSections(False) self.ui.tableOrder.horizontalHeader().setFixedHeight(24) self.ui.tableOrder.horizontalHeader().setStretchLastSection(True) self.ui.tableOrder.horizontalHeader().setStyleSheet( 'QHeaderView::section {' ' padding: 4px;' ' border-style: none;' ' border-color: #000000;' ' border-bottom: 1px solid #000000;' ' border-right: 1px solid #000000;' '}' 'QHeaderView::section:horizontal {' ' border-right: 1px solid #000000' '}') self.ui.tableOrder.verticalHeader().setVisible(False) self.ui.tableOrder.setWordWrap(True) self.ui.tableOrder.resizeRowsToContents() self.ui.tableOrder.setStyleSheet( 'QTableView { gridline-color : black}') # setup filter widgets self.ui.comboProjectFilter.setModel(self._modelDomain.dicts['project']) self.ui.comboStatusFilter.setModel(self._modelDomain.dicts['status']) self.ui.comboPriorityFilter.setModel( self._modelDomain.dicts['priority']) self.ui.comboShipmentFilter.setModel( self._modelDomain.dicts['shipment']) self.ui.dateFromFilter.setDate( QDate.fromString(self._modelDomain.getEarliestBillDate(), 'dd.MM.yyyy')) self.ui.dateUntilFilter.setDate(QDate.currentDate()) # self.btnRefresh.setVisible(False) self.buildWeekSelectionCombo() # create actions self.initActions() # setup ui widget signals # buttons self.ui.btnRefresh.clicked.connect(self.onBtnRefreshClicked) self.ui.btnAddBill.clicked.connect(self.onBtnAddBillClicked) self.ui.btnEditBill.clicked.connect(self.onBtnEditBillClicked) self.ui.btnDeleteBill.clicked.connect(self.onBtnDeleteBillClicked) self.ui.btnPrint.clicked.connect(self.onBtnPrintClicked) self.ui.btnDictEditor.clicked.connect(self.onBtnDictEditorClicked) self.ui.btnMakeBillFromOrder.clicked.connect( self.onBtnMakeBillFromOrderClicked) self.ui.btnAddOrder.clicked.connect(self.onBtnAddOrderClicked) self.ui.btnEditOrder.clicked.connect(self.onBtnEditOrderClicked) self.ui.btnDelOrder.clicked.connect(self.onBtnDelOrderClicked) self.ui.btnBillStats.clicked.connect(self.onBtnBillStatsClicked) # table widgets self.ui.tableBill.doubleClicked.connect(self.onTableBillDoubleClicked) self.ui.tableBill.clicked.connect(self.onTableBillClicked) self.ui.tableOrder.doubleClicked.connect( self.onTableOrderDoubleClicked) self.ui.tableOrder.clicked.connect(self.onTableOrderClicked) self.ui.tabWidget.currentChanged.connect(self.onTabBarCurrentChanged) # model signals self._modelDomain.beginClearModel.connect( self._modelBillList.beginClearModel) self._modelDomain.beginClearModel.connect( self._modelBillPlan.beginClearModel) self._modelDomain.beginClearModel.connect( self._modelOrderList.beginClearModel) self._modelDomain.endClearModel.connect( self._modelBillList.endClearModel) self._modelDomain.endClearModel.connect( self._modelBillPlan.endClearModel) self._modelDomain.endClearModel.connect( self._modelOrderList.endClearModel) # totals update self._uiFacade.totalsChanged.connect(self.updateTotals) # search widgets self.ui.comboWeek.currentIndexChanged.connect( self.onComboWeekCurrentIndexChanged) self.ui.editSearch.textChanged.connect(self.setSearchFilter) self.ui.comboProjectFilter.currentIndexChanged.connect( self.setSearchFilter) self.ui.comboStatusFilter.currentIndexChanged.connect( self.setSearchFilter) self.ui.comboPriorityFilter.currentIndexChanged.connect( self.setSearchFilter) self.ui.comboShipmentFilter.currentIndexChanged.connect( self.setSearchFilter) self.ui.dateFromFilter.dateChanged.connect(self.setSearchFilter) self.ui.dateUntilFilter.dateChanged.connect(self.setSearchFilter) self.setSearchFilter() # widget tweaks # self.ui.btnRefresh.setVisible(False) self.updateTotals() self.prepareUi(self._modelDomain.getLoggedUserLevel()) def initActions(self): self.actRefresh.setShortcut('Ctrl+R') self.actRefresh.setStatusTip('Обновить данные') self.actRefresh.triggered.connect(self.procActRefresh) self.actAddBillRecord.setShortcut('Ctrl+A') self.actAddBillRecord.setStatusTip('Добавить новый счёт') self.actAddBillRecord.triggered.connect(self.procActAddBillRecord) # self.actEditBillRecord.setShortcut('Ctrl+A') self.actEditBillRecord.setStatusTip('Добавить новый счёт') self.actEditBillRecord.triggered.connect(self.procActEditRecord) # self.actDeleteBillRecord.setShortcut('Ctrl+A') self.actDeleteBillRecord.setStatusTip('Добавить новый счёт') self.actDeleteBillRecord.triggered.connect(self.procActDeleteRecord) self.actPrint.setStatusTip('Напечатать текущую таблицу') self.actPrint.triggered.connect(self.procActPrint) self.actOpenDictEditor.setStatusTip('Открыть редактор словарей') self.actOpenDictEditor.triggered.connect(self.procActOpenDictEditor) self.actMakeBillFromOrder.setStatusTip('Создать счёт из заказа') self.actMakeBillFromOrder.triggered.connect( self.procActMakeBillFromOrder) self.actAddOrderRecord.setStatusTip('Добавить заказ') self.actAddOrderRecord.triggered.connect(self.procActAddOrderRecord) self.actEditOrderRecord.setStatusTip('Изменить заказ') self.actEditOrderRecord.triggered.connect(self.procActEditOrderRecord) self.actViewBillStats.setStatusTip( 'Посмотреть статистику по счетам...') self.actViewBillStats.triggered.connect(self.procActViewBillStats) self.actDeleteOrderRecord.setStatusTip('Перенести заказ в архив...') self.actDeleteOrderRecord.triggered.connect( self.procActDeleteOrderRecord) def refreshView(self): screenRect = QApplication.desktop().screenGeometry() # if self.ui.tabWidget.currentIndex() == 0: tbwidth = screenRect.width() - 50 self.ui.tableBill.setColumnWidth(0, tbwidth * 0.03) self.ui.tableBill.setColumnWidth(1, tbwidth * 0.05) self.ui.tableBill.setColumnWidth(2, tbwidth * 0.07) self.ui.tableBill.setColumnWidth(3, tbwidth * 0.05) # +0.01 self.ui.tableBill.setColumnWidth(4, tbwidth * 0.08) self.ui.tableBill.setColumnWidth(5, tbwidth * 0.05) self.ui.tableBill.setColumnWidth(6, tbwidth * 0.06) self.ui.tableBill.setColumnWidth(7, tbwidth * 0.20) # +0.01 self.ui.tableBill.setColumnWidth(8, tbwidth * 0.06) self.ui.tableBill.setColumnWidth(9, tbwidth * 0.06) self.ui.tableBill.setColumnWidth(10, tbwidth * 0.055) self.ui.tableBill.setColumnWidth(11, tbwidth * 0.055) self.ui.tableBill.setColumnWidth(12, tbwidth * 0.06) self.ui.tableBill.setColumnWidth(13, tbwidth * 0.035) # self.ui.tableBill.setColumnWidth(14, tbwidth * 0.03) self.ui.tableBill.setColumnWidth(15, tbwidth * 0.01) self.ui.tableBill.setColumnWidth(16, tbwidth * 0.01) self.ui.tableBill.setColumnWidth(17, tbwidth * 0.01) # +0.01 # elif self.ui.tabWidget.currentIndex() == 1: tpwidth = screenRect.width() - 45 # 1 2 3 5 .. week count - 1 # self.ui.tablePlan.setColumnWidth(0, tpwidth * 0.035) self.ui.tablePlan.setColumnWidth(1, tpwidth * 0.13) self.ui.tablePlan.setColumnWidth(2, tpwidth * 0.05) self.ui.tablePlan.setColumnWidth(3, tpwidth * 0.10) # self.ui.tablePlan.setColumnWidth(4, tpwidth * 0.06) self.ui.tablePlan.setColumnWidth(5, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(6, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(7, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(8, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(9, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(10, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(11, tpwidth * 0.09) self.ui.tablePlan.setColumnWidth(12, tpwidth * 0.09) # elif self.ui.tabWidget.currentIndex() == 2: towidth = screenRect.width() - 45 self.ui.tableOrder.setColumnWidth(0, towidth * 0.02) self.ui.tableOrder.setColumnWidth(1, towidth * 0.30) self.ui.tableOrder.setColumnWidth(2, towidth * 0.20) self.ui.tableOrder.setColumnWidth(3, towidth * 0.06) self.ui.tableOrder.setColumnWidth(4, towidth * 0.05) self.ui.tableOrder.setColumnWidth(5, towidth * 0.06) self.ui.tableOrder.setColumnWidth(6, towidth * 0.06) self.ui.tableOrder.setColumnWidth(7, towidth * 0.07) self.ui.tableOrder.setColumnWidth(8, towidth * 0.07) self.ui.tableOrder.setColumnWidth(9, towidth * 0.07) self.ui.tableOrder.setColumnWidth(10, towidth * 0.02) self.ui.tableOrder.setColumnWidth(11, towidth * 0.01) def prepareUi(self, level): if level == 1: self.ui.tabWidget.setCurrentIndex(0) index = self._modelBillSearchProxy.mapFromSource( self._modelBillList.index( self._modelBillList.rowCount(QModelIndex()) - 1, 0)) self.ui.tableBill.scrollTo(index, QAbstractItemView.EnsureVisible) self.ui.tableBill.selectionModel().setCurrentIndex( index, QItemSelectionModel.Select | QItemSelectionModel.Rows) elif level == 2 or level == 3: print('approver + user') self.ui.tabWidget.setCurrentIndex(2) self.ui.tabWidget.removeTab(0) self.ui.tabWidget.removeTab(0) self.ui.btnMakeBillFromOrder.hide() self.ui.btnDictEditor.hide() self.ui.btnPrint.hide() self.ui.btnRefresh.hide() self.ui.btnBillStats.hide() def hideBillTableColumns(self): # self.ui.tableBill.hideColumn(11) self.ui.tableBill.hideColumn(14) def hideOrderTableColumns(self): self.ui.tableOrder.hideColumn(3) # ui events def onBtnRefreshClicked(self): self.actRefresh.trigger() def onBtnAddBillClicked(self): self.actAddBillRecord.trigger() def onBtnEditBillClicked(self): self.actEditBillRecord.trigger() def onBtnDeleteBillClicked(self): self.actDeleteBillRecord.trigger() def onBtnDictEditorClicked(self): self.actOpenDictEditor.trigger() def onBtnPrintClicked(self): self.actPrint.trigger() def onBtnMakeBillFromOrderClicked(self): self.actMakeBillFromOrder.trigger() def onBtnBillStatsClicked(self): self.actViewBillStats.trigger() def onBtnAddOrderClicked(self): self.actAddOrderRecord.trigger() def onBtnEditOrderClicked(self): self.actEditOrderRecord.trigger() def onBtnDelOrderClicked(self): self.actDeleteOrderRecord.trigger() def onTableBillClicked(self, index): col = index.column() if col == self._modelBillList.ColumnDoc: doc = index.data(Qt.EditRole) if doc: subprocess.Popen(r'explorer /select,"' + index.data(Qt.EditRole).replace("/", "\\") + '"') elif col == self._modelBillList.ColumnOrder: orderId = index.data(const.RoleOrderId) if not orderId: return self.ui.tabWidget.setCurrentIndex(2) rowToSelect = self._modelOrderList.getRowById(orderId) indexToSelect = self._modelOrderSearchProxy.mapFromSource( self._modelOrderList.index(rowToSelect, 0, QModelIndex())) self.ui.tableOrder.selectRow(indexToSelect.row()) def onTableBillDoubleClicked(self, index): self.actEditBillRecord.trigger() def onTableOrderClicked(self, index): col = index.column() if col == self._modelOrderList.ColumnBill: billId = self._modelDomain.getBillIdForOrderId( index.data(const.RoleNodeId)) if billId: self.ui.tabWidget.setCurrentIndex(0) rowToSelect = self._modelBillList.getRowById(billId) indexToSelect = self._modelBillSearchProxy.mapFromSource( self._modelBillList.index(rowToSelect, 0, QModelIndex())) self.ui.tableBill.selectRow(indexToSelect.row()) if col == self._modelOrderList.ColumnDoc: doc = index.data(Qt.EditRole) if doc: subprocess.Popen(r'explorer /select,"' + index.data(Qt.EditRole).replace("/", "\\") + '"') def onTableOrderDoubleClicked(self, index): self.actEditOrderRecord.trigger() def onTabBarCurrentChanged(self, index): if index == 1: self._modelDomain.buildPlanData() def onComboWeekCurrentIndexChanged(self, index): self._modelBillPlan.updateHeader(index + 1) # misc events def resizeEvent(self, event): # print('resize event') self.refreshView() # self.ui.tableBill.resizeRowsToContents() # self.ui.tablePlan.resizeRowsToContents() self.ui.tableOrder.resizeRowsToContents() def closeEvent(self, *args, **kwargs): # TODO error handling on saving before exiting self._uiFacade.requestExit(self.ui.comboWeek.currentIndex()) super(MainWindow, self).closeEvent(*args, **kwargs) # action processing # send user commands to the ui facade: (command, parameters (like indexes, etc.)) def procActRefresh(self): print('act refresh trigger') try: if self._modelDomain.savePlanData(): print('plan data saved') else: QMessageBox.information( self, 'Ошибка', 'Ошибка подключения к БД при попытке отбновления данных.\nОбратитесь к разработчику.' ) except Exception as ex: print(ex) self._modelDomain.clearModel() self._modelDomain.initModel() self._modelBillList.initModel() self._modelBillPlan.initModel() self._modelOrderList.initModel() self._uiFacade.requestRefresh() self.refreshView() self.ui.tableBill.resizeRowsToContents() self.ui.tablePlan.resizeRowsToContents() self.ui.tableOrder.resizeRowsToContents() self.hideBillTableColumns() self.hideOrderTableColumns() def procActAddBillRecord(self): row = self._uiFacade.requestAddBillRecord() if row is not None: index = self._modelBillSearchProxy.mapFromSource( self._modelBillList.index(row, 0)) self.ui.tableBill.scrollTo(index) self.ui.tableBill.selectionModel().setCurrentIndex( index, QItemSelectionModel.Select | QItemSelectionModel.Rows) def procActEditRecord(self): if not self.ui.tableBill.selectionModel().hasSelection(): QMessageBox.information(self, 'Ошибка', 'Изменить: пожалуйста, выберите запись.') return selectedIndex = self.ui.tableBill.selectionModel().selectedIndexes()[0] self._uiFacade.requestEditBillRecord( self._modelBillSearchProxy.mapToSource(selectedIndex)) def procActDeleteRecord(self): # print('act delete record trigger') if not self.ui.tableBill.selectionModel().hasSelection(): QMessageBox.information(self, 'Ошибка', 'Удалить: пожалуйста, выберите запись.') return selectedIndex: QModelIndex = self.ui.tableBill.selectionModel( ).selectedIndexes()[0] # TODO: HACK -- set active to false before deleting a bill self._modelBillList.setData(selectedIndex, 2, Qt.CheckStateRole) self._uiFacade.requestDeleteRecord( self._modelBillSearchProxy.mapToSource(selectedIndex)) def procActMakeBillFromOrder(self): if not self.ui.tableOrder.selectionModel().hasSelection(): QMessageBox.information( self, 'Ошибка', 'Выберите запись о заказе для создания счёта.') return selectedIndex: QModelIndex = self.ui.tableOrder.selectionModel( ).selectedIndexes()[0] row = self._uiFacade.requestMakeBillFromOrder( self._modelOrderSearchProxy.mapToSource(selectedIndex)) if row is not None: self.ui.tabWidget.setCurrentIndex(0) index = self._modelBillSearchProxy.mapFromSource( self._modelBillList.index(row, 0)) self.ui.tableBill.scrollTo(index) self.ui.tableBill.selectionModel().setCurrentIndex( index, QItemSelectionModel.Select | QItemSelectionModel.Rows) def procActAddOrderRecord(self): row = self._uiFacade.requestAddOrderRecord() if row is not None: index = self._modelOrderSearchProxy.mapFromSource( self._modelOrderList.index(row, 0)) self.ui.tableOrder.scrollTo(index) self.ui.tableOrder.selectionModel().setCurrentIndex( index, QItemSelectionModel.Select | QItemSelectionModel.Rows) def procActEditOrderRecord(self): if not self.ui.tableOrder.selectionModel().hasSelection(): QMessageBox.information(self, 'Ошибка', 'Изменить: пожалуйста, выберите запись.') return selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes( )[0] self._uiFacade.requestEditOrderRecord( self._modelOrderSearchProxy.mapToSource(selectedIndex)) def procActDeleteOrderRecord(self): if not self.ui.tableOrder.selectionModel().hasSelection(): QMessageBox.information( self, 'Ошибка', 'Перенести в архив: пожалуйста, выберите запись.') return selectedIndex = self.ui.tableOrder.selectionModel().selectedIndexes( )[0] self._uiFacade.requestDeleteOrderRecord( self._modelOrderSearchProxy.mapToSource(selectedIndex)) def procActPrint(self): self._uiFacade.requestPrint(self.ui.tabWidget.currentIndex(), self._modelDomain.getBillTotals()) def setSearchFilter(self, dummy=0): self._modelBillSearchProxy.filterString = self.ui.editSearch.text() self._modelBillSearchProxy.filterProject = self.ui.comboProjectFilter.currentData( const.RoleNodeId) self._modelBillSearchProxy.filterStatus = self.ui.comboStatusFilter.currentData( const.RoleNodeId) self._modelBillSearchProxy.filterPriority = self.ui.comboPriorityFilter.currentData( const.RoleNodeId) self._modelBillSearchProxy.filterShipment = self.ui.comboShipmentFilter.currentData( const.RoleNodeId) self._modelBillSearchProxy.filterFromDate = self.ui.dateFromFilter.date( ) self._modelBillSearchProxy.filterUntilDate = self.ui.dateUntilFilter.date( ) self._modelBillSearchProxy.invalidate() # self._modelPlanSearchProxy.setFilterWildcard(self.ui.editSearch.text()) # self._modelPlanSearchProxy.invalidate() # # self._modelOrderSearchProxy.setFilterWildcard(self.ui.editSearch.text()) # self._modelPlanSearchProxy.invalidate() self.hideBillTableColumns() self.hideOrderTableColumns() self.refreshView() def procActOpenDictEditor(self): self._uiFacade.requestOpenDictEditor() def procActViewBillStats(self): self._uiFacade.requestViewBillStats() @pyqtSlot() def updateTotals(self): print("update totals") p, r, t = self._modelDomain.getBillTotals() self.ui.lblTotal.setText( f'Оплачено: <span style="background-color:#92D050">{f"{p/100:,.2f}".replace(",", " ")}</span><br>' f'Осталось: <span style="background-color:#FF6767">{f"{r/100:,.2f}".replace(",", " ")}</span><br>' f'Всего: {f"{t/100:,.2f}".replace(",", " ")}')
class SearchFileWidget(QWidget): language_filter_change = pyqtSignal(list) def __init__(self): QWidget.__init__(self) self._refreshing = False self.fileModel = None self.proxyFileModel = None self.videoModel = None self._state = None self.timeLastSearch = QTime.currentTime() self.ui = Ui_SearchFileWidget() self.setup_ui() def set_state(self, state): self._state = state self._state.login_status_changed.connect(self.on_login_state_changed) self._state.interface_language_changed.connect( self.on_interface_language_changed) def get_state(self): return self._state def setup_ui(self): self.ui.setupUi(self) settings = QSettings() self.ui.splitter.setSizes([600, 1000]) self.ui.splitter.setChildrenCollapsible(False) # Set up folder view lastDir = settings.value("mainwindow/workingDirectory", QDir.homePath()) log.debug('Current directory: {currentDir}'.format(currentDir=lastDir)) self.fileModel = QFileSystemModel(self) self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives | QDir.NoDotAndDotDot | QDir.Readable | QDir.Executable | QDir.Writable) self.fileModel.iconProvider().setOptions( QFileIconProvider.DontUseCustomDirectoryIcons) self.fileModel.setRootPath(QDir.rootPath()) self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded) self.proxyFileModel = QSortFilterProxyModel(self) self.proxyFileModel.setSortRole(Qt.DisplayRole) self.proxyFileModel.setSourceModel(self.fileModel) self.proxyFileModel.sort(0, Qt.AscendingOrder) self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.ui.folderView.setModel(self.proxyFileModel) self.ui.folderView.setHeaderHidden(True) self.ui.folderView.hideColumn(3) self.ui.folderView.hideColumn(2) self.ui.folderView.hideColumn(1) index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.expanded.connect(self.onFolderViewExpanded) self.ui.folderView.clicked.connect(self.onFolderTreeClicked) self.ui.buttonFind.clicked.connect(self.onButtonFind) self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh) # Set up introduction self.showInstructions() # Set up video view self.ui.filterLanguageForVideo.set_unknown_text(_('All languages')) self.ui.filterLanguageForVideo.selected_language_changed.connect( self.on_language_combobox_filter_change) # self.ui.filterLanguageForVideo.selected_language_changed.connect(self.onFilterLanguageVideo) self.videoModel = VideoModel(self) self.ui.videoView.setHeaderHidden(True) self.ui.videoView.setModel(self.videoModel) self.ui.videoView.activated.connect(self.onClickVideoTreeView) self.ui.videoView.clicked.connect(self.onClickVideoTreeView) self.ui.videoView.customContextMenuRequested.connect(self.onContext) self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged) self.language_filter_change.connect( self.videoModel.on_filter_languages_change) self.ui.buttonSearchSelectVideos.clicked.connect( self.onButtonSearchSelectVideos) self.ui.buttonSearchSelectFolder.clicked.connect( self.onButtonSearchSelectFolder) self.ui.buttonDownload.clicked.connect(self.onButtonDownload) self.ui.buttonPlay.clicked.connect(self.onButtonPlay) self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo) self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu) # Drag and Drop files to the videoView enabled self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent self.ui.videoView.__class__.dropEvent = self.dropEvent self.ui.videoView.setAcceptDrops(1) # FIXME: ok to drop this connect? # self.ui.videoView.clicked.connect(self.onClickMovieTreeView) self.retranslate() def retranslate(self): introduction = '<p align="center"><h2>{title}</h2></p>' \ '<p><b>{tab1header}</b><br/>{tab1content}</p>' \ '<p><b>{tab2header}</b><br/>{tab2content}</p>'\ '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format( title=_('How To Use {title}').format(title=PROJECT_TITLE), tab1header=_('1st Tab:'), tab2header=_('2nd Tab:'), tab3header=_('3rd Tab:'), tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos ' 'that need subtitles. {project} will then try to automatically find available ' 'subtitles.').format(project=PROJECT_TITLE), tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by ' 'introducing the title/name of the video.').format(project=PROJECT_TITLE), tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, ' 'please upload those subtitles so next users will be able to ' 'find them more easily.').format(project=PROJECT_TITLE)) self.ui.introductionHelp.setHtml(introduction) @pyqtSlot(Language) def on_interface_language_changed(self, language): self.ui.retranslateUi(self) self.retranslate() @pyqtSlot(str) def onFileModelDirectoryLoaded(self, path): settings = QSettings() lastDir = settings.value('mainwindow/workingDirectory', QDir.homePath()) qDirLastDir = QDir(lastDir) qDirLastDir.cdUp() if qDirLastDir.path() == path: index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.setCurrentIndex(proxyIndex) @pyqtSlot(int, str) def on_login_state_changed(self, state, message): log.debug( 'on_login_state_changed(state={state}, message={message}'.format( state=state, message=message)) if state in (State.LOGIN_STATUS_LOGGED_OUT, State.LOGIN_STATUS_BUSY): self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) self.ui.buttonFind.setEnabled(False) elif state == State.LOGIN_STATUS_LOGGED_IN: self.ui.buttonSearchSelectFolder.setEnabled(True) self.ui.buttonSearchSelectVideos.setEnabled(True) self.ui.buttonFind.setEnabled( self.get_current_selected_folder() is not None) else: log.warning('unknown state') @pyqtSlot(Language) def on_language_combobox_filter_change(self, language): if language.is_generic(): self.language_filter_change.emit( self.get_state().get_permanent_language_filter()) else: self.language_filter_change.emit([language]) def on_permanent_language_filter_change(self, languages): selected_language = self.ui.filterLanguageForVideo.get_selected_language( ) if selected_language.is_generic(): self.language_filter_change.emit(languages) @pyqtSlot() def subtitlesCheckedChanged(self): subs = self.videoModel.get_checked_subtitles() if subs: self.ui.buttonDownload.setEnabled(True) else: self.ui.buttonDownload.setEnabled(False) def showInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction) def hideInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult) @pyqtSlot(QModelIndex) def onFolderTreeClicked(self, proxyIndex): """What to do when a Folder in the tree is clicked""" if not proxyIndex.isValid(): return index = self.proxyFileModel.mapToSource(proxyIndex) settings = QSettings() folder_path = self.fileModel.filePath(index) settings.setValue('mainwindow/workingDirectory', folder_path) # self.ui.buttonFind.setEnabled(self.get_state().) def get_current_selected_folder(self): proxyIndex = self.ui.folderView.currentIndex() index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = self.fileModel.filePath(index) if not folder_path: return None return folder_path def get_current_selected_item_videomodel(self): current_index = self.ui.videoView.currentIndex() return self.videoModel.getSelectedItem(current_index) @pyqtSlot() def onButtonFind(self): now = QTime.currentTime() if now < self.timeLastSearch.addMSecs(500): return folder_path = self.get_current_selected_folder() settings = QSettings() settings.setValue('mainwindow/workingDirectory', folder_path) self.search_videos([folder_path]) self.timeLastSearch = QTime.currentTime() @pyqtSlot() def onButtonRefresh(self): currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) self._refreshing = True self.ui.folderView.collapseAll() currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) index = self.fileModel.index(currentPath) self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index)) @pyqtSlot(QModelIndex) def onFolderViewExpanded(self, proxyIndex): if self._refreshing: expandedPath = self.fileModel.filePath( self.proxyFileModel.mapToSource(proxyIndex)) if expandedPath == QDir.rootPath(): currentPath = self.get_current_selected_folder() if not currentPath: settings = QSettings() currentPath = settings.value('mainwindow/workingDirectory', QDir.homePath()) index = self.fileModel.index(currentPath) self.ui.folderView.scrollTo( self.proxyFileModel.mapFromSource(index)) self._refreshing = False @pyqtSlot() def onButtonSearchSelectFolder(self): settings = QSettings() path = settings.value('mainwindow/workingDirectory', QDir.homePath()) folder_path = QFileDialog.getExistingDirectory( self, _('Select the directory that contains your videos'), path) if folder_path: settings.setValue('mainwindow/workingDirectory', folder_path) self.search_videos([folder_path]) @pyqtSlot() def onButtonSearchSelectVideos(self): settings = QSettings() currentDir = settings.value('mainwindow/workingDirectory', QDir.homePath()) fileNames, t = QFileDialog.getOpenFileNames( self, _('Select the video(s) that need subtitles'), currentDir, SELECT_VIDEOS) if fileNames: settings.setValue('mainwindow/workingDirectory', QFileInfo(fileNames[0]).absolutePath()) self.search_videos(fileNames) def search_videos(self, paths): if not self.get_state().connected(): QMessageBox.about( self, _("Error"), _('You are not connected to the server. Please reconnect first.' )) return self.ui.buttonFind.setEnabled(False) self._search_videos_raw(paths) self.ui.buttonFind.setEnabled(True) def _search_videos_raw(self, paths): # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_("Scanning...")) callback.set_label_text(_("Scanning files")) callback.set_finished_text(_("Scanning finished")) callback.set_block(True) try: local_videos, local_subs = scan_videopaths(paths, callback=callback, recursive=True) except OSError: callback.cancel() QMessageBox.warning(self, _('Error'), _('Some directories are not accessible.')) if callback.canceled(): return callback.finish() log.debug("Videos found: %s" % local_videos) log.debug("Subtitles found: %s" % local_subs) self.hideInstructions() QCoreApplication.processEvents() if not local_videos: QMessageBox.about(self, _("Scan Results"), _("No video has been found!")) return total = len(local_videos) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget # callback = ProgressCallbackWidget(self) # callback.set_title_text(_("Asking Server...")) # callback.set_label_text(_("Searching subtitles...")) # callback.set_updated_text(_("Searching subtitles ( %d / %d )")) # callback.set_finished_text(_("Search finished")) callback.set_block(True) callback.set_range(0, total) callback.show() callback.set_range(0, 2) download_callback = callback.get_child_progress(0, 1) # videoSearchResults = self.get_state().get_OSDBServer().SearchSubtitles("", videos_piece) remote_subs = self.get_state().get_OSDBServer().search_videos( videos=local_videos, callback=download_callback) self.videoModel.set_videos(local_videos) # self.onFilterLanguageVideo(self.ui.filterLanguageForVideo.get_selected_language()) if remote_subs is None: QMessageBox.about( self, _("Error"), _("Error contacting the server. Please try again later")) callback.finish() # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload # self.OSDBServer.CheckSubHash(sub_hashes) @pyqtSlot() def onButtonPlay(self): settings = QSettings() programPath = settings.value('options/VideoPlayerPath', '') parameters = settings.value('options/VideoPlayerParameters', '') if programPath == '': QMessageBox.about( self, _('Error'), _('No default video player has been defined in Settings.')) return selected_subtitle = self.get_current_selected_item_videomodel() if isinstance(selected_subtitle, SubtitleFileNetwork): selected_subtitle = selected_subtitle.get_subtitles()[0] if isinstance(selected_subtitle, LocalSubtitleFile): subtitle_file_path = selected_subtitle.get_filepath() elif isinstance(selected_subtitle, RemoteSubtitleFile): subtitle_file_path = QDir.temp().absoluteFilePath( 'subdownloader.tmp.srt') log.debug( 'Temporary subtitle will be downloaded into: {temp_path}'. format(temp_path=subtitle_file_path)) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Playing video + sub')) callback.set_label_text(_('Downloading files...')) callback.set_finished_text(_('Downloading finished')) callback.set_block(True) callback.set_range(0, 100) callback.show() try: subtitle_stream = selected_subtitle.download( self.get_state().get_OSDBServer(), callback=callback) except ProviderConnectionError: log.debug('Unable to download subtitle "{}"'.format( selected_subtitle.get_filename()), exc_info=True) QMessageBox.about( self, _('Error'), _('Unable to download subtitle "{subtitle}"').format( subtitle=selected_subtitle.get_filename())) callback.finish() return callback.finish() write_stream(subtitle_stream, subtitle_file_path) video = selected_subtitle.get_parent().get_parent().get_parent() def windows_escape(text): return '"{text}"'.format(text=text.replace('"', '\\"')) params = [windows_escape(programPath)] for param in parameters.split(' '): param = param.format(video.get_filepath(), subtitle_file_path) if platform.system() in ('Windows', 'Microsoft'): param = windows_escape(param) params.append(param) pid = None log.info('Running this command: {params}'.format(params=params)) try: log.debug('Trying os.spawnvpe ...') pid = os.spawnvpe(os.P_NOWAIT, programPath, params, os.environ) log.debug('... SUCCESS. pid={pid}'.format(pid=pid)) except AttributeError: log.debug('... FAILED', exc_info=True) except Exception as e: log.debug('... FAILED', exc_info=True) if pid is None: try: log.debug('Trying os.fork ...') pid = os.fork() if not pid: log.debug('... SUCCESS. pid={pid}'.format(pid=pid)) os.execvpe(os.P_NOWAIT, programPath, params, os.environ) except: log.debug('... FAIL', exc_info=True) if pid is None: QMessageBox.about(self, _('Error'), _('Unable to launch videoplayer')) @pyqtSlot(QModelIndex) def onClickVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, SubtitleFile): self.ui.buttonPlay.setEnabled(True) else: self.ui.buttonPlay.setEnabled(False) if isinstance(data_item, VideoFile): video = data_item if True: # video.getMovieInfo(): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png')) self.ui.buttonIMDB.setText(_('Movie Info')) elif isinstance(data_item, RemoteSubtitleFile): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon( QIcon(':/images/sites/opensubtitles.png')) self.ui.buttonIMDB.setText(_('Subtitle Info')) else: self.ui.buttonIMDB.setEnabled(False) def onContext(self, point): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget menu = QMenu('Menu', self) listview = self.ui.videoView index = listview.currentIndex() data_item = listview.model().getSelectedItem(index) if data_item is not None: if isinstance(data_item, VideoFile): video = data_item movie_info = video.getMovieInfo() if movie_info: subWebsiteAction = QAction(QIcon(":/images/info.png"), _("View IMDB info"), self) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) else: subWebsiteAction = QAction(QIcon(":/images/info.png"), _("Set IMDB info..."), self) subWebsiteAction.triggered.connect(self.onSetIMDBInfo) menu.addAction(subWebsiteAction) elif isinstance(data_item, SubtitleFile): downloadAction = QAction(QIcon(":/images/download.png"), _("Download"), self) # Video tab, TODO:Replace me with a enum downloadAction.triggered.connect(self.onButtonDownload) playAction = QAction(QIcon(":/images/play.png"), _("Play video + subtitle"), self) playAction.triggered.connect(self.onButtonPlay) menu.addAction(playAction) subWebsiteAction = QAction( QIcon(":/images/sites/opensubtitles.png"), _("View online info"), self) menu.addAction(downloadAction) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) menu.addAction(subWebsiteAction) elif isinstance(data_item, Movie): subWebsiteAction = QAction(QIcon(":/images/info.png"), _("View IMDB info"), self) subWebsiteAction.triggered.connect(self.onViewOnlineInfo) menu.addAction(subWebsiteAction) # Show the context menu. menu.exec_(listview.mapToGlobal(point)) def onButtonDownload(self): # We download the subtitle in the same folder than the video subs = self.videoModel.get_checked_subtitles() replace_all = False skip_all = False if not subs: QMessageBox.about(self, _("Error"), _("No subtitles selected to be downloaded")) return total_subs = len(subs) answer = None success_downloaded = 0 # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_("Downloading...")) callback.set_label_text(_("Downloading files...")) callback.set_updated_text(_("Downloading subtitle {0} ({1}/{2})")) callback.set_finished_text( _("{0} from {1} subtitles downloaded successfully")) callback.set_block(True) callback.set_range(0, total_subs) callback.show() for i, sub in enumerate(subs): if callback.canceled(): break destinationPath = self.get_state().getDownloadPath(self, sub) if not destinationPath: break log.debug("Trying to download subtitle '%s'" % destinationPath) callback.update(i, QFileInfo(destinationPath).fileName(), i + 1, total_subs) # Check if we have write permissions, otherwise show warning window while True: # If the file and the folder don't have write access. if not QFileInfo( destinationPath).isWritable() and not QFileInfo( QFileInfo(destinationPath).absoluteDir().path() ).isWritable(): warningBox = QMessageBox( _("Error write permission"), _("%s cannot be saved.\nCheck that the folder exists and you have write-access permissions." ) % destinationPath, QMessageBox.Warning, QMessageBox.Retry | QMessageBox.Default, QMessageBox.Discard | QMessageBox.Escape, QMessageBox.NoButton, self) saveAsButton = warningBox.addButton( _("Save as..."), QMessageBox.ActionRole) answer = warningBox.exec_() if answer == QMessageBox.Retry: continue elif answer == QMessageBox.Discard: break # Let's get out from the While true # If we choose the SAVE AS elif answer == QMessageBox.NoButton: fileName, t = QFileDialog.getSaveFileName( self, _("Save subtitle as..."), destinationPath, 'All (*.*)') if fileName: destinationPath = fileName else: # If we have write access we leave the while loop. break # If we have chosen Discard subtitle button. if answer == QMessageBox.Discard: continue # Continue the next subtitle optionWhereToDownload = QSettings().value( "options/whereToDownload", "SAME_FOLDER") # Check if doesn't exists already, otherwise show fileExistsBox # dialog if QFileInfo(destinationPath).exists( ) and not replace_all and not skip_all and optionWhereToDownload != "ASK_FOLDER": # The "remote filename" below is actually not the real filename. Real name could be confusing # since we always rename downloaded sub to match movie # filename. fileExistsBox = QMessageBox( QMessageBox.Warning, _("File already exists"), _("Local: {local}\n\nRemote: {remote}\n\nHow would you like to proceed?" ).format(local=destinationPath, remote=QFileInfo(destinationPath).fileName()), QMessageBox.NoButton, self) skipButton = fileExistsBox.addButton(_("Skip"), QMessageBox.ActionRole) # skipAllButton = fileExistsBox.addButton(_("Skip all"), QMessageBox.ActionRole) replaceButton = fileExistsBox.addButton( _("Replace"), QMessageBox.ActionRole) replaceAllButton = fileExistsBox.addButton( _("Replace all"), QMessageBox.ActionRole) saveAsButton = fileExistsBox.addButton(_("Save as..."), QMessageBox.ActionRole) cancelButton = fileExistsBox.addButton(_("Cancel"), QMessageBox.ActionRole) fileExistsBox.exec_() answer = fileExistsBox.clickedButton() if answer == replaceAllButton: # Don't ask us again (for this batch of files) replace_all = True elif answer == saveAsButton: # We will find a uniqiue filename and suggest this to user. # add .<lang> to (inside) the filename. If that is not enough, start adding numbers. # There should also be a preferences setting "Autorename # files" or similar ( =never ask) FIXME suggBaseName, suggFileExt = os.path.splitext( destinationPath) fNameCtr = 0 # Counter used to generate a unique filename suggestedFileName = suggBaseName + '.' + \ sub.get_language().xxx() + suggFileExt while (os.path.exists(suggestedFileName)): fNameCtr += 1 suggestedFileName = suggBaseName + '.' + \ sub.get_language().xxx() + '-' + \ str(fNameCtr) + suggFileExt fileName, t = QFileDialog.getSaveFileName( None, _("Save subtitle as..."), suggestedFileName, 'All (*.*)') if fileName: destinationPath = fileName else: # Skip this particular file if no filename chosen continue elif answer == skipButton: continue # Skip this particular file # elif answer == skipAllButton: # count += percentage # skip_all = True # Skip all files already downloaded # continue elif answer == cancelButton: break # Break out of DL loop - cancel was pushed QCoreApplication.processEvents() # FIXME: redundant update? callback.update(i, QFileInfo(destinationPath).fileName(), i + 1, total_subs) try: if not skip_all: log.debug("Downloading subtitle '%s'" % destinationPath) download_callback = ProgressCallback() # FIXME data_stream = sub.download( provider_instance=self.get_state().get_OSDBServer(), callback=download_callback, ) write_stream(data_stream, destinationPath) except Exception as e: log.exception('Unable to Download subtitle {}'.format( sub.get_filename())) QMessageBox.about( self, _("Error"), _("Unable to download subtitle %s") % sub.get_filename()) callback.finish(success_downloaded, total_subs) def onViewOnlineInfo(self): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget # Tab for SearchByHash TODO:replace this 0 by an ENUM value listview = self.ui.videoView index = listview.currentIndex() data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item movie_info = video.getMovieInfo() if movie_info: imdb = movie_info["IDMovieImdb"] if imdb: webbrowser.open("http://www.imdb.com/title/tt%s" % imdb, new=2, autoraise=1) elif isinstance(data_item, RemoteSubtitleFile): sub = data_item webbrowser.open(sub.get_link(), new=2, autoraise=1) elif isinstance(data_item, Movie): movie = data_item imdb = movie.IMDBId if imdb: webbrowser.open("http://www.imdb.com/title/tt%s" % imdb, new=2, autoraise=1) @pyqtSlot() def onSetIMDBInfo(self): #FIXME: DUPLICATED WITH SEARCHNAMEWIDGET QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
class WatchPointViewer(QTreeView): """ Class implementing the watch expression viewer widget. Watch expressions will be shown with all their details. They can be modified through the context menu of this widget. """ def __init__(self, parent=None): """ Constructor @param parent the parent (QWidget) """ super(WatchPointViewer, self).__init__(parent) self.setObjectName("WatchExpressionViewer") self.__model = None self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setWindowTitle(self.tr("Watchpoints")) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() def setModel(self, model): """ Public slot to set the watch expression model. @param model reference to the watch expression model (WatchPointModel) """ self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setDynamicSortFilter(True) self.sortingModel.setSourceModel(self.__model) super(WatchPointViewer, self).setModel(self.sortingModel) header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) if qVersion() >= "5.0.0": header.setSectionsClickable(True) else: header.setClickable(True) self.setSortingEnabled(True) self.__layoutDisplay() def __layoutDisplay(self): """ Private slot to perform a layout operation. """ self.__resizeColumns() self.__resort() def __resizeColumns(self): """ Private slot to resize the view when items get added, edited or deleted. """ self.header().resizeSections(QHeaderView.ResizeToContents) self.header().setStretchLastSection(True) def __resort(self): """ Private slot to resort the tree. """ self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def __toSourceIndex(self, index): """ Private slot to convert an index to a source index. @param index index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """ Private slot to convert a source index to an index. @param sindex source index to be converted (QModelIndex) @return mapped index (QModelIndex) """ return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """ Private slot to select a complete row. @param index index determining the row to be selected (QModelIndex) @param selected flag indicating the action (bool) """ if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """ Private method to generate the popup menus. """ self.menu = QMenu() self.menu.addAction(self.tr("Add"), self.__addWatchPoint) self.menu.addAction(self.tr("Edit..."), self.__editWatchPoint) self.menu.addSeparator() self.menu.addAction(self.tr("Enable"), self.__enableWatchPoint) self.menu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Disable"), self.__disableWatchPoint) self.menu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Delete"), self.__deleteWatchPoint) self.menu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.menu.addSeparator() self.menu.addAction(self.tr("Configure..."), self.__configure) self.backMenuActions = {} self.backMenu = QMenu() self.backMenu.addAction(self.tr("Add"), self.__addWatchPoint) self.backMenuActions["EnableAll"] = \ self.backMenu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.backMenuActions["DisableAll"] = \ self.backMenu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.backMenuActions["DeleteAll"] = \ self.backMenu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.backMenu.addSeparator() self.backMenu.addAction(self.tr("Configure..."), self.__configure) self.backMenu.aboutToShow.connect(self.__showBackMenu) self.multiMenu = QMenu() self.multiMenu.addAction(self.tr("Add"), self.__addWatchPoint) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Enable selected"), self.__enableSelectedWatchPoints) self.multiMenu.addAction(self.tr("Enable all"), self.__enableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Disable selected"), self.__disableSelectedWatchPoints) self.multiMenu.addAction(self.tr("Disable all"), self.__disableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Delete selected"), self.__deleteSelectedWatchPoints) self.multiMenu.addAction(self.tr("Delete all"), self.__deleteAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction(self.tr("Configure..."), self.__configure) def __showContextMenu(self, coord): """ Private slot to show the context menu. @param coord the position of the mouse pointer (QPoint) """ cnt = self.__getSelectedItemsCount() if cnt <= 1: index = self.indexAt(coord) if index.isValid(): cnt = 1 self.__setRowSelected(index) coord = self.mapToGlobal(coord) if cnt > 1: self.multiMenu.popup(coord) elif cnt == 1: self.menu.popup(coord) else: self.backMenu.popup(coord) def __clearSelection(self): """ Private slot to clear the selection. """ for index in self.selectedIndexes(): self.__setRowSelected(index, False) def __findDuplicates(self, cond, special, showMessage=False, index=QModelIndex()): """ Private method to check, if an entry already exists. @param cond condition to check (string) @param special special condition to check (string) @param showMessage flag indicating a message should be shown, if a duplicate entry is found (boolean) @param index index that should not be considered duplicate (QModelIndex) @return flag indicating a duplicate entry (boolean) """ idx = self.__model.getWatchPointIndex(cond, special) duplicate = idx.isValid() and \ idx.internalPointer() != index.internalPointer() if showMessage and duplicate: if not special: msg = self.tr("""<p>A watch expression '<b>{0}</b>'""" """ already exists.</p>""")\ .format(Utilities.html_encode(cond)) else: msg = self.tr( """<p>A watch expression '<b>{0}</b>'""" """ for the variable <b>{1}</b> already exists.</p>""")\ .format(special, Utilities.html_encode(cond)) E5MessageBox.warning( self, self.tr("Watch expression already exists"), msg) return duplicate def __addWatchPoint(self): """ Private slot to handle the add watch expression context menu entry. """ from .EditWatchpointDialog import EditWatchpointDialog dlg = EditWatchpointDialog(("", False, True, 0, ""), self) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, ignorecount, special = dlg.getData() if not self.__findDuplicates(cond, special, True): self.__model.addWatchPoint(cond, special, (temp, enabled, ignorecount)) self.__resizeColumns() self.__resort() def __doubleClicked(self, index): """ Private slot to handle the double clicked signal. @param index index of the entry that was double clicked (QModelIndex) """ if index.isValid(): self.__doEditWatchPoint(index) def __editWatchPoint(self): """ Private slot to handle the edit watch expression context menu entry. """ index = self.currentIndex() if index.isValid(): self.__doEditWatchPoint(index) def __doEditWatchPoint(self, index): """ Private slot to edit a watch expression. @param index index of watch expression to be edited (QModelIndex) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): wp = self.__model.getWatchPointByIndex(sindex) if not wp: return cond, special, temp, enabled, count = wp[:5] from .EditWatchpointDialog import EditWatchpointDialog dlg = EditWatchpointDialog( (cond, temp, enabled, count, special), self) if dlg.exec_() == QDialog.Accepted: cond, temp, enabled, count, special = dlg.getData() if not self.__findDuplicates(cond, special, True, sindex): self.__model.setWatchPointByIndex( sindex, cond, special, (temp, enabled, count)) self.__resizeColumns() self.__resort() def __setWpEnabled(self, index, enabled): """ Private method to set the enabled status of a watch expression. @param index index of watch expression to be enabled/disabled (QModelIndex) @param enabled flag indicating the enabled status to be set (boolean) """ sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.setWatchPointEnabledByIndex(sindex, enabled) def __enableWatchPoint(self): """ Private slot to handle the enable watch expression context menu entry. """ index = self.currentIndex() self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __enableAllWatchPoints(self): """ Private slot to handle the enable all watch expressions context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __enableSelectedWatchPoints(self): """ Private slot to handle the enable selected watch expressions context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __disableWatchPoint(self): """ Private slot to handle the disable watch expression context menu entry. """ index = self.currentIndex() self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __disableAllWatchPoints(self): """ Private slot to handle the disable all watch expressions context menu entry. """ index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __disableSelectedWatchPoints(self): """ Private slot to handle the disable selected watch expressions context menu entry. """ for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __deleteWatchPoint(self): """ Private slot to handle the delete watch expression context menu entry. """ index = self.currentIndex() sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.deleteWatchPointByIndex(sindex) def __deleteAllWatchPoints(self): """ Private slot to handle the delete all watch expressions context menu entry. """ self.__model.deleteAll() def __deleteSelectedWatchPoints(self): """ Private slot to handle the delete selected watch expressions context menu entry. """ idxList = [] for index in self.selectedIndexes(): sindex = self.__toSourceIndex(index) if sindex.isValid() and index.column() == 0: idxList.append(sindex) self.__model.deleteWatchPoints(idxList) def __showBackMenu(self): """ Private slot to handle the aboutToShow signal of the background menu. """ if self.model().rowCount() == 0: self.backMenuActions["EnableAll"].setEnabled(False) self.backMenuActions["DisableAll"].setEnabled(False) self.backMenuActions["DeleteAll"].setEnabled(False) else: self.backMenuActions["EnableAll"].setEnabled(True) self.backMenuActions["DisableAll"].setEnabled(True) self.backMenuActions["DeleteAll"].setEnabled(True) def __getSelectedItemsCount(self): """ Private method to get the count of items selected. @return count of items selected (integer) """ count = len(self.selectedIndexes()) // (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface")\ .showPreferences("debuggerGeneralPage")
class MainWindow(QMainWindow, Ui_MainWindow): """ Main window for the application with groups and password lists """ KEY_IDX = 0 # column where key is shown in password table PASSWORD_IDX = 1 # column where password is shown in password table COMMENTS_IDX = 2 # column where comments is shown in password table NO_OF_PASSWDTABLE_COLUMNS = 3 # 3 columns: key + value/passwd/secret + comments CACHE_IDX = 0 # column of QWidgetItem in whose data we cache decrypted passwords+comments def __init__(self, pwMap, settings, dbFilename): """ @param pwMap: a PasswordMap instance with encrypted passwords @param dbFilename: file name for saving pwMap """ super(MainWindow, self).__init__() self.setupUi(self) self.logger = settings.logger self.settings = settings self.pwMap = pwMap self.selectedGroup = None self.modified = False # modified flag for "Save?" question on exit self.dbFilename = dbFilename self.groupsModel = QStandardItemModel(parent=self) self.groupsModel.setHorizontalHeaderLabels([u"Password group"]) self.groupsFilter = QSortFilterProxyModel(parent=self) self.groupsFilter.setSourceModel(self.groupsModel) self.groupsTree.setModel(self.groupsFilter) self.groupsTree.setContextMenuPolicy(Qt.CustomContextMenu) self.groupsTree.customContextMenuRequested.connect(self.showGroupsContextMenu) # Dont use e following line, it would cause loadPasswordsBySelection # to be called twice on mouse-click. # self.groupsTree.clicked.connect(self.loadPasswordsBySelection) self.groupsTree.selectionModel().selectionChanged.connect(self.loadPasswordsBySelection) self.groupsTree.setSortingEnabled(True) self.passwordTable.setContextMenuPolicy(Qt.CustomContextMenu) self.passwordTable.customContextMenuRequested.connect(self.showPasswdContextMenu) self.passwordTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.passwordTable.setSelectionMode(QAbstractItemView.SingleSelection) shortcut = QShortcut(QKeySequence(u"Ctrl+C"), self.passwordTable, self.copyPasswordFromSelection) shortcut.setContext(Qt.WidgetShortcut) self.actionQuit.triggered.connect(self.close) self.actionQuit.setShortcut(QKeySequence(u"Ctrl+Q")) self.actionExport.triggered.connect(self.exportCsv) self.actionImport.triggered.connect(self.importCsv) self.actionBackup.triggered.connect(self.saveBackup) self.actionAbout.triggered.connect(self.printAbout) self.actionSave.triggered.connect(self.saveDatabase) self.actionSave.setShortcut(QKeySequence(u"Ctrl+S")) # headerKey = QTableWidgetItem(u"Key") # headerValue = QTableWidgetItem(u"Password/Value") # headerComments = QTableWidgetItem(u"Comments") # self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS) # self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey) # self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue) # self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments) # # self.passwordTable.resizeRowsToContents() # self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) # self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) # self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.searchEdit.textChanged.connect(self.filterGroups) if pwMap is not None: self.setPwMap(pwMap) self.clipboard = QApplication.clipboard() self.timer = QTimer(parent=self) self.timer.timeout.connect(self.clearClipboard) def setPwMap(self, pwMap): """ if not done in __init__ pwMap can be supplied later """ self.pwMap = pwMap groupNames = self.pwMap.groups.keys() for groupName in groupNames: item = QStandardItem(groupName) self.groupsModel.appendRow(item) self.groupsTree.sortByColumn(0, Qt.AscendingOrder) self.settings.mlogger.log("pwMap was initialized.", logging.DEBUG, "GUI IO") def setModified(self, modified): """ Sets the modified flag so that user is notified when exiting with unsaved changes. """ self.modified = modified self.setWindowTitle("TrezorPass" + "*" * int(self.modified)) def showGroupsContextMenu(self, point): """ Show context menu for group management. @param point: point in self.groupsTree where click occured """ self.addGroupMenu = QMenu(self) newGroupAction = QAction('Add group', self) editGroupAction = QAction('Rename group', self) deleteGroupAction = QAction('Delete group', self) self.addGroupMenu.addAction(newGroupAction) self.addGroupMenu.addAction(editGroupAction) self.addGroupMenu.addAction(deleteGroupAction) # disable deleting if no point is clicked on proxyIdx = self.groupsTree.indexAt(point) itemIdx = self.groupsFilter.mapToSource(proxyIdx) item = self.groupsModel.itemFromIndex(itemIdx) if item is None: deleteGroupAction.setEnabled(False) action = self.addGroupMenu.exec_(self.groupsTree.mapToGlobal(point)) if action == newGroupAction: self.createGroupWithCheck() elif action == editGroupAction: self.editGroupWithCheck(item) elif action == deleteGroupAction: self.deleteGroupWithCheck(item) def showPasswdContextMenu(self, point): """ Show context menu for password management @param point: point in self.passwordTable where click occured """ self.passwdMenu = QMenu(self) showPasswordAction = QAction('Show password', self) copyPasswordAction = QAction('Copy password', self) copyPasswordAction.setShortcut(QKeySequence("Ctrl+C")) showCommentsAction = QAction('Show comments', self) copyCommentsAction = QAction('Copy comments', self) showAllAction = QAction('Show all of group', self) newItemAction = QAction('New item', self) deleteItemAction = QAction('Delete item', self) editItemAction = QAction('Edit item', self) self.passwdMenu.addAction(showPasswordAction) self.passwdMenu.addAction(copyPasswordAction) self.passwdMenu.addSeparator() self.passwdMenu.addAction(showCommentsAction) self.passwdMenu.addAction(copyCommentsAction) self.passwdMenu.addSeparator() self.passwdMenu.addAction(showAllAction) self.passwdMenu.addSeparator() self.passwdMenu.addAction(newItemAction) self.passwdMenu.addAction(deleteItemAction) self.passwdMenu.addAction(editItemAction) # disable creating if no group is selected if self.selectedGroup is None: newItemAction.setEnabled(False) showAllAction.setEnabled(False) # disable deleting if no point is clicked on item = self.passwordTable.itemAt(point.x(), point.y()) if item is None: deleteItemAction.setEnabled(False) showPasswordAction.setEnabled(False) copyPasswordAction.setEnabled(False) showCommentsAction.setEnabled(False) copyCommentsAction.setEnabled(False) editItemAction.setEnabled(False) action = self.passwdMenu.exec_(self.passwordTable.mapToGlobal(point)) if action == newItemAction: self.createPassword() elif action == deleteItemAction: self.deletePassword(item) elif action == editItemAction: self.editPassword(item) elif action == copyPasswordAction: self.copyPasswordFromItem(item) elif action == showPasswordAction: self.showPassword(item) elif action == copyCommentsAction: self.copyCommentsFromItem(item) elif action == showCommentsAction: self.showComments(item) elif action == showAllAction: self.showAll() def createGroup(self, groupName, group=None): """ Slot to create a password group. """ newItem = QStandardItem(groupName) self.groupsModel.appendRow(newItem) self.pwMap.addGroup(groupName) if group is not None: self.pwMap.replaceGroup(groupName, group) # make new item selected to save a few clicks itemIdx = self.groupsModel.indexFromItem(newItem) proxyIdx = self.groupsFilter.mapFromSource(itemIdx) self.groupsTree.selectionModel().select(proxyIdx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) self.groupsTree.sortByColumn(0, Qt.AscendingOrder) # Make item's passwords loaded so new key-value entries can be created # right away - better from UX perspective. self.loadPasswords(newItem) self.setModified(True) self.settings.mlogger.log("Group '%s' was created." % (groupName), logging.DEBUG, "GUI IO") def createGroupWithCheck(self): """ Slot to create a password group. """ dialog = AddGroupDialog(self.pwMap.groups, self.settings) if not dialog.exec_(): return groupName = dialog.newGroupName() self.createGroup(groupName) def createRenamedGroupWithCheck(self, groupNameOld, groupNameNew): """ Creates a copy of a group by name as utf-8 encoded string with a new group name. A more appropriate name for the method would be: createRenamedGroup(). Since the entries inside the group are encrypted with the groupName, we cannot simply make a copy. We must decrypt with old name and afterwards encrypt with new name. If the group has many entries, each entry would require a 'Confirm' press on Trezor. So, to mkae it faster and more userfriendly we use the backup key to decrypt. This requires a single Trezor 'Confirm' press independent of how many entries there are in the group. @param groupNameOld: name of group to copy and rename @type groupNameOld: string @param groupNameNew: name of group to be created @type groupNameNew: string """ if groupNameOld not in self.pwMap.groups: raise KeyError("Password group does not exist") # with less than 3 rows dont bother the user with a pop-up rowCount = len(self.pwMap.groups[groupNameOld].entries) if rowCount < 3: self.pwMap.createRenamedGroupSecure(groupNameOld, groupNameNew) return msgBox = QMessageBox(parent=self) msgBox.setText("Do you want to use the more secure way?") msgBox.setIcon(QMessageBox.Question) msgBox.setWindowTitle("How to decrypt?") msgBox.setDetailedText("The more secure way requires pressing 'Confirm' " "on Trezor once for each entry in the group. %d presses in this " "case. This is recommended. " "Select 'Yes'.\n\n" "The less secure way requires only a single 'Confirm' click on the " "Trezor. This is not recommended. " "Select 'No'." % (rowCount)) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.Yes) res = msgBox.exec_() if res == QMessageBox.Yes: moreSecure = True else: moreSecure = False groupNew = self.pwMap.createRenamedGroup(groupNameOld, groupNameNew, moreSecure) self.settings.mlogger.log("Copy of group '%s' with new name '%s' " "was created the %s way." % (groupNameOld, groupNameNew, 'secure' if moreSecure else 'fast'), logging.DEBUG, "GUI IO") return(groupNew) def editGroup(self, item, groupNameOld, groupNameNew): """ Slot to edit name a password group. """ groupNew = self.createRenamedGroupWithCheck(groupNameOld, groupNameNew) self.deleteGroup(item) self.createGroup(groupNameNew, groupNew) self.settings.mlogger.log("Group '%s' was renamed to '%s'." % (groupNameOld, groupNameNew), logging.DEBUG, "GUI IO") def editGroupWithCheck(self, item): """ Slot to edit name a password group. """ groupNameOld = encoding.normalize_nfc(item.text()) dialog = AddGroupDialog(self.pwMap.groups, self.settings) dialog.setWindowTitle("Edit group name") dialog.groupNameLabel.setText("New name for group") dialog.setNewGroupName(groupNameOld) if not dialog.exec_(): return groupNameNew = dialog.newGroupName() self.editGroup(item, groupNameOld, groupNameNew) def deleteGroup(self, item): # without checking user groupName = encoding.normalize_nfc(item.text()) self.selectedGroup = None del self.pwMap.groups[groupName] itemIdx = self.groupsModel.indexFromItem(item) self.groupsModel.takeRow(itemIdx.row()) self.passwordTable.setRowCount(0) self.groupsTree.clearSelection() self.setModified(True) self.settings.mlogger.log("Group '%s' was deleted." % (groupName), logging.DEBUG, "GUI IO") def deleteGroupWithCheck(self, item): msgBox = QMessageBox(text="Are you sure about delete?", parent=self) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) res = msgBox.exec_() if res != QMessageBox.Yes: return self.deleteGroup(item) def deletePassword(self, item): msgBox = QMessageBox(text="Are you sure about delete?", parent=self) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) res = msgBox.exec_() if res != QMessageBox.Yes: return row = self.passwordTable.row(item) self.passwordTable.removeRow(row) group = self.pwMap.groups[self.selectedGroup] group.removeEntry(row) self.passwordTable.resizeRowsToContents() self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.setModified(True) self.settings.mlogger.log("Row '%d' was deleted." % (row), logging.DEBUG, "GUI IO") def logCache(self, row): item = self.passwordTable.item(row, self.CACHE_IDX) cachedTuple = item.data(Qt.UserRole) if cachedTuple is None: cachedPassword, cachedComments = (None, None) else: cachedPassword, cachedComments = cachedTuple if cachedPassword is not None: cachedPassword = u'***' if cachedComments is not None: cachedComments = cachedComments[0:3] + u'...' self.settings.mlogger.log("Cache holds '%s' and '%s'." % (cachedPassword, cachedComments), logging.DEBUG, "Cache") def cachePasswordComments(self, row, password, comments): item = self.passwordTable.item(row, self.CACHE_IDX) item.setData(Qt.UserRole, QVariant((password, comments))) def cachedPassword(self, row): """ Retrieve cached password for given row of currently selected group. Returns password as string or None if no password cached. """ item = self.passwordTable.item(row, self.CACHE_IDX) cachedTuple = item.data(Qt.UserRole) if cachedTuple is None: cachedPassword, cachedComments = (None, None) else: cachedPassword, cachedComments = cachedTuple return cachedPassword def cachedComments(self, row): """ Retrieve cached comments for given row of currently selected group. Returns comments as string or None if no coments cached. """ item = self.passwordTable.item(row, self.CACHE_IDX) cachedTuple = item.data(Qt.UserRole) if cachedTuple is None: cachedPassword, cachedComments = (None, None) else: cachedPassword, cachedComments = cachedTuple return cachedComments def cachedOrDecryptPassword(self, row): """ Try retrieving cached password for item in given row, otherwise decrypt with Trezor. """ cached = self.cachedPassword(row) if cached is not None: return cached else: # decrypt with Trezor group = self.pwMap.groups[self.selectedGroup] pwEntry = group.entry(row) encPwComments = pwEntry[1] decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup) lngth = int(decryptedPwComments[0:4]) decryptedPassword = decryptedPwComments[4:4+lngth] decryptedComments = decryptedPwComments[4+lngth:] # while we are at it, cache the comments too self.cachePasswordComments(row, decryptedPassword, decryptedComments) self.settings.mlogger.log("Decrypted password and comments " "for '%s', row '%d'." % (pwEntry[0], row), logging.DEBUG, "GUI IO") return decryptedPassword def cachedOrDecryptComments(self, row): """ Try retrieving cached comments for item in given row, otherwise decrypt with Trezor. """ cached = self.cachedComments(row) if cached is not None: return cached else: # decrypt with Trezor group = self.pwMap.groups[self.selectedGroup] pwEntry = group.entry(row) encPwComments = pwEntry[1] decryptedPwComments = self.pwMap.decryptPassword(encPwComments, self.selectedGroup) lngth = int(decryptedPwComments[0:4]) decryptedPassword = decryptedPwComments[4:4+lngth] decryptedComments = decryptedPwComments[4+lngth:] self.cachePasswordComments(row, decryptedPassword, decryptedComments) self.settings.mlogger.log("Decrypted password and comments " "for '%s', row '%d'." % (pwEntry[0], row), logging.DEBUG, "GUI IO") return decryptedComments def showPassword(self, item): # check if this password has been decrypted, use cached version row = self.passwordTable.row(item) self.logCache(row) try: decryptedPassword = self.cachedOrDecryptPassword(row) except CallException: return item = QTableWidgetItem(decryptedPassword) self.passwordTable.setItem(row, self.PASSWORD_IDX, item) def showComments(self, item): # check if these comments has been decrypted, use cached version row = self.passwordTable.row(item) try: decryptedComments = self.cachedOrDecryptComments(row) except CallException: return item = QTableWidgetItem(decryptedComments) self.passwordTable.setItem(row, self.COMMENTS_IDX, item) def showAllSecure(self): rowCount = self.passwordTable.rowCount() for row in range(rowCount): try: decryptedPassword = self.cachedOrDecryptPassword(row) except CallException: return item = QTableWidgetItem(decryptedPassword) self.passwordTable.setItem(row, self.PASSWORD_IDX, item) try: decryptedComments = self.cachedOrDecryptComments(row) except CallException: return item = QTableWidgetItem(decryptedComments) self.passwordTable.setItem(row, self.COMMENTS_IDX, item) self.settings.mlogger.log("Showed all entries for group '%s' the secure way." % (self.selectedGroup), logging.DEBUG, "GUI IO") def showAllFast(self): try: privateKey = self.pwMap.backupKey.unwrapPrivateKey() except CallException: return group = self.pwMap.groups[self.selectedGroup] row = 0 for key, _, bkupPw in group.entries: decryptedPwComments = self.pwMap.backupKey.decryptPassword(bkupPw, privateKey) lngth = int(decryptedPwComments[0:4]) password = decryptedPwComments[4:4+lngth] comments = decryptedPwComments[4+lngth:] item = QTableWidgetItem(key) pwItem = QTableWidgetItem(password) commentsItem = QTableWidgetItem(comments) self.passwordTable.setItem(row, self.KEY_IDX, item) self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem) self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem) self.cachePasswordComments(row, password, comments) row = row+1 self.passwordTable.resizeRowsToContents() self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.settings.mlogger.log("Showed all entries for group '%s' the fast way." % (self.selectedGroup), logging.DEBUG, "GUI IO") def showAll(self): """ show all passwords and comments in plaintext in GUI can be called without any password selectedGroup a group must be selected """ # with less than 3 rows dont bother the user with a pop-up if self.passwordTable.rowCount() < 3: self.showAllSecure() return msgBox = QMessageBox(parent=self) msgBox.setText("Do you want to use the more secure way?") msgBox.setIcon(QMessageBox.Question) msgBox.setWindowTitle("How to decrypt?") msgBox.setDetailedText("The more secure way requires pressing 'Confirm' " "on Trezor once for each entry in the group. %d presses in this " "case. This is recommended. " "Select 'Yes'.\n\n" "The less secure way requires only a single 'Confirm' click on the " "Trezor. This is not recommended. " "Select 'No'." % (self.passwordTable.rowCount())) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgBox.setDefaultButton(QMessageBox.Yes) res = msgBox.exec_() if res == QMessageBox.Yes: self.showAllSecure() else: self.showAllFast() def createPassword(self): """ Slot to create key-value password entry. """ if self.selectedGroup is None: return group = self.pwMap.groups[self.selectedGroup] dialog = AddPasswordDialog(self.pwMap.trezor, self.settings) if not dialog.exec_(): return plainPw = dialog.pw1() plainComments = dialog.comments() if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS: self.settings.mlogger.log("Password and/or comments too long. " "Combined they must not be larger than %d." % basics.MAX_SIZE_OF_PASSWDANDCOMMENTS, logging.CRITICAL, "User IO") return row = self.passwordTable.rowCount() self.passwordTable.setRowCount(row+1) item = QTableWidgetItem(dialog.key()) pwItem = QTableWidgetItem("*****") commentsItem = QTableWidgetItem("*****") self.passwordTable.setItem(row, self.KEY_IDX, item) self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem) self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem) plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup) bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments) group.addEntry(dialog.key(), encPw, bkupPw) self.cachePasswordComments(row, plainPw, plainComments) self.passwordTable.resizeRowsToContents() self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.setModified(True) self.settings.mlogger.log("Password and comments entry " "for '%s', row '%d' was created." % (dialog.key(), row), logging.DEBUG, "GUI IO") def editPassword(self, item): row = self.passwordTable.row(item) group = self.pwMap.groups[self.selectedGroup] try: decrypted = self.cachedOrDecryptPassword(row) decryptedComments = self.cachedOrDecryptComments(row) except CallException: return dialog = AddPasswordDialog(self.pwMap.trezor, self.settings) entry = group.entry(row) dialog.keyEdit.setText(encoding.normalize_nfc(entry[0])) dialog.pwEdit1.setText(encoding.normalize_nfc(decrypted)) dialog.pwEdit2.setText(encoding.normalize_nfc(decrypted)) doc = QTextDocument(encoding.normalize_nfc(decryptedComments), parent=self) dialog.commentsEdit.setDocument(doc) if not dialog.exec_(): return item = QTableWidgetItem(dialog.key()) pwItem = QTableWidgetItem("*****") commentsItem = QTableWidgetItem("*****") self.passwordTable.setItem(row, self.KEY_IDX, item) self.passwordTable.setItem(row, self.PASSWORD_IDX, pwItem) self.passwordTable.setItem(row, self.COMMENTS_IDX, commentsItem) plainPw = dialog.pw1() plainComments = dialog.comments() if len(plainPw) + len(plainComments) > basics.MAX_SIZE_OF_PASSWDANDCOMMENTS: self.settings.mlogger.log("Password and/or comments too long. " "Combined they must not be larger than %d." % basics.MAX_SIZE_OF_PASSWDANDCOMMENTS, logging.CRITICAL, "User IO") return plainPwComments = ("%4d" % len(plainPw)) + plainPw + plainComments encPw = self.pwMap.encryptPassword(plainPwComments, self.selectedGroup) bkupPw = self.pwMap.backupKey.encryptPassword(plainPwComments) group.updateEntry(row, dialog.key(), encPw, bkupPw) self.cachePasswordComments(row, plainPw, plainComments) self.setModified(True) self.settings.mlogger.log("Password and comments entry " "for '%s', row '%d' was edited." % (dialog.key(), row), logging.DEBUG, "GUI IO") def copyPasswordFromSelection(self): """ Copy selected password to clipboard. Password is decrypted if necessary. """ indexes = self.passwordTable.selectedIndexes() if not indexes: return # there will be more indexes as the selection is on a row row = indexes[0].row() item = self.passwordTable.item(row, self.PASSWORD_IDX) self.copyPasswordFromItem(item) def copyPasswordFromItem(self, item): row = self.passwordTable.row(item) try: decryptedPassword = self.cachedOrDecryptPassword(row) except CallException: return self.clipboard.setText(decryptedPassword) # Do not log contents of clipboard, contains secrets! self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG, "Clipboard") if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0: self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000) # cancels previous timer def copyCommentsFromItem(self, item): row = self.passwordTable.row(item) try: decryptedComments = self.cachedOrDecryptComments(row) except CallException: return self.clipboard.setText(decryptedComments) # Do not log contents of clipboard, contains secrets! self.settings.mlogger.log("Copied text to clipboard.", logging.DEBUG, "Clipboard") if basics.CLIPBOARD_TIMEOUT_IN_SEC > 0: self.timer.start(basics.CLIPBOARD_TIMEOUT_IN_SEC*1000) # cancels previous timer def clearClipboard(self): self.clipboard.clear() self.timer.stop() # cancels previous timer self.settings.mlogger.log("Clipboard cleared.", logging.DEBUG, "Clipboard") def loadPasswords(self, item): """ Slot that should load items for group that has been clicked on. """ self.passwordTable.clear() # clears cahce, but also clears the header, the 3 titles headerKey = QTableWidgetItem(u"Key") headerValue = QTableWidgetItem(u"Password/Value") headerComments = QTableWidgetItem(u"Comments") self.passwordTable.setColumnCount(self.NO_OF_PASSWDTABLE_COLUMNS) self.passwordTable.setHorizontalHeaderItem(self.KEY_IDX, headerKey) self.passwordTable.setHorizontalHeaderItem(self.PASSWORD_IDX, headerValue) self.passwordTable.setHorizontalHeaderItem(self.COMMENTS_IDX, headerComments) groupName = encoding.normalize_nfc(item.text()) self.selectedGroup = groupName group = self.pwMap.groups[groupName] self.passwordTable.setRowCount(len(group.entries)) i = 0 for key, encValue, bkupValue in group.entries: item = QTableWidgetItem(key) pwItem = QTableWidgetItem("*****") commentsItem = QTableWidgetItem("*****") self.passwordTable.setItem(i, self.KEY_IDX, item) self.passwordTable.setItem(i, self.PASSWORD_IDX, pwItem) self.passwordTable.setItem(i, self.COMMENTS_IDX, commentsItem) i = i+1 self.passwordTable.resizeRowsToContents() self.passwordTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.passwordTable.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.passwordTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.settings.mlogger.log("Loaded password group '%s'." % (groupName), logging.DEBUG, "GUI IO") def loadPasswordsBySelection(self): proxyIdx = self.groupsTree.currentIndex() itemIdx = self.groupsFilter.mapToSource(proxyIdx) selectedItem = self.groupsModel.itemFromIndex(itemIdx) if not selectedItem: return self.loadPasswords(selectedItem) def filterGroups(self, substring): """ Filter groupsTree view to have items containing given substring. """ self.groupsFilter.setFilterFixedString(substring) self.groupsTree.sortByColumn(0, Qt.AscendingOrder) def printAbout(self): """ Show window with about and version information. """ msgBox = QMessageBox(QMessageBox.Information, "About", "About <b>TrezorPass</b>: <br><br>TrezorPass is a safe " + "Password Manager application for people owning a Trezor who prefer to " + "keep their passwords local and not on the cloud. All passwords are " + "stored locally in a single file.<br><br>" + "<b>" + basics.NAME + " Version: </b>" + basics.VERSION_STR + " from " + basics.VERSION_DATE_STR + "<br><br><b>Python Version: </b>" + sys.version.replace(" \n", "; ") + "<br><br><b>Qt Version: </b>" + QT_VERSION_STR + "<br><br><b>PyQt Version: </b>" + PYQT_VERSION_STR, parent=self) msgBox.setIconPixmap(QPixmap("icons/TrezorPass.svg")) msgBox.exec_() def saveBackup(self): """ First it saves any pending changes to the pwdb database file. Then it uses an operating system call to copy the file appending a timestamp at the end of the file name. """ if self.modified: self.saveDatabase() backupFilename = self.settings.dbFilename + u"." + time.strftime('%Y%m%d%H%M%S') copyfile(self.settings.dbFilename, backupFilename) self.settings.mlogger.log("Backup of the encrypted database file has been created " "and placed into file \"%s\" (%d bytes)." % (backupFilename, os.path.getsize(backupFilename)), logging.INFO, "User IO") def importCsv(self): """ Read a properly formated CSV file from disk and add its contents to the current entries. Import format in CSV should be : group, key, password, comments There is no error checking, so be extra careful. Make a backup first. Entries from CSV will be *added* to existing pwdb. If this is not desired create an empty pwdb file first. GroupNames are unique, so if a groupname exists then key-password-comments tuples are added to the already existing group. If a group name does not exist, a new group is created and the key-password-comments tuples are added to the newly created group. Keys are not unique. So key-password-comments are always added. If a key with a given name existed before and the CSV file contains a key with the same name, then the key-password-comments is added and after the import the given group has 2 keys with the same name. Both keys exist then, the old from before the import, and the new one from the import. Examples of valid CSV file format: Some example lines First Bank account,login,myloginname, # no comment [email protected],2-factor-authentication key,abcdef12345678,seed to regenerate 2FA codes # with comment [email protected],recovery phrase,"passwd with 2 commas , ,", # with comma [email protected],large multi-line comments,,"first line, some comma, second line" """ if self.modified: self.saveDatabase() copyfile(self.settings.dbFilename, self.settings.dbFilename + ".beforeCsvImport.backup") self.settings.mlogger.log("WARNING: You are about to import entries from a " "CSV file into your current password-database file. For safety " "reasons please make a backup copy now.\nFurthermore, this" "operation can be slow, so please be patient.", logging.NOTSET, "CSV import") dialog = QFileDialog(self, "Select CSV file to import", "", "CSV files (*.csv)") dialog.setAcceptMode(QFileDialog.AcceptOpen) res = dialog.exec_() if not res: return fname = encoding.normalize_nfc(dialog.selectedFiles()[0]) listOfAddedGroupNames = self.pwMap.importCsv(fname) for groupNameNew in listOfAddedGroupNames: item = QStandardItem(groupNameNew) self.groupsModel.appendRow(item) self.groupsTree.sortByColumn(0, Qt.AscendingOrder) self.setModified(True) def exportCsv(self): """ Uses backup key encrypted by Trezor to decrypt all passwords at once and export them into a single paintext CSV file. Export format is CSV: group, key, password, comments """ self.settings.mlogger.log("WARNING: During backup/export all passwords will be " "written in plaintext to disk. If possible you should consider performing this " "operation on an offline or air-gapped computer. Be aware of the risks.", logging.NOTSET, "CSV export") dialog = QFileDialog(self, "Select backup export file", "", "CSV files (*.csv)") dialog.setAcceptMode(QFileDialog.AcceptSave) res = dialog.exec_() if not res: return fname = encoding.normalize_nfc(dialog.selectedFiles()[0]) self.pwMap.exportCsv(fname) def saveDatabase(self): """ Save main database file. """ self.pwMap.save(self.dbFilename) self.setModified(False) self.settings.mlogger.log("TrezorPass password database file was " "saved to '%s'." % (self.dbFilename), logging.DEBUG, "GUI IO") def closeEvent(self, event): if self.modified: msgBox = QMessageBox(text="Password database is modified. Save on exit?", parent=self) msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) reply = msgBox.exec_() if not reply or reply == QMessageBox.Cancel: event.ignore() return elif reply == QMessageBox.Yes: self.saveDatabase() event.accept()
class SearchFileWidget(QWidget): language_filter_change = pyqtSignal(list) def __init__(self): QWidget.__init__(self) self._refreshing = False self.fileModel = None self.proxyFileModel = None self.videoModel = None self._state = None self.timeLastSearch = QTime.currentTime() self.ui = Ui_SearchFileWidget() self.setup_ui() QTimer.singleShot(0, self.on_event_loop_started) @pyqtSlot() def on_event_loop_started(self): lastDir = self._state.get_video_path() index = self.fileModel.index(str(lastDir)) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) def set_state(self, state): self._state = state self._state.signals.interface_language_changed.connect( self.on_interface_language_changed) self._state.signals.login_status_changed.connect( self.on_login_state_changed) def setup_ui(self): self.ui.setupUi(self) self.ui.splitter.setSizes([600, 1000]) self.ui.splitter.setChildrenCollapsible(False) # Set up folder view self.fileModel = QFileSystemModel(self) self.fileModel.setFilter(QDir.AllDirs | QDir.Dirs | QDir.Drives | QDir.NoDotAndDotDot | QDir.Readable | QDir.Executable | QDir.Writable) self.fileModel.iconProvider().setOptions( QFileIconProvider.DontUseCustomDirectoryIcons) self.fileModel.setRootPath(QDir.rootPath()) self.fileModel.directoryLoaded.connect(self.onFileModelDirectoryLoaded) self.proxyFileModel = QSortFilterProxyModel(self) self.proxyFileModel.setSortRole(Qt.DisplayRole) self.proxyFileModel.setSourceModel(self.fileModel) self.proxyFileModel.sort(0, Qt.AscendingOrder) self.proxyFileModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.ui.folderView.setModel(self.proxyFileModel) self.ui.folderView.setHeaderHidden(True) self.ui.folderView.hideColumn(3) self.ui.folderView.hideColumn(2) self.ui.folderView.hideColumn(1) self.ui.folderView.expanded.connect(self.onFolderViewExpanded) self.ui.folderView.clicked.connect(self.onFolderTreeClicked) self.ui.buttonFind.clicked.connect(self.onButtonFind) self.ui.buttonRefresh.clicked.connect(self.onButtonRefresh) # Setup and disable buttons self.ui.buttonFind.setEnabled(False) self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) # Set up introduction self.showInstructions() # Set unknown text here instead of `retranslate()` because widget translates itself self.ui.filterLanguageForVideo.set_unknown_text(_('All languages')) self.ui.filterLanguageForVideo.set_selected_language( UnknownLanguage.create_generic()) self.ui.filterLanguageForVideo.selected_language_changed.connect( self.on_language_combobox_filter_change) # Set up video view self.videoModel = VideoModel(self) self.videoModel.connect_treeview(self.ui.videoView) self.ui.videoView.setHeaderHidden(True) self.ui.videoView.clicked.connect(self.onClickVideoTreeView) self.ui.videoView.selectionModel().currentChanged.connect( self.onSelectVideoTreeView) self.ui.videoView.customContextMenuRequested.connect(self.onContext) self.ui.videoView.setUniformRowHeights(True) self.videoModel.dataChanged.connect(self.subtitlesCheckedChanged) self.language_filter_change.connect( self.videoModel.on_filter_languages_change) self.ui.buttonSearchSelectVideos.clicked.connect( self.onButtonSearchSelectVideos) self.ui.buttonSearchSelectFolder.clicked.connect( self.onButtonSearchSelectFolder) self.ui.buttonDownload.clicked.connect(self.onButtonDownload) self.ui.buttonPlay.clicked.connect(self.onButtonPlay) self.ui.buttonIMDB.clicked.connect(self.onViewOnlineInfo) self.ui.videoView.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.buttonPlay.setEnabled(False) # Drag and Drop files to the videoView enabled # FIXME: enable drag events for videoView (and instructions view) self.ui.videoView.__class__.dragEnterEvent = self.dragEnterEvent self.ui.videoView.__class__.dragMoveEvent = self.dragEnterEvent self.ui.videoView.__class__.dropEvent = self.dropEvent self.ui.videoView.setAcceptDrops(1) self.retranslate() def retranslate(self): introduction = '<p align="center"><h2>{title}</h2></p>' \ '<p><b>{tab1header}</b><br/>{tab1content}</p>' \ '<p><b>{tab2header}</b><br/>{tab2content}</p>'\ '<p><b>{tab3header}</b><br/>{tab3content}</p>'.format( title=_('How To Use {title}').format(title=PROJECT_TITLE), tab1header=_('1st Tab:'), tab2header=_('2nd Tab:'), tab3header=_('3rd Tab:'), tab1content=_('Select, from the Folder Tree on the left, the folder which contains the videos ' 'that need subtitles. {project} will then try to automatically find available ' 'subtitles.').format(project=PROJECT_TITLE), tab2content=_('If you don\'t have the videos in your machine, you can search subtitles by ' 'introducing the title/name of the video.').format(project=PROJECT_TITLE), tab3content=_('If you have found some subtitle somewhere else that is not in {project}\'s database, ' 'please upload those subtitles so next users will be able to ' 'find them more easily.').format(project=PROJECT_TITLE)) self.ui.introductionHelp.setHtml(introduction) @pyqtSlot(Language) def on_interface_language_changed(self, language): self.ui.retranslateUi(self) self.retranslate() @pyqtSlot(str) def onFileModelDirectoryLoaded(self, path): lastDir = str(self._state.get_video_path()) qDirLastDir = QDir(lastDir) qDirLastDir.cdUp() if qDirLastDir.path() == path: index = self.fileModel.index(lastDir) proxyIndex = self.proxyFileModel.mapFromSource(index) self.ui.folderView.scrollTo(proxyIndex) self.ui.folderView.setCurrentIndex(proxyIndex) @pyqtSlot() def on_login_state_changed(self): log.debug('on_login_state_changed()') nb_connected = self._state.providers.get_number_connected_providers() if nb_connected: self.ui.buttonSearchSelectFolder.setEnabled(True) self.ui.buttonSearchSelectVideos.setEnabled(True) self.ui.buttonFind.setEnabled( self.get_current_selected_folder() is not None) else: self.ui.buttonSearchSelectFolder.setEnabled(False) self.ui.buttonSearchSelectVideos.setEnabled(False) self.ui.buttonFind.setEnabled(False) @pyqtSlot(Language) def on_language_combobox_filter_change(self, language): if language.is_generic(): self.language_filter_change.emit( self._state.get_download_languages()) else: self.language_filter_change.emit([language]) def on_permanent_language_filter_change(self, languages): selected_language = self.ui.filterLanguageForVideo.get_selected_language( ) if selected_language.is_generic(): self.language_filter_change.emit(languages) @pyqtSlot() def subtitlesCheckedChanged(self): subs = self.videoModel.get_checked_subtitles() if subs: self.ui.buttonDownload.setEnabled(True) else: self.ui.buttonDownload.setEnabled(False) def showInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageIntroduction) def hideInstructions(self): self.ui.stackedSearchResult.setCurrentWidget(self.ui.pageSearchResult) @pyqtSlot(QModelIndex) def onFolderTreeClicked(self, proxyIndex): """What to do when a Folder in the tree is clicked""" if not proxyIndex.isValid(): return index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = self.fileModel.filePath(index) self._state.set_video_paths([folder_path]) def get_current_selected_folder(self): proxyIndex = self.ui.folderView.currentIndex() index = self.proxyFileModel.mapToSource(proxyIndex) folder_path = Path(self.fileModel.filePath(index)) if not folder_path: return None return folder_path def get_current_selected_item_videomodel(self): current_index = self.ui.videoView.currentIndex() return self.videoModel.getSelectedItem(current_index) @pyqtSlot() def onButtonFind(self): now = QTime.currentTime() if now < self.timeLastSearch.addMSecs(500): return folder_path = self.get_current_selected_folder() self._state.set_video_paths([folder_path]) self.search_videos([folder_path]) self.timeLastSearch = QTime.currentTime() @pyqtSlot() def onButtonRefresh(self): currentPath = self.get_current_selected_folder() self._refreshing = True self.ui.folderView.collapseAll() currentPath = self.get_current_selected_folder() if not currentPath: self._state.set_video_paths([currentPath]) index = self.fileModel.index(str(currentPath)) self.ui.folderView.scrollTo(self.proxyFileModel.mapFromSource(index)) @pyqtSlot(QModelIndex) def onFolderViewExpanded(self, proxyIndex): if self._refreshing: expandedPath = self.fileModel.filePath( self.proxyFileModel.mapToSource(proxyIndex)) if expandedPath == QDir.rootPath(): currentPath = self.get_current_selected_folder() if not currentPath: currentPath = self._state.get_video_path() index = self.fileModel.index(str(currentPath)) self.ui.folderView.scrollTo( self.proxyFileModel.mapFromSource(index)) self._refreshing = False @pyqtSlot() def onButtonSearchSelectFolder(self): paths = self._state.get_video_paths() path = paths[0] if paths else Path() selected_path = QFileDialog.getExistingDirectory( self, _('Select the directory that contains your videos'), str(path)) if selected_path: selected_paths = [Path(selected_path)] self._state.set_video_paths(selected_paths) self.search_videos(selected_paths) @pyqtSlot() def onButtonSearchSelectVideos(self): paths = self._state.get_video_paths() path = paths[0] if paths else Path() selected_files, t = QFileDialog.getOpenFileNames( self, _('Select the video(s) that need subtitles'), str(path), get_select_videos()) if selected_files: selected_files = list(Path(f) for f in selected_files) selected_dirs = list(set(p.parent for p in selected_files)) self._state.set_video_paths(selected_dirs) self.search_videos(selected_files) def search_videos(self, paths): if not self._state.providers.get_number_connected_providers(): QMessageBox.about( self, _('Error'), _('You are not connected to a server. Please connect first.')) return self.ui.buttonFind.setEnabled(False) self._search_videos_raw(paths) self.ui.buttonFind.setEnabled(True) def _search_videos_raw(self, paths): # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Scanning...')) callback.set_label_text(_('Scanning files')) callback.set_finished_text(_('Scanning finished')) callback.set_block(True) callback.show() try: local_videos, local_subs = scan_videopaths(paths, callback=callback, recursive=True) except OSError: callback.cancel() QMessageBox.warning(self, _('Error'), _('Some directories are not accessible.')) if callback.canceled(): return callback.finish() log.debug('Videos found: {}'.format(local_videos)) log.debug('Subtitles found: {}'.format(local_subs)) self.hideInstructions() if not local_videos: QMessageBox.information(self, _('Scan Results'), _('No video has been found.')) return total = len(local_videos) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget # callback = ProgressCallbackWidget(self) # callback.set_title_text(_('Asking Server...')) # callback.set_label_text(_('Searching subtitles...')) # callback.set_updated_text(_('Searching subtitles ( %d / %d )')) # callback.set_finished_text(_('Search finished')) callback.set_block(True) callback.set_range(0, total) callback.show() try: remote_subs = self._state.search_videos(local_videos, callback) except ProviderConnectionError: log.debug( 'Unable to search for subtitles of videos: videos={}'.format( list(v.get_filename() for v in local_videos))) QMessageBox.about(self, _('Error'), _('Unable to search for subtitles')) callback.finish() return self.videoModel.set_videos(local_videos) if remote_subs is None: QMessageBox.about( self, _('Error'), _('Error contacting the server. Please try again later')) callback.finish() # TODO: CHECK if our local subtitles are already in the server, otherwise suggest to upload @pyqtSlot() def onButtonPlay(self): selected_item = self.get_current_selected_item_videomodel() log.debug('Trying to play selected item: {}'.format(selected_item)) if selected_item is None: QMessageBox.warning(self, _('No subtitle selected'), _('Select a subtitle and try again')) return if isinstance(selected_item, SubtitleFileNetwork): selected_item = selected_item.get_subtitles()[0] if isinstance(selected_item, VideoFile): subtitle_file_path = None video = selected_item elif isinstance(selected_item, LocalSubtitleFile): subtitle_file_path = selected_item.get_filepath() video = selected_item.get_super_parent(VideoFile) elif isinstance(selected_item, RemoteSubtitleFile): video = selected_item.get_super_parent(VideoFile) subtitle_file_path = Path( tempfile.gettempdir()) / 'subdownloader.tmp.srt' log.debug('tmp path is {}'.format(subtitle_file_path)) log.debug( 'Temporary subtitle will be downloaded into: {temp_path}'. format(temp_path=subtitle_file_path)) # FIXME: must pass mainwindow as argument to ProgressCallbackWidget callback = ProgressCallbackWidget(self) callback.set_title_text(_('Playing video + sub')) callback.set_label_text(_('Downloading files...')) callback.set_finished_text(_('Downloading finished')) callback.set_block(True) callback.set_range(0, 100) callback.show() try: selected_item.download( subtitle_file_path, self._state.providers.get(selected_item.get_provider), callback) except ProviderConnectionError: log.debug('Unable to download subtitle "{}"'.format( selected_item.get_filename()), exc_info=sys.exc_info()) QMessageBox.about( self, _('Error'), _('Unable to download subtitle "{subtitle}"').format( subtitle=selected_item.get_filename())) callback.finish() return callback.finish() else: QMessageBox.about( self, _('Error'), '{}\n{}'.format(_('Unknown Error'), _('Please submit bug report'))) return # video = selected_item.get_parent().get_parent().get_parent() # FIXME: download subtitle with provider + use returned localSubtitleFile instead of creating one here if subtitle_file_path: local_subtitle = LocalSubtitleFile(subtitle_file_path) else: local_subtitle = None try: player = self._state.get_videoplayer() player.play_video(video, local_subtitle) except RuntimeError as e: QMessageBox.about(self, _('Error'), e.args[0]) @pyqtSlot(QModelIndex) def onClickVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item if True: # video.getMovieInfo(): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon(QIcon(':/images/info.png')) self.ui.buttonIMDB.setText(_('Movie Info')) elif isinstance(data_item, RemoteSubtitleFile): self.ui.buttonIMDB.setEnabled(True) self.ui.buttonIMDB.setIcon( QIcon(':/images/sites/opensubtitles.png')) self.ui.buttonIMDB.setText(_('Subtitle Info')) else: self.ui.buttonIMDB.setEnabled(False) @pyqtSlot(QModelIndex) def onSelectVideoTreeView(self, index): data_item = self.videoModel.getSelectedItem(index) self.ui.buttonPlay.setEnabled(True) # if isinstance(data_item, SubtitleFile): # self.ui.buttonPlay.setEnabled(True) # else: # self.ui.buttonPlay.setEnabled(False) def onContext(self, point): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget menu = QMenu('Menu', self) listview = self.ui.videoView index = listview.currentIndex() data_item = listview.model().getSelectedItem(index) if data_item is not None: if isinstance(data_item, VideoFile): video = data_item video_identities = video.get_identities() if any(video_identities.iter_imdb_identity()): online_action = QAction(QIcon(":/images/info.png"), _("View IMDb info"), self) online_action.triggered.connect(self.onViewOnlineInfo) else: online_action = QAction(QIcon(":/images/info.png"), _("Set IMDb info..."), self) online_action.triggered.connect(self.on_set_imdb_info) menu.addAction(online_action) elif isinstance(data_item, SubtitleFile): play_action = QAction(QIcon(":/images/play.png"), _("Play video + subtitle"), self) play_action.triggered.connect(self.onButtonPlay) menu.addAction(play_action) if isinstance(data_item, RemoteSubtitleFile): download_action = QAction(QIcon(":/images/download.png"), _("Download"), self) download_action.triggered.connect(self.onButtonDownload) menu.addAction(download_action) online_action = QAction( QIcon(":/images/sites/opensubtitles.png"), _("View online info"), self) online_action.triggered.connect(self.onViewOnlineInfo) menu.addAction(online_action) # Show the context menu. menu.exec_(listview.mapToGlobal(point)) def _create_choose_target_subtitle_path_cb(self): def callback(path, filename): selected_path = QFileDialog.getSaveFileName( self, _('Choose the target filename'), str(path / filename)) return selected_path return callback def onButtonDownload(self): # We download the subtitle in the same folder than the video rsubs = self.videoModel.get_checked_subtitles() sub_downloader = SubtitleDownloadProcess(parent=self.parent(), rsubtitles=rsubs, state=self._state, parent_add=True) sub_downloader.download_all() new_subs = sub_downloader.downloaded_subtitles() self.videoModel.uncheck_subtitles(new_subs) def onViewOnlineInfo(self): # FIXME: code duplication with Main.onContext and/or SearchNameWidget and/or SearchFileWidget # Tab for SearchByHash TODO:replace this 0 by an ENUM value listview = self.ui.videoView index = listview.currentIndex() data_item = self.videoModel.getSelectedItem(index) if isinstance(data_item, VideoFile): video = data_item video_identities = video.get_identities() if any(video_identities.iter_imdb_identity()): imdb_identity = next(video_identities.iter_imdb_identity()) webbrowser.open(imdb_identity.get_imdb_url(), new=2, autoraise=1) else: QMessageBox.information(self.parent(), _('imdb unknown'), _('imdb is unknown')) elif isinstance(data_item, RemoteSubtitleFile): sub = data_item webbrowser.open(sub.get_link(), new=2, autoraise=1) @pyqtSlot() def on_set_imdb_info(self): # FIXME: DUPLICATED WITH SEARCHNAMEWIDGET QMessageBox.about(self, _("Info"), "Not implemented yet. Sorry...")
class MainWindow(QMainWindow): """ The main window class """ def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = uic.loadUi("gui/main_window.ui") self.timestamp_filename = None self.video_filename = None self.media_start_time = None self.media_end_time = None self.restart_needed = False self.timer_period = 100 self.is_full_screen = False self.media_started_playing = False self.media_is_playing = False self.original_geometry = None self.mute = False self.timestamp_model = TimestampModel(None, self) self.proxy_model = QSortFilterProxyModel(self) self.ui.list_timestamp.setModel(self.timestamp_model) self.ui.list_timestamp.doubleClicked.connect( lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid() and self.run() ) self.timer = QTimer() self.timer.timeout.connect(self.update_ui) self.timer.timeout.connect(self.timer_handler) self.timer.start(self.timer_period) self.vlc_instance = vlc.Instance() self.media_player = self.vlc_instance.media_player_new() # if sys.platform == "darwin": # for MacOS # self.ui.frame_video = QMacCocoaViewContainer(0) self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen) self.ui.frame_video.wheel.connect(self.wheel_handler) self.ui.frame_video.keyPressed.connect(self.key_handler) # Set up buttons self.ui.button_run.clicked.connect(self.run) self.ui.button_timestamp_browse.clicked.connect( self.browse_timestamp_handler ) self.ui.button_video_browse.clicked.connect( self.browse_video_handler ) self.play_pause_model = ToggleButtonModel(None, self) self.play_pause_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.play", scale_factor=0.7) }, False: { "text": "", "icon": qta.icon("fa.pause", scale_factor=0.7) } } ) self.ui.button_play_pause.setModel(self.play_pause_model) self.ui.button_play_pause.clicked.connect(self.play_pause) self.mute_model = ToggleButtonModel(None, self) self.mute_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.volume-up", scale_factor=0.8) }, False: { "text": "", "icon": qta.icon("fa.volume-off", scale_factor=0.8) } } ) self.ui.button_mute_toggle.setModel(self.mute_model) self.ui.button_mute_toggle.clicked.connect(self.toggle_mute) self.ui.button_full_screen.setIcon( qta.icon("ei.fullscreen", scale_factor=0.6) ) self.ui.button_full_screen.setText("") self.ui.button_full_screen.clicked.connect(self.toggle_full_screen) self.ui.button_speed_up.clicked.connect(self.speed_up_handler) self.ui.button_speed_up.setIcon( qta.icon("fa.arrow-circle-o-up", scale_factor=0.8) ) self.ui.button_speed_up.setText("") self.ui.button_slow_down.clicked.connect(self.slow_down_handler) self.ui.button_slow_down.setIcon( qta.icon("fa.arrow-circle-o-down", scale_factor=0.8) ) self.ui.button_slow_down.setText("") self.ui.button_mark_start.setIcon( qta.icon("fa.quote-left", scale_factor=0.7) ) self.ui.button_mark_start.setText("") self.ui.button_mark_end.setIcon( qta.icon("fa.quote-right", scale_factor=0.7) ) self.ui.button_mark_end.setText("") self.ui.button_add_entry.clicked.connect(self.add_entry) self.ui.button_remove_entry.clicked.connect(self.remove_entry) self.ui.button_mark_start.clicked.connect( lambda: self.set_mark(start_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.button_mark_end.clicked.connect( lambda: self.set_mark(end_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.slider_progress.setTracking(False) self.ui.slider_progress.valueChanged.connect(self.set_media_position) self.ui.slider_volume.valueChanged.connect(self.set_volume) self.ui.entry_description.setReadOnly(True) # Mapper between the table and the entry detail self.mapper = QDataWidgetMapper() self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit) self.ui.button_save.clicked.connect(self.mapper.submit) # Set up default volume self.set_volume(self.ui.slider_volume.value()) self.vlc_events = self.media_player.event_manager() self.vlc_events.event_attach( vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler ) # Let our application handle mouse and key input instead of VLC self.media_player.video_set_mouse_input(False) self.media_player.video_set_key_input(False) self.ui.show() def add_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") row_num = self.timestamp_model.rowCount() self.timestamp_model.insertRow(row_num) start_cell = self.timestamp_model.index(row_num, 0) end_cell = self.timestamp_model.index(row_num, 1) self.timestamp_model.setData(start_cell, TimestampDelta.from_string("")) self.timestamp_model.setData(end_cell, TimestampDelta.from_string("")) def remove_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") selected = self.ui.list_timestamp.selectionModel().selectedIndexes() if len(selected) == 0: return self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit() def set_media_position(self, position): percentage = position / 10000.0 self.media_player.set_position(percentage) absolute_position = percentage * \ self.media_player.get_media().get_duration() if absolute_position > self.media_end_time: self.media_end_time = -1 def set_mark(self, start_time=None, end_time=None): if len(self.ui.list_timestamp.selectedIndexes()) == 0: blankRowIndex = self.timestamp_model.blankRowIndex() if not blankRowIndex.isValid(): self.add_entry() else: index = self.proxy_model.mapFromSource(blankRowIndex) self.ui.list_timestamp.selectRow(index.row()) selectedIndexes = self.ui.list_timestamp.selectedIndexes() if start_time: self.proxy_model.setData(selectedIndexes[0], TimestampDelta.string_from_int( start_time)) if end_time: self.proxy_model.setData(selectedIndexes[1], TimestampDelta.string_from_int( end_time)) def update_ui(self): self.ui.slider_progress.blockSignals(True) self.ui.slider_progress.setValue( self.media_player.get_position() * 10000 ) # When the video finishes self.ui.slider_progress.blockSignals(False) if self.media_started_playing and \ self.media_player.get_media().get_state() == vlc.State.Ended: self.play_pause_model.setState(True) # Apparently we need to reset the media, otherwise the player # won't play at all self.media_player.set_media(self.media_player.get_media()) self.set_volume(self.ui.slider_volume.value()) self.media_is_playing = False self.media_started_playing = False self.run() def timer_handler(self): """ This is a workaround, because for some reason we can't call set_time() inside the MediaPlayerTimeChanged handler (as the video just stops playing) """ if self.restart_needed: self.media_player.set_time(self.media_start_time) self.restart_needed = False def key_handler(self, event): if event.key() == Qt.Key_Escape and self.is_full_screen: self.toggle_full_screen() if event.key() == Qt.Key_F: self.toggle_full_screen() if event.key() == Qt.Key_Space: self.play_pause() def wheel_handler(self, event): self.modify_volume(1 if event.angleDelta().y() > 0 else -1) def toggle_mute(self): self.media_player.audio_set_mute(not self.media_player.audio_get_mute()) self.mute = not self.mute self.mute_model.setState(not self.mute) def modify_volume(self, delta_percent): new_volume = self.media_player.audio_get_volume() + delta_percent if new_volume < 0: new_volume = 0 elif new_volume > 40: new_volume = 40 self.media_player.audio_set_volume(new_volume) self.ui.slider_volume.setValue(self.media_player.audio_get_volume()) def set_volume(self, new_volume): self.media_player.audio_set_volume(new_volume) def speed_up_handler(self): self.modify_rate(0.1) def slow_down_handler(self): self.modify_rate(-0.1) def modify_rate(self, delta_percent): new_rate = self.media_player.get_rate() + delta_percent if new_rate < 0.2 or new_rate > 2.0: return self.media_player.set_rate(new_rate) def media_time_change_handler(self, _): if self.media_end_time == -1: return if self.media_player.get_time() > self.media_end_time: self.restart_needed = True def update_slider_highlight(self): if self.ui.list_timestamp.selectionModel().hasSelection(): selected_row = self.ui.list_timestamp.selectionModel(). \ selectedRows()[0] self.media_start_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 0), Qt.UserRole ) self.media_end_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 1), Qt.UserRole ) duration = self.media_player.get_media().get_duration() self.media_end_time = self.media_end_time \ if self.media_end_time != 0 else duration if self.media_start_time > self.media_end_time: raise ValueError("Start time cannot be later than end time") if self.media_start_time > duration: raise ValueError("Start time not within video duration") if self.media_end_time > duration: raise ValueError("End time not within video duration") slider_start_pos = (self.media_start_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) slider_end_pos = (self.media_end_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) self.ui.slider_progress.setHighlight( int(slider_start_pos), int(slider_end_pos) ) else: self.media_start_time = 0 self.media_end_time = -1 def run(self): """ Execute the loop """ if self.timestamp_filename is None: self._show_error("No timestamp file chosen") return if self.video_filename is None: self._show_error("No video file chosen") return try: self.update_slider_highlight() self.media_player.play() self.media_player.set_time(self.media_start_time) self.media_started_playing = True self.media_is_playing = True self.play_pause_model.setState(False) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def play_pause(self): """Toggle play/pause status """ if not self.media_started_playing: self.run() return if self.media_is_playing: self.media_player.pause() else: self.media_player.play() self.media_is_playing = not self.media_is_playing self.play_pause_model.setState(not self.media_is_playing) def toggle_full_screen(self): if self.is_full_screen: # TODO Artifacts still happen some time when exiting full screen # in X11 self.ui.frame_media.showNormal() self.ui.frame_media.restoreGeometry(self.original_geometry) self.ui.frame_media.setParent(self.ui.widget_central) self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1) # self.ui.frame_media.ensurePolished() else: self.ui.frame_media.setParent(None) self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint) self.original_geometry = self.ui.frame_media.saveGeometry() desktop = QApplication.desktop() rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos())) self.ui.frame_media.setGeometry(rect) self.ui.frame_media.showFullScreen() self.ui.frame_media.show() self.ui.frame_video.setFocus() self.is_full_screen = not self.is_full_screen def browse_timestamp_handler(self): """ Handler when the timestamp browser button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def _sort_model(self): self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder) def _select_blank_row(self, parent, start, end): self.ui.list_timestamp.selectRow(start) def set_timestamp_filename(self, filename): """ Set the timestamp file name """ if not os.path.isfile(filename): self._show_error("Cannot access timestamp file " + filename) return try: self.timestamp_model = TimestampModel(filename, self) self.timestamp_model.timeParseError.connect( lambda err: self._show_error(err) ) self.proxy_model.setSortRole(Qt.UserRole) self.proxy_model.dataChanged.connect(self._sort_model) self.proxy_model.dataChanged.connect(self.update_slider_highlight) self.proxy_model.setSourceModel(self.timestamp_model) self.proxy_model.rowsInserted.connect(self._sort_model) self.proxy_model.rowsInserted.connect(self._select_blank_row) self.ui.list_timestamp.setModel(self.proxy_model) self.timestamp_filename = filename self.ui.entry_timestamp.setText(self.timestamp_filename) self.mapper.setModel(self.proxy_model) self.mapper.addMapping(self.ui.entry_start_time, 0) self.mapper.addMapping(self.ui.entry_end_time, 1) self.mapper.addMapping(self.ui.entry_description, 2) self.ui.list_timestamp.selectionModel().selectionChanged.connect( self.timestamp_selection_changed) self._sort_model() directory = os.path.dirname(self.timestamp_filename) basename = os.path.basename(self.timestamp_filename) timestamp_name_without_ext = os.path.splitext(basename)[0] for file_in_dir in os.listdir(directory): current_filename = os.path.splitext(file_in_dir)[0] found_video = (current_filename == timestamp_name_without_ext and file_in_dir != basename) if found_video: found_video_file = os.path.join(directory, file_in_dir) self.set_video_filename(found_video_file) break except ValueError as err: self._show_error("Timestamp file is invalid") def timestamp_selection_changed(self, selected, deselected): if len(selected) > 0: self.mapper.setCurrentModelIndex(selected.indexes()[0]) self.ui.button_save.setEnabled(True) self.ui.button_remove_entry.setEnabled(True) self.ui.entry_start_time.setReadOnly(False) self.ui.entry_end_time.setReadOnly(False) self.ui.entry_description.setReadOnly(False) else: self.mapper.setCurrentModelIndex(QModelIndex()) self.ui.button_save.setEnabled(False) self.ui.button_remove_entry.setEnabled(False) self.ui.entry_start_time.clear() self.ui.entry_end_time.clear() self.ui.entry_description.clear() self.ui.entry_start_time.setReadOnly(True) self.ui.entry_end_time.setReadOnly(True) self.ui.entry_description.setReadOnly(True) def set_video_filename(self, filename): """ Set the video filename """ if not os.path.isfile(filename): self._show_error("Cannot access video file " + filename) return self.video_filename = filename media = self.vlc_instance.media_new(self.video_filename) media.parse() if not media.get_duration(): self._show_error("Cannot play this media file") self.media_player.set_media(None) self.video_filename = None else: self.media_player.set_media(media) if sys.platform.startswith('linux'): # for Linux using the X Server self.media_player.set_xwindow(self.ui.frame_video.winId()) elif sys.platform == "win32": # for Windows self.media_player.set_hwnd(self.ui.frame_video.winId()) elif sys.platform == "darwin": # for MacOS self.media_player.set_nsobject(self.ui.frame_video.winId()) self.ui.entry_video.setText(self.video_filename) self.media_started_playing = False self.media_is_playing = False self.set_volume(self.ui.slider_volume.value()) self.play_pause_model.setState(True) def browse_video_handler(self): """ Handler when the video browse button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Video file", None, "All Files (*)" ) if not tmp_name: return self.set_video_filename(QDir.toNativeSeparators(tmp_name)) def _show_error(self, message, title="Error"): QMessageBox.warning(self, title, message)
class TileStampsDock(QDockWidget): setStamp = pyqtSignal(TileStamp) def __init__(self, stampManager, parent = None): super().__init__(parent) self.mTileStampManager = stampManager self.mTileStampModel = stampManager.tileStampModel() self.mProxyModel = QSortFilterProxyModel(self.mTileStampModel) self.mFilterEdit = QLineEdit(self) self.mNewStamp = QAction(self) self.mAddVariation = QAction(self) self.mDuplicate = QAction(self) self.mDelete = QAction(self) self.mChooseFolder = QAction(self) self.setObjectName("TileStampsDock") self.mProxyModel.setSortLocaleAware(True) self.mProxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.mProxyModel.setSourceModel(self.mTileStampModel) self.mProxyModel.sort(0) self.mTileStampView = TileStampView(self) self.mTileStampView.setModel(self.mProxyModel) self.mTileStampView.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.mTileStampView.header().setStretchLastSection(False) self.mTileStampView.header().setSectionResizeMode(0, QHeaderView.Stretch) self.mTileStampView.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.mTileStampView.setContextMenuPolicy(Qt.CustomContextMenu) self.mTileStampView.customContextMenuRequested.connect(self.showContextMenu) self.mNewStamp.setIcon(QIcon(":images/16x16/document-new.png")) self.mAddVariation.setIcon(QIcon(":/images/16x16/add.png")) self.mDuplicate.setIcon(QIcon(":/images/16x16/stock-duplicate-16.png")) self.mDelete.setIcon(QIcon(":images/16x16/edit-delete.png")) self.mChooseFolder.setIcon(QIcon(":images/16x16/document-open.png")) Utils.setThemeIcon(self.mNewStamp, "document-new") Utils.setThemeIcon(self.mAddVariation, "add") Utils.setThemeIcon(self.mDelete, "edit-delete") Utils.setThemeIcon(self.mChooseFolder, "document-open") self.mFilterEdit.setClearButtonEnabled(True) self.mFilterEdit.textChanged.connect(self.mProxyModel.setFilterFixedString) self.mTileStampModel.stampRenamed.connect(self.ensureStampVisible) self.mNewStamp.triggered.connect(self.newStamp) self.mAddVariation.triggered.connect(self.addVariation) self.mDuplicate.triggered.connect(self.duplicate) self.mDelete.triggered.connect(self.delete_) self.mChooseFolder.triggered.connect(self.chooseFolder) self.mDuplicate.setEnabled(False) self.mDelete.setEnabled(False) self.mAddVariation.setEnabled(False) widget = QWidget(self) layout = QVBoxLayout(widget) layout.setContentsMargins(5, 5, 5, 5) buttonContainer = QToolBar() buttonContainer.setFloatable(False) buttonContainer.setMovable(False) buttonContainer.setIconSize(QSize(16, 16)) buttonContainer.addAction(self.mNewStamp) buttonContainer.addAction(self.mAddVariation) buttonContainer.addAction(self.mDuplicate) buttonContainer.addAction(self.mDelete) stretch = QWidget() stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) buttonContainer.addWidget(stretch) buttonContainer.addAction(self.mChooseFolder) listAndToolBar = QVBoxLayout() listAndToolBar.setSpacing(0) listAndToolBar.addWidget(self.mFilterEdit) listAndToolBar.addWidget(self.mTileStampView) listAndToolBar.addWidget(buttonContainer) layout.addLayout(listAndToolBar) selectionModel = self.mTileStampView.selectionModel() selectionModel.currentRowChanged.connect(self.currentRowChanged) self.setWidget(widget) self.retranslateUi() def changeEvent(self, e): super().changeEvent(e) x = e.type() if x==QEvent.LanguageChange: self.retranslateUi() else: pass def keyPressEvent(self, event): x = event.key() if x==Qt.Key_Delete or x==Qt.Key_Backspace: self.delete_() return super().keyPressEvent(event) def currentRowChanged(self, index): sourceIndex = self.mProxyModel.mapToSource(index) isStamp = self.mTileStampModel.isStamp(sourceIndex) self.mDuplicate.setEnabled(isStamp) self.mDelete.setEnabled(sourceIndex.isValid()) self.mAddVariation.setEnabled(isStamp) if (isStamp): self.setStamp.emit(self.mTileStampModel.stampAt(sourceIndex)) else: variation = self.mTileStampModel.variationAt(sourceIndex) if variation: # single variation clicked, use it specifically self.setStamp.emit(TileStamp(Map(variation.map))) def showContextMenu(self, pos): index = self.mTileStampView.indexAt(pos) if (not index.isValid()): return menu = QMenu() sourceIndex = self.mProxyModel.mapToSource(index) if (self.mTileStampModel.isStamp(sourceIndex)): addStampVariation = QAction(self.mAddVariation.icon(), self.mAddVariation.text(), menu) deleteStamp = QAction(self.mDelete.icon(), self.tr("Delete Stamp"), menu) deleteStamp.triggered.connect(self.delete_) addStampVariation.triggered.connect(self.addVariation) menu.addAction(addStampVariation) menu.addSeparator() menu.addAction(deleteStamp) else : removeVariation = QAction(QIcon(":/images/16x16/remove.png"), self.tr("Remove Variation"), menu) Utils.setThemeIcon(removeVariation, "remove") removeVariation.triggered.connect(self.delete_) menu.addAction(removeVariation) menu.exec(self.mTileStampView.viewport().mapToGlobal(pos)) def newStamp(self): stamp = self.mTileStampManager.createStamp() if (self.isVisible() and not stamp.isEmpty()): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): viewIndex = self.mProxyModel.mapFromSource(stampIndex) self.mTileStampView.setCurrentIndex(viewIndex) self.mTileStampView.edit(viewIndex) def delete_(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) self.mTileStampModel.removeRow(sourceIndex.row(), sourceIndex.parent()) def duplicate(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt = TileStamp(sourceIndex) self.mTileStampModel.addStamp(stamp.clone()) def addVariation(self): index = self.mTileStampView.currentIndex() if (not index.isValid()): return sourceIndex = self.mProxyModel.mapToSource(index) if (not self.mTileStampModel.isStamp(sourceIndex)): return stamp = self.mTileStampModel.stampAt(sourceIndex) self.mTileStampManager.addVariation(stamp) def chooseFolder(self): prefs = Preferences.instance() stampsDirectory = prefs.stampsDirectory() stampsDirectory = QFileDialog.getExistingDirectory(self.window(), self.tr("Choose the Stamps Folder"), stampsDirectory) if (not stampsDirectory.isEmpty()): prefs.setStampsDirectory(stampsDirectory) def ensureStampVisible(self, stamp): stampIndex = self.mTileStampModel.index(stamp) if (stampIndex.isValid()): self.mTileStampView.scrollTo(self.mProxyModel.mapFromSource(stampIndex)) def retranslateUi(self): self.setWindowTitle(self.tr("Tile Stamps")) self.mNewStamp.setText(self.tr("Add New Stamp")) self.mAddVariation.setText(self.tr("Add Variation")) self.mDuplicate.setText(self.tr("Duplicate Stamp")) self.mDelete.setText(self.tr("Delete Selected")) self.mChooseFolder.setText(self.tr("Set Stamps Folder")) self.mFilterEdit.setPlaceholderText(self.tr("Filter"))
class MainWindow(QMainWindow): """ The main window class """ def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.ui = uic.loadUi("gui/main_window.ui") self.timestamp_filename = None self.video_filename = None self.media_start_time = None self.media_end_time = None self.restart_needed = False self.timer_period = 100 self.is_full_screen = False self.media_started_playing = False self.media_is_playing = False self.original_geometry = None self.mute = False self.timestamp_model = TimestampModel(None, self) self.proxy_model = QSortFilterProxyModel(self) self.ui.list_timestamp.setModel(self.timestamp_model) self.ui.list_timestamp.doubleClicked.connect( lambda event: self.ui.list_timestamp.indexAt(event.pos()).isValid() and self.run() ) self.timer = QTimer() self.timer.timeout.connect(self.update_ui) self.timer.timeout.connect(self.timer_handler) self.timer.start(self.timer_period) self.vlc_instance = vlc.Instance() self.media_player = self.vlc_instance.media_player_new() # if sys.platform == "darwin": # for MacOS # self.ui.frame_video = QMacCocoaViewContainer(0) self.ui.frame_video.doubleClicked.connect(self.toggle_full_screen) self.ui.frame_video.wheel.connect(self.wheel_handler) self.ui.frame_video.keyPressed.connect(self.key_handler) # Set up Labels: # self.ui.lblVideoName. # self.displayed_video_title = bind("self.ui.lblVideoName", "text", str) self.ui.lblVideoName.setText(self.video_filename) self.ui.lblVideoSubtitle.setText("") self.ui.dateTimeEdit.setHidden(True) self.ui.lblCurrentFrame.setText("") self.ui.lblTotalFrames.setText("") self.ui.lblCurrentTime.setText("") self.ui.lblTotalDuration.setText("") self.ui.lblFileFPS.setText("") self.ui.spinBoxFrameJumpMultiplier.value = 1 # Set up buttons self.ui.button_run.clicked.connect(self.run) self.ui.button_timestamp_browse.clicked.connect( self.browse_timestamp_handler ) self.ui.button_timestamp_create.clicked.connect( self.create_timestamp_file_handler ) self.ui.button_video_browse.clicked.connect( self.browse_video_handler ) # Set up directional buttons self.ui.btnSkipLeft.clicked.connect(self.skip_left_handler) self.ui.btnSkipRight.clicked.connect(self.skip_right_handler) self.ui.btnLeft.clicked.connect(self.seek_left_handler) self.ui.btnRight.clicked.connect(self.seek_right_handler) self.play_pause_model = ToggleButtonModel(None, self) self.play_pause_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.play", scale_factor=0.7) }, False: { "text": "", "icon": qta.icon("fa.pause", scale_factor=0.7) } } ) self.ui.button_play_pause.setModel(self.play_pause_model) self.ui.button_play_pause.clicked.connect(self.play_pause) self.mute_model = ToggleButtonModel(None, self) self.mute_model.setStateMap( { True: { "text": "", "icon": qta.icon("fa.volume-up", scale_factor=0.8) }, False: { "text": "", "icon": qta.icon("fa.volume-off", scale_factor=0.8) } } ) self.ui.button_mute_toggle.setModel(self.mute_model) self.ui.button_mute_toggle.clicked.connect(self.toggle_mute) self.ui.button_full_screen.setIcon( qta.icon("ei.fullscreen", scale_factor=0.6) ) self.ui.button_full_screen.setText("") self.ui.button_full_screen.clicked.connect(self.toggle_full_screen) self.ui.button_speed_up.clicked.connect(self.speed_up_handler) self.ui.button_speed_up.setIcon( qta.icon("fa.arrow-circle-o-up", scale_factor=0.8) ) self.ui.button_speed_up.setText("") self.ui.button_slow_down.clicked.connect(self.slow_down_handler) self.ui.button_slow_down.setIcon( qta.icon("fa.arrow-circle-o-down", scale_factor=0.8) ) self.ui.button_slow_down.setText("") self.ui.button_mark_start.setIcon( qta.icon("fa.quote-left", scale_factor=0.7) ) self.ui.button_mark_start.setText("") self.ui.button_mark_end.setIcon( qta.icon("fa.quote-right", scale_factor=0.7) ) self.ui.button_mark_end.setText("") self.ui.button_add_entry.clicked.connect(self.add_entry) self.ui.button_remove_entry.clicked.connect(self.remove_entry) self.ui.button_mark_start.clicked.connect( lambda: self.set_mark(start_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.button_mark_end.clicked.connect( lambda: self.set_mark(end_time=int( self.media_player.get_position() * self.media_player.get_media().get_duration())) ) self.ui.slider_progress.setTracking(False) self.ui.slider_progress.valueChanged.connect(self.set_media_position) self.ui.slider_volume.valueChanged.connect(self.set_volume) self.ui.entry_description.setReadOnly(True) # Mapper between the table and the entry detail self.mapper = QDataWidgetMapper() self.mapper.setSubmitPolicy(QDataWidgetMapper.ManualSubmit) self.ui.button_save.clicked.connect(self.mapper.submit) # Set up default volume self.set_volume(self.ui.slider_volume.value()) self.vlc_events = self.media_player.event_manager() self.vlc_events.event_attach( vlc.EventType.MediaPlayerTimeChanged, self.media_time_change_handler ) # Let our application handle mouse and key input instead of VLC self.media_player.video_set_mouse_input(False) self.media_player.video_set_key_input(False) self.ui.show() def add_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") row_num = self.timestamp_model.rowCount() self.timestamp_model.insertRow(row_num) start_cell = self.timestamp_model.index(row_num, 0) end_cell = self.timestamp_model.index(row_num, 1) self.timestamp_model.setData(start_cell, TimestampDelta.from_string("")) self.timestamp_model.setData(end_cell, TimestampDelta.from_string("")) def remove_entry(self): if not self.timestamp_filename: self._show_error("You haven't chosen a timestamp file yet") selected = self.ui.list_timestamp.selectionModel().selectedIndexes() if len(selected) == 0: return self.proxy_model.removeRow(selected[0].row()) and self.mapper.submit() def set_media_position(self, position): percentage = position / 10000.0 self.media_player.set_position(percentage) absolute_position = percentage * \ self.media_player.get_media().get_duration() if absolute_position > self.media_end_time: self.media_end_time = -1 def set_mark(self, start_time=None, end_time=None): if len(self.ui.list_timestamp.selectedIndexes()) == 0: blankRowIndex = self.timestamp_model.blankRowIndex() if not blankRowIndex.isValid(): self.add_entry() else: index = self.proxy_model.mapFromSource(blankRowIndex) self.ui.list_timestamp.selectRow(index.row()) selectedIndexes = self.ui.list_timestamp.selectedIndexes() if start_time: self.proxy_model.setData(selectedIndexes[0], TimestampDelta.string_from_int( start_time)) if end_time: self.proxy_model.setData(selectedIndexes[1], TimestampDelta.string_from_int( end_time)) def update_ui(self): self.ui.slider_progress.blockSignals(True) self.ui.slider_progress.setValue( self.media_player.get_position() * 10000 ) #print(self.media_player.get_position() * 10000) self.update_video_file_play_labels() # When the video finishes self.ui.slider_progress.blockSignals(False) if self.media_started_playing and \ self.media_player.get_media().get_state() == vlc.State.Ended: self.play_pause_model.setState(True) # Apparently we need to reset the media, otherwise the player # won't play at all self.media_player.set_media(self.media_player.get_media()) self.set_volume(self.ui.slider_volume.value()) self.media_is_playing = False self.media_started_playing = False self.run() def timer_handler(self): """ This is a workaround, because for some reason we can't call set_time() inside the MediaPlayerTimeChanged handler (as the video just stops playing) """ if self.restart_needed: self.media_player.set_time(self.media_start_time) self.restart_needed = False def key_handler(self, event): if event.key() == Qt.Key_Escape and self.is_full_screen: self.toggle_full_screen() if event.key() == Qt.Key_F: self.toggle_full_screen() if event.key() == Qt.Key_Space: self.play_pause() def wheel_handler(self, event): self.modify_volume(1 if event.angleDelta().y() > 0 else -1) def toggle_mute(self): self.media_player.audio_set_mute(not self.media_player.audio_get_mute()) self.mute = not self.mute self.mute_model.setState(not self.mute) def modify_volume(self, delta_percent): new_volume = self.media_player.audio_get_volume() + delta_percent if new_volume < 0: new_volume = 0 elif new_volume > 40: new_volume = 40 self.media_player.audio_set_volume(new_volume) self.ui.slider_volume.setValue(self.media_player.audio_get_volume()) def set_volume(self, new_volume): self.media_player.audio_set_volume(new_volume) def speed_up_handler(self): self.modify_rate(0.1) def slow_down_handler(self): self.modify_rate(-0.1) def modify_rate(self, delta_percent): new_rate = self.media_player.get_rate() + delta_percent if new_rate < 0.2 or new_rate > 2.0: return self.media_player.set_rate(new_rate) def media_time_change_handler(self, _): if self.media_end_time == -1: return if self.media_player.get_time() > self.media_end_time: self.restart_needed = True def update_slider_highlight(self): if self.ui.list_timestamp.selectionModel().hasSelection(): selected_row = self.ui.list_timestamp.selectionModel(). \ selectedRows()[0] self.media_start_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 0), Qt.UserRole ) self.media_end_time = self.ui.list_timestamp.model().data( selected_row.model().index(selected_row.row(), 1), Qt.UserRole ) duration = self.media_player.get_media().get_duration() self.media_end_time = self.media_end_time \ if self.media_end_time != 0 else duration if self.media_start_time > self.media_end_time: raise ValueError("Start time cannot be later than end time") if self.media_start_time > duration: raise ValueError("Start time not within video duration") if self.media_end_time > duration: raise ValueError("End time not within video duration") slider_start_pos = (self.media_start_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) slider_end_pos = (self.media_end_time / duration) * \ (self.ui.slider_progress.maximum() - self.ui.slider_progress.minimum()) self.ui.slider_progress.setHighlight( int(slider_start_pos), int(slider_end_pos) ) else: self.media_start_time = 0 self.media_end_time = -1 def run(self): """ Execute the loop """ if self.timestamp_filename is None: self._show_error("No timestamp file chosen") return if self.video_filename is None: self._show_error("No video file chosen") return try: self.update_slider_highlight() self.media_player.play() self.media_player.set_time(self.media_start_time) self.media_started_playing = True self.media_is_playing = True self.play_pause_model.setState(False) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def play_pause(self): """Toggle play/pause status """ if not self.media_started_playing: self.run() return if self.media_is_playing: self.media_player.pause() else: self.media_player.play() self.media_is_playing = not self.media_is_playing self.play_pause_model.setState(not self.media_is_playing) def update_video_file_play_labels(self): curr_total_fps = self.media_player.get_fps() curr_total_duration = self.media_player.get_length() totalNumFrames = int(curr_total_duration * curr_total_fps) if totalNumFrames > 0: self.ui.lblTotalFrames.setText(str(totalNumFrames)) else: self.ui.lblTotalFrames.setText("--") if curr_total_duration > 0: self.ui.lblTotalDuration.setText(str(curr_total_duration)) # Gets duration in [ms] else: self.ui.lblTotalDuration.setText("--") # Changing Values: Dynamically updated each time the playhead changes curr_percent_complete = self.media_player.get_position() # Current percent complete between 0.0 and 1.0 if curr_percent_complete >= 0: self.ui.lblPlaybackPercent.setText(str(curr_percent_complete)) else: self.ui.lblPlaybackPercent.setText("--") curr_frame = int(round(curr_percent_complete * totalNumFrames)) if curr_frame >= 0: self.ui.lblCurrentFrame.setText(str(curr_frame)) else: self.ui.lblCurrentFrame.setText("--") if self.media_player.get_time() >= 0: self.ui.lblCurrentTime.setText(str(self.media_player.get_time()) + "[ms]") # Gets time in [ms] else: self.ui.lblCurrentTime.setText("-- [ms]") # Gets time in [ms] # Called only when the video file changes: def update_video_file_labels_on_file_change(self): if self.video_filename is None: self.ui.lblVideoName.setText("") else: self.ui.lblVideoName.setText(self.video_filename) # Only updated when the video file is changed: curr_total_fps = self.media_player.get_fps() self.ui.lblFileFPS.setText(str(curr_total_fps)) curr_total_duration = self.media_player.get_length() totalNumFrames = int(curr_total_duration * curr_total_fps) if totalNumFrames > 0: self.ui.lblTotalFrames.setText(str(totalNumFrames)) else: self.ui.lblTotalFrames.setText("--") if curr_total_duration > 0: self.ui.lblTotalDuration.setText(str(curr_total_duration)) # Gets duration in [ms] else: self.ui.lblTotalDuration.setText("--") self.update_video_file_play_labels() def get_frame_multipler(self): return self.ui.spinBoxFrameJumpMultiplier.value # def compute_total_number_frames(self): # self.media_player.get_length() def seek_left_handler(self): print('seek: left') self.seek_frames(-10 * self.get_frame_multipler()) def skip_left_handler(self): print('skip: left') self.seek_frames(-1 * self.get_frame_multipler()) def seek_right_handler(self): print('seek: right') self.seek_frames(10 * self.get_frame_multipler()) def skip_right_handler(self): print('skip: right') self.seek_frames(1 * self.get_frame_multipler()) def seek_frames(self, relativeFrameOffset): """Jump a certain number of frames forward or back """ if self.video_filename is None: self._show_error("No video file chosen") return # if self.media_end_time == -1: # return curr_total_fps = self.media_player.get_fps() relativeSecondsOffset = relativeFrameOffset / curr_total_fps # Desired offset in seconds curr_total_duration = self.media_player.get_length() relative_percent_offset = relativeSecondsOffset / curr_total_duration # percent of the whole that we want to skip totalNumFrames = int(curr_total_duration * curr_total_fps) try: didPauseMedia = False if self.media_is_playing: self.media_player.pause() didPauseMedia = True newPosition = self.media_player.get_position() + relative_percent_offset # newTime = int(self.media_player.get_time() + relativeFrameOffset) # self.update_slider_highlight() # self.media_player.set_time(newTime) self.media_player.set_position(newPosition) if (didPauseMedia): self.media_player.play() # else: # # Otherwise, the media was already paused, we need to very quickly play the media to update the frame with the new time, and then immediately pause it again. # self.media_player.play() # self.media_player.pause() self.media_player.next_frame() print("Setting media playback time to ", newPosition) except Exception as ex: self._show_error(str(ex)) print(traceback.format_exc()) def toggle_full_screen(self): if self.is_full_screen: # TODO Artifacts still happen some time when exiting full screen # in X11 self.ui.frame_media.showNormal() self.ui.frame_media.restoreGeometry(self.original_geometry) self.ui.frame_media.setParent(self.ui.widget_central) self.ui.layout_main.addWidget(self.ui.frame_media, 2, 3, 3, 1) # self.ui.frame_media.ensurePolished() else: self.ui.frame_media.setParent(None) self.ui.frame_media.setWindowFlags(Qt.FramelessWindowHint | Qt.CustomizeWindowHint) self.original_geometry = self.ui.frame_media.saveGeometry() desktop = QApplication.desktop() rect = desktop.screenGeometry(desktop.screenNumber(QCursor.pos())) self.ui.frame_media.setGeometry(rect) self.ui.frame_media.showFullScreen() self.ui.frame_media.show() self.ui.frame_video.setFocus() self.is_full_screen = not self.is_full_screen def browse_timestamp_handler(self): """ Handler when the timestamp browser button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def create_timestamp_file_handler(self): """ Handler when the timestamp file create button is clicked """ tmp_name, _ = QFileDialog.getSaveFileName( self, "Create New Timestamp file", None, "Timestamp File (*.tmsp);;All Files (*)" ) if not tmp_name: return try: if (os.stat(QDir.toNativeSeparators(tmp_name)).st_size == 0): # File is empty, create a non-empty one: with open(QDir.toNativeSeparators(tmp_name), "w") as fh: fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used else: pass # with open(tmp_name, 'r') as fh: # if fh.__sizeof__()>0: # # File is not empty: # pass # else: # # File is empty, create a non-empty one: # fh.close() # with open(tmp_name, "w") as fh: # fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used except WindowsError: with open(tmp_name, "w") as fh: fh.write("[]") # Write the minimal valid JSON string to the file to allow it to be used # Create new file: self.set_timestamp_filename(QDir.toNativeSeparators(tmp_name)) def _sort_model(self): self.ui.list_timestamp.sortByColumn(0, Qt.AscendingOrder) def _select_blank_row(self, parent, start, end): self.ui.list_timestamp.selectRow(start) def set_timestamp_filename(self, filename): """ Set the timestamp file name """ if not os.path.isfile(filename): self._show_error("Cannot access timestamp file " + filename) return try: self.timestamp_model = TimestampModel(filename, self) self.timestamp_model.timeParseError.connect( lambda err: self._show_error(err) ) self.proxy_model.setSortRole(Qt.UserRole) self.proxy_model.dataChanged.connect(self._sort_model) self.proxy_model.dataChanged.connect(self.update_slider_highlight) self.proxy_model.setSourceModel(self.timestamp_model) self.proxy_model.rowsInserted.connect(self._sort_model) self.proxy_model.rowsInserted.connect(self._select_blank_row) self.ui.list_timestamp.setModel(self.proxy_model) self.timestamp_filename = filename self.ui.entry_timestamp.setText(self.timestamp_filename) self.mapper.setModel(self.proxy_model) self.mapper.addMapping(self.ui.entry_start_time, 0) self.mapper.addMapping(self.ui.entry_end_time, 1) self.mapper.addMapping(self.ui.entry_description, 2) self.ui.list_timestamp.selectionModel().selectionChanged.connect( self.timestamp_selection_changed) self._sort_model() directory = os.path.dirname(self.timestamp_filename) basename = os.path.basename(self.timestamp_filename) timestamp_name_without_ext = os.path.splitext(basename)[0] for file_in_dir in os.listdir(directory): current_filename = os.path.splitext(file_in_dir)[0] found_video = (current_filename == timestamp_name_without_ext and file_in_dir != basename) if found_video: found_video_file = os.path.join(directory, file_in_dir) self.set_video_filename(found_video_file) break except ValueError as err: self._show_error("Timestamp file is invalid") def timestamp_selection_changed(self, selected, deselected): if len(selected) > 0: self.mapper.setCurrentModelIndex(selected.indexes()[0]) self.ui.button_save.setEnabled(True) self.ui.button_remove_entry.setEnabled(True) self.ui.entry_start_time.setReadOnly(False) self.ui.entry_end_time.setReadOnly(False) self.ui.entry_description.setReadOnly(False) else: self.mapper.setCurrentModelIndex(QModelIndex()) self.ui.button_save.setEnabled(False) self.ui.button_remove_entry.setEnabled(False) self.ui.entry_start_time.clear() self.ui.entry_end_time.clear() self.ui.entry_description.clear() self.ui.entry_start_time.setReadOnly(True) self.ui.entry_end_time.setReadOnly(True) self.ui.entry_description.setReadOnly(True) def set_video_filename(self, filename): """ Set the video filename """ if not os.path.isfile(filename): self._show_error("Cannot access video file " + filename) return self.video_filename = filename media = self.vlc_instance.media_new(self.video_filename) media.parse() if not media.get_duration(): self._show_error("Cannot play this media file") self.media_player.set_media(None) self.video_filename = None else: self.media_player.set_media(media) if sys.platform.startswith('linux'): # for Linux using the X Server self.media_player.set_xwindow(self.ui.frame_video.winId()) elif sys.platform == "win32": # for Windows self.media_player.set_hwnd(self.ui.frame_video.winId()) elif sys.platform == "darwin": # for MacOS self.media_player.set_nsobject(self.ui.frame_video.winId()) self.ui.entry_video.setText(self.video_filename) self.update_video_file_labels_on_file_change() self.media_started_playing = False self.media_is_playing = False self.set_volume(self.ui.slider_volume.value()) self.play_pause_model.setState(True) def browse_video_handler(self): """ Handler when the video browse button is clicked """ tmp_name, _ = QFileDialog.getOpenFileName( self, "Choose Video file", None, "All Files (*)" ) if not tmp_name: return self.set_video_filename(QDir.toNativeSeparators(tmp_name)) def _show_error(self, message, title="Error"): QMessageBox.warning(self, title, message)