예제 #1
0
파일: editor.py 프로젝트: libreblog/cells
class Editor(Observation, QScrollArea):
    def __init__(self, subject, powermode=False):
        Observation.__init__(self, subject)
        QScrollArea.__init__(self)
        self.powermode = powermode

        self.setFrameShape(QFrame.NoFrame)
        self.setMinimumSize(200, 300)

        self.codeView = CodeView(subject)
        self.trackEditor = TrackEditor(self.subject,
                                       self.powermode,
                                       confirmUpdate=False)

        self.selectedTrackIndex = -1

        self.innerLayout = QHBoxLayout()

        self.innerLayout.setSpacing(0)
        self.innerLayout.setContentsMargins(0, 0, 0, 0)
        self.innerLayout.setAlignment(Qt.AlignLeft)

        self.setAttribute(Qt.WA_StyledBackground)
        self.setStyleSheet(Theme.editor.style)
        self.setHorizontalScrollBar(ScrollBar())
        self.setVerticalScrollBar(ScrollBar())

        widget = QWidget()
        widget.setLayout(self.innerLayout)
        self.setWidget(widget)
        self.setWidgetResizable(True)

        self._initTip()
        self.setTipHidden(False)

        self.add_responder(events.document.Open, self.documentOpenResponder)
        self.add_responder(events.view.main.TrackNew, self.trackNewResponder)
        self.add_responder(events.view.browser.TrackNewFromTemplate,
                           self.trackNewFromTemplateResponder)
        self.add_responder(events.view.track.Clicked,
                           self.trackClickedResponder)
        self.add_responder(events.view.main.TrackSelectLeft,
                           self.trackSelectLeftResponder)
        self.add_responder(events.view.main.TrackSelectRight,
                           self.trackSelectRightResponder)
        self.add_responder(events.view.main.TrackMoveLeft,
                           self.trackMoveLeftResponder)
        self.add_responder(events.view.main.TrackMoveRight,
                           self.trackMoveRightResponder)
        self.add_responder(events.view.main.TrackRemove,
                           self.trackRemoveResponder)
        self.add_responder(events.view.main.RowRemove, self.rowRemoveResponder)
        self.add_responder(events.view.track.CellSelected,
                           self.cellSelectedResponder)
        self.add_responder(events.view.main.CellEvaluate,
                           self.cellEvaluateResponder)
        # we evaluate row here instead of in Track to keep left-to-right
        # evaluation order when we move tracks
        self.add_responder(events.view.main.RowEvaluate,
                           self.rowEvaluateResponder)
        self.add_responder(events.view.main.CellClear, self.cellClearResponder)
        self.add_responder(events.view.main.CellEdit, self.cellEditResponder)
        self.add_responder(events.view.main.TrackSetup,
                           self.trackSetupResponder)
        self.add_responder(events.view.main.TrackSaveAsTemplate,
                           self.trackSaveAsTemplateResponder)
        self.add_responder(events.track.TrackTemplateSaved,
                           self.trackTemplateSavedResponder)
        self.add_responder(events.view.main.TrackRestartBackend,
                           self.trackRestartBackendResponder)
        self.add_responder(events.view.main.BackendRestartAll,
                           self.backendRestartAllResponder)
        self.add_responder(events.view.track.BackendRestart,
                           self.trackConfirmationBackendRestartResponder)

    def _initTip(self):
        layout = QHBoxLayout()
        self.tip = QLabel("Add Track From Template")
        self.tip.setStyleSheet(Theme.editor.tip.style)
        self.tip.setFont(Theme.editor.tip.font)
        layout.addWidget(self.tip)
        layout.setAlignment(Qt.AlignCenter)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def setTipHidden(self, hidden):
        if self.tip.isHidden() == hidden:
            return

        self.tip.setHidden(hidden)

    def documentOpenResponder(self, e):
        self.clear()

        for (n, track) in enumerate(e.document.tracks):
            trackView = Track(self, self.subject, n, track.name,
                              track.template)
            self.innerLayout.addWidget(trackView)
            trackView.deserialize(track)

        if self.numOfTracks() > 1:
            self.setTipHidden(True)

    def trackClickedResponder(self, e):
        self.selectTrackAt(e.index)

    def trackSelectLeftResponder(self, e):
        if self.numOfTracks() < 1:
            return

        if not self.hasSelectedTrack():
            self.selectTrackAt(self.numOfTracks() - 1)
        else:
            self.selectTrackAt(self.selectedTrackIndex - 1)

    def trackSelectRightResponder(self, e):
        if self.numOfTracks() < 1:
            return

        if not self.hasSelectedTrack():
            self.selectTrackAt(0)
        else:
            self.selectTrackAt(self.selectedTrackIndex + 1)

    def trackNewResponder(self, e):
        self.newTrack(TrackTemplateModel())

    def trackNewFromTemplateResponder(self, e):
        self.newTrack(e.template)

        if self.numOfTracks() == 1:
            track = self.trackAt(0)
            track.addCell()

    def trackMoveLeftResponder(self, e):
        self.moveSelectedTrackTo(self.selectedTrackIndex - 1)

    def trackMoveRightResponder(self, e):
        self.moveSelectedTrackTo(self.selectedTrackIndex + 1)

    def moveSelectedTrackTo(self, index):
        if self.numOfTracks() < 2 or \
                not self.hasSelectedTrack() or \
                not index in range(self.numOfTracks()) or \
                self.selectedTrackIndex == index:

            return

        track = self.innerLayout.takeAt(self.selectedTrackIndex)
        self.innerLayout.insertWidget(index, track.widget())
        track.widget().setIndex(index)

        previous = self.trackAt(self.selectedTrackIndex)
        previous.setIndex(self.selectedTrackIndex)

        self.notify(events.view.track.Move(self.selectedTrackIndex, index))

        self.selectTrackAt(index)

    def trackRemoveResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)
        question = f'Do you really want to delete track {track.name()}?'
        confirmation = ConfirmationDialog("Delete Track", question)

        if confirmation.exec_() == ConfirmationDialog.No:
            return

        self.notify(
            events.view.track.Remove(self.selectedTrackIndex, track.template))
        track.delete()
        self.selectTrackAt(self.selectedTrackIndex - 1)

        for n in range(self.selectedTrackIndex + 1, self.numOfTracks()):
            track = self.trackAt(n)
            track.setIndex(track.index - 1)

        if self.numOfTracks() < 1:
            self.setTipHidden(False)

    def rowRemoveResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)

        if not track.hasSelectedCell() or len(track.cells) < 1:
            return

        confirmation = ConfirmationDialog(
            "Delete Row", "Do you really want to delete selected row?")

        if confirmation.exec_() == ConfirmationDialog.No:
            return

        self.notify(events.view.track.RowRemove(track.selectedCellIndex))

    def cellSelectedResponder(self, e):
        self.ensureTrackVisible(self.selectedTrackIndex)

    def cellEvaluateResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)

        if not track.hasSelectedCell():
            return

        track.cells[track.selectedCellIndex].evaluate()

    def rowEvaluateResponder(self, e):
        if not self.hasSelectedTrack():
            return

        for index in range(self.numOfTracks()):
            track = self.trackAt(index)

            if track.hasSelectedCell():
                track.cells[track.selectedCellIndex].evaluate()

    def cellClearResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)

        if not track.hasSelectedCell():
            return

        track.cells[track.selectedCellIndex].clear()

    def cellEditResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)

        if not track.hasSelectedCell():
            return

        track.cells[track.selectedCellIndex].edit()

    def trackSetupResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)
        track.edit()

    def trackSaveAsTemplateResponder(self, e):
        if not self.hasSelectedTrack():
            return

        self.trackAt(self.selectedTrackIndex).saveAsTemplate()

    def trackTemplateSavedResponder(self, e):
        msgBox = QMessageBox()
        msgBox.setText("Track Template Saved Succesfully")
        msgBox.setDetailedText(repr(e.template))
        msgBox.exec()

    def trackRestartBackendResponder(self, e):
        if not self.hasSelectedTrack():
            return

        track = self.trackAt(self.selectedTrackIndex)
        self.restartBackendForTrack(track)

    def backendRestartAllResponder(self, e):
        if self.numOfTracks() < 1:
            return

        templates = [
            self.trackAt(n).template for n in range(self.numOfTracks())
        ]
        self.notify(events.view.editor.BackendRestartAll(templates))

    def trackConfirmationBackendRestartResponder(self, e):
        self.restartBackendForTrack(self.trackAt(e.track_index))

    def restartBackendForTrack(self, track):
        backendName = track.template.backend_name
        templates = []

        for n in range(self.numOfTracks()):
            track = self.trackAt(n)

            if track.template.backend_name == backendName:
                templates.append(track.template)

        self.notify(events.view.editor.TrackRestartBackend(templates))

    def newTrack(self, template):
        self.setTipHidden(True)

        length = self.innerLayout.count()
        name = "Track " + str(length + 1)
        track = Track(self, self.subject, length, name, template)
        self.innerLayout.addWidget(track)
        self.notify(events.view.track.New(name, template))

        if self.numOfTracks() > 1:
            firstTrack = self.trackAt(0)

            for _ in firstTrack.cells:
                track.addCell()
            track.selectCellAt(firstTrack.selectedCellIndex)

            if not firstTrack.isPasteBufferEmpty():
                track.fillPasteBuffer()

    def selectTrackAt(self, index):
        if self.selectedTrackIndex == index:
            return

        if self.hasSelectedTrack():
            track = self.trackAt(self.selectedTrackIndex)
            track.setSelected(False)

        if index in range(self.numOfTracks()):
            track = self.trackAt(index)
            track.setSelected(True)
            self.ensureTrackVisible(index)

        self.selectedTrackIndex = min(max(-1, index), self.numOfTracks())
        self.notify(events.view.track.Select(self.selectedTrackIndex))

    def ensureTrackVisible(self, index):
        if index not in range(self.numOfTracks()):
            return

        track = self.trackAt(index)

        if track.selectedCellIndex in range(len(track.cells)):
            cell = track.cells[track.selectedCellIndex]
            self.ensureWidgetVisible(cell, track.header.width(),
                                     track.header.height())
        else:
            self.ensureWidgetVisible(track, track.header.width(),
                                     track.header.height())

    def hasSelectedTrack(self):
        return self.selectedTrackIndex in range(self.numOfTracks())

    def numOfTracks(self):
        return self.innerLayout.count()

    def clear(self):
        for i in reversed(range(self.numOfTracks())):
            self.trackAt(i).setParent(None)

    def trackAt(self, index):
        return self.innerLayout.itemAt(index).widget()

    def closeEvent(self, e):
        self.codeView.delete()
        self.trackEditor.delete()
        self.unregister()

        return super().closeEvent(e)
