def add_menu_density(self, parent, menu): """""" self.menu_density_ = menu action_group = QActionGroup(menu) try: action_group.setExclusive(True) except: action_group.exclusive = True for density in map(str, range(-3, 4)): action = QAction(parent) # action.triggered.connect(self._wrapper(parent, density, self.extra_values, self.update_buttons)) action.triggered.connect(lambda: self.update_theme_event(parent)) try: action.setText(density) action.setCheckable(True) action.setChecked(density == '0') action.setActionGroup(action_group) menu.addAction(action) action_group.addAction(action) except: # snake_case, true_property action.text = density action.checkable = True action.checked = density == '0' action.action_group = action_group menu.add_action(action) action_group.add_action(action)
def add_menu_theme(self, parent, menu): """""" self.menu_theme_ = menu action_group = QActionGroup(menu) try: action_group.setExclusive(True) except: action_group.exclusive = True for i, theme in enumerate(['default'] + list_themes()): action = QAction(parent) # action.triggered.connect(self._wrapper(parent, theme, self.extra_values, self.update_buttons)) action.triggered.connect(lambda: self.update_theme_event(parent)) try: action.setText(theme) action.setCheckable(True) action.setChecked(not bool(i)) action.setActionGroup(action_group) menu.addAction(action) action_group.addAction(action) except: # snake_case, true_property action.text = theme action.checkable = True action.checked = not bool(i) action.action_group = action_group menu.add_action(action) action_group.add_action(action)
def __init__(self, game_connection: GameConnection, options: Options): super().__init__() self.setupUi(self) self.game_connection = game_connection self.options = options common_qt_lib.set_default_window_icon(self) self._action_to_theme = { self.action_theme_2d_style: TrackerTheme.CLASSIC, self.action_theme_game_icons: TrackerTheme.GAME, } theme_group = QActionGroup(self) for action, theme in self._action_to_theme.items(): theme_group.addAction(action) action.setChecked(theme == options.tracker_theme) action.triggered.connect(self._on_action_theme) self._tracker_elements: List[Element] = [] self.create_tracker(RandovaniaGame.PRIME2) self.game_connection_setup = GameConnectionSetup( self, self.game_connection_tool, self.connection_status_label, self.game_connection, options) self.force_update_button.clicked.connect(self.on_force_update_button) self._update_timer = QTimer(self) self._update_timer.setInterval(100) self._update_timer.timeout.connect(self._on_timer_update) self._update_timer.setSingleShot(True)
def toolbar(self): toolbar = QToolBar("My main toolbar") toolbar.setIconSize(QSize(16, 16)) self.addToolBar(toolbar) mouse = QAction(QIcon('./icons/icons-01.png'), 'mouse', self, checkable=True) mouse.setStatusTip('mouse') mouse.triggered.connect(lambda: self.set_pointer('mouse')) square = QAction(QIcon('./icons/icons_Square.png'), 'square', self, checkable=True) square.setStatusTip('square') square.triggered.connect(lambda: self.set_pointer('square')) circle = QAction(QIcon('./icons_Circle.png'), 'circle', self, checkable=True) circle.setStatusTip('circle') circle.triggered.connect(lambda: self.set_pointer('circle')) crosshair = QAction(QIcon('./icons/icons_Crosshair.png'), 'crosshair', self, checkable=True) crosshair.setStatusTip('crosshair') crosshair.triggered.connect(lambda: self.set_pointer('cross')) brush = QAction(QIcon('./icons/icons_Brush.png'), 'brush', self, checkable=True) brush.setStatusTip('crosshair') brush.triggered.connect(lambda: self.set_pointer('brush')) group = QActionGroup(self, exclusive=True) for action in (mouse, square, circle, crosshair, brush): toolbar.addAction(action) group.addAction(action) annotations = QAction(QIcon('./icons/icons_Circle.png'), 'Annot', self, checkable=True) annotations.setStatusTip('Toggle annotations') annotations.triggered.connect(self.toggel_annot) toolbar.addAction(annotations) clear = QAction(QIcon('./icons/icons_Square.png'), 'Clear', self) clear.setStatusTip('Clear annotations') clear.triggered.connect(self.clear_annot) toolbar.addAction(clear) self.setStatusBar(QStatusBar(self))
def setup_menu_actions(self): # flow designs for d in Design.flow_themes: design_action = QAction(d, self) self.ui.menuFlow_Design_Style.addAction(design_action) design_action.triggered.connect(self.on_design_action_triggered) self.flow_design_actions.append(design_action) self.ui.actionImport_Nodes.triggered.connect( self.on_import_nodes_triggered) self.ui.actionSave_Project.triggered.connect( self.on_save_project_triggered) self.ui.actionEnableDebugging.triggered.connect( self.on_enable_debugging_triggered) self.ui.actionDisableDebugging.triggered.connect( self.on_disable_debugging_triggered) self.ui.actionSave_Pic_Viewport.triggered.connect( self.on_save_scene_pic_viewport_triggered) self.ui.actionSave_Pic_Whole_Scene_scaled.triggered.connect( self.on_save_scene_pic_whole_triggered) # performance mode self.action_set_performance_mode_fast = QAction('Fast', self) self.action_set_performance_mode_fast.setCheckable(True) self.action_set_performance_mode_pretty = QAction('Pretty', self) self.action_set_performance_mode_pretty.setCheckable(True) performance_mode_AG = QActionGroup(self) performance_mode_AG.addAction(self.action_set_performance_mode_fast) performance_mode_AG.addAction(self.action_set_performance_mode_pretty) self.action_set_performance_mode_fast.setChecked(True) performance_mode_AG.triggered.connect(self.on_performance_mode_changed) performance_menu = QMenu('Performance Mode', self) performance_menu.addAction(self.action_set_performance_mode_fast) performance_menu.addAction(self.action_set_performance_mode_pretty) self.ui.menuView.addMenu(performance_menu) # animations self.action_set_animation_active = QAction('Enabled', self) self.action_set_animation_active.setCheckable(True) self.action_set_animations_inactive = QAction('Disabled', self) self.action_set_animations_inactive.setCheckable(True) animation_enabled_AG = QActionGroup(self) animation_enabled_AG.addAction(self.action_set_animation_active) animation_enabled_AG.addAction(self.action_set_animations_inactive) self.action_set_animation_active.setChecked(True) animation_enabled_AG.triggered.connect( self.on_animation_enabling_changed) animations_menu = QMenu('Animations', self) animations_menu.addAction(self.action_set_animation_active) animations_menu.addAction(self.action_set_animations_inactive) self.ui.menuView.addMenu(animations_menu)
def setup_menu_actions(self): self.ui.actionImport_Nodes.triggered.connect( self.on_import_nodes_triggered) self.ui.actionSave_Project.triggered.connect( self.on_save_project_triggered) self.ui.actionDesignDark_Std.triggered.connect( self.on_dark_std_design_triggered) self.ui.actionDesignDark_Tron.triggered.connect( self.on_dark_tron_design_triggered) self.ui.actionEnableDebugging.triggered.connect( self.on_enable_debugging_triggered) self.ui.actionDisableDebugging.triggered.connect( self.on_disable_debugging_triggered) self.ui.actionSave_Pic_Viewport.triggered.connect( self.on_save_scene_pic_viewport_triggered) self.ui.actionSave_Pic_Whole_Scene_scaled.triggered.connect( self.on_save_scene_pic_whole_triggered) # algorithm self.set_sync_mode_use_existent_data = QAction('Use Existent Data', self) self.set_sync_mode_use_existent_data.setCheckable(True) self.set_sync_mode_gen_data = QAction('Generate Data On Request', self) self.set_sync_mode_gen_data.setCheckable(True) algorithm_sync_mode_AG = QActionGroup(self) algorithm_sync_mode_AG.addAction(self.set_sync_mode_use_existent_data) algorithm_sync_mode_AG.addAction(self.set_sync_mode_gen_data) self.set_sync_mode_use_existent_data.setChecked(True) algorithm_sync_mode_AG.triggered.connect( self.on_algorithm_sync_mode_changed) algorithm_menu = QMenu('Algorithm', self) algorithm_menu.addAction(self.set_sync_mode_use_existent_data) algorithm_menu.addAction(self.set_sync_mode_gen_data) self.ui.menuBar.addMenu(algorithm_menu) # performance mode self.set_performance_mode_fast = QAction('Fast', self) self.set_performance_mode_fast.setCheckable(True) self.set_performance_mode_pretty = QAction('Pretty', self) self.set_performance_mode_pretty.setCheckable(True) performance_mode_AG = QActionGroup(self) performance_mode_AG.addAction(self.set_performance_mode_fast) performance_mode_AG.addAction(self.set_performance_mode_pretty) self.set_performance_mode_fast.setChecked(True) performance_mode_AG.triggered.connect(self.on_performance_mode_changed) performance_menu = QMenu('Performance Mode', self) performance_menu.addAction(self.set_performance_mode_fast) performance_menu.addAction(self.set_performance_mode_pretty) self.ui.menuView.addMenu(performance_menu)
def _display_age_column_header_context_menu(self, point: QPoint): menu = QMenu(self) format_menu = menu.addMenu('Format') format_action_group = QActionGroup(self) for age_format in self._age_formats: format_action = format_menu.addAction(age_format.NAME) format_action.age_format = age_format format_action.setCheckable(True) if self._age_format == age_format: format_action.setChecked(True) format_action_group.addAction(format_action) triggered_action = menu.exec_( self.horizontalHeader().viewport().mapToGlobal(point)) if triggered_action: self.age_format = triggered_action.age_format
def _show_menu(self, colleague, pos): if not self._is_owner and not colleague.is_you or colleague.is_deleting: return menu = QMenu(self._ui.colleagues_list) menu.setStyleSheet("background-color: #EFEFF4; ") if colleague.is_you: if colleague.is_owner: action = menu.addAction(tr("Quit collaboration")) action.triggered.connect(self._on_quit_collaboration) else: action = menu.addAction(tr("Leave collaboration")) action.triggered.connect(self._on_leave_collaboration) else: rights_group = QActionGroup(menu) rights_group.setExclusive(True) menu.addSection(tr("Access rights")) action = menu.addAction(tr("Can view")) action.setCheckable(True) rights_action = rights_group.addAction(action) rights_action.setData(False) rights_action.setChecked(not colleague.can_edit) action = menu.addAction(tr("Can edit")) action.setCheckable(True) rights_action = rights_group.addAction(action) rights_action.setChecked(colleague.can_edit) rights_action.setData(True) rights_group.triggered.connect( lambda a: self._on_grant_edit(colleague, a)) menu.addSeparator() action = menu.addAction(tr("Remove user")) action.triggered.connect(lambda: self._on_remove_user(colleague)) pos_to_show = QPoint(pos.x(), pos.y() + 10) menu.exec_(pos_to_show)
def createLanguagesMenu(self, parent=None): """Create and return a menu for selecting the spell-check language.""" curr_lang = self.highlighter.dict().tag lang_menu = QMenu("Language", parent) lang_actions = QActionGroup(lang_menu) for lang in enchant.list_languages(): action = lang_actions.addAction(lang) action.setCheckable(True) action.setChecked(lang == curr_lang) action.setData(lang) lang_menu.addAction(action) lang_menu.triggered.connect(self.cb_set_language) return lang_menu
def createFormatsMenu(self, parent=None): """Create and return a menu for selecting the spell-check language.""" fmt_menu = QMenu("Format", parent) fmt_actions = QActionGroup(fmt_menu) curr_format = self.highlighter.chunkers() for name, chunkers in (('Text', []), ('HTML', [tokenize.HTMLChunker])): action = fmt_actions.addAction(name) action.setCheckable(True) action.setChecked(chunkers == curr_format) action.setData(chunkers) fmt_menu.addAction(action) fmt_menu.triggered.connect(self.cb_set_format) return fmt_menu
class AlignToolBar(QToolBar): def __init__(self, title="Alignment Toolbar", parent=None): super().__init__(title, parent) self._parent = parent self.setObjectName("alitoolbar") self.setStyleSheet(""" QWidget[objectName^="alitoolbar"]{background-color: #777777;} QPushButton{background-color: #777777;} QToolButton{background-color: #777777;}; """) # ALIGNMENT FORMATTING #********************* # Align Actions #------------------------------------------------------------ self.alignl_action = QAction( QIcon(os.path.join('images', 'edit-alignment.png')), "Align left", self) self.alignc_action = QAction( QIcon(os.path.join('images', 'edit-alignment-center.png')), "Align center", self) self.alignr_action = QAction( QIcon(os.path.join('images', 'edit-alignment-right.png')), "Align right", self) self.alignj_action = QAction( QIcon(os.path.join('images', 'edit-alignment-justify.png')), "Justify", self) # Align Settings #------------------------------------------------------------ # Align Left self.alignl_action.setStatusTip("Align text left") self.alignl_action.setCheckable(True) self.alignl_action.toggled.connect( lambda toggled: self._parent.activeNotepad().setAlignment( Qt.AlignLeft if toggled else Qt.AlignJustify)) # Align Center self.alignc_action.setStatusTip("Align text center") self.alignc_action.setCheckable(True) self.alignc_action.toggled.connect( lambda toggled: self._parent.activeNotepad().setAlignment( Qt.AlignCenter if toggled else Qt.AlignLeft)) # Align Right self.alignr_action.setStatusTip("Align text right") self.alignr_action.setCheckable(True) self.alignr_action.toggled.connect( lambda toggled: self._parent.activeNotepad().setAlignment( Qt.AlignRight if toggled else Qt.AlignLeft)) # Justify self.alignj_action.setStatusTip("Justify text") self.alignj_action.setCheckable(True) self.alignj_action.toggled.connect( lambda toggled: self._parent.activeNotepad().setAlignment( Qt.AlignJustify if toggled else Qt.AlignLeft)) # Align Group ############################################### self.align_group = QActionGroup(self) self.align_group.setExclusionPolicy( QActionGroup.ExclusionPolicy.ExclusiveOptional) self.align_group.addAction(self.alignl_action) self.align_group.addAction(self.alignc_action) self.align_group.addAction(self.alignr_action) self.align_group.addAction(self.alignj_action) # Add actions to the tool bar self.addAction(self.alignl_action) self.addAction(self.alignc_action) self.addAction(self.alignr_action) self.addAction(self.alignj_action) ############################################### # LIST FORMATTING #***************** # List Actions #------------------------------------------------------------ self.list_action = QAction( QIcon(os.path.join('images', 'edit-list.png')), "List", self) self.ord_list_action = QAction( QIcon(os.path.join('images', 'edit-list-order.png')), "Ordered List", self) # List Widgets #------------------------------------------------------------ self.list_style_combo = QComboBox() self.ord_list_style_combo = QComboBox() # List Settings #------------------------------------------------------------ # List self.list_action.setStatusTip("Create list") self.list_action.setCheckable(True) self.list_action.toggled.connect(self.createList) # List Style list_styles = ["Disc", "Circle", "Square"] self.list_style_combo.addItems(list_styles) self.list_style_combo.activated.connect(self.changeListStyle) # Ordered List self.ord_list_action.setStatusTip("Create ordered list") self.ord_list_action.setCheckable(True) self.ord_list_action.toggled.connect(self.createOrdList) # Ordered List Style ord_list_styles = [ "Decimal", "Lower Alpha", "Upper Alpha", "Lower Roman", "Upper Roman" ] self.ord_list_style_combo.addItems(ord_list_styles) self.ord_list_style_combo.activated.connect(self.changeOrdListStyle) # Align Group (and widgets) ############################################### self.list_group = QActionGroup(self) self.list_group.setExclusionPolicy( QActionGroup.ExclusionPolicy.ExclusiveOptional) self.list_group.addAction(self.list_action) self.list_group.addAction(self.ord_list_action) # Add Actions and Widgets to the tool bar self.addAction(self.list_action) self.addWidget(self.list_style_combo) self.addAction(self.ord_list_action) self.addWidget(self.ord_list_style_combo) ############################################### def createList(self, toggled): cursor = self._parent.activeNotepad().textCursor() list_format = QTextListFormat() list_styles = { "Disc": QTextListFormat.ListDisc, "Circle": QTextListFormat.ListCircle, "Square": QTextListFormat.ListSquare } style = list_styles[self.list_style_combo.currentText()] if toggled: list_format.setStyle(style) cursor.createList(list_format) self._parent.activeNotepad().setTextCursor(cursor) else: current_list = cursor.currentList() if current_list: list_format.setIndent(0) list_format.setStyle(style) current_list.setFormat(list_format) for i in range(current_list.count() - 1, -1, -1): current_list.removeItem(i) def changeListStyle(self): cursor = self._parent.activeNotepad().textCursor() current_list = cursor.currentList() list_format = QTextListFormat() list_styles = { "Disc": QTextListFormat.ListDisc, "Circle": QTextListFormat.ListCircle, "Square": QTextListFormat.ListSquare } style = list_styles[self.list_style_combo.currentText()] list_format.setStyle(style) current_list.setFormat(list_format) self._parent.activeNotepad().setTextCursor(cursor) def createOrdList(self, toggled): cursor = self._parent.activeNotepad().textCursor() ord_list_format = QTextListFormat() ord_list_styles = { "Decimal": QTextListFormat.ListDecimal, "Lower Alpha": QTextListFormat.ListLowerAlpha, "Upper Alpha": QTextListFormat.ListUpperAlpha, "Lower Roman": QTextListFormat.ListLowerRoman, "Upper Roman": QTextListFormat.ListUpperRoman } style = ord_list_styles[self.ord_list_style_combo.currentText()] if toggled: ord_list_format.setStyle(style) cursor.createList(ord_list_format) self._parent.activeNotepad().setTextCursor(cursor) else: current_list = cursor.currentList() if current_list: ord_list_format.setIndent(0) ord_list_format.setStyle(style) current_list.setFormat(ord_list_format) for i in range(current_list.count() - 1, -1, -1): current_list.removeItem(i) def changeOrdListStyle(self): cursor = self._parent.activeNotepad().textCursor() current_list = cursor.currentList() list_format = QTextListFormat() ord_list_styles = { "Decimal": QTextListFormat.ListDecimal, "Lower Alpha": QTextListFormat.ListLowerAlpha, "Upper Alpha": QTextListFormat.ListUpperAlpha, "Lower Roman": QTextListFormat.ListLowerRoman, "Upper Roman": QTextListFormat.ListUpperRoman } style = ord_list_styles[self.ord_list_style_combo.currentText()] list_format.setStyle(style) current_list.setFormat(list_format) self._parent.activeNotepad().setTextCursor(cursor)
class AthenaWindow(QMainWindow): default_ui_path = os.path.join(ATHENA_DIR, 'ui', 'AthenaMainWindow.ui') def __init__(self, ui_filepath=default_ui_path): super().__init__(None) UiLoader.populateUI(self, ui_filepath) self.centralWidget().setAttribute(Qt.WA_AcceptTouchEvents, False) #self.centralWidget().setAttribute(Qt.WA_TransparentForMouseEvents, True) self.toolresults = None self.statusMsg = QLabel("Ready.") self.statusBar().addWidget(self.statusMsg) self.logWindow = logwindow.LogWindow(self) self.actionShowLogWindow.triggered.connect(self.logWindow.show) self.actionShowInputSidebar.toggled.connect( self.inputSidebar.setVisible) self.actionShowOutputSidebar.toggled.connect( self.outputSidebar.setVisible) self.actionShowInputSidebar.setShortcut( QKeySequence(Qt.CTRL + Qt.Key_BracketLeft)) self.actionShowOutputSidebar.setShortcut( QKeySequence(Qt.CTRL + Qt.Key_BracketRight)) # Menu shortcuts cannot be set up in a cross-platform way within Qt Designer, # so do that here. self.actionOpen.setShortcut(QKeySequence.StandardKey.Open) self.actionQuit.setShortcut(QKeySequence.StandardKey.Quit) self.actionOverlayResults.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_1)) self.actionSeparateResults.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_2)) self.resetScaffoldBox() self.geomView = viewer.AthenaViewer() self.viewerWidget_dummy.deleteLater() del self.viewerWidget_dummy self.geomViewWidget = QWidget.createWindowContainer( self.geomView, self, Qt.SubWindow) self.upperLayout.insertWidget(1, self.geomViewWidget) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(1) self.geomViewWidget.setSizePolicy(sizePolicy) self.geomViewWidget.setFocusPolicy(Qt.NoFocus) self.screenshotDialog = screenshot.ScreenshotDialog( self, self.geomView) self.actionScreenshot.triggered.connect(self.screenshotDialog.show) self.screenshotDialog.screenshotSaved.connect( self.notifyScreenshotDone) self.setupToolDefaults() self.enable2DControls() self.toolRunButton.clicked.connect(self.runTool) self.saveButton.clicked.connect(self.saveOutput) self.actionQuit.triggered.connect(self.close) self.actionNew.triggered.connect(self.newSession) self.actionAbout.triggered.connect(self.showAbout) self.actionOpen.triggered.connect(self.selectAndAddFileToGeomList) self.actionAddScaffold.triggered.connect(self.selectAndAddScaffoldFile) self.actionResetViewerOptions.triggered.connect( self.resetDisplayOptions) self.actionResetCamera.triggered.connect(self.geomView.resetCamera) # action groups cannot be set up in Qt Designer, so do that here self.resultsActionGroup = QActionGroup(self) self.resultsActionGroup.addAction(self.actionOverlayResults) self.resultsActionGroup.addAction(self.actionSeparateResults) self.resultsActionGroup.triggered.connect(self._setViewSplit) # Likewise for button groups self.projectionButtonGroup = QButtonGroup(self) self.projectionButtonGroup.addButton(self.orthoCamButton) self.projectionButtonGroup.addButton(self.perspectiveCamButton) self.projectionButtonGroup.buttonClicked.connect(self.selectProjection) self.camMotionButtonGroup = QButtonGroup(self) self.camMotionButtonGroup.addButton(self.rotateButton) self.camMotionButtonGroup.addButton(self.rotateButton) self.camMotionButtonGroup.addButton(self.panButton) self.camMotionButtonGroup.addButton(self.zoomButton) self.camMotionButtonGroup.buttonClicked.connect(self.selectCameraTool) # On Windows, flat some QGroupBox's ... it looks better there, # but worse on OSX. if platform.system() == 'Windows': self.controls_wireframe.setFlat(True) self.controls_2D.setFlat(True) self.controls_3D.setFlat(True) # On OSX, add a separator to the bottom of the view menu, which # will isolate the appkit default "full screen" option on its own. if platform.system() == 'Darwin': self.menuView.setSeparatorsCollapsible(False) self.menuView.addSeparator() self.displayCylinderBox.toggled.connect(self.geomView.toggleCylDisplay) self.geomView.toggleCylDisplay(self.displayCylinderBox.isChecked()) self.displayRoutingBox.toggled.connect(self.toggleRoutingDisplay) self.routingColorButtonGroup.setId(self.routingMulticolorButton, 0) self.routingColorButtonGroup.setId(self.routingBicolorButton, 1) self.routingColorButtonGroup.buttonClicked[int].connect( self.toggleRoutingDisplayVariant) self.toggleRoutingDisplay(self.displayRoutingBox.isChecked()) self.displayPAtomBox.toggled.connect(self.toggleAtomicDisplay) self.atomicColorButtonGroup.setId(self.atomicMulticolorButton, 0) self.atomicColorButtonGroup.setId(self.atomicBicolorButton, 1) self.atomicColorButtonGroup.buttonClicked[int].connect( self.toggleAtomicDisplayVariant) self.toggleAtomicDisplay(self.displayPAtomBox.isChecked()) self.geometryList.newFileSelected.connect(self.newMesh) def _setupColorButton(button, setter, signal, init_value): button.colorChosen.connect(setter) signal.connect(button.setColor) button.setColor(init_value) _setupColorButton(self.lineColorButton, self.geomView.setLineColor, self.geomView.lineColorChanged, self.geomView.lineColor()) _setupColorButton(self.flatColorButton, self.geomView.setFlatColor, self.geomView.flatColorChanged, self.geomView.flatColor()) _setupColorButton(self.bgColorButton, self.geomView.setBackgroundColor, self.geomView.backgroundColorChanged, self.geomView.backgroundColor()) _setupColorButton(self.warmColorButton, self.geomView.setWarmColor, self.geomView.warmColorChanged, self.geomView.warmColor()) _setupColorButton(self.coolColorButton, self.geomView.setCoolColor, self.geomView.coolColorChanged, self.geomView.coolColor()) self.alphaSlider.valueChanged.connect(self.setViewerAlpha) self.geomView.alphaChanged.connect(self.handleAlphaChanged) self.lineWidthSlider.valueChanged.connect(self.setViewerLinewidth) self.geomView.lineWidthChanged.connect( self.handleViewerLinewidthChanged) self.lightDial.valueChanged.connect(self.geomView.setLightOrientation) self.geomView.lightOrientationChanged.connect(self.lightDial.setValue) self.controls_2D.toggled.connect(self.geomView.toggleFaceRendering) self.controls_3D.toggled.connect(self.geomView.toggleFaceRendering) self.controls_wireframe.toggled.connect( self.geomView.toggleWireframeRendering) self.geomView.faceRenderingEnabledChanged.connect( self.controls_2D.setChecked) self.geomView.faceRenderingEnabledChanged.connect( self.controls_3D.setChecked) self.geomView.wireframeRenderingEnabledChanged.connect( self.controls_wireframe.setChecked) self.newMesh(None) self.show() self.log("Athena version {}".format(__version__)) def resetScaffoldBox(self): self.scaffoldBox.clear() self.scaffoldBox.addItem("Default", "m13") self.scaffoldBox.view().setTextElideMode(Qt.ElideRight) self.scaffoldBox.setCurrentIndex(0) def newSession(self): self.resetScaffoldBox() self.newMesh(None) self.geometryList.clearSelection() self.geometryList.topLevelItem(2).takeChildren() for idx in range(3): self.geometryList.collapseItem(self.geometryList.topLevelItem(idx)) self.geomView.clearAllGeometry() self.geomView.resetBackgroundColor() self.geomView.resetParameters() self.geomView.resetCamera() def resetDisplayOptions(self): self.geomView.resetBackgroundColor() self.geomView.resetParameters() def selectProjection(self, widget): if widget == self.orthoCamButton: self.geomView.setOrthoCam() elif widget == self.perspectiveCamButton: self.geomView.setPerspectiveCam() def selectCameraTool(self, widget): if widget == self.rotateButton: self.geomView.setRotateTool() elif widget == self.panButton: self.geomView.setPanTool() elif widget == self.zoomButton: self.geomView.setZoomTool() def setViewerAlpha(self, value): alpha = float(value) / 255.0 self.geomView.setAlpha(alpha) def handleAlphaChanged(self, newvalue): intvalue = newvalue * 255 self.alphaSlider.setValue(intvalue) def setViewerLinewidth(self, value): new_width = float(value) / 10. self.geomView.setLineWidth(new_width) def handleViewerLinewidthChanged(self, newvalue): intvalue = newvalue * 10 self.lineWidthSlider.setValue(intvalue) def toggleRoutingDisplay(self, boolvalue): self.geomView.toggleRoutDisplay( boolvalue, self.routingColorButtonGroup.checkedId()) self.geomView.toggleRoutDisplay( False, abs(self.routingColorButtonGroup.checkedId() - 1)) def toggleRoutingDisplayVariant(self, buttonid): other = abs(buttonid - 1) self.geomView.toggleRoutDisplay(False, other) self.geomView.toggleRoutDisplay(self.displayRoutingBox.isChecked(), buttonid) def toggleAtomicDisplay(self, boolvalue): self.geomView.toggleAtomDisplay( boolvalue, self.atomicColorButtonGroup.checkedId()) self.geomView.toggleAtomDisplay( False, abs(self.atomicColorButtonGroup.checkedId() - 1)) def toggleAtomicDisplayVariant(self, buttonid): other = abs(buttonid - 1) self.geomView.toggleAtomDisplay(False, other) self.geomView.toggleAtomDisplay(self.displayPAtomBox.isChecked(), buttonid) def _setViewSplit(self, action): if action is self.actionOverlayResults: self.geomView.setSplitViewEnabled(False) elif action is self.actionSeparateResults: self.geomView.setSplitViewEnabled(True) else: print("ERROR in _setViewSplit") def setupToolDefaults(self): perdix_inputs = Path(ATHENA_DIR, "sample_inputs", "2D") for ply in sorted(perdix_inputs.glob('*.ply')): self.geometryList.add2DExampleFile(ply) perdix_inputs = Path(ATHENA_DIR, "sample_inputs", "3D") for ply in sorted(perdix_inputs.glob('*.ply')): self.geometryList.add3DExampleFile(ply) def selectAndAddFileToGeomList(self): fileName = QFileDialog.getOpenFileName( self, "Open geometry file", os.path.join(ATHENA_DIR, 'sample_inputs'), "Geometry files (*.ply)") filepath = Path(fileName[0]) if (filepath.is_file()): self.geometryList.addUserFile(filepath, force_select=True) def selectAndAddScaffoldFile(self): fileName = QFileDialog.getOpenFileName(self, "Open scaffold file", ATHENA_DIR, "Text files (*)") filepath = Path(fileName[0]) if (filepath.is_file()): self.scaffoldBox.addItem(filepath.name, filepath) self.scaffoldBox.setCurrentIndex(self.scaffoldBox.count() - 1) w = self.scaffoldBox.minimumSizeHint().width() self.scaffoldBox.view().setMinimumWidth(w) self.scaffoldBox.view().reset() def enable2DControls(self): self.renderControls.setCurrentIndex(0) self.toolControls.setCurrentIndex(0) self.panButton.click() def enable3DControls(self): self.renderControls.setCurrentIndex(1) self.toolControls.setCurrentIndex(1) self.rotateButton.click() def toggleOutputControls(self, value): self.saveResultsBox.setEnabled(value) self.showResultsBox.setEnabled(value) # On Mac, the child widgets don't seem to be properly repainted # unless we insist on it here self.saveResultsBox.repaint() self.showResultsBox.repaint() def log(self, text): self.logWindow.appendText(text) def newMesh(self, meshFile): if (meshFile): self.log('Loading ' + str(meshFile)) mesh_3d = self.geomView.reloadGeom(meshFile) if (mesh_3d): self.enable3DControls() else: self.enable2DControls() self.toggleOutputControls(False) self.toolresults = None self.updateStatus('Ready.', log=False) def newOutputs(self, toolresults): if toolresults is None or toolresults.bildfiles is None: return scale_factor = toolresults.toolinfo['scale_factor'] self.geomView.clearDecorations() for path in toolresults.bildfiles: if path.match('*target_geometry.bild'): base_bild = bildparser.parseBildFile(path, scale_factor) base_aabb = geom.AABB(base_bild) for path in toolresults.bildfiles: if path.match('*_cylinder_model.bild'): self.geomView.setCylDisplay(bildparser.parseBildFile(path), base_aabb) elif path.match('*_atomic_model_multi.bild'): self.geomView.setAtomDisplay(bildparser.parseBildFile(path), base_aabb, 0) elif path.match('*_atomic_model_two.bild'): self.geomView.setAtomDisplay(bildparser.parseBildFile(path), base_aabb, 1) elif path.match('*_routing_multi.bild'): self.geomView.setRoutDisplay(bildparser.parseBildFile(path), base_aabb, 0) elif path.match('*_routing_two.bild'): self.geomView.setRoutDisplay(bildparser.parseBildFile(path), base_aabb, 1) self.toggleOutputControls(True) self.toolresults = toolresults # Request a redraw to avoid a bug where disabled entities might be visible at first self.geomView.requestUpdate() def generatePDB(self): if (self.toolresults and self.toolresults.cndofile): cndofile = self.toolresults.cndofile dirstr = str(cndofile.parent.resolve()) + os.path.sep pdbgen.pdbgen(cndofile.stem, 'B', 'DNA', dirstr, dirstr, logwindow.WriteWrapper(self.logWindow)) else: print("ERROR: No current pdb file") def saveOutput(self): if (self.toolresults): container_dir = QFileDialog.getExistingDirectory( self, "Save Location") new_output_dir = Path( container_dir) / self.toolresults.output_dir.name if (self.includePDBBox.isChecked()): self.generatePDB() print(self.toolresults.output_dir, '->', container_dir) newdir = shutil.copytree(self.toolresults.output_dir, new_output_dir) self.updateStatus('Saved results to {}'.format(newdir)) else: print("ERROR: No current results to save") def updateStatus(self, msg, log=True): if log: self.log(msg) self.statusMsg.setText(msg) def _toolFilenames(self, toolname): active_item = self.geometryList.currentItem() infile_path = active_item.data(0, Qt.UserRole) infile_name = active_item.text(0) output_subdir = datetime.now().strftime('%y%m%d%H%M%S_' + toolname + '_' + infile_name) outfile_dir_path = ATHENA_OUTPUT_DIR / output_subdir return infile_path, outfile_dir_path def runTool(self): tool_chooser = self.toolControls.currentWidget() if tool_chooser == self.tools_2D: toolkey = (0, self.toolBox_2D.currentIndex()) elif tool_chooser == self.tools_3D: toolkey = (1, self.toolBox_3D.currentIndex()) else: print("ERROR: No available tool") self.toolMap[toolkey](self) def _humanReadableReturnValue(self, process): if process.returncode == 0: human_retval = 'success' else: msg = process.toolinfo.get('error', process.returncode) human_retval = 'failure ({})'.format(msg) return human_retval def runPERDIX(self): self.updateStatus('Running PERDIX...') infile_path, outfile_dir_path = self._toolFilenames('PERDIX') process = runLCBBTool( 'PERDIX', p1_output_dir=outfile_dir_path, p2_input_file=infile_path, p3_scaffold=self.scaffoldBox.currentData(), p7_edge_length=self.perdixEdgeLengthSpinner.value()) self.log(process.stdout) self.updateStatus('PERDIX returned {}.'.format( self._humanReadableReturnValue(process))) self.newOutputs(process) def runTALOS(self): self.updateStatus('Running TALOS...') infile_path, outfile_dir_path = self._toolFilenames('TALOS') process = runLCBBTool( 'TALOS', p1_output_dir=outfile_dir_path, p2_input_file=infile_path, p3_scaffold=self.scaffoldBox.currentData(), p4_edge_sections=self.talosEdgeSectionBox.currentIndex() + 2, p5_vertex_design=self.talosVertexDesignBox.currentIndex() + 1, p7_edge_length=self.talosEdgeLengthSpinner.value()) self.log(process.stdout) self.updateStatus('TALOS returned {}.'.format( self._humanReadableReturnValue(process))) self.newOutputs(process) def runDAEDALUS2(self): self.updateStatus('Running DAEDALUS...') infile_path, outfile_dir_path = self._toolFilenames('DAEDALUS2') process = runLCBBTool( 'DAEDALUS2', p1_output_dir=outfile_dir_path, p2_input_file=infile_path, p3_scaffold=self.scaffoldBox.currentData(), p4_edge_sections=1, p5_vertex_design=2, p7_edge_length=self.daedalusEdgeLengthSpinner.value()) self.log(process.stdout) self.updateStatus('DAEDALUS returned {}.'.format( self._humanReadableReturnValue(process))) self.newOutputs(process) def runMETIS(self): self.updateStatus('Running METIS...') infile_path, outfile_dir_path = self._toolFilenames('METIS') process = runLCBBTool( 'METIS', p1_output_dir=outfile_dir_path, p2_input_file=infile_path, p3_scaffold=self.scaffoldBox.currentData(), p4_edge_sections=3, p5_vertex_design=2, p7_edge_length=self.metisEdgeLengthSpinner.value()) self.log(process.stdout) self.updateStatus('METIS returned {}.'.format( self._humanReadableReturnValue(process))) self.newOutputs(process) toolMap = { (0, 0): runPERDIX, (0, 1): runMETIS, (1, 0): runDAEDALUS2, (1, 1): runTALOS } def showAbout(self): about_text = open(Path(ATHENA_SRC_DIR) / 'txt' / 'About.txt', 'r', encoding='utf8').read() about_text = about_text.format(version=__version__) QMessageBox.about(self, "About Athena", about_text) def notifyScreenshotDone(self, path): self.updateStatus('Saved screenshot to {}'.format(path))
def __init__( self, document: Optional[vp.Document] = None, view_mode: ViewMode = ViewMode.PREVIEW, show_pen_up: bool = False, show_points: bool = False, parent=None, ): super().__init__(parent) self._settings = QSettings() self._settings.beginGroup("viewer") self.setWindowTitle("vpype viewer") self.setStyleSheet(""" QToolButton:pressed { background-color: rgba(0, 0, 0, 0.2); } """) self._viewer_widget = QtViewerWidget(parent=self) # setup toolbar self._toolbar = QToolBar() self._icon_size = QSize(32, 32) self._toolbar.setIconSize(self._icon_size) view_mode_grp = QActionGroup(self._toolbar) if _DEBUG_ENABLED: act = view_mode_grp.addAction("None") act.setCheckable(True) act.setChecked(view_mode == ViewMode.NONE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.NONE)) act = view_mode_grp.addAction("Outline Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE)) act = view_mode_grp.addAction("Outline Mode (Colorful)") act.setCheckable(True) act.setChecked(view_mode == ViewMode.OUTLINE_COLORFUL) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.OUTLINE_COLORFUL)) act = view_mode_grp.addAction("Preview Mode") act.setCheckable(True) act.setChecked(view_mode == ViewMode.PREVIEW) act.triggered.connect( functools.partial(self.set_view_mode, ViewMode.PREVIEW)) self.set_view_mode(view_mode) # VIEW MODE # view modes view_mode_btn = QToolButton() view_mode_menu = QMenu(view_mode_btn) act = view_mode_menu.addAction("View Mode:") act.setEnabled(False) view_mode_menu.addActions(view_mode_grp.actions()) view_mode_menu.addSeparator() # show pen up act = view_mode_menu.addAction("Show Pen-Up Trajectories") act.setCheckable(True) act.setChecked(show_pen_up) act.toggled.connect(self.set_show_pen_up) self._viewer_widget.engine.show_pen_up = show_pen_up # show points act = view_mode_menu.addAction("Show Points") act.setCheckable(True) act.setChecked(show_points) act.toggled.connect(self.set_show_points) self._viewer_widget.engine.show_points = show_points # preview mode options view_mode_menu.addSeparator() act = view_mode_menu.addAction("Preview Mode Options:") act.setEnabled(False) # pen width pen_width_menu = view_mode_menu.addMenu("Pen Width") act_grp = PenWidthActionGroup(0.3, parent=pen_width_menu) act_grp.triggered.connect(self.set_pen_width_mm) pen_width_menu.addActions(act_grp.actions()) self.set_pen_width_mm(0.3) # pen opacity pen_opacity_menu = view_mode_menu.addMenu("Pen Opacity") act_grp = PenOpacityActionGroup(0.8, parent=pen_opacity_menu) act_grp.triggered.connect(self.set_pen_opacity) pen_opacity_menu.addActions(act_grp.actions()) self.set_pen_opacity(0.8) # debug view if _DEBUG_ENABLED: act = view_mode_menu.addAction("Debug View") act.setCheckable(True) act.toggled.connect(self.set_debug) # rulers view_mode_menu.addSeparator() act = view_mode_menu.addAction("Show Rulers") act.setCheckable(True) val = bool(self._settings.value("show_rulers", True)) act.setChecked(val) act.toggled.connect(self.set_show_rulers) self._viewer_widget.engine.show_rulers = val # units units_menu = view_mode_menu.addMenu("Units") unit_action_grp = QActionGroup(units_menu) unit_type = UnitType(self._settings.value("unit_type", UnitType.METRIC)) act = unit_action_grp.addAction("Metric") act.setCheckable(True) act.setChecked(unit_type == UnitType.METRIC) act.setData(UnitType.METRIC) act = unit_action_grp.addAction("Imperial") act.setCheckable(True) act.setChecked(unit_type == UnitType.IMPERIAL) act.setData(UnitType.IMPERIAL) act = unit_action_grp.addAction("Pixel") act.setCheckable(True) act.setChecked(unit_type == UnitType.PIXELS) act.setData(UnitType.PIXELS) unit_action_grp.triggered.connect(self.set_unit_type) units_menu.addActions(unit_action_grp.actions()) self._viewer_widget.engine.unit_type = unit_type view_mode_btn.setMenu(view_mode_menu) view_mode_btn.setIcon(load_icon("eye-outline.svg")) view_mode_btn.setText("View") view_mode_btn.setPopupMode(QToolButton.InstantPopup) view_mode_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(view_mode_btn) # LAYER VISIBILITY self._layer_visibility_btn = QToolButton() self._layer_visibility_btn.setIcon( load_icon("layers-triple-outline.svg")) self._layer_visibility_btn.setText("Layer") self._layer_visibility_btn.setMenu(QMenu(self._layer_visibility_btn)) self._layer_visibility_btn.setPopupMode(QToolButton.InstantPopup) self._layer_visibility_btn.setStyleSheet( "QToolButton::menu-indicator { image: none; }") self._toolbar.addWidget(self._layer_visibility_btn) # FIT TO PAGE fit_act = self._toolbar.addAction(load_icon("fit-to-page-outline.svg"), "Fit") fit_act.triggered.connect(self._viewer_widget.engine.fit_to_viewport) # RULER # TODO: not implemented yet # self._toolbar.addAction(load_icon("ruler-square.svg"), "Units") # MOUSE COORDINATES> self._mouse_coord_lbl = QLabel("") self._mouse_coord_lbl.setMargin(6) self._mouse_coord_lbl.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self._mouse_coord_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) self._toolbar.addWidget(self._mouse_coord_lbl) # noinspection PyUnresolvedReferences self._viewer_widget.mouse_coords.connect( self.set_mouse_coords) # type: ignore # setup horizontal layout for optional side widgets self._hlayout = QHBoxLayout() self._hlayout.setSpacing(0) self._hlayout.setMargin(0) self._hlayout.addWidget(self._viewer_widget) widget = QWidget() widget.setLayout(self._hlayout) # setup global vertical layout layout = QVBoxLayout() layout.setSpacing(0) layout.setMargin(0) layout.addWidget(self._toolbar) layout.addWidget(widget) self.setLayout(layout) if document is not None: self.set_document(document)
def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) layout = QVBoxLayout() self.editor = TextEdit() # Setup the QTextEdit editor configuration self.editor.setAutoFormatting(QTextEdit.AutoAll) self.editor.selectionChanged.connect(self.update_format) # Initialize default font size. font = QFont('Times', 12) self.editor.setFont(font) # We need to repeat the size to init the current format. self.editor.setFontPointSize(12) # self.path holds the path of the currently open file. # If none, we haven't got a file open yet (or creating new). self.path = None layout.addWidget(self.editor) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) self.status = QStatusBar() self.setStatusBar(self.status) # Uncomment to disable native menubar on Mac # self.menuBar().setNativeMenuBar(False) file_toolbar = QToolBar("File") file_toolbar.setIconSize(QSize(14, 14)) self.addToolBar(file_toolbar) file_menu = self.menuBar().addMenu("&File") open_file_action = QAction( QIcon(os.path.join('images', 'blue-folder-open-document.png')), "Open file...", self) open_file_action.setStatusTip("Open file") open_file_action.triggered.connect(self.file_open) file_menu.addAction(open_file_action) file_toolbar.addAction(open_file_action) save_file_action = QAction(QIcon(os.path.join('images', 'disk.png')), "Save", self) save_file_action.setStatusTip("Save current page") save_file_action.triggered.connect(self.file_save) file_menu.addAction(save_file_action) file_toolbar.addAction(save_file_action) saveas_file_action = QAction( QIcon(os.path.join('images', 'disk--pencil.png')), "Save As...", self) saveas_file_action.setStatusTip("Save current page to specified file") saveas_file_action.triggered.connect(self.file_saveas) file_menu.addAction(saveas_file_action) file_toolbar.addAction(saveas_file_action) print_action = QAction(QIcon(os.path.join('images', 'printer.png')), "Print...", self) print_action.setStatusTip("Print current page") print_action.triggered.connect(self.file_print) file_menu.addAction(print_action) file_toolbar.addAction(print_action) edit_toolbar = QToolBar("Edit") edit_toolbar.setIconSize(QSize(16, 16)) self.addToolBar(edit_toolbar) edit_menu = self.menuBar().addMenu("&Edit") undo_action = QAction( QIcon(os.path.join('images', 'arrow-curve-180-left.png')), "Undo", self) undo_action.setStatusTip("Undo last change") undo_action.triggered.connect(self.editor.undo) edit_menu.addAction(undo_action) redo_action = QAction(QIcon(os.path.join('images', 'arrow-curve.png')), "Redo", self) redo_action.setStatusTip("Redo last change") redo_action.triggered.connect(self.editor.redo) edit_toolbar.addAction(redo_action) edit_menu.addAction(redo_action) edit_menu.addSeparator() cut_action = QAction(QIcon(os.path.join('images', 'scissors.png')), "Cut", self) cut_action.setStatusTip("Cut selected text") cut_action.setShortcut(QKeySequence.Cut) cut_action.triggered.connect(self.editor.cut) edit_toolbar.addAction(cut_action) edit_menu.addAction(cut_action) copy_action = QAction( QIcon(os.path.join('images', 'document-copy.png')), "Copy", self) copy_action.setStatusTip("Copy selected text") cut_action.setShortcut(QKeySequence.Copy) copy_action.triggered.connect(self.editor.copy) edit_toolbar.addAction(copy_action) edit_menu.addAction(copy_action) paste_action = QAction( QIcon(os.path.join('images', 'clipboard-paste-document-text.png')), "Paste", self) paste_action.setStatusTip("Paste from clipboard") cut_action.setShortcut(QKeySequence.Paste) paste_action.triggered.connect(self.editor.paste) edit_toolbar.addAction(paste_action) edit_menu.addAction(paste_action) select_action = QAction( QIcon(os.path.join('images', 'selection-input.png')), "Select all", self) select_action.setStatusTip("Select all text") cut_action.setShortcut(QKeySequence.SelectAll) select_action.triggered.connect(self.editor.selectAll) edit_menu.addAction(select_action) edit_menu.addSeparator() wrap_action = QAction( QIcon(os.path.join('images', 'arrow-continue.png')), "Wrap text to window", self) wrap_action.setStatusTip("Toggle wrap text to window") wrap_action.setCheckable(True) wrap_action.setChecked(True) wrap_action.triggered.connect(self.edit_toggle_wrap) edit_menu.addAction(wrap_action) format_toolbar = QToolBar("Format") format_toolbar.setIconSize(QSize(16, 16)) self.addToolBar(format_toolbar) format_menu = self.menuBar().addMenu("&Format") # We need references to these actions/settings to update as selection changes, so attach to self. self.fonts = QFontComboBox() self.fonts.currentFontChanged.connect(self.editor.setCurrentFont) format_toolbar.addWidget(self.fonts) self.fontsize = QComboBox() self.fontsize.addItems([str(s) for s in FONT_SIZES]) # Connect to the signal producing the text of the current selection. Convert the string to float # and set as the pointsize. We could also use the index + retrieve from FONT_SIZES. self.fontsize.currentIndexChanged[str].connect( lambda s: self.editor.setFontPointSize(float(s))) format_toolbar.addWidget(self.fontsize) self.bold_action = QAction( QIcon(os.path.join('images', 'edit-bold.png')), "Bold", self) self.bold_action.setStatusTip("Bold") self.bold_action.setShortcut(QKeySequence.Bold) self.bold_action.setCheckable(True) self.bold_action.toggled.connect(lambda x: self.editor.setFontWeight( QFont.Bold if x else QFont.Normal)) format_toolbar.addAction(self.bold_action) format_menu.addAction(self.bold_action) self.italic_action = QAction( QIcon(os.path.join('images', 'edit-italic.png')), "Italic", self) self.italic_action.setStatusTip("Italic") self.italic_action.setShortcut(QKeySequence.Italic) self.italic_action.setCheckable(True) self.italic_action.toggled.connect(self.editor.setFontItalic) format_toolbar.addAction(self.italic_action) format_menu.addAction(self.italic_action) self.underline_action = QAction( QIcon(os.path.join('images', 'edit-underline.png')), "Underline", self) self.underline_action.setStatusTip("Underline") self.underline_action.setShortcut(QKeySequence.Underline) self.underline_action.setCheckable(True) self.underline_action.toggled.connect(self.editor.setFontUnderline) format_toolbar.addAction(self.underline_action) format_menu.addAction(self.underline_action) format_menu.addSeparator() self.alignl_action = QAction( QIcon(os.path.join('images', 'edit-alignment.png')), "Align left", self) self.alignl_action.setStatusTip("Align text left") self.alignl_action.setCheckable(True) self.alignl_action.triggered.connect( lambda: self.editor.setAlignment(Qt.AlignLeft)) format_toolbar.addAction(self.alignl_action) format_menu.addAction(self.alignl_action) self.alignc_action = QAction( QIcon(os.path.join('images', 'edit-alignment-center.png')), "Align center", self) self.alignc_action.setStatusTip("Align text center") self.alignc_action.setCheckable(True) self.alignc_action.triggered.connect( lambda: self.editor.setAlignment(Qt.AlignCenter)) format_toolbar.addAction(self.alignc_action) format_menu.addAction(self.alignc_action) self.alignr_action = QAction( QIcon(os.path.join('images', 'edit-alignment-right.png')), "Align right", self) self.alignr_action.setStatusTip("Align text right") self.alignr_action.setCheckable(True) self.alignr_action.triggered.connect( lambda: self.editor.setAlignment(Qt.AlignRight)) format_toolbar.addAction(self.alignr_action) format_menu.addAction(self.alignr_action) self.alignj_action = QAction( QIcon(os.path.join('images', 'edit-alignment-justify.png')), "Justify", self) self.alignj_action.setStatusTip("Justify text") self.alignj_action.setCheckable(True) self.alignj_action.triggered.connect( lambda: self.editor.setAlignment(Qt.AlignJustify)) format_toolbar.addAction(self.alignj_action) format_menu.addAction(self.alignj_action) format_group = QActionGroup(self) format_group.setExclusive(True) format_group.addAction(self.alignl_action) format_group.addAction(self.alignc_action) format_group.addAction(self.alignr_action) format_group.addAction(self.alignj_action) format_menu.addSeparator() # A list of all format-related widgets/actions, so we can disable/enable signals when updating. self._format_actions = [ self.fonts, self.fontsize, self.bold_action, self.italic_action, self.underline_action, # We don't need to disable signals for alignment, as they are paragraph-wide. ] # Initialize. self.update_format() self.update_title() self.show()
class GuiActions(object): """ Actions triggered by the user interface (buttons) """ def __init__(self, mainWindow, apiActions): self.mainWindow = mainWindow self.apiActions = apiActions #Basic actions self.basicActions = QActionGroup(self.mainWindow) self.actionOpen = self.basicActions.addAction(QIcon(":/icons/save.png"), "Open Database") self.actionOpen.triggered.connect(self.openDB) self.actionNew = self.basicActions.addAction(QIcon(":/icons/new.png"), "New Database") self.actionNew.triggered.connect(self.makeDB) #Database actions self.databaseActions = QActionGroup(self.mainWindow) self.actionExport = self.databaseActions.addAction(QIcon(":/icons/export.png"), "Export Data") self.actionExport.setToolTip(wraptip("Export selected node(s) and their children to a .csv file. \n If no or all node(s) are selected inside the data-view, a complete export of all data in the DB is performed")) self.actionExport.triggered.connect(self.exportNodes) self.actionAdd = self.databaseActions.addAction(QIcon(":/icons/add.png"), "Add Nodes") self.actionAdd.setToolTip(wraptip("Add new node(s) as a starting point for further data collection")) self.actionAdd.triggered.connect(self.addNodes) self.actionDelete = self.databaseActions.addAction(QIcon(":/icons/delete.png"), "Delete Nodes") self.actionDelete.setToolTip(wraptip("Delete nodes(s) and their children")) self.actionDelete.triggered.connect(self.deleteNodes) #Data actions self.dataActions = QActionGroup(self.mainWindow) self.actionQuery = self.dataActions.addAction(QIcon(":/icons/fetch.png"), "Query") self.actionQuery.triggered.connect(self.querySelectedNodes) self.actionSettings = self.dataActions.addAction(QIcon(":/icons/more.png"), "More settings") self.actionSettings.setToolTip(wraptip("Can't get enough? Here you will find even more settings.")) self.actionSettings.triggered.connect(self.openSettings) self.actionBrowse = self.dataActions.addAction(QIcon(":/icons/browser.png"), "Open") self.actionBrowse.setToolTip(wraptip("Open the resulting URL in the browser.")) self.actionBrowse.triggered.connect(self.openBrowser) self.actionTimer = self.dataActions.addAction(QIcon(":/icons/fetch.png"), "Time") self.actionTimer.setToolTip(wraptip("Time your data collection with a timer. Fetches the data for the selected node(s) in user-defined intervalls")) self.actionTimer.triggered.connect(self.setupTimer) self.actionHelp = self.dataActions.addAction(QIcon(":/icons/help.png"), "Help") self.actionHelp.triggered.connect(self.help) self.actionLoadPreset = self.dataActions.addAction(QIcon(":/icons/presets.png"), "Presets") self.actionLoadPreset.triggered.connect(self.openPresets) self.actionLoadAPIs = self.dataActions.addAction(QIcon(":/icons/apis.png"), "APIs") self.actionLoadAPIs.triggered.connect(self.loadAPIs) #Detail actions self.detailActions = QActionGroup(self.mainWindow) self.actionJsonCopy = self.detailActions.addAction(QIcon(":/icons/toclip.png"), "Copy JSON to Clipboard") self.actionJsonCopy.setToolTip(wraptip("Copy the selected JSON-data to the clipboard")) self.actionJsonCopy.triggered.connect(self.jsonCopy) self.actionUnpack = self.detailActions.addAction(QIcon(":/icons/unpack.png"),"Extract Data") self.actionUnpack.setToolTip(wraptip("Extract new nodes from the data using keys." \ "You can pipe the value to css selectors (e.g. text|div.main)" \ "or xpath selectors (e.g. text|//div[@class='main']/text()")) self.actionUnpack.triggered.connect(self.unpackList) self.actionFieldDoc = self.detailActions.addAction(QIcon(":/icons/help.png"),"") self.actionFieldDoc.setToolTip(wraptip("Open the documentation for the selected item if available.")) self.actionFieldDoc.triggered.connect(self.showFieldDoc) # Column setup actions self.columnActions = QActionGroup(self.mainWindow) self.actionAddColumn = self.columnActions.addAction(QIcon(":/icons/addcolumn.png"), "Add Column") self.actionAddColumn.setToolTip(wraptip("Add the current JSON-key as a column in the data view")) self.actionAddColumn.triggered.connect(self.addColumn) self.actionAddAllolumns = self.columnActions.addAction(QIcon(":/icons/addcolumn.png"), "Add All Columns") self.actionAddAllolumns.setToolTip( wraptip("Analyzes all selected nodes in the data view and adds all found keys as columns")) self.actionAddAllolumns.triggered.connect(self.addAllColumns) self.actionShowColumns = self.columnActions.addAction(QIcon(":/icons/apply.png"), "Apply Column Setup") self.actionShowColumns.setToolTip(wraptip(("Show the columns in the central data view. " + "Scroll right or left to see hidden columns."))) self.actionShowColumns.triggered.connect(self.showColumns) self.actionClearColumns = self.columnActions.addAction(QIcon(":/icons/clear.png"), "Clear Columns") self.actionClearColumns.setToolTip(wraptip("Remove all columns to get space for a new setup.")) self.actionClearColumns.triggered.connect(self.clearColumns) #Tree actions self.treeActions = QActionGroup(self.mainWindow) self.actionExpandAll = self.treeActions.addAction(QIcon(":/icons/expand.png"), "Expand nodes") self.actionExpandAll.triggered.connect(self.expandAll) self.actionCollapseAll = self.treeActions.addAction(QIcon(":/icons/collapse.png"), "Collapse nodes") self.actionCollapseAll.triggered.connect(self.collapseAll) self.actionFind = self.treeActions.addAction(QIcon(":/icons/search.png"), "Find nodes") self.actionFind.triggered.connect(self.selectNodes) #self.actionSelectNodes=self.treeActions.addAction(QIcon(":/icons/collapse.png"),"Select nodes") #self.actionSelectNodes.triggered.connect(self.selectNodes) self.actionClipboard = self.treeActions.addAction(QIcon(":/icons/toclip.png"), "Copy Node(s) to Clipboard") self.actionClipboard.setToolTip(wraptip("Copy the selected nodes(s) to the clipboard")) self.actionClipboard.triggered.connect(self.clipboardNodes) self.actionTransfer = self.treeActions.addAction(QIcon(":/icons/transfer.png"), "Transfer nodes") self.actionTransfer.setToolTip(wraptip("Add the Object IDs of the selected nodes as seed nodes. Duplicates will be ignored. " \ "Useful for crawling: after fetching data, add new nodes to the list.")) self.actionTransfer.triggered.connect(self.duplicateNodes) @Slot() def help(self): self.mainWindow.helpwindow.show() @Slot() def openDB(self): #open a file dialog with a .db filter datadir = self.mainWindow.database.filename if not os.path.exists(datadir): datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) if not os.path.exists(datadir): datadir = os.path.expanduser("~") datadir = os.path.dirname(datadir) fldg = QFileDialog(caption="Open DB File", directory=datadir, filter="DB files (*.db)") fldg.setFileMode(QFileDialog.ExistingFile) if fldg.exec_(): self.apiActions.openDatabase(fldg.selectedFiles()[0]) @Slot() def makeDB(self): datadir = self.mainWindow.database.filename if not os.path.exists(datadir): datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) if not os.path.exists(datadir): datadir = os.path.expanduser("~") datadir = os.path.dirname(datadir) fldg = QFileDialog(caption="Save DB File", directory=datadir, filter="DB files (*.db)") fldg.setAcceptMode(QFileDialog.AcceptSave) fldg.setDefaultSuffix("db") if fldg.exec_(): self.apiActions.createDatabase(fldg.selectedFiles()[0], True) @Slot() def deleteNodes(self): reply = QMessageBox.question(self.mainWindow, 'Delete Nodes', "Are you sure to delete all selected nodes?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return progress = ProgressBar("Deleting data...", self.mainWindow) self.mainWindow.tree.setUpdatesEnabled(False) try: todo = self.mainWindow.tree.selectedIndexesAndChildren({'persistent': True}) todo = list(todo) progress.setMaximum(len(todo)) for index in todo: progress.step() self.mainWindow.tree.treemodel.deleteNode(index, delaycommit=True) if progress.wasCanceled: break finally: # commit the operation on the db-layer afterwards (delaycommit is True) self.mainWindow.tree.treemodel.commitNewNodes() self.mainWindow.tree.setUpdatesEnabled(True) progress.close() @Slot() def clipboardNodes(self): progress = ProgressBar("Copy to clipboard", self.mainWindow) indexes = self.mainWindow.tree.selectionModel().selectedRows() progress.setMaximum(len(indexes)) output = io.StringIO() try: writer = csv.writer(output, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL, doublequote=True, lineterminator='\r\n') #headers row = [str(val) for val in self.mainWindow.tree.treemodel.getRowHeader()] writer.writerow(row) #rows for no in range(len(indexes)): if progress.wasCanceled: break row = [str(val) for val in self.mainWindow.tree.treemodel.getRowData(indexes[no])] writer.writerow(row) progress.step() clipboard = QApplication.clipboard() clipboard.setText(output.getvalue()) finally: output.close() progress.close() @Slot() def exportNodes(self): fldg = ExportFileDialog(self.mainWindow, filter ="CSV Files (*.csv)") @Slot() def addNodes(self): if not self.mainWindow.database.connected: return False # makes the user add a new facebook object into the db dialog = QDialog(self.mainWindow) dialog.setWindowTitle("Add Nodes") layout = QVBoxLayout() label = QLabel("One <b>Object ID</b> per line") layout.addWidget(label) input = QPlainTextEdit() input.setMinimumWidth(500) input.LineWrapMode = QPlainTextEdit.NoWrap #input.acceptRichText=False input.setFocus() layout.addWidget(input) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) filesbutton = buttons.addButton("Add files", QDialogButtonBox.ResetRole) filesbutton.setToolTip(wraptip("Add the names of files in a directory as nodes. Useful for uploading files in the Generic module or for importing data you downloaded before. The filenames are URIs an can be processed like any API or website.")) loadbutton = buttons.addButton("Load CSV", QDialogButtonBox.ResetRole) loadbutton.setToolTip(wraptip("Import nodes from a csv file. Use semicolon as seperator. The first column becomes the Object ID, all columns are added to the data view as key value pairs.")) layout.addWidget(buttons) dialog.setLayout(layout) self.progressUpdate = datetime.now() def createNodes(): newnodes = [node.strip() for node in input.toPlainText().splitlines()] self.apiActions.addNodes(newnodes) dialog.close() def updateProgress(): if datetime.now() >= self.progressUpdate: self.progressUpdate = datetime.now() + timedelta(milliseconds=50) QApplication.processEvents() return True def loadCSV(): datadir = os.path.dirname(self.mainWindow.settings.value('lastpath', '')) datadir = os.path.expanduser('~') if datadir == '' else datadir filename, filetype = QFileDialog.getOpenFileName(dialog, "Load CSV", datadir, "CSV files (*.csv)") if filename != "": self.apiActions.addCsv(filename,updateProgress) dialog.close() def loadFilenames(): datadir = os.path.dirname(self.mainWindow.settings.value('lastpath', '')) datadir = os.path.expanduser('~') if datadir == '' else datadir filenames, filter = QFileDialog.getOpenFileNames(dialog, "Add filenames", datadir) for filename in filenames: #with open(filename, encoding="UTF-8-sig") as file: data = {} data['fileurl'] = 'file:' + pathname2url(filename) data['filename'] = os.path.basename(filename) data['filepath'] = filename self.mainWindow.tree.treemodel.addSeedNodes([data]) self.mainWindow.tree.selectLastRow() dialog.close() self.mainWindow.tree.selectLastRow() dialog.close() def close(): dialog.close() #connect the nested functions above to the dialog-buttons buttons.accepted.connect(createNodes) buttons.rejected.connect(close) loadbutton.clicked.connect(loadCSV) filesbutton.clicked.connect(loadFilenames) dialog.exec_() @Slot() def showColumns(self): cols = self.mainWindow.fieldList.toPlainText().splitlines() cols = [x.strip() for x in cols] self.mainWindow.tree.treemodel.setCustomColumns(cols) @Slot() def clearColumns(self): self.mainWindow.fieldList.clear() self.mainWindow.tree.treemodel.setCustomColumns([]) @Slot() def addColumn(self): key = self.mainWindow.detailTree.selectedKey() if key != '': self.mainWindow.fieldList.append(key) self.mainWindow.tree.treemodel.setCustomColumns(self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def addAllColumns(self): progress = ProgressBar("Analyzing data...", self.mainWindow) columns = self.mainWindow.fieldList.toPlainText().splitlines() try: indexes = self.mainWindow.tree.selectedIndexesAndChildren() indexes = list(indexes) progress.setMaximum(len(indexes)) for no in range(len(indexes)): progress.step() item = indexes[no].internalPointer() columns.extend([key for key in recursiveIterKeys(item.data['response']) if not key in columns]) if progress.wasCanceled: break finally: self.mainWindow.fieldList.setPlainText("\n".join(columns)) self.mainWindow.tree.treemodel.setCustomColumns(columns) progress.close() @Slot() def openPresets(self): self.mainWindow.presetWindow.showPresets() @Slot() def loadAPIs(self): self.mainWindow.apiWindow.showWindow() @Slot() def jsonCopy(self): self.mainWindow.detailTree.copyToClipboard() @Slot() def unpackList(self): key = self.mainWindow.detailTree.selectedKey() self.mainWindow.dataWindow.showValue(key) @Slot() def duplicateNodes(self): self.mainWindow.transferWindow.show() @Slot() def showFieldDoc(self): tree = self.mainWindow.detailTree key = tree.selectedKey() if key == '': return False key = tree.treemodel.fieldprefix + key if tree.treemodel.itemtype is not None: self.mainWindow.apiWindow.showDoc(tree.treemodel.module, tree.treemodel.basepath, tree.treemodel.path, key) @Slot() def expandAll(self): self.mainWindow.tree.expandAll() @Slot() def collapseAll(self): self.mainWindow.tree.collapseAll() @Slot() def selectNodes(self): self.mainWindow.selectNodesWindow.showWindow() def getQueryOptions(self, apimodule=False, options=None): if isinstance(apimodule, str): apimodule = self.mainWindow.getModule(apimodule) if apimodule == False: apimodule = self.mainWindow.RequestTabs.currentWidget() # Get global options globaloptions = apimodule.getGlobalOptions() apimodule.getProxies(True) # Get module option if options is None: options = apimodule.getSettings() else: options = options.copy() options.update(globaloptions) return (apimodule, options) def getIndexes(self, options= {}, indexes=None, progress=None): # Get selected nodes if indexes is None: objecttypes = self.mainWindow.typesEdit.text().replace(' ', '').split(',') level = self.mainWindow.levelEdit.value() - 1 select_all = options['allnodes'] select_filter = {'level': level, '!objecttype': objecttypes} conditions = {'filter': select_filter, 'selectall': select_all, 'options': options} self.progressUpdate = datetime.now() def updateProgress(current, total, level=0): if not progress: return True if datetime.now() >= self.progressUpdate: progress.showInfo('input', "Adding nodes to queue ({}/{}).".format(current, total)) QApplication.processEvents() self.progressUpdate = datetime.now() + timedelta(milliseconds=60) return not progress.wasCanceled indexes = self.mainWindow.tree.selectedIndexesAndChildren(conditions, updateProgress) elif isinstance(indexes, list): indexes = iter(indexes) return indexes # Copy node data and options def prepareJob(self, index, options): treenode = index.internalPointer() node_data = deepcopy(treenode.data) node_options = deepcopy(options) node_options['lastdata'] = treenode.lastdata if hasattr(treenode, 'lastdata') else None job = {'nodeindex': index, 'nodedata': node_data, 'options': node_options} return job @Slot() def querySelectedNodes(self): modifiers = QApplication.keyboardModifiers() if modifiers == Qt.ControlModifier: self.openBrowser() else: self.apiActions.fetchData() @Slot() def setupTimer(self): # Get data level = self.mainWindow.levelEdit.value() - 1 objecttypes = self.mainWindow.typesEdit.text().replace(' ', '').split(',') conditions = {'persistent': True, 'filter': { 'level': level, '!objecttype': objecttypes } } indexes = self.mainWindow.tree.selectedIndexesAndChildren(conditions) module = self.mainWindow.RequestTabs.currentWidget() options = module.getSettings() pipeline = [{'module': module, 'options': options}] # Show timer window self.mainWindow.timerWindow.setupTimer({'indexes': list(indexes), 'pipeline': pipeline}) @Slot() def timerStarted(self, time): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired at " + time.toString("d MMM yyyy - hh:mm") + " ") @Slot() def timerStopped(self): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:black;}") self.mainWindow.timerStatus.setText("Timer stopped ") @Slot() def timerCountdown(self, countdown): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired in " + str(countdown) + " seconds ") @Slot() def timerFired(self, data): self.mainWindow.timerStatus.setText("Timer fired ") self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") pipeline = data.get('pipeline',[]) indexes = data.get('indexes',[]) for preset in pipeline: module = preset.get('module') options = preset.get('options') self.apiActions.fetchData(indexes, module, options) break @Slot() def openSettings(self): dialog = QDialog(self.mainWindow) dialog.setWindowTitle("More settings") layout = QVBoxLayout() dialog.setLayout(layout) layout.addWidget(self.mainWindow.settingsWidget) buttons = QDialogButtonBox(QDialogButtonBox.Ok) layout.addWidget(buttons) buttons.accepted.connect(dialog.close) dialog.exec_() @Slot() def openBrowser(self, indexes=None, apimodule=False, options=None): # Check seed nodes if not (self.mainWindow.tree.selectedCount() or self.mainWindow.allnodesCheckbox.isChecked() or ( indexes is not None)): return False # Get options apimodule, options = self.apiActions.getQueryOptions(apimodule, options) if not apimodule.auth_userauthorized and apimodule.auth_preregistered: msg = 'You are not authorized, login please!' QMessageBox.critical(self.mainWindow, "Not authorized",msg,QMessageBox.StandardButton.Ok) return False # Get seed nodes indexes = self.apiActions.getIndexes(options, indexes) index = next(indexes, False) if not index or not index.isValid(): return False # Prepare job job = self.apiActions.prepareJob(index, options) # Open browser def logData(data, options, headers): data = sliceData(data, headers, options) # Add data treeindex = job['nodeindex'] treenode = treeindex.internalPointer() newcount = treenode.appendNodes(data, options, False) if options.get('expand', False): self.mainWindow.tree.setExpanded(treeindex, True) apimodule.captureData(job['nodedata'], job['options'], logData, self.mainWindow.logmessage, logProgress=None) @Slot() def treeNodeSelected(self, current): #show details self.mainWindow.detailTree.clear() if current.isValid(): item = current.internalPointer() self.mainWindow.detailTree.showDict(item.data['response'],item.data['querytype'], item.data['queryparams']) # update preview in extract data window if self.mainWindow.dataWindow.isVisible(): self.mainWindow.dataWindow.updateNode(current) # update node level in duplicate nodes window if self.mainWindow.transferWindow.isVisible(): self.mainWindow.transferWindow.updateNode(current) #select level level = 0 c = current while c.isValid(): level += 1 c = c.parent() self.mainWindow.levelEdit.setValue(level) #show node count selcount = self.mainWindow.tree.selectedCount() self.mainWindow.selectionStatus.setText(str(selcount) + ' node(s) selected ') self.actionQuery.setDisabled(selcount == 0)
class GuiLogger(ConsoleLogger): """ Logging service in GUI mode. """ def __init__(self): super().__init__() srv = Services.getService("MainWindow") self.dockWidget = srv.newDockWidget( "Log", parent=None, defaultArea=Qt.BottomDockWidgetArea, allowedArea=Qt.LeftDockWidgetArea | Qt.BottomDockWidgetArea) self.logWidget = LogView() self.dockWidget.setWidget(self.logWidget) logMenu = srv.menuBar().addMenu("&Log") mainLogger = logging.getLogger() self.handler = LogHandler(self.logWidget) mainLogger.addHandler(self.handler) self.logWidget.destroyed.connect( lambda: mainLogger.removeHandler(self.handler)) self.actFollow = QAction("Follow") self.actFollow.setCheckable(True) self.actFollow.setChecked(True) self.actClear = QAction("Clear") self.actSingleLine = QAction("Single Line") self.actSingleLine.setCheckable(True) self.actSingleLine.setChecked(True) self.logWidget.setUniformRowHeights(True) self.actFollow.toggled.connect(self.logWidget.setFollow) self.actClear.triggered.connect(self.logWidget.clear) self.actSingleLine.toggled.connect(self.logWidget.setUniformRowHeights) self.actDisable = QAction("Disable") self.actDisable.triggered.connect(self.setLogLevel) self.actGroup = QActionGroup(self) self.actGroup.setExclusive(True) levelno = mainLogger.level self.loglevelMap = {} for lv in ["INTERNAL", "DEBUG", "INFO", "WARNING", "ERROR"]: a = QAction(lv[:1] + lv[1:].lower()) a.setCheckable(True) loglevel = getattr(logging, lv) self.loglevelMap[a] = loglevel setattr(self, "setLogLevel_" + lv, self.setLogLevel) a.triggered.connect(getattr(self, "setLogLevel_" + lv)) self.actGroup.addAction(a) if levelno == loglevel: a.setChecked(True) else: a.setChecked(False) logMenu.addAction(a) self.loglevelMap[self.actDisable] = 100 logMenu.addAction(self.actDisable) logMenu.addSeparator() logMenu.addAction(self.actClear) logMenu.addAction(self.actFollow) logMenu.addAction(self.actSingleLine) def setLogLevel(self): """ Sets the current log level from the calling action. :return: None """ lv = self.loglevelMap[self.sender()] logging.getLogger().setLevel(lv)
class MainWindow(QMainWindow, Ui_LedgerMainWindow): def __init__(self, db, own_path, language): QMainWindow.__init__(self, None) self.setupUi(self) self.db = db self.own_path = own_path self.currentLanguage = language self.ledger = Ledger(self.db) self.downloader = QuoteDownloader(self.db) self.downloader.download_completed.connect( self.onQuotesDownloadCompletion) self.taxes = TaxesRus(self.db) self.statements = StatementLoader(self, self.db) self.statements.load_completed.connect(self.onStatementLoaded) self.statements.load_failed.connect(self.onStatementLoadFailure) self.actionImportSlipRU.setEnabled( dependency_present(['pyzbar', 'PIL'])) # Customize Status bar and logs self.NewLogEventLbl = QLabel(self) self.StatusBar.addPermanentWidget(VLine()) self.StatusBar.addPermanentWidget(self.NewLogEventLbl) self.Logs.setNotificationLabel(self.NewLogEventLbl) self.Logs.setFormatter( logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) self.logger = logging.getLogger() self.logger.addHandler(self.Logs) self.logger.setLevel(logging.INFO) # Setup reports tab self.ReportAccountBtn.init_db(self.db) self.ReportCategoryEdit.init_db(self.db) self.reports = Reports(self.db, self.ReportTableView) self.reports.report_failure.connect(self.onReportFailure) # Customize UI configuration self.operations = LedgerOperationsView(self.OperationsTableView) self.ui_config = TableViewConfig(self) self.ui_config.configure_all() self.operation_details = { TransactionType.Action: (g_tr('TableViewConfig', "Income / Spending"), self.ui_config.mappers[self.ui_config.ACTIONS], 'actions', self.ActionDetailsTableView, 'action_details', LedgerInitValues[TransactionType.Action]), TransactionType.Trade: (g_tr('TableViewConfig', "Trade"), self.ui_config.mappers[self.ui_config.TRADES], 'trades', None, None, LedgerInitValues[TransactionType.Trade]), TransactionType.Dividend: (g_tr('TableViewConfig', "Dividend"), self.ui_config.mappers[self.ui_config.DIVIDENDS], 'dividends', None, None, LedgerInitValues[TransactionType.Dividend]), TransactionType.Transfer: (g_tr('TableViewConfig', "Transfer"), self.ui_config.mappers[self.ui_config.TRANSFERS], 'transfers_combined', None, None, LedgerInitValues[TransactionType.Transfer]) } self.operations.setOperationsDetails(self.operation_details) self.operations.activateOperationView.connect(self.ShowOperationTab) self.operations.stateIsCommitted.connect(self.showCommitted) self.operations.stateIsModified.connect(self.showModified) # Setup balance and holdings tables self.ledger.setViews(self.BalancesTableView, self.HoldingsTableView) self.BalanceDate.setDateTime(QDateTime.currentDateTime()) self.BalancesCurrencyCombo.init_db( self.db ) # this line will trigger onBalanceDateChange -> view updated self.HoldingsDate.setDateTime(QDateTime.currentDateTime()) self.HoldingsCurrencyCombo.init_db( self.db ) # and this will trigger onHoldingsDateChange -> view updated # Create menu for different operations self.ChooseAccountBtn.init_db(self.db) self.NewOperationMenu = QMenu() for operation in self.operation_details: self.NewOperationMenu.addAction( self.operation_details[operation][ LedgerOperationsView.OP_NAME], partial(self.operations.addNewOperation, operation)) self.NewOperationBtn.setMenu(self.NewOperationMenu) self.ActionDetailsTableView.horizontalHeader().moveSection( self.ActionDetailsTableView.model().fieldIndex("note"), self.ActionDetailsTableView.model().fieldIndex("name")) self.langGroup = QActionGroup(self.menuLanguage) self.createLanguageMenu() self.langGroup.triggered.connect(self.onLanguageChanged) self.OperationsTableView.selectRow( 0) # TODO find a way to select last row from self.operations self.OnOperationsRangeChange(0) @Slot() def closeEvent(self, event): self.logger.removeHandler( self.Logs ) # Removing handler (but it doesn't prevent exception at exit) logging.raiseExceptions = False # Silencing logging module exceptions self.db.close() # Closing database file def createLanguageMenu(self): langPath = self.own_path + "languages" + os.sep langDirectory = QDir(langPath) for language_file in langDirectory.entryList(['*.qm']): language_code = language_file.split('.')[0] language = QLocale.languageToString( QLocale(language_code).language()) language_icon = QIcon(langPath + language_code + '.png') action = QAction(language_icon, language, self) action.setCheckable(True) action.setData(language_code) self.menuLanguage.addAction(action) self.langGroup.addAction(action) @Slot() def onLanguageChanged(self, action): language_code = action.data() if language_code != self.currentLanguage: executeSQL( self.db, "UPDATE settings " "SET value=(SELECT id FROM languages WHERE language = :new_language) WHERE name ='Language'", [(':new_language', language_code)]) QMessageBox().information( self, g_tr('MainWindow', "Restart required"), g_tr('MainWindow', "Language was changed to ") + QLocale.languageToString(QLocale(language_code).language()) + "\n" + g_tr( 'MainWindow', "You should restart application to apply changes\n" "Application will be terminated now"), QMessageBox.Ok) self.close() def Backup(self): backup_directory = QFileDialog.getExistingDirectory( self, g_tr('MainWindow', "Select directory to save backup")) if backup_directory: MakeBackup(get_dbfilename(self.own_path), backup_directory) def Restore(self): restore_directory = QFileDialog.getExistingDirectory( self, g_tr('MainWindow', "Select directory to restore from")) if restore_directory: self.db.close() RestoreBackup(get_dbfilename(self.own_path), restore_directory) QMessageBox().information( self, g_tr('MainWindow', "Data restored"), g_tr('MainWindow', "Database was loaded from the backup.\n") + g_tr( 'MainWindow', "You should restart application to apply changes\n" "Application will be terminated now"), QMessageBox.Ok) self.close() @Slot() def onBalanceDateChange(self, _new_date): self.ledger.setBalancesDate( self.BalanceDate.dateTime().toSecsSinceEpoch()) @Slot() def onHoldingsDateChange(self, _new_date): self.ledger.setHoldingsDate( self.HoldingsDate.dateTime().toSecsSinceEpoch()) @Slot() def OnBalanceCurrencyChange(self, _currency_index): self.ledger.setBalancesCurrency( self.BalancesCurrencyCombo.selected_currency(), self.BalancesCurrencyCombo.selected_currency_name()) @Slot() def OnHoldingsCurrencyChange(self, _currency_index): self.ledger.setHoldingsCurrency( self.HoldingsCurrencyCombo.selected_currency(), self.HoldingsCurrencyCombo.selected_currency_name()) @Slot() def OnBalanceInactiveChange(self, state): if state == 0: self.ledger.setActiveBalancesOnly(1) else: self.ledger.setActiveBalancesOnly(0) @Slot() def onReportRangeChange(self, range_index): report_ranges = { 0: lambda: (0, 0), 1: ManipulateDate.Last3Months, 2: ManipulateDate.RangeYTD, 3: ManipulateDate.RangeThisYear, 4: ManipulateDate.RangePreviousYear } begin, end = report_ranges[range_index]() self.ReportFromDate.setDateTime(QDateTime.fromSecsSinceEpoch(begin)) self.ReportToDate.setDateTime(QDateTime.fromSecsSinceEpoch(end)) @Slot() def onRunReport(self): types = { 0: ReportType.IncomeSpending, 1: ReportType.ProfitLoss, 2: ReportType.Deals, 3: ReportType.ByCategory } report_type = types[self.ReportTypeCombo.currentIndex()] begin = self.ReportFromDate.dateTime().toSecsSinceEpoch() end = self.ReportToDate.dateTime().toSecsSinceEpoch() group_dates = 1 if self.ReportGroupCheck.isChecked() else 0 if report_type == ReportType.ByCategory: self.reports.runReport(report_type, begin, end, self.ReportCategoryEdit.selected_id, group_dates) else: self.reports.runReport(report_type, begin, end, self.ReportAccountBtn.account_id, group_dates) @Slot() def onReportFailure(self, error_msg): self.StatusBar.showMessage(error_msg, timeout=30000) @Slot() def OnSearchTextChange(self): self.operations.setSearchText(self.SearchString.text()) @Slot() def OnOperationsRangeChange(self, range_index): view_ranges = { 0: ManipulateDate.startOfPreviousWeek, 1: ManipulateDate.startOfPreviousMonth, 2: ManipulateDate.startOfPreviousQuarter, 3: ManipulateDate.startOfPreviousYear, 4: lambda: 0 } self.operations.setOperationsRange(view_ranges[range_index]()) @Slot() def onQuotesDownloadCompletion(self): self.StatusBar.showMessage(g_tr('MainWindow', "Quotes download completed"), timeout=60000) self.ledger.updateBalancesView() self.ledger.updateBalancesView() @Slot() def onStatementLoaded(self): self.StatusBar.showMessage(g_tr('MainWindow', "Statement load completed"), timeout=60000) self.ledger.rebuild() @Slot() def onStatementLoadFailure(self): self.StatusBar.showMessage(g_tr('MainWindow', "Statement load failed"), timeout=60000) @Slot() def ShowOperationTab(self, operation_type): tab_list = { TransactionType.NA: 0, TransactionType.Action: 1, TransactionType.Transfer: 4, TransactionType.Trade: 2, TransactionType.Dividend: 3 } self.OperationsTabs.setCurrentIndex(tab_list[operation_type]) @Slot() def showCommitted(self): self.ledger.rebuild() self.SaveOperationBtn.setEnabled(False) self.RevertOperationBtn.setEnabled(False) @Slot() def showModified(self): self.SaveOperationBtn.setEnabled(True) self.RevertOperationBtn.setEnabled(True) @Slot() def importSlip(self): dialog = ImportSlipDialog(self, self.db) dialog.show()
def __initModeMenu(self): modeMenuGroup = QActionGroup(self) modeMenuGroup.addAction(self.ui.actionDigraph_Mode) modeMenuGroup.addAction(self.ui.actionRedigraph_Mode)
class ProfileManager(QObject): profile_changed = Signal(Profile) def __init__(self, menu, parent=None): super().__init__(parent) self.menu = menu actions = self.menu.actions() self.sep = actions[-2] self.parent = parent self.profiles = [] self.actGroup = None self.active_profile = None self.load() QApplication.instance().aboutToQuit.connect(self.save) def load(self): settings = QSettings() settings.beginGroup('profiles') groups = settings.childGroups() self.profiles = [ Profile(p, settings.value(f'{p}/path'), settings.value(f'{p}/mask'), settings.value(f'{p}/pattern')) for p in groups ] settings.endGroup() self.actGroup = QActionGroup(self.parent) self.actGroup.triggered.connect(self.set_active_profile) active = settings.value('active_profile') self.active_profile = self.get_profile(active) if len(self.profiles) > 0: for name in self.names(): action = self.do_add_action(name) if name == active: action.setChecked(True) def save(self): settings = QSettings() settings.beginGroup('profiles') settings.remove('') for p in self.profiles: settings.setValue(f'{p.name}/path', p.path) settings.setValue(f'{p.name}/mask', p.mask) settings.setValue(f'{p.name}/pattern', p.pattern) settings.endGroup() if self.active_profile is not None: settings.setValue('active_profile', self.active_profile.name) def names(self): for p in self.profiles: yield p.name def add_action(self, name, path, mask, pattern): if name in self.names(): app = QApplication.instance() QMessageBox.warning( self.parent, app.applicationName(), app.translate('profile_manager', '{} already exists').format(name)) else: self.profiles.append(Profile(name, path, mask, pattern)) self.do_add_action(name) def do_add_action(self, name): action = QAction(name, self.menu) self.menu.insertAction(self.sep, action) action.setCheckable(True) self.actGroup.addAction(action) return action def add_from_dialog(self, dialog): self.add_action(dialog.get_name(), dialog.get_path(), dialog.get_mask(), dialog.get_pattern()) def get_profile(self, name): for p in self.profiles: if name == p.name: return p return None def set_active_profile(self): action = self.actGroup.checkedAction() self.active_profile = self.get_profile(action.text()) if action \ is not None else None self.profile_changed.emit(self.active_profile) def reset_profiles(self, profiles): self.clear_menu() self.profiles = profiles if len(profiles) > 0: if self.active_profile.name not in (p.name for p in profiles): active = profiles[0].name else: active = self.active_profile.name for name in self.names(): action = self.do_add_action(name) if name == active: action.setChecked(True) else: active = None self.active_profile = self.get_profile(active) self.profile_changed.emit(self.active_profile) def clear_menu(self): while len(self.actGroup.actions()) > 0: self.actGroup.removeAction(self.actGroup.actions()[0])
class Actions(object): def __init__(self, mainWindow): self.mainWindow = mainWindow #Basic actions self.basicActions = QActionGroup(self.mainWindow) self.actionOpen = self.basicActions.addAction( QIcon(":/icons/save.png"), "Open Database") self.actionOpen.triggered.connect(self.openDB) self.actionNew = self.basicActions.addAction(QIcon(":/icons/new.png"), "New Database") self.actionNew.triggered.connect(self.makeDB) #Database actions self.databaseActions = QActionGroup(self.mainWindow) self.actionExport = self.databaseActions.addAction( QIcon(":/icons/export.png"), "Export Data") self.actionExport.setToolTip( wraptip( "Export selected node(s) and their children to a .csv file. \n If no or all node(s) are selected inside the data-view, a complete export of all data in the DB is performed" )) self.actionExport.triggered.connect(self.exportNodes) self.actionAdd = self.databaseActions.addAction( QIcon(":/icons/add.png"), "Add Nodes") self.actionAdd.setToolTip( wraptip( "Add new node(s) as a starting point for further data collection" )) self.actionAdd.triggered.connect(self.addNodes) self.actionDelete = self.databaseActions.addAction( QIcon(":/icons/delete.png"), "Delete Nodes") self.actionDelete.setToolTip( wraptip("Delete nodes(s) and their children")) self.actionDelete.triggered.connect(self.deleteNodes) #Data actions self.dataActions = QActionGroup(self.mainWindow) self.actionQuery = self.dataActions.addAction( QIcon(":/icons/fetch.png"), "Query") self.actionQuery.triggered.connect(self.querySelectedNodes) self.actionSettings = self.dataActions.addAction( QIcon(":/icons/more.png"), "More settings") self.actionSettings.setToolTip( wraptip( "Can't get enough? Here you will find even more settings.")) self.actionSettings.triggered.connect(self.openSettings) self.actionBrowse = self.dataActions.addAction( QIcon(":/icons/browser.png"), "Open") self.actionBrowse.setToolTip( wraptip("Open the resulting URL in the browser.")) self.actionBrowse.triggered.connect(self.openBrowser) self.actionTimer = self.dataActions.addAction( QIcon(":/icons/fetch.png"), "Time") self.actionTimer.setToolTip( wraptip( "Time your data collection with a timer. Fetches the data for the selected node(s) in user-defined intervalls" )) self.actionTimer.triggered.connect(self.setupTimer) self.actionHelp = self.dataActions.addAction(QIcon(":/icons/help.png"), "Help") self.actionHelp.triggered.connect(self.help) self.actionLoadPreset = self.dataActions.addAction( QIcon(":/icons/presets.png"), "Presets") self.actionLoadPreset.triggered.connect(self.loadPreset) self.actionLoadAPIs = self.dataActions.addAction( QIcon(":/icons/apis.png"), "APIs") self.actionLoadAPIs.triggered.connect(self.loadAPIs) #Detail actions self.detailActions = QActionGroup(self.mainWindow) self.actionAddColumn = self.detailActions.addAction( QIcon(":/icons/addcolumn.png"), "Add Column") self.actionAddColumn.setToolTip( wraptip("Add the current JSON-key as a column in the data view")) self.actionAddColumn.triggered.connect(self.addColumn) self.actionAddAllolumns = self.detailActions.addAction( QIcon(":/icons/addcolumn.png"), "Add All Columns") self.actionAddAllolumns.setToolTip( wraptip( "Analyzes all selected nodes in the data view and adds all found keys as columns" )) self.actionAddAllolumns.triggered.connect(self.addAllColumns) self.actionUnpack = self.detailActions.addAction( QIcon(":/icons/unpack.png"), "Extract Data") self.actionUnpack.setToolTip(wraptip("Extract new nodes from the data using keys." \ "You can pipe the value to css selectors (e.g. text|div.main)" \ "or xpath selectors (e.g. text|//div[@class='main']/text()")) self.actionUnpack.triggered.connect(self.unpackList) self.actionJsonCopy = self.detailActions.addAction( QIcon(":/icons/toclip.png"), "Copy JSON to Clipboard") self.actionJsonCopy.setToolTip( wraptip("Copy the selected JSON-data to the clipboard")) self.actionJsonCopy.triggered.connect(self.jsonCopy) self.actionFieldDoc = self.detailActions.addAction( QIcon(":/icons/help.png"), "") self.actionFieldDoc.setToolTip( wraptip( "Open the documentation for the selected item if available.")) self.actionFieldDoc.triggered.connect(self.showFieldDoc) # Column setup actions self.columnActions = QActionGroup(self.mainWindow) self.actionShowColumns = self.columnActions.addAction( QIcon(":/icons/apply.png"), "Apply Column Setup") self.actionShowColumns.setToolTip( wraptip(("Show the columns in the central data view. " + "Scroll right or left to see hidden columns."))) self.actionShowColumns.triggered.connect(self.showColumns) self.actionClearColumns = self.columnActions.addAction( QIcon(":/icons/clear.png"), "Clear Columns") self.actionClearColumns.setToolTip( wraptip("Remove all columns to get space for a new setup.")) self.actionClearColumns.triggered.connect(self.clearColumns) #Tree actions self.treeActions = QActionGroup(self.mainWindow) self.actionExpandAll = self.treeActions.addAction( QIcon(":/icons/expand.png"), "Expand nodes") self.actionExpandAll.triggered.connect(self.expandAll) self.actionCollapseAll = self.treeActions.addAction( QIcon(":/icons/collapse.png"), "Collapse nodes") self.actionCollapseAll.triggered.connect(self.collapseAll) self.actionFind = self.treeActions.addAction( QIcon(":/icons/search.png"), "Find nodes") self.actionFind.triggered.connect(self.selectNodes) #self.actionSelectNodes=self.treeActions.addAction(QIcon(":/icons/collapse.png"),"Select nodes") #self.actionSelectNodes.triggered.connect(self.selectNodes) self.actionClipboard = self.treeActions.addAction( QIcon(":/icons/toclip.png"), "Copy Node(s) to Clipboard") self.actionClipboard.setToolTip( wraptip("Copy the selected nodes(s) to the clipboard")) self.actionClipboard.triggered.connect(self.clipboardNodes) @Slot() def help(self): self.mainWindow.helpwindow.show() @Slot() def openDB(self): #open a file dialog with a .db filter datadir = self.mainWindow.database.filename if not os.path.exists(datadir): datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) if not os.path.exists(datadir): datadir = os.path.expanduser("~") datadir = os.path.dirname(datadir) fldg = QFileDialog(caption="Open DB File", directory=datadir, filter="DB files (*.db)") fldg.setFileMode(QFileDialog.ExistingFile) if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.connect(fldg.selectedFiles()[0]) self.mainWindow.updateUI() self.mainWindow.tree.loadData(self.mainWindow.database) self.mainWindow.actions.actionShowColumns.trigger() @Slot() def makeDB(self): datadir = self.mainWindow.database.filename if not os.path.exists(datadir): datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) if not os.path.exists(datadir): datadir = os.path.expanduser("~") datadir = os.path.dirname(datadir) fldg = QFileDialog(caption="Save DB File", directory=datadir, filter="DB files (*.db)") fldg.setAcceptMode(QFileDialog.AcceptSave) fldg.setDefaultSuffix("db") if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.createconnect(fldg.selectedFiles()[0]) self.mainWindow.updateUI() @Slot() def deleteNodes(self): reply = QMessageBox.question( self.mainWindow, 'Delete Nodes', "Are you sure to delete all selected nodes?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return progress = ProgressBar("Deleting data...", self.mainWindow) self.mainWindow.tree.setUpdatesEnabled(False) try: todo = self.mainWindow.tree.selectedIndexesAndChildren( {'persistent': True}) todo = list(todo) progress.setMaximum(len(todo)) for index in todo: progress.step() self.mainWindow.tree.treemodel.deleteNode(index, delaycommit=True) if progress.wasCanceled: break finally: # commit the operation on the db-layer afterwards (delaycommit is True) self.mainWindow.tree.treemodel.commitNewNodes() self.mainWindow.tree.setUpdatesEnabled(True) progress.close() @Slot() def clipboardNodes(self): progress = ProgressBar("Copy to clipboard", self.mainWindow) indexes = self.mainWindow.tree.selectionModel().selectedRows() progress.setMaximum(len(indexes)) output = io.StringIO() try: writer = csv.writer(output, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL, doublequote=True, lineterminator='\r\n') #headers row = [ str(val) for val in self.mainWindow.tree.treemodel.getRowHeader() ] writer.writerow(row) #rows for no in range(len(indexes)): if progress.wasCanceled: break row = [ str(val) for val in self.mainWindow.tree.treemodel.getRowData(indexes[no]) ] writer.writerow(row) progress.step() clipboard = QApplication.clipboard() clipboard.setText(output.getvalue()) finally: output.close() progress.close() @Slot() def exportNodes(self): fldg = ExportFileDialog(self.mainWindow, filter="CSV Files (*.csv)") @Slot() def addNodes(self): if not self.mainWindow.database.connected: return False # makes the user add a new facebook object into the db dialog = QDialog(self.mainWindow) dialog.setWindowTitle("Add Nodes") layout = QVBoxLayout() label = QLabel("One <b>Object ID</b> per line") layout.addWidget(label) input = QPlainTextEdit() input.setMinimumWidth(500) input.LineWrapMode = QPlainTextEdit.NoWrap #input.acceptRichText=False input.setFocus() layout.addWidget(input) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) filesbutton = buttons.addButton("Add files", QDialogButtonBox.ResetRole) loadbutton = buttons.addButton("Load CSV", QDialogButtonBox.ResetRole) layout.addWidget(buttons) dialog.setLayout(layout) self.progressUpdate = datetime.now() def createNodes(): newnodes = [ node.strip() for node in input.toPlainText().splitlines() ] self.mainWindow.tree.treemodel.addSeedNodes(newnodes, True) self.mainWindow.tree.selectLastRow() dialog.close() def updateProgress(): if datetime.now() >= self.progressUpdate: self.progressUpdate = datetime.now() + timedelta( milliseconds=50) QApplication.processEvents() return True def loadCSV(): datadir = os.path.dirname( self.mainWindow.settings.value('lastpath', '')) datadir = os.path.expanduser('~') if datadir == '' else datadir filename, filetype = QFileDialog.getOpenFileName( dialog, "Load CSV", datadir, "CSV files (*.csv)") if filename != "": dialog.close() progress = ProgressBar("Adding nodes...", self.mainWindow) try: with open(filename, encoding="UTF-8-sig") as csvfile: rows = csv.DictReader(csvfile, delimiter=';', quotechar='"', doublequote=True) #rows = [row for row in csvreader] self.mainWindow.tree.treemodel.addSeedNodes( rows, progress=updateProgress) self.mainWindow.tree.selectLastRow() dialog.close() self.mainWindow.tree.selectLastRow() finally: progress.close() def loadFilenames(): datadir = os.path.dirname( self.mainWindow.settings.value('lastpath', '')) datadir = os.path.expanduser('~') if datadir == '' else datadir filenames, filter = QFileDialog.getOpenFileNames( dialog, "Add filenames", datadir) for filename in filenames: #with open(filename, encoding="UTF-8-sig") as file: data = {} data['fileurl'] = 'file:' + pathname2url(filename) data['filename'] = os.path.basename(filename) data['filepath'] = filename self.mainWindow.tree.treemodel.addSeedNodes([data]) self.mainWindow.tree.selectLastRow() dialog.close() self.mainWindow.tree.selectLastRow() dialog.close() def close(): dialog.close() #connect the nested functions above to the dialog-buttons buttons.accepted.connect(createNodes) buttons.rejected.connect(close) loadbutton.clicked.connect(loadCSV) filesbutton.clicked.connect(loadFilenames) dialog.exec_() @Slot() def showColumns(self): self.mainWindow.tree.treemodel.setCustomColumns( self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def clearColumns(self): self.mainWindow.fieldList.clear() self.mainWindow.tree.treemodel.setCustomColumns([]) @Slot() def addColumn(self): key = self.mainWindow.detailTree.selectedKey() if key != '': self.mainWindow.fieldList.append(key) self.mainWindow.tree.treemodel.setCustomColumns( self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def addAllColumns(self): progress = ProgressBar("Analyzing data...", self.mainWindow) columns = self.mainWindow.fieldList.toPlainText().splitlines() try: indexes = self.mainWindow.tree.selectedIndexesAndChildren() indexes = list(indexes) progress.setMaximum(len(indexes)) for no in range(len(indexes)): progress.step() item = indexes[no].internalPointer() columns.extend([ key for key in recursiveIterKeys(item.data['response']) if not key in columns ]) if progress.wasCanceled: break finally: self.mainWindow.fieldList.setPlainText("\n".join(columns)) self.mainWindow.tree.treemodel.setCustomColumns(columns) progress.close() @Slot() def loadPreset(self): self.mainWindow.presetWindow.showPresets() @Slot() def loadAPIs(self): self.mainWindow.apiWindow.showWindow() @Slot() def jsonCopy(self): self.mainWindow.detailTree.copyToClipboard() @Slot() def unpackList(self): key = self.mainWindow.detailTree.selectedKey() self.mainWindow.dataWindow.showValue(key) @Slot() def showFieldDoc(self): tree = self.mainWindow.detailTree key = tree.selectedKey() if key == '': return False key = tree.treemodel.fieldprefix + key if tree.treemodel.itemtype is not None: self.mainWindow.apiWindow.showDoc(tree.treemodel.module, tree.treemodel.basepath, tree.treemodel.path, key) @Slot() def expandAll(self): self.mainWindow.tree.expandAll() @Slot() def collapseAll(self): self.mainWindow.tree.collapseAll() @Slot() def selectNodes(self): self.mainWindow.selectNodesWindow.showWindow() def getQueryOptions(self, apimodule=False, options=None): # Get global options globaloptions = {} globaloptions['threads'] = self.mainWindow.threadsEdit.value() globaloptions['speed'] = self.mainWindow.speedEdit.value() globaloptions['errors'] = self.mainWindow.errorEdit.value() globaloptions['expand'] = self.mainWindow.autoexpandCheckbox.isChecked( ) globaloptions['logrequests'] = self.mainWindow.logCheckbox.isChecked() globaloptions[ 'saveheaders'] = self.mainWindow.headersCheckbox.isChecked() globaloptions['allnodes'] = self.mainWindow.allnodesCheckbox.isChecked( ) globaloptions['resume'] = self.mainWindow.resumeCheckbox.isChecked() # Get module option if isinstance(apimodule, str): apimodule = self.mainWindow.getModule(apimodule) if apimodule == False: apimodule = self.mainWindow.RequestTabs.currentWidget() apimodule.getProxies(True) if options is None: options = apimodule.getOptions() else: options = options.copy() options.update(globaloptions) return (apimodule, options) def getIndexes(self, options={}, indexes=None, progress=None): # Get selected nodes if indexes is None: objecttypes = self.mainWindow.typesEdit.text().replace( ' ', '').split(',') level = self.mainWindow.levelEdit.value() - 1 select_all = options['allnodes'] select_filter = {'level': level, '!objecttype': objecttypes} conditions = { 'filter': select_filter, 'selectall': select_all, 'options': options } self.progressUpdate = datetime.now() def updateProgress(current, total, level=0): if not progress: return True if datetime.now() >= self.progressUpdate: progress.showInfo( 'input', "Adding nodes to queue ({}/{}).".format( current, total)) QApplication.processEvents() self.progressUpdate = datetime.now() + timedelta( milliseconds=60) return not progress.wasCanceled indexes = self.mainWindow.tree.selectedIndexesAndChildren( conditions, updateProgress) elif isinstance(indexes, list): indexes = iter(indexes) return indexes # Copy node data and options def prepareJob(self, index, options): treenode = index.internalPointer() node_data = deepcopy(treenode.data) node_options = deepcopy(options) node_options['lastdata'] = treenode.lastdata if hasattr( treenode, 'lastdata') else None job = { 'nodeindex': index, 'nodedata': node_data, 'options': node_options } return job def queryNodes(self, indexes=None, apimodule=False, options=None): if not (self.mainWindow.tree.selectedCount() or self.mainWindow.allnodesCheckbox.isChecked() or (indexes is not None)): return False #Show progress window progress = ProgressBar("Fetching Data", parent=self.mainWindow) try: apimodule, options = self.getQueryOptions(apimodule, options) indexes = self.getIndexes(options, indexes, progress) # Update progress window self.mainWindow.logmessage("Start fetching data.") totalnodes = 0 hasindexes = True progress.setMaximum(totalnodes) self.mainWindow.tree.treemodel.nodecounter = 0 #Init status messages statuscount = defaultdict(int) errorcount = 0 ratelimitcount = 0 allowedstatus = [ 'fetched (200)', 'downloaded (200)', 'fetched (202)' ] try: #Spawn Threadpool threadpool = ApiThreadPool(apimodule) threadpool.spawnThreads(options.get("threads", 1)) #Process Logging/Input/Output Queue while True: try: #Logging (sync logs in threads with main thread) msg = threadpool.getLogMessage() if msg is not None: self.mainWindow.logmessage(msg) # Jobs in: packages of 100 at a time jobsin = 0 while hasindexes and (jobsin < 100): index = next(indexes, False) if index: jobsin += 1 totalnodes += 1 if index.isValid(): job = self.prepareJob(index, options) threadpool.addJob(job) else: threadpool.applyJobs() progress.setRemaining(threadpool.getJobCount()) progress.resetRate() hasindexes = False progress.removeInfo('input') self.mainWindow.logmessage( "Added {} node(s) to queue.".format( totalnodes)) if jobsin > 0: progress.setMaximum(totalnodes) #Jobs out job = threadpool.getJob() #-Finished all nodes (sentinel)... if job is None: break #-Finished one node... elif 'progress' in job: progresskey = 'nodeprogress' + str( job.get('threadnumber', '')) # Update single progress if 'current' in job: percent = int((job.get('current', 0) * 100.0 / job.get('total', 1))) progress.showInfo( progresskey, "{}% of current node processed.".format( percent)) elif 'page' in job: if job.get('page', 0) > 1: progress.showInfo( progresskey, "{} page(s) of current node processed." .format(job.get('page', 0))) # Update total progress else: progress.removeInfo(progresskey) if not threadpool.suspended: progress.step() #-Add data... elif 'data' in job and (not progress.wasCanceled): if not job['nodeindex'].isValid(): continue # Add data treeindex = job['nodeindex'] treenode = treeindex.internalPointer() newcount = treenode.appendNodes( job['data'], job['options'], True) if options.get('expand', False): self.mainWindow.tree.setExpanded( treeindex, True) # Count status and errors status = job['options'].get('querystatus', 'empty') statuscount[status] += 1 errorcount += int(not status in allowedstatus) # Detect rate limit ratelimit = job['options'].get('ratelimit', False) #ratelimit = ratelimit or (not newcount) ratelimitcount += int(ratelimit) autoretry = (ratelimitcount) or ( status == "request error") # Clear errors when everything is ok if not threadpool.suspended and ( status in allowedstatus) and (not ratelimit): #threadpool.clearRetry() errorcount = 0 ratelimitcount = 0 # Suspend on error or ratelimit elif (errorcount >= options['errors']) or (ratelimitcount > 0): threadpool.suspendJobs() if ratelimit: msg = "You reached the rate limit of the API." else: msg = "{} consecutive errors occurred.\nPlease check your settings.".format( errorcount) timeout = 60 * 5 # 5 minutes # Adjust progress progress.showError(msg, timeout, autoretry) self.mainWindow.tree.treemodel.commitNewNodes() # Add job for retry if not status in allowedstatus: threadpool.addError(job) # Show info progress.showInfo( status, "{} response(s) with status: {}".format( statuscount[status], status)) progress.showInfo( 'newnodes', "{} new node(s) created".format( self.mainWindow.tree.treemodel.nodecounter) ) progress.showInfo( 'threads', "{} active thread(s)".format( threadpool.getThreadCount())) progress.setRemaining(threadpool.getJobCount()) # Custom info from modules info = job['options'].get('info', {}) for name, value in info.items(): progress.showInfo(name, value) # Abort elif progress.wasCanceled: progress.showInfo( 'cancel', "Disconnecting from stream, may take some time." ) threadpool.stopJobs() # Retry elif progress.wasResumed: if progress.wasRetried: threadpool.retryJobs() else: threadpool.clearRetry() # errorcount = 0 # ratelimitcount = 0 threadpool.resumeJobs() progress.setRemaining(threadpool.getJobCount()) progress.hideError() # Continue elif not threadpool.suspended: threadpool.resumeJobs() # Finished with pending errors if not threadpool.hasJobs( ) and threadpool.hasErrorJobs(): msg = "All nodes finished but you have {} pending errors. Skip or retry?".format( threadpool.getErrorJobsCount()) autoretry = False timeout = 60 * 5 # 5 minutes progress.showError(msg, timeout, autoretry) # Finished if not threadpool.hasJobs(): progress.showInfo( 'cancel', "Work finished, shutting down threads.") threadpool.stopJobs() #-Waiting... progress.computeRate() time.sleep(1.0 / 1000.0) finally: QApplication.processEvents() finally: request_summary = [ str(val) + " x " + key for key, val in statuscount.items() ] request_summary = ", ".join(request_summary) request_end = "Fetching completed" if not progress.wasCanceled else 'Fetching cancelled by user' self.mainWindow.logmessage( "{}, {} new node(s) created. Summary of responses: {}.". format(request_end, self.mainWindow.tree.treemodel.nodecounter, request_summary)) self.mainWindow.tree.treemodel.commitNewNodes() except Exception as e: self.mainWindow.logmessage( "Error in scheduler, fetching aborted: {}.".format(str(e))) finally: progress.close() return not progress.wasCanceled @Slot() def querySelectedNodes(self): modifiers = QApplication.keyboardModifiers() if modifiers == Qt.ControlModifier: self.openBrowser() else: self.queryNodes() @Slot() def setupTimer(self): # Get data level = self.mainWindow.levelEdit.value() - 1 objecttypes = self.mainWindow.typesEdit.text().replace(' ', '').split(',') conditions = { 'persistent': True, 'filter': { 'level': level, '!objecttype': objecttypes } } indexes = self.mainWindow.tree.selectedIndexesAndChildren(conditions) module = self.mainWindow.RequestTabs.currentWidget() options = module.getOptions() pipeline = [{'module': module, 'options': options}] # Show timer window self.mainWindow.timerWindow.setupTimer({ 'indexes': list(indexes), 'pipeline': pipeline }) @Slot() def timerStarted(self, time): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText( "Timer will be fired at " + time.toString("d MMM yyyy - hh:mm") + " ") @Slot() def timerStopped(self): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:black;}") self.mainWindow.timerStatus.setText("Timer stopped ") @Slot() def timerCountdown(self, countdown): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired in " + str(countdown) + " seconds ") @Slot() def timerFired(self, data): self.mainWindow.timerStatus.setText("Timer fired ") self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") pipeline = data.get('pipeline', []) indexes = data.get('indexes', []) self.queryPipeline(pipeline, indexes) def queryPipeline(self, pipeline, indexes=None): columns = [] for preset in pipeline: # Select item in preset window item = preset.get('item') if item is not None: self.mainWindow.presetWindow.presetList.setCurrentItem(item) columns.extend(preset.get('columns', [])) module = preset.get('module') options = preset.get('options') finished = self.queryNodes(indexes, module, options) # todo: increase level of indexes instead of levelEdit if not finished or (indexes is not None): return False else: level = self.mainWindow.levelEdit.value() self.mainWindow.levelEdit.setValue(level + 1) # Set columns columns = list(dict.fromkeys(columns)) self.mainWindow.fieldList.setPlainText("\n".join(columns)) self.showColumns() @Slot() def openSettings(self): dialog = QDialog(self.mainWindow) dialog.setWindowTitle("More settings") layout = QVBoxLayout() dialog.setLayout(layout) layout.addWidget(self.mainWindow.settingsWidget) buttons = QDialogButtonBox(QDialogButtonBox.Ok) layout.addWidget(buttons) buttons.accepted.connect(dialog.close) dialog.exec_() @Slot() def openBrowser(self): apimodule, options = self.getQueryOptions() indexes = self.getIndexes(options) index = next(indexes, False) if not index or not index.isValid(): return False job = self.prepareJob(index, options) options = job['options'] nodedata = job['nodedata'] options = apimodule.initPagingOptions(nodedata, options) method, urlpath, urlparams, payload, requestheaders = apimodule.buildUrl( nodedata, options) if not urlpath: return False url = apimodule.getLogURL(urlpath, urlparams, options, False) caption = "Browser" apimodule.showBrowser(caption, url, requestheaders) @Slot() def treeNodeSelected(self, current): #show details self.mainWindow.detailTree.clear() if current.isValid(): item = current.internalPointer() self.mainWindow.detailTree.showDict(item.data['response'], item.data['querytype'], item.data['queryparams']) # update preview in extract data window if self.mainWindow.dataWindow.isVisible(): self.mainWindow.dataWindow.updateNode(current) #select level level = 0 c = current while c.isValid(): level += 1 c = c.parent() self.mainWindow.levelEdit.setValue(level) #show node count selcount = self.mainWindow.tree.selectedCount() self.mainWindow.selectionStatus.setText( str(selcount) + ' node(s) selected ') self.actionQuery.setDisabled(selcount == 0)
class Actions(object): def __init__(self, mainWindow): self.mainWindow = mainWindow #Basic actions self.basicActions = QActionGroup(self.mainWindow) self.actionOpen = self.basicActions.addAction(QIcon(":/icons/save.png"), "Open Database") self.actionOpen.triggered.connect(self.openDB) self.actionNew = self.basicActions.addAction(QIcon(":/icons/new.png"), "New Database") self.actionNew.triggered.connect(self.makeDB) #Database actions self.databaseActions = QActionGroup(self.mainWindow) self.actionExport = self.databaseActions.addAction(QIcon(":/icons/export.png"), "Export Data") self.actionExport.setToolTip("Export selected node(s) and their children to a .csv file. \n If no or all node(s) are selected inside the data-view, a complete export of all data in the DB is performed") self.actionExport.triggered.connect(self.exportNodes) self.actionAdd = self.databaseActions.addAction(QIcon(":/icons/add.png"), "Add Nodes") self.actionAdd.setToolTip("Add new node(s) as a starting point for further data collection") self.actionAdd.triggered.connect(self.addNodes) self.actionDelete = self.databaseActions.addAction(QIcon(":/icons/delete.png"), "Delete Nodes") self.actionDelete.setToolTip("Delete nodes(s) and their children") self.actionDelete.triggered.connect(self.deleteNodes) #Data actions self.dataActions = QActionGroup(self.mainWindow) self.actionQuery = self.dataActions.addAction(QIcon(":/icons/fetch.png"), "Query") self.actionQuery.triggered.connect(self.querySelectedNodes) self.actionTimer = self.dataActions.addAction(QIcon(":/icons/fetch.png"), "Time") self.actionTimer.setToolTip("Time your data collection with a timer. Fetches the data for the selected node(s) in user-defined intervalls") self.actionTimer.triggered.connect(self.setupTimer) self.actionHelp = self.dataActions.addAction(QIcon(":/icons/help.png"), "Help") self.actionHelp.triggered.connect(self.help) self.actionLoadPreset = self.dataActions.addAction(QIcon(":/icons/presets.png"), "Presets") self.actionLoadPreset.triggered.connect(self.loadPreset) self.actionLoadAPIs = self.dataActions.addAction(QIcon(":/icons/apis.png"), "APIs") self.actionLoadAPIs.triggered.connect(self.loadAPIs) self.actionShowColumns = self.dataActions.addAction("Show Columns") self.actionShowColumns.triggered.connect(self.showColumns) self.actionClearColumns = self.dataActions.addAction("Clear Columns") self.actionClearColumns.triggered.connect(self.clearColumns) #Detail actions self.detailActions = QActionGroup(self.mainWindow) self.actionAddColumn = self.detailActions.addAction(QIcon(":/icons/addcolumn.png"),"Add Column") self.actionAddColumn.setToolTip("Add the current JSON-key as a column in the data view") self.actionAddColumn.triggered.connect(self.addColumn) self.actionAddAllolumns = self.detailActions.addAction(QIcon(":/icons/addcolumn.png"),"Add All Columns") self.actionAddAllolumns.setToolTip("Analyzes all selected nodes in the data view and adds all found keys as columns") self.actionAddAllolumns.triggered.connect(self.addAllColumns) self.actionUnpack = self.detailActions.addAction(QIcon(":/icons/unpack.png"),"Unpack List") self.actionUnpack.setToolTip("Unpacks a list in the JSON-data and creates a new node containing the list content") self.actionUnpack.triggered.connect(self.unpackList) self.actionJsonCopy = self.detailActions.addAction(QIcon(":/icons/toclip.png"),"Copy JSON to Clipboard") self.actionJsonCopy.setToolTip("Copy the selected JSON-data to the clipboard") self.actionJsonCopy.triggered.connect(self.jsonCopy) self.actionFieldDoc = self.detailActions.addAction(QIcon(":/icons/help.png"),"") self.actionFieldDoc.setToolTip("Open the documentation for the selected item if available.") self.actionFieldDoc.triggered.connect(self.showFieldDoc) #Tree actions self.treeActions = QActionGroup(self.mainWindow) self.actionExpandAll = self.treeActions.addAction(QIcon(":/icons/expand.png"), "Expand nodes") self.actionExpandAll.triggered.connect(self.expandAll) self.actionCollapseAll = self.treeActions.addAction(QIcon(":/icons/collapse.png"), "Collapse nodes") self.actionCollapseAll.triggered.connect(self.collapseAll) #self.actionSelectNodes=self.treeActions.addAction(QIcon(":/icons/collapse.png"),"Select nodes") #self.actionSelectNodes.triggered.connect(self.selectNodes) self.actionClipboard = self.treeActions.addAction(QIcon(":/icons/toclip.png"), "Copy Node(s) to Clipboard") self.actionClipboard.setToolTip("Copy the selected nodes(s) to the clipboard") self.actionClipboard.triggered.connect(self.clipboardNodes) @Slot() def help(self): self.mainWindow.helpwindow.show() @Slot() def openDB(self): #open a file dialog with a .db filter datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) datadir = datadir if os.path.exists(datadir) else os.path.expanduser("~") fldg = QFileDialog(caption="Open DB File", directory=datadir, filter="DB files (*.db)") fldg.setFileMode(QFileDialog.ExistingFile) if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.connect(fldg.selectedFiles()[0]) self.mainWindow.settings.setValue("lastpath", fldg.selectedFiles()[0]) self.mainWindow.updateUI() self.mainWindow.tree.loadData(self.mainWindow.database) self.mainWindow.actions.actionShowColumns.trigger() @Slot() def openDBFolder(self): path = self.mainWindow.settings.value("lastpath",None) if (path is not None) and (os.path.exists(path)): if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": subprocess.Popen(["open", path]) else: subprocess.Popen(["xdg-open", path]) @Slot() def makeDB(self): #same as openDB-Slot, but now for creating a new one on the file system datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) datadir = datadir if os.path.exists(datadir) else os.path.expanduser("~") fldg = QFileDialog(caption="Save DB File", directory=datadir, filter="DB files (*.db)") fldg.setAcceptMode(QFileDialog.AcceptSave) fldg.setDefaultSuffix("db") if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.createconnect(fldg.selectedFiles()[0]) self.mainWindow.settings.setValue("lastpath", fldg.selectedFiles()[0]) self.mainWindow.updateUI() @Slot() def deleteNodes(self): reply = QMessageBox.question(self.mainWindow, 'Delete Nodes', "Are you sure to delete all selected nodes?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return progress = ProgressBar("Deleting data...", self.mainWindow) try: todo = self.mainWindow.tree.selectedIndexesAndChildren(True) progress.setMaximum(len(todo)) for index in todo: progress.step() self.mainWindow.tree.treemodel.deleteNode(index, delaycommit=True) if progress.wasCanceled: break finally: # commit the operation on the db-layer afterwards (delaycommit is True) self.mainWindow.tree.treemodel.commitNewNodes() progress.close() @Slot() def clipboardNodes(self): progress = ProgressBar("Copy to clipboard", self.mainWindow) indexes = self.mainWindow.tree.selectionModel().selectedRows() progress.setMaximum(len(indexes)) output = io.StringIO() try: writer = csv.writer(output, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL, doublequote=True, lineterminator='\r\n') #headers row = [str(val) for val in self.mainWindow.tree.treemodel.getRowHeader()] writer.writerow(row) #rows for no in range(len(indexes)): if progress.wasCanceled: break row = [str(val) for val in self.mainWindow.tree.treemodel.getRowData(indexes[no])] writer.writerow(row) progress.step() clipboard = QApplication.clipboard() clipboard.setText(output.getvalue()) finally: output.close() progress.close() @Slot() def exportNodes(self): fldg = ExportFileDialog(self.mainWindow, filter ="CSV Files (*.csv)") @Slot() def addNodes(self): if not self.mainWindow.database.connected: return False # makes the user add a new facebook object into the db dialog = QDialog(self.mainWindow) dialog.setWindowTitle("Add Nodes") layout = QVBoxLayout() label = QLabel("<b>Object IDs (one ID per line):</b>") layout.addWidget(label) input = QPlainTextEdit() input.setMinimumWidth(500) input.LineWrapMode = QPlainTextEdit.NoWrap #input.acceptRichText=False input.setFocus() layout.addWidget(input) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) layout.addWidget(buttons) dialog.setLayout(layout) def createNodes(): newnodes = [node.strip() for node in input.toPlainText().splitlines()] self.mainWindow.tree.treemodel.addNodes(newnodes) self.mainWindow.tree.selectLastRow() dialog.close() def close(): dialog.close() #connect the nested functions above to the dialog-buttons buttons.accepted.connect(createNodes) buttons.rejected.connect(close) dialog.exec_() @Slot() def showColumns(self): self.mainWindow.tree.treemodel.setCustomColumns(self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def clearColumns(self): self.mainWindow.fieldList.clear() self.mainWindow.tree.treemodel.setCustomColumns([]) @Slot() def addColumn(self): key = self.mainWindow.detailTree.selectedKey() if key != '': self.mainWindow.fieldList.append(key) self.mainWindow.tree.treemodel.setCustomColumns(self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def addAllColumns(self): progress = ProgressBar("Analyzing data...", self.mainWindow) columns = self.mainWindow.fieldList.toPlainText().splitlines() try: indexes = self.mainWindow.tree.selectedIndexesAndChildren() progress.setMaximum(len(indexes)) for no in range(len(indexes)): progress.step() item = indexes[no].internalPointer() columns.extend([key for key in recursiveIterKeys(item.data['response']) if not key in columns]) if progress.wasCanceled: break finally: self.mainWindow.fieldList.setPlainText("\n".join(columns)) self.mainWindow.tree.treemodel.setCustomColumns(columns) progress.close() @Slot() def loadPreset(self): self.mainWindow.presetWindow.showPresets() @Slot() def loadAPIs(self): self.mainWindow.apiWindow.showWindow() @Slot() def jsonCopy(self): self.mainWindow.detailTree.copyToClipboard() @Slot() def unpackList(self): try: key = self.mainWindow.detailTree.selectedKey() if key == '': return False selected = self.mainWindow.tree.selectionModel().selectedRows() for item in selected: if not item.isValid(): continue treenode = item.internalPointer() treenode.unpackList(key) except Exception as e: self.mainWindow.logmessage(e) @Slot() def showFieldDoc(self): tree = self.mainWindow.detailTree key = tree.selectedKey() if key == '': return False key = tree.treemodel.fieldprefix + key if tree.treemodel.itemtype is not None: self.mainWindow.apiWindow.showDoc(tree.treemodel.module, tree.treemodel.basepath, tree.treemodel.path, key) @Slot() def expandAll(self): self.mainWindow.tree.expandAll() @Slot() def collapseAll(self): self.mainWindow.tree.collapseAll() @Slot() def selectNodes(self): self.mainWindow.selectNodesWindow.show() def queryNodes(self, indexes=None, apimodule=False, options=False): if not self.actionQuery.isEnabled() or not ((self.mainWindow.tree.selectedCount() > 0) or (indexes is not None)): return (False) #Show progress window progress = ProgressBar("Fetching Data", parent=self.mainWindow) try: #Get global options globaloptions = {} globaloptions['threads'] = self.mainWindow.threadsEdit.value() globaloptions['speed'] = self.mainWindow.speedEdit.value() globaloptions['errors'] = self.mainWindow.errorEdit.value() globaloptions['expand'] = self.mainWindow.autoexpandCheckbox.isChecked() globaloptions['logrequests'] = self.mainWindow.logCheckbox.isChecked() globaloptions['saveheaders'] = self.mainWindow.headersCheckbox.isChecked() objecttypes = self.mainWindow.typesEdit.text().replace(' ','').split(',') level = self.mainWindow.levelEdit.value() - 1 #Get selected nodes if indexes is None: indexes = self.mainWindow.tree.selectedIndexesAndChildren(False, {'level': level, 'objecttype':objecttypes}) if (len(indexes) == 0): return (False) #Update progress window self.mainWindow.logmessage("Start fetching data for {} node(s).".format(len(indexes))) progress.setMaximum(len(indexes)) self.mainWindow.tree.treemodel.nodecounter = 0 #Init status messages statuscount = {} errorcount = 0 ratelimitcount = 0 allowedstatus = ['fetched (200)','downloaded (200)','fetched (202)','stream'] #,'error (400)' if apimodule == False: apimodule = self.mainWindow.RequestTabs.currentWidget() if options == False: options = apimodule.getOptions() options.update(globaloptions) try: #Spawn Threadpool threadpool = ApiThreadPool(apimodule) threadpool.spawnThreads(options.get("threads", 1)) #Init input Queue indexes = deque(indexes) #Process Logging/Input/Output Queue while True: try: #Logging (sync logs in threads with main thread) msg = threadpool.getLogMessage() if msg is not None: self.mainWindow.logmessage(msg) #Jobs in if (len(indexes) > 0): index = indexes.popleft() if index.isValid(): treenode = index.internalPointer() job = {'nodeindex': index, 'nodedata': deepcopy(treenode.data), 'options': deepcopy(options)} threadpool.addJob(job) if len(indexes) == 0: threadpool.applyJobs() progress.setRemaining(threadpool.getJobCount()) progress.resetRate() #Jobs out job = threadpool.getJob() #-Finished all nodes (sentinel)... if job is None: break #-Finished one node... elif 'progress' in job: progresskey = 'nodeprogress' + str(job.get('threadnumber', '')) # Update single progress if 'current' in job: percent = int((job.get('current',0) * 100.0 / job.get('total',1))) progress.showInfo(progresskey, "{}% of current node processed.".format(percent)) elif 'page' in job: if job.get('page', 0) > 1: progress.showInfo(progresskey, "{} page(s) of current node processed.".format(job.get('page',0))) # Update total progress else: progress.removeInfo(progresskey) if not threadpool.suspended: progress.step() #-Add data... elif 'data' in job and (not progress.wasCanceled): if not job['nodeindex'].isValid(): continue # Add data treeindex = job['nodeindex'] treenode = treeindex.internalPointer() treenode.appendNodes(job['data'], job['options'], job['headers'], True) if options.get('expand',False): self.mainWindow.tree.setExpanded(treeindex,True) # Count status status = job['options'].get('querystatus', 'empty') if not status in statuscount: statuscount[status] = 1 else: statuscount[status] = statuscount[status]+1 # Collect errors for automatic retry if not status in allowedstatus: threadpool.addError(job) errorcount += 1 # Detect rate limit ratelimit = job['options'].get('ratelimit', False) if ratelimit: ratelimitcount += 1 # Clear errors if not threadpool.suspended and (status in allowedstatus) and not ratelimit: threadpool.clearRetry() errorcount = 0 ratelimitcount = 0 # Suspend on error elif (errorcount > (globaloptions['errors']-1)) or (ratelimitcount > 0): threadpool.suspendJobs() if ratelimit: msg = "You reached the rate limit of the API." else: msg = "{} consecutive errors occurred.\nPlease check your settings.".format(errorcount) timeout = 60 * 5 #5 minutes # Adjust progress progress.setRemaining(threadpool.getJobCount() + threadpool.getRetryCount()) progress.showError(msg, timeout, ratelimitcount > 0) self.mainWindow.tree.treemodel.commitNewNodes() # Show info progress.showInfo(status,"{} response(s) with status: {}".format(statuscount[status],status)) progress.showInfo('newnodes',"{} new node(s) created".format(self.mainWindow.tree.treemodel.nodecounter)) progress.showInfo('threads',"{} active thread(s)".format(threadpool.getThreadCount())) progress.setRemaining(threadpool.getJobCount()) # Custom info from modules info = job['options'].get('info', {}) for name, value in info.items(): progress.showInfo(name, value) # Abort elif progress.wasCanceled: progress.showInfo('cancel', "Disconnecting from stream, may take some time.") threadpool.stopJobs() # Retry elif progress.wasResumed: if progress.wasRetried: threadpool.retryJobs() else: threadpool.clearRetry() threadpool.resumeJobs() progress.setRemaining(threadpool.getJobCount()) progress.hideError() # Continue elif not threadpool.suspended: threadpool.resumeJobs() # Finished if not threadpool.hasJobs(): progress.showInfo('cancel', "Work finished, shutting down threads.") threadpool.stopJobs() #-Waiting... progress.computeRate() time.sleep(1.0 / 1000.0) finally: QApplication.processEvents() finally: request_summary = [str(val)+" x "+key for key,val in statuscount.items()] request_summary = ", ".join(request_summary) request_end = "Fetching completed" if not progress.wasCanceled else 'Fetching cancelled by user' self.mainWindow.logmessage("{}, {} new node(s) created. Summary of responses: {}.".format(request_end, self.mainWindow.tree.treemodel.nodecounter,request_summary)) self.mainWindow.tree.treemodel.commitNewNodes() finally: progress.close() @Slot() def querySelectedNodes(self): self.queryNodes() @Slot() def setupTimer(self): #Get data level = self.mainWindow.levelEdit.value() - 1 indexes = self.mainWindow.tree.selectedIndexesAndChildren(True, {'level': level, 'objecttype': ['seed', 'data', 'unpacked']}) module = self.mainWindow.RequestTabs.currentWidget() options = module.getOptions() #show timer window self.mainWindow.timerWindow.setupTimer( {'indexes': indexes, 'nodecount': len(indexes), 'module': module, 'options': options}) @Slot() def timerStarted(self, time): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired at " + time.toString("d MMM yyyy - hh:mm") + " ") @Slot() def timerStopped(self): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:black;}") self.mainWindow.timerStatus.setText("Timer stopped ") @Slot() def timerCountdown(self, countdown): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired in " + str(countdown) + " seconds ") @Slot() def timerFired(self, data): self.mainWindow.timerStatus.setText("Timer fired ") self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.queryNodes(data.get('indexes', []), data.get('module', None), data.get('options', {}).copy()) @Slot() def treeNodeSelected(self, current, selected): #show details self.mainWindow.detailTree.clear() if current.isValid(): item = current.internalPointer() self.mainWindow.detailTree.showDict(item.data['response'],item.data['querytype'], item.data['queryparams']) #select level level = 0 c = current while c.isValid(): level += 1 c = c.parent() self.mainWindow.levelEdit.setValue(level) #show node count self.mainWindow.selectionStatus.setText(str(len(selected)) + ' node(s) selected ') self.actionQuery.setDisabled(len(selected) == 0)
class TabularViewMixin: """Provides the pivot table and its frozen table for the DS form.""" _PARAMETER_VALUE = "Parameter value" _INDEX_EXPANSION = "Index expansion" _RELATIONSHIP = "Relationship" _PARAMETER = "parameter" _INDEX = "index" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # current state of ui self.current = None # Current QModelIndex selected in one of the entity tree views self.current_class_type = None self.current_class_id = None self.current_input_type = self._PARAMETER_VALUE self.filter_menus = {} self.class_pivot_preferences = {} self.PivotPreferences = namedtuple( "PivotPreferences", ["index", "columns", "frozen", "frozen_value"]) title_action = TitleWidgetAction("Input type", self) self.ui.menuPivot_table.addAction(title_action) self.input_type_action_group = QActionGroup(self) actions = { input_type: self.input_type_action_group.addAction(input_type) for input_type in [self._PARAMETER_VALUE, self._INDEX_EXPANSION, self._RELATIONSHIP] } for action in actions.values(): action.setCheckable(True) self.ui.menuPivot_table.addAction(action) actions[self.current_input_type].setChecked(True) self.pivot_table_proxy = PivotTableSortFilterProxy() self.pivot_table_model = None self.frozen_table_model = FrozenTableModel(self) self.ui.pivot_table.setModel(self.pivot_table_proxy) self.ui.pivot_table.connect_data_store_form(self) self.ui.frozen_table.setModel(self.frozen_table_model) self.ui.frozen_table.verticalHeader().setDefaultSectionSize( self.default_row_height) def add_menu_actions(self): """Adds toggle view actions to View menu.""" super().add_menu_actions() self.ui.menuView.addSeparator() self.ui.menuView.addAction( self.ui.dockWidget_pivot_table.toggleViewAction()) self.ui.menuView.addAction( self.ui.dockWidget_frozen_table.toggleViewAction()) def connect_signals(self): """Connects signals to slots.""" super().connect_signals() self.ui.treeView_object.selectionModel().currentChanged.connect( self._handle_entity_tree_current_changed) self.ui.treeView_relationship.selectionModel().currentChanged.connect( self._handle_entity_tree_current_changed) self.ui.pivot_table.horizontalHeader().header_dropped.connect( self.handle_header_dropped) self.ui.pivot_table.verticalHeader().header_dropped.connect( self.handle_header_dropped) self.ui.frozen_table.header_dropped.connect(self.handle_header_dropped) self.ui.frozen_table.selectionModel().currentChanged.connect( self.change_frozen_value) self.input_type_action_group.triggered.connect( self.do_reload_pivot_table) self.ui.dockWidget_pivot_table.visibilityChanged.connect( self._handle_pivot_table_visibility_changed) self.ui.dockWidget_frozen_table.visibilityChanged.connect( self._handle_frozen_table_visibility_changed) def init_models(self): """Initializes models.""" super().init_models() self.clear_pivot_table() @Slot("QModelIndex", object) def _set_model_data(self, index, value): self.pivot_table_proxy.setData(index, value) @property def current_object_class_id_list(self): if self.current_class_type == "object class": return [self.current_class_id] relationship_class = self.db_mngr.get_item(self.db_map, "relationship class", self.current_class_id) return [ int(id_) for id_ in relationship_class["object_class_id_list"].split(",") ] @property def current_object_class_name_list(self): if self.current_class_type == "object class": return [ self.db_mngr.get_item(self.db_map, "object class", self.current_class_id)["name"] ] relationship_class = self.db_mngr.get_item(self.db_map, "relationship class", self.current_class_id) return fix_name_ambiguity( relationship_class["object_class_name_list"].split(",")) @staticmethod def _is_class_index(index): """Returns whether or not the given tree index is a class index. Args: index (QModelIndex): index from object or relationship tree Returns: bool """ return index.column() == 0 and not index.parent().parent().isValid() @Slot(bool) def _handle_pivot_table_visibility_changed(self, visible): if visible: self.reload_pivot_table() self.reload_frozen_table() self.ui.dockWidget_frozen_table.setVisible(True) @Slot(bool) def _handle_frozen_table_visibility_changed(self, visible): if visible: self.ui.dockWidget_pivot_table.show() @Slot("QModelIndex", "QModelIndex") def _handle_entity_tree_current_changed(self, current, previous): if self.ui.dockWidget_pivot_table.isVisible(): self.reload_pivot_table(current=current) self.reload_frozen_table() def _get_entities(self, class_id=None, class_type=None): """Returns a list of dict items from the object or relationship tree model corresponding to the given class id. Args: class_id (int) class_type (str) Returns: list(dict) """ if class_id is None: class_id = self.current_class_id if class_type is None: class_type = self.current_class_type entity_type = { "object class": "object", "relationship class": "relationship" }[class_type] return self.db_mngr.get_items_by_field(self.db_map, entity_type, "class_id", class_id) def load_empty_relationship_data(self, objects_per_class=None): """Returns a dict containing all possible relationships in the current class. Args: objects_per_class (dict) Returns: dict: Key is object id tuple, value is None. """ if objects_per_class is None: objects_per_class = dict() if self.current_class_type == "object class": return {} object_id_sets = [] for obj_cls_id in self.current_object_class_id_list: objects = objects_per_class.get(obj_cls_id, None) if objects is None: objects = self._get_entities(obj_cls_id, "object class") id_set = {item["id"]: None for item in objects} object_id_sets.append(list(id_set.keys())) return dict.fromkeys(product(*object_id_sets)) def load_full_relationship_data(self, relationships=None, action="add"): """Returns a dict of relationships in the current class. Returns: dict: Key is object id tuple, value is relationship id. """ if self.current_class_type == "object class": return {} if relationships is None: relationships = self._get_entities() get_id = {"add": lambda x: x["id"], "remove": lambda x: None}[action] return { tuple(int(id_) for id_ in x["object_id_list"].split(',')): get_id(x) for x in relationships } def load_relationship_data(self): """Returns a dict that merges empty and full relationship data. Returns: dict: Key is object id tuple, value is True if a relationship exists, False otherwise. """ data = self.load_empty_relationship_data() data.update(self.load_full_relationship_data()) return data def _get_parameter_value_or_def_ids(self, item_type): """Returns a list of integer ids from the parameter model corresponding to the currently selected class and the given item type. Args: item_type (str): either "parameter value" or "parameter definition" Returns: list(int) """ class_id_field = { "object class": "object_class_id", "relationship class": "relationship_class_id" }[self.current_class_type] return [ x["id"] for x in self.db_mngr.get_items_by_field( self.db_map, item_type, class_id_field, self.current_class_id) ] def _get_parameter_values_or_defs(self, item_type): """Returns a list of dict items from the parameter model corresponding to the currently selected class and the given item type. Args: item_type (str): either "parameter value" or "parameter definition" Returns: list(dict) """ ids = self._get_parameter_value_or_def_ids(item_type) return [ self.db_mngr.get_item(self.db_map, item_type, id_) for id_ in ids ] def load_empty_parameter_value_data(self, entities=None, parameter_ids=None): """Returns a dict containing all possible combinations of entities and parameters for the current class. Args: entities (list, optional): if given, only load data for these entities parameter_ids (set, optional): if given, only load data for these parameter definitions Returns: dict: Key is a tuple object_id, ..., parameter_id, value is None. """ if entities is None: entities = self._get_entities() if parameter_ids is None: parameter_ids = self._get_parameter_value_or_def_ids( "parameter definition") if self.current_class_type == "relationship class": entity_ids = [ tuple(int(id_) for id_ in e["object_id_list"].split(',')) for e in entities ] else: entity_ids = [(e["id"], ) for e in entities] if not entity_ids: entity_ids = [ tuple(None for _ in self.current_object_class_id_list) ] if not parameter_ids: parameter_ids = [None] return { entity_id + (parameter_id, ): None for entity_id in entity_ids for parameter_id in parameter_ids } def load_full_parameter_value_data(self, parameter_values=None, action="add"): """Returns a dict of parameter values for the current class. Args: parameter_values (list, optional) action (str) Returns: dict: Key is a tuple object_id, ..., parameter_id, value is the parameter value. """ if parameter_values is None: parameter_values = self._get_parameter_values_or_defs( "parameter value") get_id = {"add": lambda x: x["id"], "remove": lambda x: None}[action] if self.current_class_type == "object class": return {(x["object_id"], x["parameter_id"]): get_id(x) for x in parameter_values} return { tuple(int(id_) for id_ in x["object_id_list"].split(',')) + (x["parameter_id"], ): get_id(x) for x in parameter_values } def load_parameter_value_data(self): """Returns a dict that merges empty and full parameter value data. Returns: dict: Key is a tuple object_id, ..., parameter_id, value is the parameter value or None if not specified. """ data = self.load_empty_parameter_value_data() data.update(self.load_full_parameter_value_data()) return data def load_expanded_parameter_value_data(self): """ Returns all permutations of entities as well as parameter indexes and values for the current class. Returns: dict: Key is a tuple object_id, ..., index, while value is None. """ data = self.load_parameter_value_data() return { key[:-1] + (index, key[-1]): id_ for key, id_ in data.items() for index in self.db_mngr.get_value_indexes( self.db_map, "parameter value", id_) } def get_pivot_preferences(self): """Returns saved pivot preferences. Returns: tuple, NoneType: pivot tuple, or None if no preference stored """ selection_key = (self.current_class_id, self.current_class_type, self.current_input_type) if selection_key in self.class_pivot_preferences: rows = self.class_pivot_preferences[selection_key].index columns = self.class_pivot_preferences[selection_key].columns frozen = self.class_pivot_preferences[selection_key].frozen frozen_value = self.class_pivot_preferences[ selection_key].frozen_value return (rows, columns, frozen, frozen_value) return None @Slot(str) def reload_pivot_table(self, current=None): """Updates current class (type and id) and reloads pivot table for it.""" if current is not None: self.current = current if self.current is None: return if self._is_class_index(self.current): item = self.current.model().item_from_index(self.current) class_id = item.db_map_id(self.db_map) if self.current_class_id == class_id: return self.current_class_type = item.item_type self.current_class_id = class_id self.do_reload_pivot_table() @busy_effect @Slot("QAction") def do_reload_pivot_table(self, action=None): """Reloads pivot table. """ if self.current_class_id is None: return qApp.processEvents() # pylint: disable=undefined-variable if action is None: action = self.input_type_action_group.checkedAction() self.current_input_type = action.text() self.pivot_table_model = { self._PARAMETER_VALUE: ParameterValuePivotTableModel, self._RELATIONSHIP: RelationshipPivotTableModel, self._INDEX_EXPANSION: IndexExpansionPivotTableModel, }[self.current_input_type](self) self.pivot_table_proxy.setSourceModel(self.pivot_table_model) delegate = { self._PARAMETER_VALUE: ParameterPivotTableDelegate, self._RELATIONSHIP: RelationshipPivotTableDelegate, self._INDEX_EXPANSION: ParameterPivotTableDelegate, }[self.current_input_type](self) self.ui.pivot_table.setItemDelegate(delegate) self.pivot_table_model.modelReset.connect(self.make_pivot_headers) if self.current_input_type == self._RELATIONSHIP and self.current_class_type != "relationship class": self.clear_pivot_table() return pivot = self.get_pivot_preferences() self.wipe_out_filter_menus() object_class_ids = dict( zip(self.current_object_class_name_list, self.current_object_class_id_list)) self.pivot_table_model.call_reset_model(object_class_ids, pivot) self.pivot_table_proxy.clear_filter() def clear_pivot_table(self): self.wipe_out_filter_menus() if self.pivot_table_model: self.pivot_table_model.clear_model() self.pivot_table_proxy.clear_filter() def wipe_out_filter_menus(self): while self.filter_menus: _, menu = self.filter_menus.popitem() menu.wipe_out() @Slot() def make_pivot_headers(self): """ Turns top left indexes in the pivot table into TabularViewHeaderWidget. """ top_indexes, left_indexes = self.pivot_table_model.top_left_indexes() for index in left_indexes: proxy_index = self.pivot_table_proxy.mapFromSource(index) widget = self.create_header_widget( proxy_index.data(Qt.DisplayRole), "columns") self.ui.pivot_table.setIndexWidget(proxy_index, widget) for index in top_indexes: proxy_index = self.pivot_table_proxy.mapFromSource(index) widget = self.create_header_widget( proxy_index.data(Qt.DisplayRole), "rows") self.ui.pivot_table.setIndexWidget(proxy_index, widget) QTimer.singleShot(0, self._resize_pivot_header_columns) @Slot() def _resize_pivot_header_columns(self): top_indexes, _ = self.pivot_table_model.top_left_indexes() for index in top_indexes: self.ui.pivot_table.resizeColumnToContents(index.column()) def make_frozen_headers(self): """ Turns indexes in the first row of the frozen table into TabularViewHeaderWidget. """ for column in range(self.frozen_table_model.columnCount()): index = self.frozen_table_model.index(0, column) widget = self.create_header_widget(index.data(Qt.DisplayRole), "frozen", with_menu=False) self.ui.frozen_table.setIndexWidget(index, widget) self.ui.frozen_table.horizontalHeader().resizeSection( column, widget.size().width()) def create_filter_menu(self, identifier): """Returns a filter menu for given given object class identifier. Args: identifier (int) Returns: TabularViewFilterMenu """ _get_field = lambda *args: self.db_mngr.get_field(self.db_map, *args) if identifier not in self.filter_menus: pivot_top_left_header = self.pivot_table_model.top_left_headers[ identifier] data_to_value = pivot_top_left_header.header_data self.filter_menus[identifier] = menu = TabularViewFilterMenu( self, identifier, data_to_value, show_empty=False) index_values = dict.fromkeys( self.pivot_table_model.model.index_values.get(identifier, [])) index_values.pop(None, None) menu.set_filter_list(index_values.keys()) menu.filterChanged.connect(self.change_filter) return self.filter_menus[identifier] def create_header_widget(self, identifier, area, with_menu=True): """ Returns a TabularViewHeaderWidget for given object class identifier. Args: identifier (str) area (str) with_menu (bool) Returns: TabularViewHeaderWidget """ menu = self.create_filter_menu(identifier) if with_menu else None widget = TabularViewHeaderWidget(identifier, area, menu=menu, parent=self) widget.header_dropped.connect(self.handle_header_dropped) return widget @staticmethod def _get_insert_index(pivot_list, catcher, position): """Returns an index for inserting a new element in the given pivot list. Returns: int """ if isinstance(catcher, TabularViewHeaderWidget): i = pivot_list.index(catcher.identifier) if position == "after": i += 1 else: i = 0 return i @Slot(object, object, str) def handle_header_dropped(self, dropped, catcher, position=""): """ Updates pivots when a header is dropped. Args: dropped (TabularViewHeaderWidget) catcher (TabularViewHeaderWidget, PivotTableHeaderView, FrozenTableView) position (str): either "before", "after", or "" """ top_indexes, left_indexes = self.pivot_table_model.top_left_indexes() rows = [index.data(Qt.DisplayRole) for index in top_indexes] columns = [index.data(Qt.DisplayRole) for index in left_indexes] frozen = self.frozen_table_model.headers dropped_list = { "columns": columns, "rows": rows, "frozen": frozen }[dropped.area] catcher_list = { "columns": columns, "rows": rows, "frozen": frozen }[catcher.area] dropped_list.remove(dropped.identifier) i = self._get_insert_index(catcher_list, catcher, position) catcher_list.insert(i, dropped.identifier) if dropped.area == "frozen" or catcher.area == "frozen": if frozen: frozen_values = self.find_frozen_values(frozen) self.frozen_table_model.reset_model(frozen_values, frozen) self.make_frozen_headers() self.ui.frozen_table.resizeColumnsToContents() else: self.frozen_table_model.clear_model() frozen_value = self.get_frozen_value( self.ui.frozen_table.currentIndex()) self.pivot_table_model.set_pivot(rows, columns, frozen, frozen_value) # save current pivot self.class_pivot_preferences[( self.current_class_id, self.current_class_type, self.current_input_type)] = self.PivotPreferences( rows, columns, frozen, frozen_value) self.make_pivot_headers() def get_frozen_value(self, index): """ Returns the value in the frozen table corresponding to the given index. Args: index (QModelIndex) Returns: tuple """ if not index.isValid(): return tuple(None for _ in range(self.frozen_table_model.columnCount())) return self.frozen_table_model.row(index) @Slot("QModelIndex", "QModelIndex") def change_frozen_value(self, current, previous): """Sets the frozen value from selection in frozen table. """ frozen_value = self.get_frozen_value(current) self.pivot_table_model.set_frozen_value(frozen_value) # store pivot preferences self.class_pivot_preferences[( self.current_class_id, self.current_class_type, self.current_input_type)] = self.PivotPreferences( self.pivot_table_model.model.pivot_rows, self.pivot_table_model.model.pivot_columns, self.pivot_table_model.model.pivot_frozen, self.pivot_table_model.model.frozen_value, ) @Slot(str, set, bool) def change_filter(self, identifier, valid_values, has_filter): if has_filter: self.pivot_table_proxy.set_filter(identifier, valid_values) else: self.pivot_table_proxy.set_filter( identifier, None) # None means everything passes def reload_frozen_table(self): """Resets the frozen model according to new selection in entity trees.""" if not self.pivot_table_model: return frozen = self.pivot_table_model.model.pivot_frozen frozen_value = self.pivot_table_model.model.frozen_value frozen_values = self.find_frozen_values(frozen) self.frozen_table_model.reset_model(frozen_values, frozen) self.make_frozen_headers() if frozen_value in frozen_values: # update selected row ind = frozen_values.index(frozen_value) self.ui.frozen_table.selectionModel().blockSignals( True) # prevent selectionChanged signal when updating self.ui.frozen_table.selectRow(ind) self.ui.frozen_table.selectionModel().blockSignals(False) else: # frozen value not found, remove selection self.ui.frozen_table.selectionModel().blockSignals( True) # prevent selectionChanged signal when updating self.ui.frozen_table.clearSelection() self.ui.frozen_table.selectionModel().blockSignals(False) self.ui.frozen_table.resizeColumnsToContents() def find_frozen_values(self, frozen): """Returns a list of tuples containing unique values (object ids) for the frozen indexes (object class ids). Args: frozen (tuple(int)): A tuple of currently frozen indexes Returns: list(tuple(list(int))) """ return list( dict.fromkeys( zip(*[ self.pivot_table_model.model.index_values.get(k, []) for k in frozen ])).keys()) # FIXME: Move this to the models @staticmethod def refresh_table_view(table_view): top_left = table_view.indexAt(table_view.rect().topLeft()) bottom_right = table_view.indexAt(table_view.rect().bottomRight()) if not bottom_right.isValid(): model = table_view.model() bottom_right = table_view.model().index(model.rowCount() - 1, model.columnCount() - 1) table_view.model().dataChanged.emit(top_left, bottom_right) @Slot(str) def update_filter_menus(self, action): for identifier, menu in self.filter_menus.items(): index_values = dict.fromkeys( self.pivot_table_model.model.index_values.get(identifier, [])) index_values.pop(None, None) if action == "add": menu.add_items_to_filter_list(list(index_values.keys())) elif action == "remove": previous = menu._filter._filter_model._data_set menu.remove_items_from_filter_list( list(previous - index_values.keys())) self.reload_frozen_table() def receive_objects_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return items = db_map_data.get(self.db_map, set()) if self.pivot_table_model.receive_objects_added_or_removed( items, action): self.update_filter_menus(action) def receive_relationships_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.current_class_type != "relationship class": return items = db_map_data.get(self.db_map, set()) relationships = [ x for x in items if x["class_id"] == self.current_class_id ] if not relationships: return if self.pivot_table_model.receive_relationships_added_or_removed( relationships, action): self.update_filter_menus(action) def receive_parameter_definitions_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return items = db_map_data.get(self.db_map, set()) parameters = [ x for x in items if (x.get("object_class_id") or x.get("relationship_class_id") ) == self.current_class_id ] if not parameters: return if self.pivot_table_model.receive_parameter_definitions_added_or_removed( parameters, action): self.update_filter_menus(action) def receive_parameter_values_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return items = db_map_data.get(self.db_map, set()) parameter_values = [ x for x in items if (x.get("object_class_id") or x.get("relationship_class_id") ) == self.current_class_id ] if not parameter_values: return if self.pivot_table_model.receive_parameter_values_added_or_removed( parameter_values, action): self.update_filter_menus(action) def receive_db_map_data_updated(self, db_map_data, get_class_id): if not self.pivot_table_model: return items = db_map_data.get(self.db_map, set()) for item in items: if get_class_id(item) == self.current_class_id: self.refresh_table_view(self.ui.pivot_table) self.refresh_table_view(self.ui.frozen_table) self.make_pivot_headers() break def receive_classes_removed(self, db_map_data): if not self.pivot_table_model: return items = db_map_data.get(self.db_map, set()) for item in items: if item["id"] == self.current_class_id: self.current_class_type = None self.current_class_id = None self.clear_pivot_table() break def receive_objects_added(self, db_map_data): """Reacts to objects added event.""" super().receive_objects_added(db_map_data) self.receive_objects_added_or_removed(db_map_data, action="add") def receive_relationships_added(self, db_map_data): """Reacts to relationships added event.""" super().receive_relationships_added(db_map_data) self.receive_relationships_added_or_removed(db_map_data, action="add") def receive_parameter_definitions_added(self, db_map_data): """Reacts to parameter definitions added event.""" super().receive_parameter_definitions_added(db_map_data) self.receive_parameter_definitions_added_or_removed(db_map_data, action="add") def receive_parameter_values_added(self, db_map_data): """Reacts to parameter values added event.""" super().receive_parameter_values_added(db_map_data) self.receive_parameter_values_added_or_removed(db_map_data, action="add") def receive_object_classes_updated(self, db_map_data): """Reacts to object classes updated event.""" super().receive_object_classes_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["id"]) def receive_objects_updated(self, db_map_data): """Reacts to objects updated event.""" super().receive_objects_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["class_id"]) def receive_relationship_classes_updated(self, db_map_data): """Reacts to relationship classes updated event.""" super().receive_relationship_classes_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["id"]) def receive_relationships_updated(self, db_map_data): """Reacts to relationships updated event.""" super().receive_relationships_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["class_id"]) def receive_parameter_values_updated(self, db_map_data): """Reacts to parameter values added event.""" super().receive_parameter_values_updated(db_map_data) self.receive_db_map_data_updated( db_map_data, get_class_id=lambda x: x.get("object_class_id") or x.get( "relationship_class_id")) def receive_parameter_definitions_updated(self, db_map_data): """Reacts to parameter definitions updated event.""" super().receive_parameter_definitions_updated(db_map_data) self.receive_db_map_data_updated( db_map_data, get_class_id=lambda x: x.get("object_class_id") or x.get( "relationship_class_id")) def receive_object_classes_removed(self, db_map_data): """Reacts to object classes removed event.""" super().receive_object_classes_removed(db_map_data) self.receive_classes_removed(db_map_data) def receive_objects_removed(self, db_map_data): """Reacts to objects removed event.""" super().receive_objects_removed(db_map_data) self.receive_objects_added_or_removed(db_map_data, action="remove") def receive_relationship_classes_removed(self, db_map_data): """Reacts to relationship classes remove event.""" super().receive_relationship_classes_removed(db_map_data) self.receive_classes_removed(db_map_data) def receive_relationships_removed(self, db_map_data): """Reacts to relationships removed event.""" super().receive_relationships_removed(db_map_data) self.receive_relationships_added_or_removed(db_map_data, action="remove") def receive_parameter_definitions_removed(self, db_map_data): """Reacts to parameter definitions removed event.""" super().receive_parameter_definitions_removed(db_map_data) self.receive_parameter_definitions_added_or_removed(db_map_data, action="remove") def receive_parameter_values_removed(self, db_map_data): """Reacts to parameter values removed event.""" super().receive_parameter_values_removed(db_map_data) self.receive_parameter_values_added_or_removed(db_map_data, action="remove") def receive_session_rolled_back(self, db_maps): """Reacts to session rolled back event.""" super().receive_session_rolled_back(db_maps) self.reload_pivot_table() self.reload_frozen_table()
class MainWindow(QMainWindow, Ui_MainWindow): """docstring for MainWindow.""" def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self._csvFilePath = "" self.serialport = serial.Serial() self.receiver_thread = readerThread(self) self.receiver_thread.setPort(self.serialport) self._localEcho = None self.setupUi(self) self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) font = QtGui.QFont() font.setFamily(EDITOR_FONT) font.setPointSize(10) self.txtEdtOutput.setFont(font) self.txtEdtInput.setFont(font) self.quickSendTable.setFont(font) if UI_FONT is not None: font = QtGui.QFont() font.setFamily(UI_FONT) font.setPointSize(9) self.dockWidget_PortConfig.setFont(font) self.dockWidget_SendHex.setFont(font) self.dockWidget_QuickSend.setFont(font) self.setupFlatUi() self.onEnumPorts() icon = QtGui.QIcon(":/icon.ico") self.setWindowIcon(icon) self.actionAbout.setIcon(icon) icon = QtGui.QIcon(":/qt_logo_16.ico") self.actionAbout_Qt.setIcon(icon) self._viewGroup = QActionGroup(self) self._viewGroup.addAction(self.actionAscii) self._viewGroup.addAction(self.actionHex_lowercase) self._viewGroup.addAction(self.actionHEX_UPPERCASE) self._viewGroup.setExclusive(True) # bind events self.actionOpen_Cmd_File.triggered.connect(self.openCSV) self.actionSave_Log.triggered.connect(self.onSaveLog) self.actionExit.triggered.connect(self.onExit) self.actionOpen.triggered.connect(self.openPort) self.actionClose.triggered.connect(self.closePort) self.actionPort_Config_Panel.triggered.connect(self.onTogglePrtCfgPnl) self.actionQuick_Send_Panel.triggered.connect(self.onToggleQckSndPnl) self.actionSend_Hex_Panel.triggered.connect(self.onToggleHexPnl) self.dockWidget_PortConfig.visibilityChanged.connect(self.onVisiblePrtCfgPnl) self.dockWidget_QuickSend.visibilityChanged.connect(self.onVisibleQckSndPnl) self.dockWidget_SendHex.visibilityChanged.connect(self.onVisibleHexPnl) self.actionLocal_Echo.triggered.connect(self.onLocalEcho) self.actionAlways_On_Top.triggered.connect(self.onAlwaysOnTop) self.actionAscii.triggered.connect(self.onViewChanged) self.actionHex_lowercase.triggered.connect(self.onViewChanged) self.actionHEX_UPPERCASE.triggered.connect(self.onViewChanged) self.actionAbout.triggered.connect(self.onAbout) self.actionAbout_Qt.triggered.connect(self.onAboutQt) self.btnOpen.clicked.connect(self.onOpen) self.btnClear.clicked.connect(self.onClear) self.btnSaveLog.clicked.connect(self.onSaveLog) self.btnEnumPorts.clicked.connect(self.onEnumPorts) self.btnSendHex.clicked.connect(self.sendHex) self.receiver_thread.read.connect(self.receive) self.receiver_thread.exception.connect(self.readerExcept) self._signalMap = QSignalMapper(self) self._signalMap.mapped[int].connect(self.tableClick) # initial action self.actionHEX_UPPERCASE.setChecked(True) self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE) self.initQuickSend() self.restoreLayout() self.moveScreenCenter() self.syncMenu() if self.isMaximized(): self.setMaximizeButton("restore") else: self.setMaximizeButton("maximize") self.LoadSettings() def setupFlatUi(self): self._dragPos = self.pos() self._isDragging = False self.setMouseTracking(True) self.setWindowFlags(Qt.FramelessWindowHint) self.setStyleSheet(""" QWidget { background-color:#99d9ea; /*background-image: url(:/background.png);*/ outline: 1px solid #0057ff; } QLabel { color:#202020; font-size:13px; font-family:Century; } QComboBox { color:#202020; font-size:13px; font-family:Century Schoolbook; } QComboBox { border: none; padding: 1px 18px 1px 3px; } QComboBox:editable { background: white; } QComboBox:!editable, QComboBox::drop-down:editable { background: #62c7e0; } QComboBox:!editable:hover, QComboBox::drop-down:editable:hover { background: #c7eaf3; } QComboBox:!editable:pressed, QComboBox::drop-down:editable:pressed { background: #35b6d7; } QComboBox:on { padding-top: 3px; padding-left: 4px; } QComboBox::drop-down { subcontrol-origin: padding; subcontrol-position: top right; width: 16px; border: none; } QComboBox::down-arrow { image: url(:/downarrow.png); } QComboBox::down-arrow:on { image: url(:/uparrow.png); } QGroupBox { color:#202020; font-size:12px; font-family:Century Schoolbook; border: 1px solid gray; margin-top: 15px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left:5px; top:3px; } QCheckBox { color:#202020; spacing: 5px; font-size:12px; font-family:Century Schoolbook; } QScrollBar:horizontal { background-color:#99d9ea; border: none; height: 15px; margin: 0px 20px 0 20px; } QScrollBar::handle:horizontal { background: #61b9e1; min-width: 20px; } QScrollBar::add-line:horizontal { image: url(:/rightarrow.png); border: none; background: #7ecfe4; width: 20px; subcontrol-position: right; subcontrol-origin: margin; } QScrollBar::sub-line:horizontal { image: url(:/leftarrow.png); border: none; background: #7ecfe4; width: 20px; subcontrol-position: left; subcontrol-origin: margin; } QScrollBar:vertical { background-color:#99d9ea; border: none; width: 15px; margin: 20px 0px 20px 0px; } QScrollBar::handle::vertical { background: #61b9e1; min-height: 20px; } QScrollBar::add-line::vertical { image: url(:/downarrow.png); border: none; background: #7ecfe4; height: 20px; subcontrol-position: bottom; subcontrol-origin: margin; } QScrollBar::sub-line::vertical { image: url(:/uparrow.png); border: none; background: #7ecfe4; height: 20px; subcontrol-position: top; subcontrol-origin: margin; } QTableView { background-color: white; /*selection-background-color: #FF92BB;*/ border: 1px solid #eeeeee; color: #2f2f2f; } QTableView::focus { /*border: 1px solid #2a7fff;*/ } QTableView QTableCornerButton::section { border: none; border-right: 1px solid #eeeeee; border-bottom: 1px solid #eeeeee; background-color: #8ae6d2; } QTableView QWidget { background-color: white; } QTableView::item:focus { border: 1px red; background-color: transparent; color: #2f2f2f; } QHeaderView::section { border: none; border-right: 1px solid #eeeeee; border-bottom: 1px solid #eeeeee; padding-left: 2px; padding-right: 2px; color: #444444; background-color: #8ae6d2; } QTextEdit { background-color:white; color:#2f2f2f; border: 1px solid white; } QTextEdit::focus { border: 1px solid #2a7fff; } QPushButton { background-color:#30a7b8; border:none; color:#ffffff; font-size:14px; font-family:Century Schoolbook; } QPushButton:hover { background-color:#51c0d1; } QPushButton:pressed { background-color:#3a9ecc; } QMenuBar { color: #2f2f2f; } QMenuBar::item { background-color: transparent; margin: 8px 0px 0px 0px; padding: 1px 8px 1px 8px; height: 15px; } QMenuBar::item:selected { background: #51c0d1; } QMenuBar::item:pressed { } QMenu { color: #2f2f2f; } QMenu { margin: 2px; } QMenu::item { padding: 2px 25px 2px 21px; border: 1px solid transparent; } QMenu::item:selected { background: #51c0d1; } QMenu::icon { background: transparent; border: 2px inset transparent; } QDockWidget { font-size:13px; font-family:Century; color: #202020; titlebar-close-icon: none; titlebar-normal-icon: none; } QDockWidget::title { margin: 0; padding: 2px; subcontrol-origin: content; subcontrol-position: right top; text-align: left; background: #67baed; } QDockWidget::float-button { max-width: 12px; max-height: 12px; background-color:transparent; border:none; image: url(:/restore_inactive.png); } QDockWidget::float-button:hover { background-color:#227582; image: url(:/restore_active.png); } QDockWidget::float-button:pressed { padding: 0; background-color:#14464e; image: url(:/restore_active.png); } QDockWidget::close-button { max-width: 12px; max-height: 12px; background-color:transparent; border:none; image: url(:/close_inactive.png); } QDockWidget::close-button:hover { background-color:#ea5e00; image: url(:/close_active.png); } QDockWidget::close-button:pressed { background-color:#994005; image: url(:/close_active.png); padding: 0; } """) self.dockWidgetContents.setStyleSheet(""" QPushButton { min-height:23px; } """) self.dockWidget_QuickSend.setStyleSheet(""" QPushButton { background-color:#27b798; font-family:Consolas; font-size:12px; min-width:46px; } QPushButton:hover { background-color:#3bd5b4; } QPushButton:pressed { background-color:#1d8770; } """) self.dockWidgetContents_2.setStyleSheet(""" QPushButton { min-height:23px; min-width:50px; } """) w = self.frameGeometry().width() self._minBtn = QPushButton(self) self._minBtn.setGeometry(w-103,0,28,24) self._minBtn.clicked.connect(self.onMinimize) self._minBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/minimize_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/minimize_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/minimize_active.png); } """) self._maxBtn = QPushButton(self) self._maxBtn.setGeometry(w-74,0,28,24) self._maxBtn.clicked.connect(self.onMaximize) self.setMaximizeButton("maximize") self._closeBtn = QPushButton(self) self._closeBtn.setGeometry(w-45,0,36,24) self._closeBtn.clicked.connect(self.onExit) self._closeBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/close_inactive.png); } QPushButton:hover { background-color:#ea5e00; image: url(:/close_active.png); } QPushButton:pressed { background-color:#994005; image: url(:/close_active.png); } """) def resizeEvent(self, event): w = event.size().width() self._minBtn.move(w-103,0) self._maxBtn.move(w-74,0) self._closeBtn.move(w-45,0) def onMinimize(self): self.showMinimized() def isMaximized(self): return ((self.windowState() == Qt.WindowMaximized)) def onMaximize(self): if self.isMaximized(): self.showNormal() self.setMaximizeButton("maximize") else: self.showMaximized() self.setMaximizeButton("restore") def setMaximizeButton(self, style): if "maximize" == style: self._maxBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/maximize_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/maximize_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/maximize_active.png); } """) elif "restore" == style: self._maxBtn.setStyleSheet(""" QPushButton { background-color:transparent; border:none; outline: none; image: url(:/restore_inactive.png); } QPushButton:hover { background-color:#227582; image: url(:/restore_active.png); } QPushButton:pressed { background-color:#14464e; image: url(:/restore_active.png); } """) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self._isDragging = True self._dragPos = event.globalPos() - self.pos() event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self._isDragging and not self.isMaximized(): self.move(event.globalPos() - self._dragPos) event.accept() def mouseReleaseEvent(self, event): self._isDragging = False event.accept() def SaveSettings(self): root = ET.Element("MyTerm") GUISettings = ET.SubElement(root, "GUISettings") PortCfg = ET.SubElement(GUISettings, "PortConfig") ET.SubElement(PortCfg, "port").text = self.cmbPort.currentText() ET.SubElement(PortCfg, "baudrate").text = self.cmbBaudRate.currentText() ET.SubElement(PortCfg, "databits").text = self.cmbDataBits.currentText() ET.SubElement(PortCfg, "parity").text = self.cmbParity.currentText() ET.SubElement(PortCfg, "stopbits").text = self.cmbStopBits.currentText() ET.SubElement(PortCfg, "rtscts").text = self.chkRTSCTS.isChecked() and "on" or "off" ET.SubElement(PortCfg, "xonxoff").text = self.chkXonXoff.isChecked() and "on" or "off" View = ET.SubElement(GUISettings, "View") ET.SubElement(View, "LocalEcho").text = self.actionLocal_Echo.isChecked() and "on" or "off" ET.SubElement(View, "ReceiveView").text = self._viewGroup.checkedAction().text() with open(get_config_path('settings.xml'), 'w') as f: f.write('<?xml version="1.0" encoding="UTF-8"?>\n') f.write(ET.tostring(root, encoding='utf-8', pretty_print=True).decode("utf-8")) def LoadSettings(self): if os.path.isfile(get_config_path("settings.xml")): with open(get_config_path("settings.xml"), 'r') as f: tree = safeET.parse(f) port = tree.findtext('GUISettings/PortConfig/port', default='') if port != '': self.cmbPort.setCurrentText(port) baudrate = tree.findtext('GUISettings/PortConfig/baudrate', default='38400') if baudrate != '': self.cmbBaudRate.setCurrentText(baudrate) databits = tree.findtext('GUISettings/PortConfig/databits', default='8') id = self.cmbDataBits.findText(databits) if id >= 0: self.cmbDataBits.setCurrentIndex(id) parity = tree.findtext('GUISettings/PortConfig/parity', default='None') id = self.cmbParity.findText(parity) if id >= 0: self.cmbParity.setCurrentIndex(id) stopbits = tree.findtext('GUISettings/PortConfig/stopbits', default='1') id = self.cmbStopBits.findText(stopbits) if id >= 0: self.cmbStopBits.setCurrentIndex(id) rtscts = tree.findtext('GUISettings/PortConfig/rtscts', default='off') if 'on' == rtscts: self.chkRTSCTS.setChecked(True) else: self.chkRTSCTS.setChecked(False) xonxoff = tree.findtext('GUISettings/PortConfig/xonxoff', default='off') if 'on' == xonxoff: self.chkXonXoff.setChecked(True) else: self.chkXonXoff.setChecked(False) LocalEcho = tree.findtext('GUISettings/View/LocalEcho', default='off') if 'on' == LocalEcho: self.actionLocal_Echo.setChecked(True) self._localEcho = True else: self.actionLocal_Echo.setChecked(False) self._localEcho = False ReceiveView = tree.findtext('GUISettings/View/ReceiveView', default='HEX(UPPERCASE)') if 'Ascii' in ReceiveView: self.actionAscii.setChecked(True) elif 'lowercase' in ReceiveView: self.actionHex_lowercase.setChecked(True) elif 'UPPERCASE' in ReceiveView: self.actionHEX_UPPERCASE.setChecked(True) def closeEvent(self, event): self.saveLayout() self.saveCSV() self.SaveSettings() event.accept() def tableClick(self, row): self.sendTableRow(row) def initQuickSend(self): #self.quickSendTable.horizontalHeader().setDefaultSectionSize(40) #self.quickSendTable.horizontalHeader().setMinimumSectionSize(25) self.quickSendTable.setRowCount(50) self.quickSendTable.setColumnCount(20) for row in range(50): item = QPushButton(str("Send")) item.clicked.connect(self._signalMap.map) self._signalMap.setMapping(item, row) self.quickSendTable.setCellWidget(row, 0, item) self.quickSendTable.setRowHeight(row, 20) if os.path.isfile(get_config_path('QckSndBckup.csv')): self.loadCSV(get_config_path('QckSndBckup.csv')) self.quickSendTable.resizeColumnsToContents() def openCSV(self): fileName = QFileDialog.getOpenFileName(self, "Select a file", os.getcwd(), "CSV Files (*.csv)")[0] if fileName: self.loadCSV(fileName, notifyExcept = True) def saveCSV(self): # scan table rows = self.quickSendTable.rowCount() cols = self.quickSendTable.columnCount() tmp_data = [[self.quickSendTable.item(row, col) is not None and self.quickSendTable.item(row, col).text() or '' for col in range(1, cols)] for row in range(rows)] data = [] # delete trailing blanks for row in tmp_data: for idx, d in enumerate(row[::-1]): if '' != d: break new_row = row[:len(row) - idx] data.append(new_row) #import pprint #pprint.pprint(data, width=120, compact=True) # write to file with open(get_config_path('QckSndBckup.csv'), 'w') as csvfile: csvwriter = csv.writer(csvfile, delimiter=',', lineterminator='\n') csvwriter.writerows(data) def loadCSV(self, path, notifyExcept = False): data = [] set_rows = 0 set_cols = 0 try: with open(path) as csvfile: csvData = csv.reader(csvfile) for row in csvData: data.append(row) set_rows = set_rows + 1 if len(row) > set_cols: set_cols = len(row) except IOError as e: print("({})".format(e)) if notifyExcept: QMessageBox.critical(self, "Open failed", str(e), QMessageBox.Close) return rows = self.quickSendTable.rowCount() cols = self.quickSendTable.columnCount() # clear table for col in range(cols): for row in range(rows): self.quickSendTable.setItem(row, col, QTableWidgetItem("")) self._csvFilePath = path if (cols - 1) < set_cols: # first colume is used by the "send" buttons. cols = set_cols + 10 self.quickSendTable.setColumnCount(cols) if rows < set_rows: rows = set_rows + 20 self.quickSendTable.setRowCount(rows) for row, rowdat in enumerate(data): if len(rowdat) > 0: for col, cell in enumerate(rowdat, 1): self.quickSendTable.setItem(row, col, QTableWidgetItem(str(cell))) self.quickSendTable.resizeColumnsToContents() #self.quickSendTable.resizeRowsToContents() def sendTableRow(self, row): cols = self.quickSendTable.columnCount() try: data = ['0' + self.quickSendTable.item(row, col).text() for col in range(1, cols) if self.quickSendTable.item(row, col) is not None and self.quickSendTable.item(row, col).text() is not ''] except: print("Exception in get table data(row = %d)" % (row + 1)) else: tmp = [d[-2] + d[-1] for d in data if len(d) >= 2] for t in tmp: if not is_hex(t): QMessageBox.critical(self, "Error", "'%s' is not hexadecimal." % (t), QMessageBox.Close) return h = [int(t, 16) for t in tmp] self.transmitHex(h) def sendHex(self): hexStr = self.txtEdtInput.toPlainText() hexStr = ''.join(hexStr.split(" ")) hexarray = [] for i in range(0, len(hexStr), 2): hexarray.append(int(hexStr[i:i+2], 16)) self.transmitHex(hexarray) def readerExcept(self, e): self.closePort() QMessageBox.critical(self, "Read failed", str(e), QMessageBox.Close) def timestamp(self): return datetime.datetime.now().time().isoformat()[:-3] def receive(self, data): self.appendOutputText("\n%s R<-:%s" % (self.timestamp(), data)) def appendOutputText(self, data, color=Qt.black): # the qEditText's "append" methon will add a unnecessary newline. # self.txtEdtOutput.append(data.decode('utf-8')) tc=self.txtEdtOutput.textColor() self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End) self.txtEdtOutput.setTextColor(QtGui.QColor(color)) self.txtEdtOutput.insertPlainText(data) self.txtEdtOutput.moveCursor(QtGui.QTextCursor.End) self.txtEdtOutput.setTextColor(tc) def transmitHex(self, hexarray): if len(hexarray) > 0: byteArray = bytearray(hexarray) if self.serialport.isOpen(): try: self.serialport.write(byteArray) except serial.SerialException as e: print("Exception in transmitHex(%s)" % repr(hexarray)) QMessageBox.critical(self, "Exception in transmitHex", str(e), QMessageBox.Close) else: # self.txCount += len( b ) # self.frame.statusbar.SetStatusText('Tx:%d' % self.txCount, 2) text = ''.join(['%02X ' % i for i in hexarray]) self.appendOutputText("\n%s T->:%s" % (self.timestamp(), text), Qt.blue) def GetPort(self): return self.cmbPort.currentText() def GetDataBits(self): s = self.cmbDataBits.currentText() if s == '5': return serial.FIVEBITS elif s == '6': return serial.SIXBITS elif s == '7': return serial.SEVENBITS elif s == '8': return serial.EIGHTBITS def GetParity(self): s = self.cmbParity.currentText() if s == 'None': return serial.PARITY_NONE elif s == 'Even': return serial.PARITY_EVEN elif s == 'Odd': return serial.PARITY_ODD elif s == 'Mark': return serial.PARITY_MARK elif s == 'Space': return serial.PARITY_SPACE def GetStopBits(self): s = self.cmbStopBits.currentText() if s == '1': return serial.STOPBITS_ONE elif s == '1.5': return serial.STOPBITS_ONE_POINT_FIVE elif s == '2': return serial.STOPBITS_TWO def openPort(self): if self.serialport.isOpen(): return _port = self.GetPort() if '' == _port: QMessageBox.information(self, "Invalid parameters", "Port is empty.") return _baudrate = self.cmbBaudRate.currentText() if '' == _baudrate: QMessageBox.information(self, "Invalid parameters", "Baudrate is empty.") return self.serialport.port = _port self.serialport.baudrate = _baudrate self.serialport.bytesize = self.GetDataBits() self.serialport.stopbits = self.GetStopBits() self.serialport.parity = self.GetParity() self.serialport.rtscts = self.chkRTSCTS.isChecked() self.serialport.xonxoff = self.chkXonXoff.isChecked() # self.serialport.timeout = THREAD_TIMEOUT # self.serialport.writeTimeout = SERIAL_WRITE_TIMEOUT try: self.serialport.open() except serial.SerialException as e: QMessageBox.critical(self, "Could not open serial port", str(e), QMessageBox.Close) else: self._start_reader() self.setWindowTitle("%s on %s [%s, %s%s%s%s%s]" % ( appInfo.title, self.serialport.portstr, self.serialport.baudrate, self.serialport.bytesize, self.serialport.parity, self.serialport.stopbits, self.serialport.rtscts and ' RTS/CTS' or '', self.serialport.xonxoff and ' Xon/Xoff' or '', ) ) pal = self.btnOpen.palette() pal.setColor(QtGui.QPalette.Button, QtGui.QColor(0,0xff,0x7f)) self.btnOpen.setAutoFillBackground(True) self.btnOpen.setPalette(pal) self.btnOpen.setText('Close') self.btnOpen.update() def closePort(self): if self.serialport.isOpen(): self._stop_reader() self.serialport.close() self.setWindowTitle(appInfo.title) pal = self.btnOpen.style().standardPalette() self.btnOpen.setAutoFillBackground(True) self.btnOpen.setPalette(pal) self.btnOpen.setText('Open') self.btnOpen.update() def _start_reader(self): """Start reader thread""" self.receiver_thread.start() def _stop_reader(self): """Stop reader thread only, wait for clean exit of thread""" self.receiver_thread.join() def onTogglePrtCfgPnl(self): if self.actionPort_Config_Panel.isChecked(): self.dockWidget_PortConfig.show() else: self.dockWidget_PortConfig.hide() def onToggleQckSndPnl(self): if self.actionQuick_Send_Panel.isChecked(): self.dockWidget_QuickSend.show() else: self.dockWidget_QuickSend.hide() def onToggleHexPnl(self): if self.actionSend_Hex_Panel.isChecked(): self.dockWidget_SendHex.show() else: self.dockWidget_SendHex.hide() def onVisiblePrtCfgPnl(self, visible): self.actionPort_Config_Panel.setChecked(visible) def onVisibleQckSndPnl(self, visible): self.actionQuick_Send_Panel.setChecked(visible) def onVisibleHexPnl(self, visible): self.actionSend_Hex_Panel.setChecked(visible) def onLocalEcho(self): self._localEcho = self.actionLocal_Echo.isChecked() def onAlwaysOnTop(self): if self.actionAlways_On_Top.isChecked(): style = self.windowFlags() self.setWindowFlags(style|Qt.WindowStaysOnTopHint) self.show() else: style = self.windowFlags() self.setWindowFlags(style & ~Qt.WindowStaysOnTopHint) self.show() def onOpen(self): if self.serialport.isOpen(): self.closePort() else: self.openPort() def onClear(self): self.txtEdtOutput.clear() def onSaveLog(self): fileName = QFileDialog.getSaveFileName(self, "Save as", os.getcwd(), "Log files (*.log);;Text files (*.txt);;All files (*.*)")[0] if fileName: import codecs f = codecs.open(fileName, 'w', 'utf-8') f.write(self.txtEdtOutput.toPlainText()) f.close() def moveScreenCenter(self): w = self.frameGeometry().width() h = self.frameGeometry().height() desktop = QDesktopWidget() screenW = desktop.screen().width() screenH = desktop.screen().height() self.setGeometry((screenW-w)/2, (screenH-h)/2, w, h) def onEnumPorts(self): for p in enum_ports(): self.cmbPort.addItem(p) # self.cmbPort.update() def onAbout(self): q = QWidget() icon = QtGui.QIcon(":/icon.ico") q.setWindowIcon(icon) QMessageBox.about(q, "About MyTerm", appInfo.aboutme) def onAboutQt(self): QMessageBox.aboutQt(None) def onExit(self): if self.serialport.isOpen(): self.closePort() self.close() def restoreLayout(self): if os.path.isfile(get_config_path("layout.dat")): try: f=open(get_config_path("layout.dat"), 'rb') geometry, state=pickle.load(f) self.restoreGeometry(geometry) self.restoreState(state) except Exception as e: print("Exception on restoreLayout, {}".format(e)) else: try: f=QFile(':/default_layout.dat') f.open(QIODevice.ReadOnly) geometry, state=pickle.loads(f.readAll()) self.restoreGeometry(geometry) self.restoreState(state) except Exception as e: print("Exception on restoreLayout, {}".format(e)) def saveLayout(self): with open(get_config_path("layout.dat"), 'wb') as f: pickle.dump((self.saveGeometry(), self.saveState()), f) def syncMenu(self): self.actionPort_Config_Panel.setChecked(not self.dockWidget_PortConfig.isHidden()) self.actionQuick_Send_Panel.setChecked(not self.dockWidget_QuickSend.isHidden()) self.actionSend_Hex_Panel.setChecked(not self.dockWidget_SendHex.isHidden()) def onViewChanged(self): checked = self._viewGroup.checkedAction() if checked is None: self.actionHEX_UPPERCASE.setChecked(True) self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE) else: if 'Ascii' in checked.text(): self.receiver_thread.setViewMode(VIEWMODE_ASCII) elif 'lowercase' in checked.text(): self.receiver_thread.setViewMode(VIEWMODE_HEX_LOWERCASE) elif 'UPPERCASE' in checked.text(): self.receiver_thread.setViewMode(VIEWMODE_HEX_UPPERCASE)
class Actions(object): def __init__(self, mainWindow): self.mainWindow = mainWindow #Basic actions self.basicActions = QActionGroup(self.mainWindow) self.actionOpen = self.basicActions.addAction( QIcon(":/icons/save.png"), "Open Database") self.actionOpen.triggered.connect(self.openDB) self.actionNew = self.basicActions.addAction(QIcon(":/icons/new.png"), "New Database") self.actionNew.triggered.connect(self.makeDB) #Database actions self.databaseActions = QActionGroup(self.mainWindow) self.actionExport = self.databaseActions.addAction( QIcon(":/icons/export.png"), "Export Data") self.actionExport.setToolTip( wraptip( "Export selected node(s) and their children to a .csv file. \n If no or all node(s) are selected inside the data-view, a complete export of all data in the DB is performed" )) self.actionExport.triggered.connect(self.exportNodes) self.actionAdd = self.databaseActions.addAction( QIcon(":/icons/add.png"), "Add Nodes") self.actionAdd.setToolTip( wraptip( "Add new node(s) as a starting point for further data collection" )) self.actionAdd.triggered.connect(self.addNodes) self.actionDelete = self.databaseActions.addAction( QIcon(":/icons/delete.png"), "Delete Nodes") self.actionDelete.setToolTip( wraptip("Delete nodes(s) and their children")) self.actionDelete.triggered.connect(self.deleteNodes) #Data actions self.dataActions = QActionGroup(self.mainWindow) self.actionQuery = self.dataActions.addAction( QIcon(":/icons/fetch.png"), "Query") self.actionQuery.triggered.connect(self.querySelectedNodes) self.actionTimer = self.dataActions.addAction( QIcon(":/icons/fetch.png"), "Time") self.actionTimer.setToolTip( wraptip( "Time your data collection with a timer. Fetches the data for the selected node(s) in user-defined intervalls" )) self.actionTimer.triggered.connect(self.setupTimer) self.actionHelp = self.dataActions.addAction(QIcon(":/icons/help.png"), "Help") self.actionHelp.triggered.connect(self.help) self.actionLoadPreset = self.dataActions.addAction( QIcon(":/icons/presets.png"), "Presets") self.actionLoadPreset.triggered.connect(self.loadPreset) self.actionLoadAPIs = self.dataActions.addAction( QIcon(":/icons/apis.png"), "APIs") self.actionLoadAPIs.triggered.connect(self.loadAPIs) self.actionShowColumns = self.dataActions.addAction("Show Columns") self.actionShowColumns.triggered.connect(self.showColumns) self.actionClearColumns = self.dataActions.addAction("Clear Columns") self.actionClearColumns.triggered.connect(self.clearColumns) #Detail actions self.detailActions = QActionGroup(self.mainWindow) self.actionAddColumn = self.detailActions.addAction( QIcon(":/icons/addcolumn.png"), "Add Column") self.actionAddColumn.setToolTip( wraptip("Add the current JSON-key as a column in the data view")) self.actionAddColumn.triggered.connect(self.addColumn) self.actionAddAllolumns = self.detailActions.addAction( QIcon(":/icons/addcolumn.png"), "Add All Columns") self.actionAddAllolumns.setToolTip( wraptip( "Analyzes all selected nodes in the data view and adds all found keys as columns" )) self.actionAddAllolumns.triggered.connect(self.addAllColumns) self.actionUnpack = self.detailActions.addAction( QIcon(":/icons/unpack.png"), "Unpack List") self.actionUnpack.setToolTip( wraptip( "Unpacks a list in the JSON-data and creates a new node containing the list content" )) self.actionUnpack.triggered.connect(self.unpackList) self.actionJsonCopy = self.detailActions.addAction( QIcon(":/icons/toclip.png"), "Copy JSON to Clipboard") self.actionJsonCopy.setToolTip( wraptip("Copy the selected JSON-data to the clipboard")) self.actionJsonCopy.triggered.connect(self.jsonCopy) self.actionFieldDoc = self.detailActions.addAction( QIcon(":/icons/help.png"), "") self.actionFieldDoc.setToolTip( wraptip( "Open the documentation for the selected item if available.")) self.actionFieldDoc.triggered.connect(self.showFieldDoc) #Tree actions self.treeActions = QActionGroup(self.mainWindow) self.actionExpandAll = self.treeActions.addAction( QIcon(":/icons/expand.png"), "Expand nodes") self.actionExpandAll.triggered.connect(self.expandAll) self.actionCollapseAll = self.treeActions.addAction( QIcon(":/icons/collapse.png"), "Collapse nodes") self.actionCollapseAll.triggered.connect(self.collapseAll) #self.actionSelectNodes=self.treeActions.addAction(QIcon(":/icons/collapse.png"),"Select nodes") #self.actionSelectNodes.triggered.connect(self.selectNodes) self.actionClipboard = self.treeActions.addAction( QIcon(":/icons/toclip.png"), "Copy Node(s) to Clipboard") self.actionClipboard.setToolTip( wraptip("Copy the selected nodes(s) to the clipboard")) self.actionClipboard.triggered.connect(self.clipboardNodes) @Slot() def help(self): self.mainWindow.helpwindow.show() @Slot() def openDB(self): #open a file dialog with a .db filter datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) datadir = datadir if os.path.exists(datadir) else os.path.expanduser( "~") fldg = QFileDialog(caption="Open DB File", directory=datadir, filter="DB files (*.db)") fldg.setFileMode(QFileDialog.ExistingFile) if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.connect(fldg.selectedFiles()[0]) self.mainWindow.settings.setValue("lastpath", fldg.selectedFiles()[0]) self.mainWindow.updateUI() self.mainWindow.tree.loadData(self.mainWindow.database) self.mainWindow.actions.actionShowColumns.trigger() @Slot() def openDBFolder(self): path = self.mainWindow.settings.value("lastpath", None) if (path is not None) and (os.path.exists(path)): if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": subprocess.Popen(["open", path]) else: subprocess.Popen(["xdg-open", path]) @Slot() def makeDB(self): #same as openDB-Slot, but now for creating a new one on the file system datadir = self.mainWindow.settings.value("lastpath", os.path.expanduser("~")) datadir = datadir if os.path.exists(datadir) else os.path.expanduser( "~") fldg = QFileDialog(caption="Save DB File", directory=datadir, filter="DB files (*.db)") fldg.setAcceptMode(QFileDialog.AcceptSave) fldg.setDefaultSuffix("db") if fldg.exec_(): self.mainWindow.timerWindow.cancelTimer() self.mainWindow.tree.treemodel.clear() self.mainWindow.database.createconnect(fldg.selectedFiles()[0]) self.mainWindow.settings.setValue("lastpath", fldg.selectedFiles()[0]) self.mainWindow.updateUI() @Slot() def deleteNodes(self): reply = QMessageBox.question( self.mainWindow, 'Delete Nodes', "Are you sure to delete all selected nodes?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply != QMessageBox.Yes: return progress = ProgressBar("Deleting data...", self.mainWindow) try: todo = self.mainWindow.tree.selectedIndexesAndChildren(True) progress.setMaximum(0) #len(todo) todo = list(todo) for index in todo: progress.step() self.mainWindow.tree.treemodel.deleteNode(index, delaycommit=True) if progress.wasCanceled: break finally: # commit the operation on the db-layer afterwards (delaycommit is True) self.mainWindow.tree.treemodel.commitNewNodes() progress.close() @Slot() def clipboardNodes(self): progress = ProgressBar("Copy to clipboard", self.mainWindow) indexes = self.mainWindow.tree.selectionModel().selectedRows() progress.setMaximum(len(indexes)) output = io.StringIO() try: writer = csv.writer(output, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL, doublequote=True, lineterminator='\r\n') #headers row = [ str(val) for val in self.mainWindow.tree.treemodel.getRowHeader() ] writer.writerow(row) #rows for no in range(len(indexes)): if progress.wasCanceled: break row = [ str(val) for val in self.mainWindow.tree.treemodel.getRowData(indexes[no]) ] writer.writerow(row) progress.step() clipboard = QApplication.clipboard() clipboard.setText(output.getvalue()) finally: output.close() progress.close() @Slot() def exportNodes(self): fldg = ExportFileDialog(self.mainWindow, filter="CSV Files (*.csv)") @Slot() def addNodes(self): if not self.mainWindow.database.connected: return False # makes the user add a new facebook object into the db dialog = QDialog(self.mainWindow) dialog.setWindowTitle("Add Nodes") layout = QVBoxLayout() label = QLabel( "One <b>Object ID</b> per line. You can provide additional JSON data after a pipe |" ) layout.addWidget(label) input = QPlainTextEdit() input.setMinimumWidth(500) input.LineWrapMode = QPlainTextEdit.NoWrap #input.acceptRichText=False input.setFocus() layout.addWidget(input) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) loadbutton = buttons.addButton("Load CSV", QDialogButtonBox.ResetRole) layout.addWidget(buttons) dialog.setLayout(layout) def createNodes(): newnodes = [ node.strip() for node in input.toPlainText().splitlines() ] self.mainWindow.tree.treemodel.addNodes(newnodes, True) self.mainWindow.tree.selectLastRow() dialog.close() def loadCSV(): datadir = os.path.dirname( self.mainWindow.settings.value('lastpath', '')) datadir = os.path.expanduser('~') if datadir == '' else datadir filename, filetype = QFileDialog.getOpenFileName( dialog, "Load CSV", datadir, "CSV files (*.csv)") if filename != "": with open(filename, encoding="UTF-8-sig") as csvfile: csvreader = csv.DictReader(csvfile, delimiter=';', quotechar='"', doublequote=True) rows = [row for row in csvreader] self.mainWindow.tree.treemodel.addNodes(rows) self.mainWindow.tree.selectLastRow() dialog.close() self.mainWindow.tree.selectLastRow() dialog.close() def close(): dialog.close() #connect the nested functions above to the dialog-buttons buttons.accepted.connect(createNodes) buttons.rejected.connect(close) loadbutton.clicked.connect(loadCSV) dialog.exec_() @Slot() def showColumns(self): self.mainWindow.tree.treemodel.setCustomColumns( self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def clearColumns(self): self.mainWindow.fieldList.clear() self.mainWindow.tree.treemodel.setCustomColumns([]) @Slot() def addColumn(self): key = self.mainWindow.detailTree.selectedKey() if key != '': self.mainWindow.fieldList.append(key) self.mainWindow.tree.treemodel.setCustomColumns( self.mainWindow.fieldList.toPlainText().splitlines()) @Slot() def addAllColumns(self): progress = ProgressBar("Analyzing data...", self.mainWindow) columns = self.mainWindow.fieldList.toPlainText().splitlines() try: indexes = self.mainWindow.tree.selectedIndexesAndChildren() indexes = list(indexes) progress.setMaximum(len(indexes)) for no in range(len(indexes)): progress.step() item = indexes[no].internalPointer() columns.extend([ key for key in recursiveIterKeys(item.data['response']) if not key in columns ]) if progress.wasCanceled: break finally: self.mainWindow.fieldList.setPlainText("\n".join(columns)) self.mainWindow.tree.treemodel.setCustomColumns(columns) progress.close() @Slot() def loadPreset(self): self.mainWindow.presetWindow.showPresets() @Slot() def loadAPIs(self): self.mainWindow.apiWindow.showWindow() @Slot() def jsonCopy(self): self.mainWindow.detailTree.copyToClipboard() @Slot() def unpackList(self): try: key = self.mainWindow.detailTree.selectedKey() if key == '': return False selected = self.mainWindow.tree.selectionModel().selectedRows() for item in selected: if not item.isValid(): continue treenode = item.internalPointer() treenode.unpackList(key) except Exception as e: self.mainWindow.logmessage(e) @Slot() def showFieldDoc(self): tree = self.mainWindow.detailTree key = tree.selectedKey() if key == '': return False key = tree.treemodel.fieldprefix + key if tree.treemodel.itemtype is not None: self.mainWindow.apiWindow.showDoc(tree.treemodel.module, tree.treemodel.basepath, tree.treemodel.path, key) @Slot() def expandAll(self): self.mainWindow.tree.expandAll() @Slot() def collapseAll(self): self.mainWindow.tree.collapseAll() @Slot() def selectNodes(self): self.mainWindow.selectNodesWindow.show() def queryNodes(self, indexes=None, apimodule=False, options=False): if not (self.mainWindow.tree.selectedCount() or self.mainWindow.allnodesCheckbox.isChecked() or (indexes is not None)): return (False) #Show progress window progress = ProgressBar("Fetching Data", parent=self.mainWindow) try: #Get global options globaloptions = {} globaloptions['threads'] = self.mainWindow.threadsEdit.value() globaloptions['speed'] = self.mainWindow.speedEdit.value() globaloptions['errors'] = self.mainWindow.errorEdit.value() globaloptions[ 'expand'] = self.mainWindow.autoexpandCheckbox.isChecked() globaloptions[ 'logrequests'] = self.mainWindow.logCheckbox.isChecked() globaloptions[ 'saveheaders'] = self.mainWindow.headersCheckbox.isChecked() globaloptions[ 'allnodes'] = self.mainWindow.allnodesCheckbox.isChecked() objecttypes = self.mainWindow.typesEdit.text().replace( ' ', '').split(',') level = self.mainWindow.levelEdit.value() - 1 #Get selected nodes if indexes is None: select_all = globaloptions['allnodes'] select_filter = {'level': level, 'objecttype': objecttypes} indexes = self.mainWindow.tree.selectedIndexesAndChildren( False, select_filter, select_all) elif isinstance(indexes, list): indexes = iter(indexes) # if (len(indexes) == 0): # return (False) #Update progress window #self.mainWindow.logmessage("Start fetching data for {} node(s).".format(len(indexes))) self.mainWindow.logmessage("Start fetching data.") totalnodes = 0 hasindexes = True progress.setMaximum(totalnodes) self.mainWindow.tree.treemodel.nodecounter = 0 #Init status messages statuscount = {} errorcount = 0 ratelimitcount = 0 allowedstatus = [ 'fetched (200)', 'downloaded (200)', 'fetched (202)', 'stream' ] #,'error (400)' if apimodule == False: apimodule = self.mainWindow.RequestTabs.currentWidget() if options == False: options = apimodule.getOptions() options.update(globaloptions) try: #Spawn Threadpool threadpool = ApiThreadPool(apimodule) threadpool.spawnThreads(options.get("threads", 1)) #Init input Queue #indexes = deque(list(indexes)) #Process Logging/Input/Output Queue while True: try: #Logging (sync logs in threads with main thread) msg = threadpool.getLogMessage() if msg is not None: self.mainWindow.logmessage(msg) # Jobs in: packages of 100 at a time jobsin = 0 while (hasindexes and (jobsin < 100)): index = next(indexes, False) if index: jobsin += 1 totalnodes += 1 if index.isValid(): treenode = index.internalPointer() job = { 'nodeindex': index, 'nodedata': deepcopy(treenode.data), 'options': deepcopy(options) } threadpool.addJob(job) else: threadpool.applyJobs() progress.setRemaining(threadpool.getJobCount()) progress.resetRate() hasindexes = False self.mainWindow.logmessage( "Added {} node(s) to queue.".format( totalnodes)) if jobsin > 0: progress.setMaximum(totalnodes) #Jobs out job = threadpool.getJob() #-Finished all nodes (sentinel)... if job is None: break #-Finished one node... elif 'progress' in job: progresskey = 'nodeprogress' + str( job.get('threadnumber', '')) # Update single progress if 'current' in job: percent = int((job.get('current', 0) * 100.0 / job.get('total', 1))) progress.showInfo( progresskey, "{}% of current node processed.".format( percent)) elif 'page' in job: if job.get('page', 0) > 1: progress.showInfo( progresskey, "{} page(s) of current node processed." .format(job.get('page', 0))) # Update total progress else: progress.removeInfo(progresskey) if not threadpool.suspended: progress.step() #-Add data... elif 'data' in job and (not progress.wasCanceled): if not job['nodeindex'].isValid(): continue # Add data treeindex = job['nodeindex'] treenode = treeindex.internalPointer() treenode.appendNodes(job['data'], job['options'], job['headers'], True) if options.get('expand', False): self.mainWindow.tree.setExpanded( treeindex, True) # Count status status = job['options'].get('querystatus', 'empty') if not status in statuscount: statuscount[status] = 1 else: statuscount[status] = statuscount[status] + 1 # Collect errors for automatic retry if not status in allowedstatus: threadpool.addError(job) errorcount += 1 # Detect rate limit ratelimit = job['options'].get('ratelimit', False) if ratelimit: ratelimitcount += 1 # Clear errors if not threadpool.suspended and ( status in allowedstatus) and not ratelimit: threadpool.clearRetry() errorcount = 0 ratelimitcount = 0 # Suspend on error elif (errorcount > (globaloptions['errors'] - 1)) or ( ratelimitcount > 0): threadpool.suspendJobs() if ratelimit: msg = "You reached the rate limit of the API." else: msg = "{} consecutive errors occurred.\nPlease check your settings.".format( errorcount) timeout = 60 * 5 #5 minutes # Adjust progress progress.setRemaining( threadpool.getJobCount() + threadpool.getRetryCount()) progress.showError(msg, timeout, ratelimitcount > 0) self.mainWindow.tree.treemodel.commitNewNodes() # Show info progress.showInfo( status, "{} response(s) with status: {}".format( statuscount[status], status)) progress.showInfo( 'newnodes', "{} new node(s) created".format( self.mainWindow.tree.treemodel.nodecounter) ) progress.showInfo( 'threads', "{} active thread(s)".format( threadpool.getThreadCount())) progress.setRemaining(threadpool.getJobCount()) # Custom info from modules info = job['options'].get('info', {}) for name, value in info.items(): progress.showInfo(name, value) # Abort elif progress.wasCanceled: progress.showInfo( 'cancel', "Disconnecting from stream, may take some time." ) threadpool.stopJobs() # Retry elif progress.wasResumed: if progress.wasRetried: threadpool.retryJobs() else: threadpool.clearRetry() errorcount = 0 ratelimitcount = 0 threadpool.resumeJobs() progress.setRemaining(threadpool.getJobCount()) progress.hideError() # Continue elif not threadpool.suspended: threadpool.resumeJobs() # Finished if not threadpool.hasJobs(): progress.showInfo( 'cancel', "Work finished, shutting down threads.") threadpool.stopJobs() #-Waiting... progress.computeRate() time.sleep(1.0 / 1000.0) finally: QApplication.processEvents() finally: request_summary = [ str(val) + " x " + key for key, val in statuscount.items() ] request_summary = ", ".join(request_summary) request_end = "Fetching completed" if not progress.wasCanceled else 'Fetching cancelled by user' self.mainWindow.logmessage( "{}, {} new node(s) created. Summary of responses: {}.". format(request_end, self.mainWindow.tree.treemodel.nodecounter, request_summary)) self.mainWindow.tree.treemodel.commitNewNodes() finally: progress.close() @Slot() def querySelectedNodes(self): self.queryNodes() @Slot() def setupTimer(self): #Get data level = self.mainWindow.levelEdit.value() - 1 indexes = self.mainWindow.tree.selectedIndexesAndChildren( True, { 'level': level, 'objecttype': ['seed', 'data', 'unpacked'] }) module = self.mainWindow.RequestTabs.currentWidget() options = module.getOptions() #show timer window self.mainWindow.timerWindow.setupTimer({ 'indexes': list(indexes), 'module': module, 'options': options }) @Slot() def timerStarted(self, time): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText( "Timer will be fired at " + time.toString("d MMM yyyy - hh:mm") + " ") @Slot() def timerStopped(self): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:black;}") self.mainWindow.timerStatus.setText("Timer stopped ") @Slot() def timerCountdown(self, countdown): self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.mainWindow.timerStatus.setText("Timer will be fired in " + str(countdown) + " seconds ") @Slot() def timerFired(self, data): self.mainWindow.timerStatus.setText("Timer fired ") self.mainWindow.timerStatus.setStyleSheet("QLabel {color:red;}") self.queryNodes(data.get('indexes', []), data.get('module', None), data.get('options', {}).copy()) @Slot() def treeNodeSelected(self, current): #show details self.mainWindow.detailTree.clear() if current.isValid(): item = current.internalPointer() self.mainWindow.detailTree.showDict(item.data['response'], item.data['querytype'], item.data['queryparams']) #select level level = 0 c = current while c.isValid(): level += 1 c = c.parent() self.mainWindow.levelEdit.setValue(level) #show node count selcount = self.mainWindow.tree.selectedCount() self.mainWindow.selectionStatus.setText( str(selcount) + ' node(s) selected ') self.actionQuery.setDisabled(selcount == 0)
class TabularViewMixin: """Provides the pivot table and its frozen table for the DS form.""" _PARAMETER_VALUE = "&Value" _INDEX_EXPANSION = "&Index" _RELATIONSHIP = "Re&lationship" _SCENARIO_ALTERNATIVE = "&Scenario" _PARAMETER = "parameter" _ALTERNATIVE = "alternative" _INDEX = "index" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._pending_index = None # current state of ui self.current_class_item = None # Current QModelIndex selected in one of the entity tree views self.current_class_type = None self.current_class_id = {} # Mapping from db_map to class_id self.current_class_name = None self.current_input_type = self._PARAMETER_VALUE self.filter_menus = {} self.class_pivot_preferences = {} self.PivotPreferences = namedtuple( "PivotPreferences", ["index", "columns", "frozen", "frozen_value"]) self.pivot_action_group = QActionGroup(self) self.populate_pivot_action_group() self.pivot_table_proxy = PivotTableSortFilterProxy() self.pivot_table_model = None self.frozen_table_model = FrozenTableModel(self) self.ui.pivot_table.setModel(self.pivot_table_proxy) self.ui.pivot_table.connect_spine_db_editor(self) self.ui.frozen_table.setModel(self.frozen_table_model) self.ui.frozen_table.verticalHeader().setDefaultSectionSize( self.default_row_height) def populate_pivot_action_group(self): actions = { input_type: self.pivot_action_group.addAction(QIcon(CharIconEngine(icon_code)), input_type) for input_type, icon_code in ( (self._PARAMETER_VALUE, "\uf292"), (self._INDEX_EXPANSION, "\uf12c"), (self._RELATIONSHIP, "\uf1b3"), (self._SCENARIO_ALTERNATIVE, "\uf008"), ) } for action in actions.values(): action.setCheckable(True) actions[self.current_input_type].setChecked(True) def connect_signals(self): """Connects signals to slots.""" super().connect_signals() self.ui.pivot_table.horizontalHeader().header_dropped.connect( self.handle_header_dropped) self.ui.pivot_table.verticalHeader().header_dropped.connect( self.handle_header_dropped) self.ui.frozen_table.header_dropped.connect(self.handle_header_dropped) self.ui.frozen_table.selectionModel().currentChanged.connect( self.change_frozen_value) self.pivot_action_group.triggered.connect(self.do_reload_pivot_table) self.ui.dockWidget_pivot_table.visibilityChanged.connect( self._handle_pivot_table_visibility_changed) self.ui.dockWidget_frozen_table.visibilityChanged.connect( self._handle_frozen_table_visibility_changed) def init_models(self): """Initializes models.""" super().init_models() self.clear_pivot_table() @Slot("QModelIndex", object) def _set_model_data(self, index, value): self.pivot_table_proxy.setData(index, value) @property def current_object_class_id_list(self): if self.current_class_type == "object_class": return [self.current_class_id] current_object_class_id_list = [ {} for _ in self.current_object_class_name_list ] for db_map, class_id in self.current_class_id.items(): relationship_class = self.db_mngr.get_item(db_map, "relationship_class", class_id) for k, id_ in enumerate( relationship_class["object_class_id_list"].split(",")): current_object_class_id_list[k][db_map] = int(id_) return current_object_class_id_list @property def current_object_class_name_list(self): db_map, class_id = next(iter(self.current_class_id.items())) if self.current_class_type == "object_class": return [ self.db_mngr.get_item(db_map, "object_class", class_id)["name"] ] relationship_class = self.db_mngr.get_item(db_map, "relationship_class", class_id) return fix_name_ambiguity( relationship_class["object_class_name_list"].split(",")) @property def current_object_class_ids(self): return dict( zip(self.current_object_class_name_list, self.current_object_class_id_list)) @staticmethod def _is_class_index(index): """Returns whether or not the given tree index is a class index. Args: index (QModelIndex): index from object or relationship tree Returns: bool """ return index.column() == 0 and not index.parent().parent().isValid() @Slot(bool) def _handle_pivot_table_visibility_changed(self, visible): if not visible: return self.ui.dockWidget_frozen_table.setVisible(True) if self._pending_index is not None: QTimer.singleShot( 100, lambda: self.reload_pivot_table(self._pending_index)) @Slot(bool) def _handle_frozen_table_visibility_changed(self, visible): if visible: self.ui.dockWidget_pivot_table.show() @Slot(dict) def _handle_object_tree_selection_changed(self, selected_indexes): super()._handle_object_tree_selection_changed(selected_indexes) current = self.ui.treeView_object.currentIndex() self._handle_entity_tree_current_changed(current) @Slot(dict) def _handle_relationship_tree_selection_changed(self, selected_indexes): super()._handle_relationship_tree_selection_changed(selected_indexes) current = self.ui.treeView_relationship.currentIndex() self._handle_entity_tree_current_changed(current) def _handle_entity_tree_current_changed(self, current_index): if self.current_input_type == self._SCENARIO_ALTERNATIVE: return if not self.ui.dockWidget_pivot_table.isVisible(): self._pending_index = current_index return self.reload_pivot_table(current_index=current_index) @staticmethod def _make_get_id(action): """Returns a function to compute the db_map-id tuple of an item.""" return { "add": lambda db_map, x: (db_map, x["id"]), "remove": lambda db_map, x: None }[action] def _get_db_map_entities(self): """Returns a dict mapping db maps to a list of dict entity items in the current class. Returns: dict """ entity_type = { "object_class": "object", "relationship_class": "relationship" }[self.current_class_type] return { db_map: self.db_mngr.get_items_by_field(db_map, entity_type, "class_id", class_id) for db_map, class_id in self.current_class_id.items() } def load_empty_relationship_data(self, db_map_class_objects=None): """Returns a dict containing all possible relationships in the current class. Args: db_map_class_objects (dict) Returns: dict: Key is db_map-object_id tuple, value is None. """ if db_map_class_objects is None: db_map_class_objects = dict() if self.current_class_type == "object_class": return {} data = {} for db_map in self.db_maps: object_id_lists = [] all_given_ids = set() for db_map_class_id in self.current_object_class_id_list: class_id = db_map_class_id.get(db_map) objects = self.db_mngr.get_items_by_field( db_map, "object", "class_id", class_id) ids = {item["id"]: None for item in objects} given_objects = db_map_class_objects.get(db_map, {}).get(class_id) if given_objects is not None: given_ids = {item["id"]: None for item in given_objects} ids.update(given_ids) all_given_ids.update(given_ids.keys()) object_id_lists.append(list(ids.keys())) db_map_data = { tuple((db_map, id_) for id_ in objects_ids) + (db_map, ): None for objects_ids in product(*object_id_lists) if not all_given_ids or all_given_ids.intersection(objects_ids) } data.update(db_map_data) return data def load_full_relationship_data(self, db_map_relationships=None, action="add"): """Returns a dict of relationships in the current class. Args: db_map_relationships (dict) Returns: dict: Key is db_map-object id tuple, value is relationship id. """ if self.current_class_type == "object_class": return {} if db_map_relationships is None: db_map_relationships = self._get_db_map_entities() get_id = self._make_get_id(action) return { tuple((db_map, int(id_)) for id_ in rel["object_id_list"].split(',')) + (db_map, ): get_id(db_map, rel) for db_map, relationships in db_map_relationships.items() for rel in relationships } def load_relationship_data(self): """Returns a dict that merges empty and full relationship data. Returns: dict: Key is object id tuple, value is True if a relationship exists, False otherwise. """ data = self.load_empty_relationship_data() data.update(self.load_full_relationship_data()) return data def load_scenario_alternative_data(self, db_map_scenarios=None, db_map_alternatives=None): """Returns a dict containing all scenario alternatives. Returns: dict: Key is db_map-id tuple, value is None or rank. """ if db_map_scenarios is None: db_map_scenarios = { db_map: self.db_mngr.get_items(db_map, "scenario") for db_map in self.db_maps } if db_map_alternatives is None: db_map_alternatives = { db_map: self.db_mngr.get_items(db_map, "alternative") for db_map in self.db_maps } data = {} for db_map in self.db_maps: scenario_alternative_ranks = { x["id"]: { alt_id: k + 1 for k, alt_id in enumerate( self.db_mngr.get_scenario_alternative_id_list( db_map, x["id"])) } for x in db_map_scenarios.get(db_map, []) } alternative_ids = [ x["id"] for x in db_map_alternatives.get(db_map, []) ] db_map_data = {((db_map, scen_id), (db_map, alt_id), db_map): alternative_ranks.get(alt_id) for scen_id, alternative_ranks in scenario_alternative_ranks.items() for alt_id in alternative_ids} data.update(db_map_data) return data def _get_parameter_value_or_def_ids(self, item_type): """Returns a dict mapping db maps to a list of integer parameter (value or def) ids from the current class. Args: item_type (str): either "parameter_value" or "parameter_definition" Returns: dict """ class_id_field = { "object_class": "object_class_id", "relationship_class": "relationship_class_id" }[self.current_class_type] return { db_map: [ x["id"] for x in self.db_mngr.get_items_by_field( db_map, item_type, class_id_field, class_id) ] for db_map, class_id in self.current_class_id.items() } def _get_db_map_parameter_values_or_defs(self, item_type): """Returns a dict mapping db maps to list of dict parameter (value or def) items from the current class. Args: item_type (str): either "parameter_value" or "parameter_definition" Returns: dict """ db_map_ids = self._get_parameter_value_or_def_ids(item_type) return { db_map: [self.db_mngr.get_item(db_map, item_type, id_) for id_ in ids] for db_map, ids in db_map_ids.items() } def load_empty_parameter_value_data(self, db_map_entities=None, db_map_parameter_ids=None, db_map_alternative_ids=None): """Returns a dict containing all possible combinations of entities and parameters for the current class in all db_maps. Args: db_map_entities (dict, optional): if given, only load data for these db maps and entities db_map_parameter_ids (dict, optional): if given, only load data for these db maps and parameter definitions db_map_alternative_ids (dict, optional): if given, only load data for these db maps and alternatives Returns: dict: Key is a tuple object_id, ..., parameter_id, value is None. """ if db_map_entities is None: db_map_entities = self._get_db_map_entities() if db_map_parameter_ids is None: db_map_parameter_ids = { db_map: [(db_map, id_) for id_ in ids] for db_map, ids in self._get_parameter_value_or_def_ids( "parameter_definition").items() } if db_map_alternative_ids is None: db_map_alternative_ids = { db_map: [(db_map, a["id"]) for a in self.db_mngr.get_items(db_map, "alternative")] for db_map in self.db_maps } if self.current_class_type == "relationship_class": db_map_entity_ids = { db_map: [ tuple((db_map, int(id_)) for id_ in e["object_id_list"].split(',')) for e in entities ] for db_map, entities in db_map_entities.items() } else: db_map_entity_ids = { db_map: [((db_map, e["id"]), ) for e in entities] for db_map, entities in db_map_entities.items() } if not db_map_entity_ids: db_map_entity_ids = { db_map: [ tuple((db_map, None) for _ in self.current_object_class_id_list) ] for db_map in self.db_maps } if not db_map_parameter_ids: db_map_parameter_ids = { db_map: [(db_map, None)] for db_map in self.db_maps } return { entity_id + (parameter_id, alt_id, db_map): None for db_map in self.db_maps for entity_id in db_map_entity_ids.get(db_map, []) for parameter_id in db_map_parameter_ids.get(db_map, []) for alt_id in db_map_alternative_ids.get(db_map, []) } def load_full_parameter_value_data(self, db_map_parameter_values=None, action="add"): """Returns a dict of parameter values for the current class. Args: db_map_parameter_values (list, optional) action (str) Returns: dict: Key is a tuple object_id, ..., parameter_id, value is the parameter_value. """ if db_map_parameter_values is None: db_map_parameter_values = self._get_db_map_parameter_values_or_defs( "parameter_value") get_id = self._make_get_id(action) if self.current_class_type == "object_class": return {((db_map, x["object_id"]), (db_map, x["parameter_id"]), (db_map, x["alternative_id"]), db_map): get_id(db_map, x) for db_map, items in db_map_parameter_values.items() for x in items} return { tuple((db_map, int(id_)) for id_ in x["object_id_list"].split(',')) + ((db_map, x["parameter_id"]), (db_map, x["alternative_id"]), db_map): get_id(db_map, x) for db_map, items in db_map_parameter_values.items() for x in items } def _indexes(self, value): if value is None: return [] db_map, id_ = value return self.db_mngr.get_value_indexes(db_map, "parameter_value", id_) def load_empty_expanded_parameter_value_data(self, db_map_entities=None, db_map_parameter_ids=None, db_map_alternative_ids=None): """Makes a dict of expanded parameter values for the current class. Args: db_map_parameter_values (list, optional) action (str) Returns: dict: mapping from unique value id tuple to value tuple """ data = self.load_empty_parameter_value_data(db_map_entities, db_map_parameter_ids, db_map_alternative_ids) return { key[:-3] + ((None, index), ) + key[-3:]: value for key, value in data.items() for index in self._indexes(value) } def load_full_expanded_parameter_value_data(self, db_map_parameter_values=None, action="add"): """Makes a dict of expanded parameter values for the current class. Args: db_map_parameter_values (list, optional) action (str) Returns: dict: mapping from unique value id tuple to value tuple """ data = self.load_full_parameter_value_data(db_map_parameter_values, action) return { key[:-3] + ((None, index), ) + key[-3:]: value for key, value in data.items() for index in self._indexes(value) } def load_parameter_value_data(self): """Returns a dict that merges empty and full parameter_value data. Returns: dict: Key is a tuple object_id, ..., parameter_id, value is the parameter_value or None if not specified. """ data = self.load_empty_parameter_value_data() data.update(self.load_full_parameter_value_data()) return data def load_expanded_parameter_value_data(self): """ Returns all permutations of entities as well as parameter indexes and values for the current class. Returns: dict: Key is a tuple object_id, ..., index, while value is None. """ data = self.load_empty_expanded_parameter_value_data() data.update(self.load_full_expanded_parameter_value_data()) return data def get_pivot_preferences(self): """Returns saved pivot preferences. Returns: tuple, NoneType: pivot tuple, or None if no preference stored """ selection_key = (self.current_class_name, self.current_class_type, self.current_input_type) if selection_key in self.class_pivot_preferences: rows = self.class_pivot_preferences[selection_key].index columns = self.class_pivot_preferences[selection_key].columns frozen = self.class_pivot_preferences[selection_key].frozen frozen_value = self.class_pivot_preferences[ selection_key].frozen_value return (rows, columns, frozen, frozen_value) return None def reload_pivot_table(self, current_index=None): """Updates current class (type and id) and reloads pivot table for it.""" self._pending_index = None if current_index is not None: self.current_class_item = self._get_current_class_item( current_index) if self.current_class_item is None: self.current_class_id = {} self.clear_pivot_table() return class_id = self.current_class_item.db_map_ids if self.current_class_id == class_id: return self.clear_pivot_table() self.current_class_type = self.current_class_item.item_type self.current_class_id = class_id self.current_class_name = self.current_class_item.display_data self.do_reload_pivot_table() @staticmethod def _get_current_class_item(current_index): item = current_index.model().item_from_index(current_index) while item.item_type != "root": if item.item_type in ("object_class", "relationship_class"): return item item = item.parent_item return None @busy_effect @Slot("QAction") def do_reload_pivot_table(self, action=None): """Reloads pivot table. """ qApp.processEvents() # pylint: disable=undefined-variable if action is None: action = self.pivot_action_group.checkedAction() self.current_input_type = action.text() if not self._can_build_pivot_table(): return self.pivot_table_model = { self._PARAMETER_VALUE: ParameterValuePivotTableModel, self._RELATIONSHIP: RelationshipPivotTableModel, self._INDEX_EXPANSION: IndexExpansionPivotTableModel, self._SCENARIO_ALTERNATIVE: ScenarioAlternativePivotTableModel, }[self.current_input_type](self) self.pivot_table_proxy.setSourceModel(self.pivot_table_model) delegate = self.pivot_table_model.make_delegate(self) self.ui.pivot_table.setItemDelegate(delegate) self.pivot_table_model.modelReset.connect(self.make_pivot_headers) pivot = self.get_pivot_preferences() self.wipe_out_filter_menus() self.pivot_table_model.call_reset_model(pivot) self.pivot_table_proxy.clear_filter() self.reload_frozen_table() def _can_build_pivot_table(self): if self.current_input_type != self._SCENARIO_ALTERNATIVE and not self.current_class_id: return False if self.current_input_type == self._RELATIONSHIP and self.current_class_type != "relationship_class": return False return True def clear_pivot_table(self): self.wipe_out_filter_menus() if self.pivot_table_model: self.pivot_table_model.clear_model() self.pivot_table_proxy.clear_filter() if self.frozen_table_model: self.frozen_table_model.clear_model() def wipe_out_filter_menus(self): while self.filter_menus: _, menu = self.filter_menus.popitem() menu.wipe_out() @Slot() def make_pivot_headers(self): """ Turns top left indexes in the pivot table into TabularViewHeaderWidget. """ top_indexes, left_indexes = self.pivot_table_model.top_left_indexes() for index in left_indexes: proxy_index = self.pivot_table_proxy.mapFromSource(index) widget = self.create_header_widget( proxy_index.data(Qt.DisplayRole), "columns") self.ui.pivot_table.setIndexWidget(proxy_index, widget) for index in top_indexes: proxy_index = self.pivot_table_proxy.mapFromSource(index) widget = self.create_header_widget( proxy_index.data(Qt.DisplayRole), "rows") self.ui.pivot_table.setIndexWidget(proxy_index, widget) QTimer.singleShot(0, self._resize_pivot_header_columns) @Slot() def _resize_pivot_header_columns(self): top_indexes, _ = self.pivot_table_model.top_left_indexes() for index in top_indexes: self.ui.pivot_table.resizeColumnToContents(index.column()) def make_frozen_headers(self): """ Turns indexes in the first row of the frozen table into TabularViewHeaderWidget. """ for column in range(self.frozen_table_model.columnCount()): index = self.frozen_table_model.index(0, column) widget = self.create_header_widget(index.data(Qt.DisplayRole), "frozen", with_menu=False) self.ui.frozen_table.setIndexWidget(index, widget) column_width = self.ui.frozen_table.horizontalHeader().sectionSize( column) header_width = widget.size().width() width = max(column_width, header_width) self.ui.frozen_table.horizontalHeader().resizeSection( column, width) def create_filter_menu(self, identifier): """Returns a filter menu for given given object_class identifier. Args: identifier (int) Returns: TabularViewFilterMenu """ if identifier not in self.filter_menus: pivot_top_left_header = self.pivot_table_model.top_left_headers[ identifier] data_to_value = pivot_top_left_header.header_data self.filter_menus[identifier] = menu = TabularViewFilterMenu( self, identifier, data_to_value, show_empty=False) index_values = dict.fromkeys( self.pivot_table_model.model.index_values.get(identifier, [])) index_values.pop(None, None) menu.set_filter_list(index_values.keys()) menu.filterChanged.connect(self.change_filter) return self.filter_menus[identifier] def create_header_widget(self, identifier, area, with_menu=True): """ Returns a TabularViewHeaderWidget for given object_class identifier. Args: identifier (str) area (str) with_menu (bool) Returns: TabularViewHeaderWidget """ menu = self.create_filter_menu(identifier) if with_menu else None widget = TabularViewHeaderWidget(identifier, area, menu=menu, parent=self) widget.header_dropped.connect(self.handle_header_dropped) return widget @staticmethod def _get_insert_index(pivot_list, catcher, position): """Returns an index for inserting a new element in the given pivot list. Returns: int """ if isinstance(catcher, TabularViewHeaderWidget): i = pivot_list.index(catcher.identifier) if position == "after": i += 1 else: i = 0 return i @Slot(object, object, str) def handle_header_dropped(self, dropped, catcher, position=""): """ Updates pivots when a header is dropped. Args: dropped (TabularViewHeaderWidget) catcher (TabularViewHeaderWidget, PivotTableHeaderView, FrozenTableView) position (str): either "before", "after", or "" """ top_indexes, left_indexes = self.pivot_table_model.top_left_indexes() rows = [index.data(Qt.DisplayRole) for index in top_indexes] columns = [index.data(Qt.DisplayRole) for index in left_indexes] frozen = self.frozen_table_model.headers dropped_list = { "columns": columns, "rows": rows, "frozen": frozen }[dropped.area] catcher_list = { "columns": columns, "rows": rows, "frozen": frozen }[catcher.area] dropped_list.remove(dropped.identifier) i = self._get_insert_index(catcher_list, catcher, position) catcher_list.insert(i, dropped.identifier) if dropped.area == "frozen" or catcher.area == "frozen": if frozen: frozen_values = self.find_frozen_values(frozen) self.frozen_table_model.reset_model(frozen_values, frozen) self.ui.frozen_table.resizeColumnsToContents() self.make_frozen_headers() else: self.frozen_table_model.clear_model() frozen_value = self.get_frozen_value( self.ui.frozen_table.currentIndex()) self.pivot_table_model.set_pivot(rows, columns, frozen, frozen_value) # save current pivot self.class_pivot_preferences[( self.current_class_name, self.current_class_type, self.current_input_type)] = self.PivotPreferences( rows, columns, frozen, frozen_value) self.make_pivot_headers() def get_frozen_value(self, index): """ Returns the value in the frozen table corresponding to the given index. Args: index (QModelIndex) Returns: tuple """ if not index.isValid(): return tuple(None for _ in range(self.frozen_table_model.columnCount())) return self.frozen_table_model.row(index) @Slot("QModelIndex", "QModelIndex") def change_frozen_value(self, current, previous): """Sets the frozen value from selection in frozen table. """ frozen_value = self.get_frozen_value(current) self.pivot_table_model.set_frozen_value(frozen_value) # store pivot preferences self.class_pivot_preferences[( self.current_class_name, self.current_class_type, self.current_input_type)] = self.PivotPreferences( self.pivot_table_model.model.pivot_rows, self.pivot_table_model.model.pivot_columns, self.pivot_table_model.model.pivot_frozen, self.pivot_table_model.model.frozen_value, ) @Slot(str, set, bool) def change_filter(self, identifier, valid_values, has_filter): if has_filter: self.pivot_table_proxy.set_filter(identifier, valid_values) else: self.pivot_table_proxy.set_filter( identifier, None) # None means everything passes def reload_frozen_table(self): """Resets the frozen model according to new selection in entity trees.""" if not self.pivot_table_model: return frozen = self.pivot_table_model.model.pivot_frozen frozen_value = self.pivot_table_model.model.frozen_value frozen_values = self.find_frozen_values(frozen) self.frozen_table_model.reset_model(frozen_values, frozen) self.ui.frozen_table.resizeColumnsToContents() self.make_frozen_headers() if frozen_value in frozen_values: # update selected row ind = frozen_values.index(frozen_value) self.ui.frozen_table.selectionModel().blockSignals( True) # prevent selectionChanged signal when updating self.ui.frozen_table.selectRow(ind + 1) self.ui.frozen_table.selectionModel().blockSignals(False) else: # frozen value not found, remove selection self.ui.frozen_table.selectionModel().blockSignals( True) # prevent selectionChanged signal when updating self.ui.frozen_table.clearSelection() self.ui.frozen_table.selectionModel().blockSignals(False) def find_frozen_values(self, frozen): """Returns a list of tuples containing unique values (object ids) for the frozen indexes (object_class ids). Args: frozen (tuple(int)): A tuple of currently frozen indexes Returns: list(tuple(list(int))) """ return list( dict.fromkeys( zip(*[ self.pivot_table_model.model.index_values.get(k, []) for k in frozen ])).keys()) # TODO: Move this to the models? @staticmethod def refresh_table_view(table_view): top_left = table_view.indexAt(table_view.rect().topLeft()) bottom_right = table_view.indexAt(table_view.rect().bottomRight()) if not bottom_right.isValid(): model = table_view.model() bottom_right = table_view.model().index(model.rowCount() - 1, model.columnCount() - 1) table_view.model().dataChanged.emit(top_left, bottom_right) @Slot(str) def update_filter_menus(self, action): for identifier, menu in self.filter_menus.items(): index_values = dict.fromkeys( self.pivot_table_model.model.index_values.get(identifier, [])) index_values.pop(None, None) if action == "add": menu.add_items_to_filter_list(list(index_values.keys())) elif action == "remove": previous = menu._filter._filter_model._data_set menu.remove_items_from_filter_list( list(previous - index_values.keys())) self.reload_frozen_table() def receive_objects_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_objects_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_relationships_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_relationships_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_parameter_definitions_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_parameter_definitions_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_alternatives_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_alternatives_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_parameter_values_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_parameter_values_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_scenarios_added_or_removed(self, db_map_data, action): if not self.pivot_table_model: return if self.pivot_table_model.receive_scenarios_added_or_removed( db_map_data, action): self.update_filter_menus(action) def receive_db_map_data_updated(self, db_map_data, get_class_id): if not self.pivot_table_model: return for db_map, items in db_map_data.items(): for item in items: if get_class_id(item) == self.current_class_id.get(db_map): self.refresh_table_view(self.ui.pivot_table) self.refresh_table_view(self.ui.frozen_table) self.make_pivot_headers() return def receive_classes_updated(self, db_map_data): if not self.pivot_table_model: return for db_map, items in db_map_data.items(): for item in items: if item["id"] == self.current_class_id.get(db_map): self.do_reload_pivot_table() return def receive_classes_removed(self, db_map_data): if not self.pivot_table_model: return for db_map, items in db_map_data.items(): for item in items: if item["id"] == self.current_class_id.get(db_map): self.current_class_type = None self.current_class_id = {} self.clear_pivot_table() return def receive_alternatives_added(self, db_map_data): """Reacts to alternatives added event.""" super().receive_alternatives_added(db_map_data) self.receive_alternatives_added_or_removed(db_map_data, action="add") def receive_scenarios_added(self, db_map_data): """Reacts to scenarios added event.""" super().receive_scenarios_added(db_map_data) self.receive_scenarios_added_or_removed(db_map_data, action="add") def receive_objects_added(self, db_map_data): """Reacts to objects added event.""" super().receive_objects_added(db_map_data) self.receive_objects_added_or_removed(db_map_data, action="add") def receive_relationships_added(self, db_map_data): """Reacts to relationships added event.""" super().receive_relationships_added(db_map_data) self.receive_relationships_added_or_removed(db_map_data, action="add") def receive_parameter_definitions_added(self, db_map_data): """Reacts to parameter definitions added event.""" super().receive_parameter_definitions_added(db_map_data) self.receive_parameter_definitions_added_or_removed(db_map_data, action="add") def receive_parameter_values_added(self, db_map_data): """Reacts to parameter values added event.""" super().receive_parameter_values_added(db_map_data) self.receive_parameter_values_added_or_removed(db_map_data, action="add") def receive_alternatives_updated(self, db_map_data): """Reacts to alternatives updated event.""" super().receive_alternatives_updated(db_map_data) if not self.pivot_table_model: return self.refresh_table_view(self.ui.pivot_table) self.refresh_table_view(self.ui.frozen_table) self.make_pivot_headers() def receive_object_classes_updated(self, db_map_data): """Reacts to object classes updated event.""" super().receive_object_classes_updated(db_map_data) self.receive_classes_updated(db_map_data) def receive_relationship_classes_updated(self, db_map_data): """Reacts to relationship classes updated event.""" super().receive_relationship_classes_updated(db_map_data) self.receive_classes_updated(db_map_data) def receive_objects_updated(self, db_map_data): """Reacts to objects updated event.""" super().receive_objects_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["class_id"]) def receive_relationships_updated(self, db_map_data): """Reacts to relationships updated event.""" super().receive_relationships_updated(db_map_data) self.receive_db_map_data_updated(db_map_data, get_class_id=lambda x: x["class_id"]) def receive_parameter_values_updated(self, db_map_data): """Reacts to parameter values added event.""" super().receive_parameter_values_updated(db_map_data) self.receive_db_map_data_updated( db_map_data, get_class_id=lambda x: x.get("object_class_id") or x.get( "relationship_class_id")) def receive_parameter_definitions_updated(self, db_map_data): """Reacts to parameter definitions updated event.""" super().receive_parameter_definitions_updated(db_map_data) self.receive_db_map_data_updated( db_map_data, get_class_id=lambda x: x.get("object_class_id") or x.get( "relationship_class_id")) def receive_scenarios_updated(self, db_map_data): super().receive_scenarios_updated(db_map_data) if not self.pivot_table_model: return self.pivot_table_model.receive_scenarios_updated(db_map_data) def receive_alternatives_removed(self, db_map_data): """Reacts to alternatives removed event.""" super().receive_alternatives_removed(db_map_data) self.receive_alternatives_added_or_removed(db_map_data, action="remove") def receive_scenarios_removed(self, db_map_data): """Reacts to scenarios removed event.""" super().receive_scenarios_removed(db_map_data) self.receive_scenarios_added_or_removed(db_map_data, action="remove") def receive_object_classes_removed(self, db_map_data): """Reacts to object classes removed event.""" super().receive_object_classes_removed(db_map_data) self.receive_classes_removed(db_map_data) def receive_relationship_classes_removed(self, db_map_data): """Reacts to relationship classes remove event.""" super().receive_relationship_classes_removed(db_map_data) self.receive_classes_removed(db_map_data) def receive_objects_removed(self, db_map_data): """Reacts to objects removed event.""" super().receive_objects_removed(db_map_data) self.receive_objects_added_or_removed(db_map_data, action="remove") def receive_relationships_removed(self, db_map_data): """Reacts to relationships removed event.""" super().receive_relationships_removed(db_map_data) self.receive_relationships_added_or_removed(db_map_data, action="remove") def receive_parameter_definitions_removed(self, db_map_data): """Reacts to parameter definitions removed event.""" super().receive_parameter_definitions_removed(db_map_data) self.receive_parameter_definitions_added_or_removed(db_map_data, action="remove") def receive_parameter_values_removed(self, db_map_data): """Reacts to parameter values removed event.""" super().receive_parameter_values_removed(db_map_data) self.receive_parameter_values_added_or_removed(db_map_data, action="remove") def receive_session_rolled_back(self, db_maps): """Reacts to session rolled back event.""" super().receive_session_rolled_back(db_maps) self.reload_pivot_table()
class ProjectsMenu(QMenu): """The ProjectsMenu class provides a menu with all the currently active project, and allows changing between them when clicking.""" def __init__(self, project_manager: "ProjectManagerGUI", parent: "QWidget" = None): super().__init__("Projects", parent) # Components self.__project_manager = project_manager # The menu is regenerated each time it needs to change: When a project is # opened, or when a project is removed self.__project_manager.project_added.connect( self.__add_project_to_menu) self.__project_manager.active_project_changed.connect( self.__set_active_project) self.__project_manager.project_removed.connect( lambda: self.__generate_menu_from_projects()) # Actions self.__projects_actions_group = QActionGroup(self) self.__generate_menu_from_projects() self.__set_active_project(self.__project_manager.active) def mouseReleaseEvent(self, event): """Ignores right clicks on the QMenu (Avoids unintentional clicks)""" if event.button() == Qt.RightButton: # Ignore right clicks return super().mouseReleaseEvent(event) def __generate_menu_from_projects(self): """Adds an entry to the menu for each Project in the ProjectManagerGUI""" self.clear() for project in self.__project_manager.projects: self.__add_project_to_menu(project) def __add_project_to_menu(self, project: "Project"): """Creates a new entry for the passed project. Clicking on the project will make it the active project on the project manager.""" project_action = QAction(project.name, self) project_action.setCheckable(True) index = self.__project_manager.projects.index(project) project_action.triggered.connect( lambda _=False, index=index: self.__project_manager. set_active_project(index)) self.__projects_actions_group.addAction(project_action) self.addAction(project_action) LOGGER.debug("Created ProjectMenu Action (Index %s): %s", index, project_action) def __set_active_project(self, project: "Project"): """Makes the passed project the active one on the menu (will be marked).""" index = self.__project_manager.projects.index(project) self.actions()[index].setChecked(True) LOGGER.debug("Project with index %s is now active on menu: %s", index, project)
class ContextMenu(QObject): def __init__(self, quarterwidget): QObject.__init__(self, quarterwidget) #QObject.__init__(quarterwidget) self._quarterwidget = quarterwidget self._rendermanager = self._quarterwidget.getSoRenderManager() self.contextmenu = QMenu() self.functionsmenu = QMenu("Functions") self.rendermenu = QMenu("Render Mode") self.stereomenu = QMenu("Stereo Mode") self.transparencymenu = QMenu("Transparency Type") self.functionsgroup = QActionGroup(self) self.stereomodegroup = QActionGroup(self) self.rendermodegroup = QActionGroup(self) self.transparencytypegroup = QActionGroup(self) self.rendermodes = [] self.rendermodes.append((SoRenderManager.AS_IS, "as is")) self.rendermodes.append((SoRenderManager.WIREFRAME, "wireframe")) self.rendermodes.append( (SoRenderManager.WIREFRAME_OVERLAY, "wireframe overlay")) self.rendermodes.append((SoRenderManager.POINTS, "points")) self.rendermodes.append((SoRenderManager.HIDDEN_LINE, "hidden line")) self.rendermodes.append((SoRenderManager.BOUNDING_BOX, "bounding box")) self.stereomodes = [] self.stereomodes.append((SoRenderManager.MONO, "mono")) self.stereomodes.append((SoRenderManager.ANAGLYPH, "anaglyph")) self.stereomodes.append((SoRenderManager.QUAD_BUFFER, "quad buffer")) self.stereomodes.append( (SoRenderManager.INTERLEAVED_ROWS, "interleaved rows")) self.stereomodes.append( (SoRenderManager.INTERLEAVED_COLUMNS, "interleaved columns")) self.transparencytypes = [] self.transparencytypes.append((SoGLRenderAction.NONE, "none")) self.transparencytypes.append( (SoGLRenderAction.SCREEN_DOOR, "screen door")) self.transparencytypes.append((SoGLRenderAction.ADD, "add")) self.transparencytypes.append( (SoGLRenderAction.DELAYED_ADD, "delayed add")) self.transparencytypes.append( (SoGLRenderAction.SORTED_OBJECT_ADD, "sorted object add")) self.transparencytypes.append((SoGLRenderAction.BLEND, "blend")) self.transparencytypes.append( (SoGLRenderAction.DELAYED_BLEND, "delayed blend")) self.transparencytypes.append( (SoGLRenderAction.SORTED_OBJECT_BLEND, "sorted object blend")) self.transparencytypes.append( (SoGLRenderAction.SORTED_OBJECT_SORTED_TRIANGLE_ADD, "sorted object sorted triangle add")) self.transparencytypes.append( (SoGLRenderAction.SORTED_OBJECT_SORTED_TRIANGLE_BLEND, "sorted object sorted triangle blend")) self.transparencytypes.append( (SoGLRenderAction.SORTED_LAYERS_BLEND, "sorted layers blend")) self.rendermodeactions = [] for first, second in self.rendermodes: action = QAction(second, self) action.setCheckable(True) action.setChecked(self._rendermanager.getRenderMode() == first) action.setData(first) self.rendermodeactions.append(action) self.rendermodegroup.addAction(action) self.rendermenu.addAction(action) self.stereomodeactions = [] for first, second in self.stereomodes: action = QAction(second, self) action.setCheckable(True) action.setChecked(self._rendermanager.getStereoMode() == first) action.setData(first) self.stereomodeactions.append(action) self.stereomodegroup.addAction(action) self.stereomenu.addAction(action) self.transparencytypeactions = [] for first, second in self.transparencytypes: action = QAction(second, self) action.setCheckable(True) action.setChecked(self._rendermanager.getGLRenderAction(). getTransparencyType() == first) action.setData(first) self.transparencytypeactions.append(action) self.transparencytypegroup.addAction(action) self.transparencymenu.addAction(action) viewall = QAction("View All", self) seek = QAction("Seek", self) self.functionsmenu.addAction(viewall) self.functionsmenu.addAction(seek) self.connect(seek, QtCore.SIGNAL("triggered(bool)"), self.seek) self.connect(viewall, QtCore.SIGNAL("triggered(bool)"), self.viewAll) self.connect(self.rendermodegroup, QtCore.SIGNAL("triggered(QAction *)"), self.changeRenderMode) self.connect(self.stereomodegroup, QtCore.SIGNAL("triggered(QAction *)"), self.changeStereoMode) self.connect(self.transparencytypegroup, QtCore.SIGNAL("triggered(QAction *)"), self.changeTransparencyType) self.contextmenu.addMenu(self.functionsmenu) self.contextmenu.addMenu(self.rendermenu) self.contextmenu.addMenu(self.stereomenu) self.contextmenu.addMenu(self.transparencymenu) def __del__(self): del self.functionsmenu del self.rendermenu del self.stereomenu del self.transparencymenu del self.contextmenu def getMenu(self): return self.contextmenu def exec_(self, pos): self._processEvent("sim.coin3d.coin.PopupMenuOpen") self.contextmenu.exec_(pos) def seek(self, checked): self._processEvent("sim.coin3d.coin.navigation.Seek") def viewAll(self, checked): self._processEvent("sim.coin3d.coin.navigation.ViewAll") def _processEvent(self, eventname): eventmanager = self._quarterwidget.getSoEventManager() for c in range(eventmanager.getNumSoScXMLStateMachines()): sostatemachine = eventmanager.getSoScXMLStateMachine(c) sostatemachine.queueEvent(coin.SbName(eventname)) sostatemachine.processEventQueue() def changeRenderMode(self, action): try: self._rendermanager.setRenderMode(action.data().toInt()[0]) except AttributeError: self._rendermanager.setRenderMode(action.data()) def changeStereoMode(self, action): try: self._rendermanager.setStereoMode(action.data().toInt()[0]) except AttributeError: self._rendermanager.setStereoMode(action.data()) def changeTransparencyType(self, action): try: self._quarterwidget.setTransparencyType(action.data().toInt()[0]) except AttributeError: self._quarterwidget.setTransparencyType(action.data())
class MainWindow(QMainWindow, Ui_MainWindow): """ The main GUI window """ HELP_URL = "https://www.github.com/jakoma02/pyCovering" model_type_changed = Signal() view_type_changed = Signal() model_changed = Signal(GeneralCoveringModel) view_changed = Signal(GeneralView) info_updated = Signal(GeneralCoveringModel, GeneralView) settings_changed = Signal() def __init__(self): QMainWindow.__init__(self) self.model = None self.view = None self.setupUi(self) self.create_action_groups() # A dict Action name -> GeneralView, so that we can set the # correct view upon view type action trigger self.action_views = dict() self.actionAbout_2.triggered.connect(self.show_about_dialog) self.actionDocumentation.triggered.connect(self.show_help) self.actionChange_dimensions.triggered.connect( self.show_dimensions_dialog) self.actionChange_tile_size.triggered.connect( self.show_block_size_dialog) self.actionGenerate.triggered.connect(self.start_covering) self.model_type_changed.connect(self.update_model_type) self.model_type_changed.connect(self.update_view_type_menu) self.model_type_changed.connect(self.update_constraints_menu) self.model_type_changed.connect(self.enable_model_menu_buttons) self.model_changed.connect( lambda _: self.info_updated.emit(self.model, self.view)) self.view_type_changed.connect(self.update_view_type) self.view_changed.connect( lambda _: self.info_updated.emit(self.model, self.view)) self.info_updated.connect(self.infoText.update) self.info_updated.connect(self.update_view) self.tiles_list_model = BlockListModel() self.tilesList.setModel(self.tiles_list_model) self.model_changed.connect(self.tiles_list_model.update_data) self.tiles_list_model.checkedChanged.connect(self.set_block_visibility) self.model_changed.emit(self.model) self.update_view_type_menu() def set_block_visibility(self, block, visible): """ Update model visibility based on block list checkbox change """ block.visible = visible self.model_changed.emit(self.model) def show_about_dialog(self): """ Shows the "About" dialog """ dialog = AboutDialog(self) dialog.open() def start_covering(self): """ Starts covering, shows the corresponding dialog """ if self.model is None: QMessageBox.warning(self, "No model", "No model selected!") return self.model.reset() self.thread = GenerateModelThread(self.model) dialog = CoveringDialog(self) dialog.rejected.connect(self.cancel_covering) self.thread.success.connect(dialog.accept) self.thread.success.connect(self.covering_success) self.thread.failed.connect(dialog.reject) self.thread.failed.connect(self.covering_failed) self.thread.done.connect(lambda: self.model_changed.emit(self.model)) self.thread.start() dialog.open() def show_block_size_dialog(self): """ Shows "Change block size" dialog """ if self.model is None: QMessageBox.warning(self, "No model", "No model selected!") return curr_min = self.model.min_block_size curr_max = self.model.max_block_size dialog = BlockSizeDialog(self) dialog.sizesAccepted.connect(self.block_sizes_accepted) dialog.set_values(curr_min, curr_max) dialog.open() def show_dimensions_dialog(self): """ Shows "Change dimensions" dialog """ if self.model is None: QMessageBox.warning(self, "No model", "No model selected!") return if isinstance(self.model, TwoDCoveringModel): curr_width = self.model.width curr_height = self.model.height dialog = TwoDDimensionsDialog(self) dialog.set_values(curr_width, curr_height) dialog.dimensionsAccepted.connect(self.two_d_dimensions_accepted) dialog.show() elif isinstance(self.model, PyramidCoveringModel): curr_size = self.model.size dialog = PyramidDimensionsDialog(self) dialog.set_value(curr_size) dialog.dimensionsAccepted.connect(self.pyramid_dimensions_accepted) dialog.show() def two_d_dimensions_accepted(self, width, height): """ Updates TwoDCoveringModel dimensions (after dialog confirmation) """ assert isinstance(self.model, TwoDCoveringModel) self.model.set_size(width, height) self.model_changed.emit(self.model) self.message("Size updated") def pyramid_dimensions_accepted(self, size): """ Updates PyramidCoveringModel dimensions (after dialog confirmation) """ assert isinstance(self.model, PyramidCoveringModel) # PyLint doesn't know that this is a `PyramidCoveringModel` # and not a `TwoDCoveringModel` # pylint: disable=no-value-for-parameter self.model.set_size(size) self.model_changed.emit(self.model) self.message("Size updated") def block_sizes_accepted(self, min_val, max_val): """ Updates covering model block size (after dialog confirmation) """ assert self.model is not None self.model.set_block_size(min_val, max_val) self.model_changed.emit(self.model) self.message("Block size updated") @staticmethod def update_view(model, view): """ Refreshes contents of given view """ if view is None: return if model is not None and model.is_filled(): view.show(model) else: view.close() def message(self, msg): """ Shows a log message in the "Messages" window """ self.messagesText.add_message(msg) def show_help(self): """ Opens a webpage with help """ webbrowser.open(self.HELP_URL) def create_action_groups(self): """ Groups exclusive choice menu buttons in action groups. This should ideally be done in UI files, but Qt designer doesn't support it. """ self.model_type_group = QActionGroup(self) self.model_type_group.addAction(self.action2D_Rectangle_2) self.model_type_group.addAction(self.actionPyramid_2) self.view_type_group = QActionGroup(self) self.model_type_group.triggered.connect(self.model_type_changed) self.view_type_group.triggered.connect(self.view_type_changed) def update_model_type(self): """ Sets the current model after model type changed in menu """ selected_model = self.model_type_group.checkedAction() if selected_model == self.action2D_Rectangle_2: model = TwoDCoveringModel(10, 10, 4, 4) elif selected_model == self.actionPyramid_2: model = PyramidCoveringModel(10, 4, 4) else: model = None self.model = model self.model_changed.emit(model) self.message("Model type updated") def enable_model_menu_buttons(self): """ Enable menu buttons that are disabled at program start """ self.actionChange_dimensions.setEnabled(True) self.actionChange_tile_size.setEnabled(True) self.actionGenerate.setEnabled(True) def update_view_type(self): """ Sets the current view after view type changed in menu """ if self.view is not None: self.view.close() selected_action = self.view_type_group.checkedAction() if selected_action is None: # Model was probably changed self.view = None else: action_name = selected_action.objectName() selected_view = self.action_views[action_name] self.view = selected_view() # New instance of that view self.message("View type updated") self.view_changed.emit(self.view) def cancel_covering(self): """ Stops ongoing covering """ if self.thread.isRunning(): # The thread is being terminated self.model.stop_covering() self.message("Covering terminated") def covering_success(self): """ Prints a success log message (for now) """ self.message("Covering successful") def covering_failed(self): """ Prints a fail log message and shows an error window (for now) """ self.message("Covering failed") QMessageBox.critical(self, "Failed", "Covering failed") def model_views(self, model): """ Returns a list of tuples for all views for given mode as (name, class) """ if isinstance(model, TwoDCoveringModel): return [ ("2D Print view", text_view_decorator(TwoDPrintView, self)), ("2D Visual view", parented_decorator(TwoDVisualView, self)) ] if isinstance(model, PyramidCoveringModel): return [("Pyramid Print view", text_view_decorator(PyramidPrintView, self)), ("Pyramid Visual view", PyramidVisualView)] return [] @staticmethod def model_constraints(model): """ Returns a list of tuples for all constraint watchers for given mode as (name, class) """ if isinstance(model, TwoDCoveringModel): return [("Path blocks", PathConstraintWatcher)] if isinstance(model, PyramidCoveringModel): return [("Path blocks", PathConstraintWatcher), ("Planar blocks", PlanarConstraintWatcher)] return [] def update_view_type_menu(self): """ Updates options for view type menu afted model type change """ view_type_menu = self.menuType_2 view_type_menu.clear() for action in self.view_type_group.actions(): self.view_type_group.removeAction(action) all_views = self.model_views(self.model) if not all_views: # Likely no model selected view_type_menu.setEnabled(False) return view_type_menu.setEnabled(True) self.action_views.clear() for i, view_tuple in enumerate(all_views): name, view = view_tuple # As good as any, we just need to distinguish the actions action_name = f"Action{i}" action = QAction(self) action.setText(name) action.setCheckable(True) action.setObjectName(action_name) # So that we can later see which view should be activated self.action_views[action_name] = view view_type_menu.addAction(action) self.view_type_group.addAction(action) self.update_view_type() def watcher_set_active(self, constraint, value): """ A slot, activate/deactivate constraint depending on value (True/False) """ if value is True: self.model.add_constraint(constraint) else: self.model.remove_constraint(constraint) self.model_changed.emit(self.model) self.message("Constraint settings changed") def update_constraints_menu(self): """ Updates options for model constraints after model type change """ cstr_menu = self.menuConstraints cstr_menu.clear() all_constraints = self.model_constraints(self.model) for name, watcher in all_constraints: action = QAction(self) action.setText(name) action.setCheckable(True) action.toggled.connect(lambda val, watcher=watcher: self. watcher_set_active(watcher, val)) cstr_menu.addAction(action) cstr_menu.setEnabled(True) def close(self): """ While closing the window also closes the view """ if self.view is not None: self.view.close() super().close()
def create_menus(self): """Create all required items for menus. """ # File Menu self.fileMenu = self.menuBar().addMenu("&File") self.fileMenuActions = (self.newAct, self.openAct, self.saveAct, self.saveAsAct, self.saveFormatAct, None, self.exitAct) self.update_file_menu() self.fileMenu.aboutToShow.connect(self.update_file_menu) # Edit Menu self.editMenu = self.menuBar().addMenu("&Edit") self.editMenu.addAction(self.undoAct) self.editMenu.addAction(self.redoAct) self.editMenu.addSeparator().setText('Objects') self.editMenu.addAction(self.newObjAct) self.editMenu.addAction(self.dupObjAct) self.editMenu.addSeparator() self.editMenu.addAction(self.cutObjAct) self.editMenu.addAction(self.copyObjAct) self.editMenu.addAction(self.pasteObjAct) self.editMenu.addAction(self.pasteExtAct) self.editMenu.addSeparator() self.editMenu.addAction(self.delObjAct) self.editMenu.addSeparator().setText('Values') self.editMenu.addAction(self.copyAct) self.editMenu.addAction(self.pasteAct) self.editMenu.addSeparator() self.editMenu.addAction(self.fillRightAction) self.editMenu.addSeparator() self.editMenu.addAction(self.showSearchAction) # Tools Menu self.toolsMenu = self.menuBar().addMenu("&Tools") self.toolsMenu.addAction(self.showInFolderAct) self.toolsMenu.addAction(self.openInEditorAct) self.toolsMenu.addSeparator() self.toolsMenu.addAction(self.showPrefsAction) # View Menu self.viewMenu = self.menuBar().addMenu("&View") action_group = QActionGroup(self) self.viewMenu.addAction(action_group.addAction(self.setSIUnitsAction)) self.viewMenu.addAction(action_group.addAction(self.setIPUnitsAction)) self.viewMenu.addSeparator().setText('Dockable Widgets') self.viewMenu.addAction(self.classTreeDockWidget.toggleViewAction()) self.viewMenu.addAction(self.infoView.parent().toggleViewAction()) self.viewMenu.addAction(self.commentView.parent().toggleViewAction()) self.viewMenu.addAction(self.logDockWidgetAct) self.viewMenu.addAction(self.undoView.parent().toggleViewAction()) self.viewMenu.addSeparator().setText('Toolbars') self.viewMenu.addAction(self.fileToolBar.toggleViewAction()) self.viewMenu.addAction(self.editToolBar.toggleViewAction()) # self.viewMenu.addAction(self.navToolBar.toggleViewAction()) self.viewMenu.addAction(self.filterToolBar.toggleViewAction()) self.viewMenu.addSeparator() self.viewMenu.addAction(self.classWithObjsAction) self.viewMenu.addAction(self.groupAct) self.viewMenu.addSeparator() self.viewMenu.addAction(self.transposeAct) # Jump Menu self.jumpToMenu = self.menuBar().addMenu("&Jump") self.update_jump_menu() self.jumpToMenu.aboutToShow.connect(self.update_jump_menu) self.jumpFilterGeometry.setEnabled(False) # Help Menu self.helpMenu = self.menuBar().addMenu("&Help") self.helpMenu.addAction(self.helpAct) self.helpMenu.addAction(self.aboutAct) self.helpMenu.addSeparator() self.helpMenu.addAction(self.epDocGettingStartedAction) self.helpMenu.addAction(self.epDocIORefAction) self.helpMenu.addAction(self.epDocOutputDetailsAction) self.helpMenu.addAction(self.epDocEngineeringRefAction) self.helpMenu.addAction(self.epDocAuxiliaryProgsAction) self.helpMenu.addAction(self.epDocEMSGuideAction) self.helpMenu.addAction(self.epDocComplianceAction) self.helpMenu.addAction(self.epDocInterfaceAction) self.helpMenu.addAction(self.epDocTipsTricksAction) self.helpMenu.addAction(self.epDocPlantGuideAction) self.helpMenu.addAction(self.epDocAcknowledgmentsAction)
class App(QApplication): def __init__(self, argv): super().__init__(argv) self._create_tray_icon() self._create_ui() self._create_interaction_server() self._session = None def open_preferences(self): prefs_dialog = PreferencesDialog() prefs_dialog.exec() def _mode_changed(self): action = self._mode_group.checkedAction() if action == self._mode_off: self._stop_session() elif action == self._mode_enabled: self._interaction_server.train = False self._start_session() elif action == self._mode_training: self._interaction_server.train = True self._start_session() def _start_session(self): if self._session is not None: return self._session = QProcess(self) self._session.finished.connect(self._session_ended) self._session.readyReadStandardOutput.connect(self._log_append_stdout) self._session.readyReadStandardError.connect(self._log_append_stderr) settings = QSettings() self._session.start(sys.executable, [ 'run_session.py', settings.value('CyKitAddress', app.DEFAULT_CYKIT_ADDRESS), str(settings.value('CyKitPort', app.DEFAULT_CYKIT_PORT)), str(self._interaction_server.port) ]) def _stop_session(self): if self._session is not None: self._session.close() # TODO: Handle non-null exit codes def _session_ended(self): self._session = None self._mode_off.setChecked(True) def _log_append_stdout(self): process = self.sender() self._log_window.moveCursor(QTextCursor.End) self._log_window.insertPlainText( process.readAllStandardOutput().data().decode('utf-8')) self._log_window.moveCursor(QTextCursor.End) def _log_append_stderr(self): process = self.sender() self._log_window.moveCursor(QTextCursor.End) self._log_window.insertPlainText( process.readAllStandardError().data().decode('utf-8')) self._log_window.moveCursor(QTextCursor.End) def _select_letter(self, letter): self._letter_ui.setText(letter) def _create_tray_icon(self): menu = QMenu() self._mode_group = QActionGroup(menu) self._mode_group.triggered.connect(self._mode_changed) self._mode_off = QAction("&Off", parent=menu) self._mode_off.setCheckable(True) self._mode_off.setChecked(True) self._mode_group.addAction(self._mode_off) menu.addAction(self._mode_off) self._mode_enabled = QAction("&Enabled", parent=menu) self._mode_enabled.setCheckable(True) self._mode_group.addAction(self._mode_enabled) menu.addAction(self._mode_enabled) self._mode_training = QAction("&Training mode", parent=menu) self._mode_training.setCheckable(True) self._mode_group.addAction(self._mode_training) menu.addAction(self._mode_training) menu.addSeparator() menu.addAction("&Preferences", self.open_preferences) menu.addSeparator() menu.addAction("E&xit", self.exit) pixmap = QPixmap(32, 32) pixmap.fill(Qt.white) icon = QIcon(pixmap) self._tray_icon = QSystemTrayIcon(parent=self) self._tray_icon.setContextMenu(menu) self._tray_icon.setIcon(icon) self._tray_icon.show() def _create_ui(self): self._keyboard_ui = KeyboardUI() self._keyboard_ui.show() # TODO: Get rid of this in favor of os_interaction self._letter_ui = QLabel("-") self._letter_ui.setWindowTitle("Selected letter") self._letter_ui.setStyleSheet('font-size: 72pt') self._letter_ui.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self._letter_ui.setGeometry(600, 0, 100, 100) self._letter_ui.show() # TODO: Replace with more user-friendly log self._log_window = QTextBrowser() self._log_window.setWindowTitle("Session Log") self._log_window.setGeometry(700, 0, 500, 500) self._log_window.show() def _create_interaction_server(self): self._interaction_server = InteractionServer(self) self._interaction_server.keyboard_flash_row.connect( self._keyboard_ui.flash_row) self._interaction_server.keyboard_flash_col.connect( self._keyboard_ui.flash_col) self._interaction_server.keyboard_highlight_letter.connect( self._keyboard_ui.highlight_letter) self._interaction_server.keyboard_select_letter.connect( self._select_letter)
class MainWindow(QMainWindow, Ui_JAL_MainWindow): def __init__(self, own_path, language): QMainWindow.__init__(self, None) self.setupUi(self) self.own_path = own_path self.currentLanguage = language self.current_index = None # this is used in onOperationContextMenu() to track item for menu self.ledger = Ledger() self.downloader = QuoteDownloader() self.taxes = TaxesRus() self.statements = StatementLoader() self.backup = JalBackup(self, get_dbfilename(self.own_path)) self.estimator = None self.actionImportSlipRU.setEnabled( dependency_present(['pyzbar', 'PIL'])) self.actionAbout = QAction(text=g_tr('MainWindow', "About"), parent=self) self.MainMenu.addAction(self.actionAbout) self.langGroup = QActionGroup(self.menuLanguage) self.createLanguageMenu() self.statementGroup = QActionGroup(self.menuStatement) self.createStatementsImportMenu() # Operations view context menu self.contextMenu = QMenu(self.OperationsTableView) self.actionReconcile = QAction(text=g_tr('MainWindow', "Reconcile"), parent=self) self.actionCopy = QAction(text=g_tr('MainWindow', "Copy"), parent=self) self.actionDelete = QAction(text=g_tr('MainWindow', "Delete"), parent=self) self.contextMenu.addAction(self.actionReconcile) self.contextMenu.addSeparator() self.contextMenu.addAction(self.actionCopy) self.contextMenu.addAction(self.actionDelete) # Customize Status bar and logs self.NewLogEventLbl = QLabel(self) self.StatusBar.addWidget(self.NewLogEventLbl) self.Logs.setNotificationLabel(self.NewLogEventLbl) self.Logs.setFormatter( logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) self.logger = logging.getLogger() self.logger.addHandler(self.Logs) log_level = os.environ.get('LOGLEVEL', 'INFO').upper() self.logger.setLevel(log_level) # Setup reports tab self.reports = Reports(self.ReportTableView, self.ReportTreeView) # Customize UI configuration self.balances_model = BalancesModel(self.BalancesTableView) self.BalancesTableView.setModel(self.balances_model) self.balances_model.configureView() self.holdings_model = HoldingsModel(self.HoldingsTableView) self.HoldingsTableView.setModel(self.holdings_model) self.holdings_model.configureView() self.HoldingsTableView.setContextMenuPolicy(Qt.CustomContextMenu) self.operations_model = OperationsModel(self.OperationsTableView) self.OperationsTableView.setModel(self.operations_model) self.operations_model.configureView() self.OperationsTableView.setContextMenuPolicy(Qt.CustomContextMenu) self.connect_signals_and_slots() self.NewOperationMenu = QMenu() for i in range(self.OperationsTabs.count()): if hasattr(self.OperationsTabs.widget(i), "isCustom"): self.OperationsTabs.widget(i).dbUpdated.connect( self.ledger.rebuild) self.OperationsTabs.widget(i).dbUpdated.connect( self.operations_model.refresh) self.NewOperationMenu.addAction( self.OperationsTabs.widget(i).name, partial(self.createOperation, i)) self.NewOperationBtn.setMenu(self.NewOperationMenu) # Setup balance and holdings parameters self.BalanceDate.setDateTime(QDateTime.currentDateTime()) self.BalancesCurrencyCombo.setIndex( JalSettings().getValue('BaseCurrency')) self.HoldingsDate.setDateTime(QDateTime.currentDateTime()) self.HoldingsCurrencyCombo.setIndex( JalSettings().getValue('BaseCurrency')) self.OperationsTabs.setCurrentIndex(TransactionType.NA) self.OperationsTableView.selectRow(0) self.OnOperationsRangeChange(0) def connect_signals_and_slots(self): self.actionExit.triggered.connect(QApplication.instance().quit) self.actionAbout.triggered.connect(self.showAboutWindow) self.langGroup.triggered.connect(self.onLanguageChanged) self.statementGroup.triggered.connect(self.statements.load) self.actionReconcile.triggered.connect( self.reconcileAtCurrentOperation) self.action_Load_quotes.triggered.connect( partial(self.downloader.showQuoteDownloadDialog, self)) self.actionImportSlipRU.triggered.connect(self.importSlip) self.actionBackup.triggered.connect(self.backup.create) self.actionRestore.triggered.connect(self.backup.restore) self.action_Re_build_Ledger.triggered.connect( partial(self.ledger.showRebuildDialog, self)) self.actionAccountTypes.triggered.connect( partial(self.onDataDialog, "account_types")) self.actionAccounts.triggered.connect( partial(self.onDataDialog, "accounts")) self.actionAssets.triggered.connect( partial(self.onDataDialog, "assets")) self.actionPeers.triggered.connect(partial(self.onDataDialog, "agents")) self.actionCategories.triggered.connect( partial(self.onDataDialog, "categories")) self.actionTags.triggered.connect(partial(self.onDataDialog, "tags")) self.actionCountries.triggered.connect( partial(self.onDataDialog, "countries")) self.actionQuotes.triggered.connect( partial(self.onDataDialog, "quotes")) self.PrepareTaxForms.triggered.connect( partial(self.taxes.showTaxesDialog, self)) self.BalanceDate.dateChanged.connect( self.BalancesTableView.model().setDate) self.HoldingsDate.dateChanged.connect( self.HoldingsTableView.model().setDate) self.BalancesCurrencyCombo.changed.connect( self.BalancesTableView.model().setCurrency) self.BalancesTableView.doubleClicked.connect(self.OnBalanceDoubleClick) self.HoldingsCurrencyCombo.changed.connect( self.HoldingsTableView.model().setCurrency) self.ReportRangeCombo.currentIndexChanged.connect( self.onReportRangeChange) self.RunReportBtn.clicked.connect(self.onRunReport) self.SaveReportBtn.clicked.connect(self.reports.saveReport) self.ShowInactiveCheckBox.stateChanged.connect( self.BalancesTableView.model().toggleActive) self.DateRangeCombo.currentIndexChanged.connect( self.OnOperationsRangeChange) self.ChooseAccountBtn.changed.connect( self.OperationsTableView.model().setAccount) self.SearchString.editingFinished.connect(self.updateOperationsFilter) self.HoldingsTableView.customContextMenuRequested.connect( self.onHoldingsContextMenu) self.OperationsTableView.selectionModel().selectionChanged.connect( self.OnOperationChange) self.OperationsTableView.customContextMenuRequested.connect( self.onOperationContextMenu) self.DeleteOperationBtn.clicked.connect(self.deleteOperation) self.actionDelete.triggered.connect(self.deleteOperation) self.CopyOperationBtn.clicked.connect(self.copyOperation) self.actionCopy.triggered.connect(self.copyOperation) self.downloader.download_completed.connect(self.balances_model.update) self.downloader.download_completed.connect(self.holdings_model.update) self.statements.load_completed.connect(self.ledger.rebuild) self.ledger.updated.connect(self.balances_model.update) self.ledger.updated.connect(self.holdings_model.update) @Slot() def closeEvent(self, event): self.logger.removeHandler( self.Logs ) # Removing handler (but it doesn't prevent exception at exit) logging.raiseExceptions = False # Silencing logging module exceptions def createLanguageMenu(self): langPath = self.own_path + "languages" + os.sep langDirectory = QDir(langPath) for language_file in langDirectory.entryList(['*.qm']): language_code = language_file.split('.')[0] language = QLocale.languageToString( QLocale(language_code).language()) language_icon = QIcon(langPath + language_code + '.png') action = QAction(language_icon, language, self) action.setCheckable(True) action.setData(language_code) self.menuLanguage.addAction(action) self.langGroup.addAction(action) @Slot() def onLanguageChanged(self, action): language_code = action.data() if language_code != self.currentLanguage: JalSettings().setValue('Language', JalDB().get_language_id(language_code)) QMessageBox().information( self, g_tr('MainWindow', "Restart required"), g_tr('MainWindow', "Language was changed to ") + QLocale.languageToString(QLocale(language_code).language()) + "\n" + g_tr( 'MainWindow', "You should restart application to apply changes\n" "Application will be terminated now"), QMessageBox.Ok) self.close() # Create import menu for all known statements based on self.statements.sources values def createStatementsImportMenu(self): for i, source in enumerate(self.statements.sources): if 'icon' in source: source_icon = QIcon(self.own_path + "img" + os.sep + source['icon']) action = QAction(source_icon, source['name'], self) else: action = QAction(source['name'], self) action.setData(i) self.menuStatement.addAction(action) self.statementGroup.addAction(action) @Slot() def showAboutWindow(self): about_box = QMessageBox(self) about_box.setAttribute(Qt.WA_DeleteOnClose) about_box.setWindowTitle(g_tr('MainWindow', "About")) title = g_tr( 'MainWindow', "<h3>JAL</h3><p>Just Another Ledger, version {version}</p>".format( version=__version__)) about_box.setText(title) about = g_tr( 'MainWindow', "<p>More information, manuals and problem reports are at " "<a href=https://github.com/titov-vv/jal>github home page</a></p>" "<p>Questions, comments, donations: <a href=mailto:[email protected]>[email protected]</a></p>" ) about_box.setInformativeText(about) about_box.show() @Slot() def OnBalanceDoubleClick(self, index): self.ChooseAccountBtn.account_id = index.model().getAccountId( index.row()) @Slot() def onReportRangeChange(self, range_index): report_ranges = { 0: lambda: (0, 0), 1: ManipulateDate.Last3Months, 2: ManipulateDate.RangeYTD, 3: ManipulateDate.RangeThisYear, 4: ManipulateDate.RangePreviousYear } begin, end = report_ranges[range_index]() self.ReportFromDate.setDateTime(QDateTime.fromSecsSinceEpoch(begin)) self.ReportToDate.setDateTime(QDateTime.fromSecsSinceEpoch(end)) @Slot() def onRunReport(self): types = { 0: ReportType.IncomeSpending, 1: ReportType.ProfitLoss, 2: ReportType.Deals, 3: ReportType.ByCategory } report_type = types[self.ReportTypeCombo.currentIndex()] begin = self.ReportFromDate.dateTime().toSecsSinceEpoch() end = self.ReportToDate.dateTime().toSecsSinceEpoch() group_dates = 1 if self.ReportGroupCheck.isChecked() else 0 if report_type == ReportType.ByCategory: self.reports.runReport(report_type, begin, end, self.ReportCategoryEdit.selected_id, group_dates) else: self.reports.runReport(report_type, begin, end, self.ReportAccountBtn.account_id, group_dates) @Slot() def OnOperationsRangeChange(self, range_index): view_ranges = { 0: ManipulateDate.startOfPreviousWeek, 1: ManipulateDate.startOfPreviousMonth, 2: ManipulateDate.startOfPreviousQuarter, 3: ManipulateDate.startOfPreviousYear, 4: lambda: 0 } self.OperationsTableView.model().setDateRange( view_ranges[range_index]()) @Slot() def importSlip(self): dialog = ImportSlipDialog(self) dialog.show() @Slot() def onHoldingsContextMenu(self, pos): index = self.HoldingsTableView.indexAt(pos) contextMenu = QMenu(self.HoldingsTableView) actionEstimateTax = QAction(text=g_tr('Ledger', "Estimate Russian Tax"), parent=self.HoldingsTableView) actionEstimateTax.triggered.connect( partial(self.estimateRussianTax, self.HoldingsTableView.viewport().mapToGlobal(pos), index)) contextMenu.addAction(actionEstimateTax) contextMenu.popup(self.HoldingsTableView.viewport().mapToGlobal(pos)) @Slot() def estimateRussianTax(self, position, index): model = index.model() account, asset, asset_qty = model.get_data_for_tax(index) self.estimator = TaxEstimator(account, asset, asset_qty, position) if self.estimator.ready: self.estimator.open() @Slot() def OnOperationChange(self, selected, _deselected): self.checkForUncommittedChanges() if len(self.OperationsTableView.selectionModel().selectedRows()) != 1: self.OperationsTabs.setCurrentIndex(TransactionType.NA) else: idx = selected.indexes() if idx: selected_row = idx[0].row() operation_type, operation_id = self.OperationsTableView.model( ).get_operation(selected_row) self.OperationsTabs.setCurrentIndex(operation_type) self.OperationsTabs.widget(operation_type).setId(operation_id) @Slot() def checkForUncommittedChanges(self): for i in range(self.OperationsTabs.count()): if hasattr(self.OperationsTabs.widget(i), "isCustom") and self.OperationsTabs.widget(i).modified: reply = QMessageBox().warning( None, g_tr('MainWindow', "You have unsaved changes"), self.OperationsTabs.widget(i).name + g_tr('MainWindow', " has uncommitted changes,\ndo you want to save it?"), QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.OperationsTabs.widget(i).saveChanges() else: self.OperationsTabs.widget(i).revertChanges() @Slot() def onOperationContextMenu(self, pos): self.current_index = self.OperationsTableView.indexAt(pos) if len(self.OperationsTableView.selectionModel().selectedRows()) != 1: self.actionReconcile.setEnabled(False) self.actionCopy.setEnabled(False) else: self.actionReconcile.setEnabled(True) self.actionCopy.setEnabled(True) self.contextMenu.popup( self.OperationsTableView.viewport().mapToGlobal(pos)) @Slot() def reconcileAtCurrentOperation(self): self.operations_model.reconcile_operation(self.current_index.row()) @Slot() def deleteOperation(self): if QMessageBox().warning( None, g_tr('MainWindow', "Confirmation"), g_tr('MainWindow', "Are you sure to delete selected transacion(s)?"), QMessageBox.Yes, QMessageBox.No) == QMessageBox.No: return rows = [] for index in self.OperationsTableView.selectionModel().selectedRows(): rows.append(index.row()) self.operations_model.deleteRows(rows) self.ledger.rebuild() @Slot() def createOperation(self, operation_type): self.checkForUncommittedChanges() self.OperationsTabs.widget(operation_type).createNew( account_id=self.operations_model.getAccount()) self.OperationsTabs.setCurrentIndex(operation_type) @Slot() def copyOperation(self): operation_type = self.OperationsTabs.currentIndex() if operation_type == TransactionType.NA: return self.checkForUncommittedChanges() self.OperationsTabs.widget(operation_type).copyNew() @Slot() def updateOperationsFilter(self): self.OperationsTableView.model().filterText(self.SearchString.text()) @Slot() def onDataDialog(self, dlg_type): if dlg_type == "account_types": AccountTypeListDialog().exec_() elif dlg_type == "accounts": AccountListDialog().exec_() elif dlg_type == "assets": AssetListDialog().exec_() elif dlg_type == "agents": PeerListDialog().exec_() elif dlg_type == "categories": CategoryListDialog().exec_() elif dlg_type == "tags": TagsListDialog().exec_() elif dlg_type == "countries": CountryListDialog().exec_() elif dlg_type == "quotes": QuotesListDialog().exec_() else: assert False