Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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))
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
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))
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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()
Ejemplo n.º 15
0
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)
Ejemplo n.º 16
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)
Ejemplo n.º 17
0
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()
Ejemplo n.º 18
0
 def __initModeMenu(self):
     modeMenuGroup = QActionGroup(self)
     modeMenuGroup.addAction(self.ui.actionDigraph_Mode)
     modeMenuGroup.addAction(self.ui.actionRedigraph_Mode)
Ejemplo n.º 19
0
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])
Ejemplo n.º 20
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)
Ejemplo n.º 21
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)
Ejemplo n.º 22
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()
Ejemplo n.º 23
0
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)
Ejemplo n.º 24
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.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)
Ejemplo n.º 25
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()
Ejemplo n.º 26
0
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)
Ejemplo n.º 27
0
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())
Ejemplo n.º 28
0
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()
Ejemplo n.º 29
0
    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)
Ejemplo n.º 30
0
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)
Ejemplo n.º 31
0
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