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)
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))