예제 #2
0
class VqlEditorWidget(plugin.PluginWidget):
    """Exposed class to manage VQL/SQL queries from the mainwindow"""

    LOCATION = plugin.FOOTER_LOCATION
    ENABLE = True

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle(self.tr("VQL Editor"))

        # Top toolbar
        self.top_bar = QToolBar()
        self.top_bar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.run_action = self.top_bar.addAction(FIcon(0xF040A),
                                                 self.tr("Run"), self.run_vql)
        self.run_action.setShortcuts(
            [Qt.CTRL + Qt.Key_R, QKeySequence.Refresh])
        self.run_action.setToolTip(
            self.tr("Run VQL query (%s)" %
                    self.run_action.shortcut().toString()))

        # Syntax highlighter and autocompletion
        self.text_edit = CodeEdit()
        # Error handling
        self.log_edit = QLabel()
        self.log_edit.setMinimumHeight(40)
        self.log_edit.setStyleSheet(
            "QWidget{{background-color:'{}'; color:'{}'}}".format(
                style.WARNING_BACKGROUND_COLOR, style.WARNING_TEXT_COLOR))
        self.log_edit.hide()
        self.log_edit.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.top_bar)
        main_layout.addWidget(self.text_edit)
        main_layout.addWidget(self.log_edit)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        self.setLayout(main_layout)

    def on_open_project(self, conn: sqlite3.Connection):
        """overrided from PluginWidget : Do not call this methods

        Args:
            conn (sqlite3.Connection): sqlite3 connection
        """
        self.conn = conn
        self._fill_completer()

        self.on_refresh()

    def on_refresh(self):
        """overrided from PluginWidget"""

        vql_query = build_vql_query(
            self.mainwindow.state.fields,
            self.mainwindow.state.source,
            self.mainwindow.state.filters,
            self.mainwindow.state.group_by,
            self.mainwindow.state.having,
        )

        self.set_vql(vql_query)

    def set_vql(self, text: str):
        """Set vql source code without executed

        Args:
            text (str): VQL query
        """
        self.text_edit.blockSignals(True)
        self.text_edit.setPlainText(text)
        self.text_edit.blockSignals(False)

    def _fill_completer(self):
        """Create Completer with his model

        Fill the model with the SQL keywords and database fields
        """
        # preload samples , selection and wordset
        samples = [i["name"] for i in sql.get_samples(self.conn)]
        selections = [i["name"] for i in sql.get_selections(self.conn)]
        wordsets = [i["name"] for i in sql.get_wordsets(self.conn)]

        # keywords = []
        self.text_edit.completer.model.clear()
        self.text_edit.completer.model.beginResetModel()

        # register keywords
        for keyword in self.text_edit.syntax.sql_keywords:
            self.text_edit.completer.model.add_item(keyword, "VQL keywords",
                                                    FIcon(0xF0169), "#f6ecf0")

        for selection in selections:
            self.text_edit.completer.model.add_item(selection, "Source table",
                                                    FIcon(0xF04EB), "#f6ecf0")

        for wordset in wordsets:
            self.text_edit.completer.model.add_item(f"WORDSET['{wordset}']",
                                                    "WORDSET", FIcon(0xF04EB),
                                                    "#f6ecf0")

        for field in sql.get_fields(self.conn):
            name = field["name"]
            description = "<b>{}</b> ({}) from {} <br/><br/> {}".format(
                field["name"], field["type"], field["category"],
                field["description"])
            color = style.FIELD_TYPE.get(field["type"], "str")["color"]
            icon = FIcon(
                style.FIELD_TYPE.get(field["type"], "str")["icon"], "white")

            if field["category"] == "variants" or field[
                    "category"] == "annotations":
                self.text_edit.completer.model.add_item(
                    name, description, icon, color)

            if field["category"] == "samples":
                # Overwrite name
                for sample in samples:
                    name = "sample['{}'].{}".format(sample, field["name"])
                    description = "<b>{}</b> ({}) from {} {} <br/><br/> {}".format(
                        field["name"],
                        field["type"],
                        field["category"],
                        sample,
                        field["description"],
                    )
                    self.text_edit.completer.model.add_item(
                        name, description, icon, color)

        self.text_edit.completer.model.endResetModel()

        # if field["category"] == "samples":
        #     for sample in samples:
        #         keywords.append("sample['{}'].{}".format(sample, field["name"]))
        # else:
        #     keywords.append(field["name"])

    def check_vql(self) -> bool:
        """Check VQL statement; return True if OK, False when an error occurs

        Notes:
            This function also sets the error message to the bottom of the view.

        Returns:
            bool: Status of VQL query (True if valid, False otherwise).
        """
        try:
            self.log_edit.hide()
            _ = [i for i in vql.parse_vql(self.text_edit.toPlainText())]
        except (TextXSyntaxError, VQLSyntaxError) as e:
            # Show the error message on the ui
            # Available attributes: e.message, e.line, e.col
            self.set_message("%s: %s, col: %d" %
                             (e.__class__.__name__, e.message, e.col))
            return False
        return True

    def run_vql(self):
        """Execute VQL code

        Suported commands and the plugins that need to be refreshed in consequence:
            - select_cmd: main ui (all plugins in fact)
            - count_cmd: *not supported*
            - drop_cmd: selections & wordsets
            - create_cmd: selections
            - set_cmd: selections
            - bed_cmd: selections
            - show_cmd: *not supported*
            - import_cmd: wordsets
        """
        # Check VQL syntax first
        if not self.check_vql():
            return

        for cmd in vql.parse_vql(self.text_edit.toPlainText()):

            LOGGER.debug("VQL command %s", cmd)
            cmd_type = cmd["cmd"]

            # If command is a select kind
            if cmd_type == "select_cmd":
                # => Command will be executed in different widgets (variant_view)
                # /!\ VQL Editor will not check SQL validity of the command
                # columns from variant table
                self.mainwindow.state.fields = cmd["fields"]
                # name of the variant selection
                self.mainwindow.state.source = cmd["source"]
                self.mainwindow.state.filters = cmd["filters"]
                self.mainwindow.state.group_by = cmd["group_by"]
                self.mainwindow.state.having = cmd["having"]
                # Refresh all plugins
                self.mainwindow.refresh_plugins(sender=self)
                continue

            try:
                # Check SQL validity of selections related commands
                command.create_command_from_obj(self.conn, cmd)()
            except (sqlite3.DatabaseError, VQLSyntaxError) as e:
                # Display errors in VQL editor
                self.set_message(str(e))
                LOGGER.exception(e)
                continue

            # Selections related commands
            if cmd_type in ("create_cmd", "set_cmd", "bed_cmd"):
                # refresh source editor plugin for selections
                self.mainwindow.refresh_plugin("source_editor")
                continue

            if cmd_type == "drop_cmd":
                # refresh source editor plugin for selections
                self.mainwindow.refresh_plugin("source_editor")
                # refresh wordset plugin
                self.mainwindow.refresh_plugin("word_set")

            if cmd_type == "import_cmd":
                # refresh wordset plugin
                self.mainwindow.refresh_plugin("word_set")

    def set_message(self, message: str):
        """Show message error at the bottom of the view

        Args:
            message (str): Error message
        """
        if self.log_edit.isHidden():
            self.log_edit.show()

        icon_64 = FIcon(0xF0027, style.WARNING_TEXT_COLOR).to_base64(18, 18)

        self.log_edit.setText("""<div height=100%>
            <img src="data:image/png;base64,{}" align="left"/>
             <span> {} </span>
            </div>""".format(icon_64, message))