def test_tool_grid(self): w = ToolGrid() w.show() self.app.processEvents() def buttonsOrderedVisual(): # Process layout events so the buttons have right positions self.app.processEvents() buttons = w.findChildren(QToolButton) return list(sorted(buttons, key=lambda b: (b.y(), b.x()))) def buttonsOrderedLogical(): return list(map(w.buttonForAction, w.actions())) def assertOrdered(): self.assertSequenceEqual(buttonsOrderedLogical(), buttonsOrderedVisual()) action_a = QAction("A", w) action_b = QAction("B", w) action_c = QAction("C", w) action_d = QAction("D", w) w.addAction(action_b) w.insertAction(0, action_a) self.assertSequenceEqual(w.actions(), [action_a, action_b]) assertOrdered() w.addAction(action_d) w.insertAction(action_d, action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() w.removeAction(action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.removeAction(action_a) self.assertSequenceEqual(w.actions(), [action_b, action_d]) assertOrdered() w.insertAction(0, action_a) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.setColumnCount(2) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.insertAction(2, action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() w.clear() # test no 'before' action edge case w.insertAction(0, action_a) self.assertIs(action_a, w.actions()[0]) w.insertAction(1, action_b) self.assertSequenceEqual(w.actions(), [action_a, action_b]) w.clear() w.setActions([action_a, action_b, action_c, action_d]) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() triggered_actions = [] def p(action): print(action.text()) w.actionTriggered.connect(p) w.actionTriggered.connect(triggered_actions.append) action_a.trigger() w.show() self.app.exec_()
def test_tool_grid(self): w = ToolGrid() w.show() self.app.processEvents() def buttonsOrderedVisual(): # Process layout events so the buttons have right positions self.app.processEvents() buttons = w.findChildren(QToolButton) return sorted(buttons, key=lambda b: (b.y(), b.x())) def buttonsOrderedLogical(): return list(map(w.buttonForAction, w.actions())) def assertOrdered(): self.assertSequenceEqual(buttonsOrderedLogical(), buttonsOrderedVisual()) action_a = QAction("A", w) action_b = QAction("B", w) action_c = QAction("C", w) action_d = QAction("D", w) w.addAction(action_b) w.insertAction(0, action_a) self.assertSequenceEqual(w.actions(), [action_a, action_b]) assertOrdered() w.addAction(action_d) w.insertAction(action_d, action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() w.removeAction(action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.removeAction(action_a) self.assertSequenceEqual(w.actions(), [action_b, action_d]) assertOrdered() w.insertAction(0, action_a) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.setColumnCount(2) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_d]) assertOrdered() w.insertAction(2, action_c) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() w.clear() # test no 'before' action edge case w.insertAction(0, action_a) self.assertIs(action_a, w.actions()[0]) w.insertAction(1, action_b) self.assertSequenceEqual(w.actions(), [action_a, action_b]) w.clear() w.setActions([action_a, action_b, action_c, action_d]) self.assertSequenceEqual(w.actions(), [action_a, action_b, action_c, action_d]) assertOrdered() triggered_actions = [] def p(action): print(action.text()) w.actionTriggered.connect(p) w.actionTriggered.connect(triggered_actions.append) action_a.trigger() w.show() self.app.exec_()
class OWPythonScript(OWWidget): name = "Python Script" description = "Write a Python script and run it on input data or models." category = "Transform" icon = "icons/PythonScript.svg" priority = 3150 keywords = ["program", "function"] class Inputs: data = MultiInput("Data", Table, replaces=["in_data"], default=True) learner = MultiInput("Learner", Learner, replaces=["in_learner"], default=True) classifier = MultiInput("Classifier", Model, replaces=["in_classifier"], default=True) object = MultiInput("Object", object, replaces=["in_object"], default=False) class Outputs: data = Output("Data", Table, replaces=["out_data"]) learner = Output("Learner", Learner, replaces=["out_learner"]) classifier = Output("Classifier", Model, replaces=["out_classifier"]) object = Output("Object", object, replaces=["out_object"]) signal_names = ("data", "learner", "classifier", "object") settings_version = 2 scriptLibrary: 'List[_ScriptData]' = Setting([{ "name": "Table from numpy", "script": DEFAULT_SCRIPT, "filename": None }]) currentScriptIndex = Setting(0) scriptText: Optional[str] = Setting(None, schema_only=True) splitterState: Optional[bytes] = Setting(None) vimModeEnabled = Setting(False) class Error(OWWidget.Error): pass def __init__(self): super().__init__() for name in self.signal_names: setattr(self, name, []) self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea) self.mainArea.layout().addWidget(self.splitCanvas) # Styling self.defaultFont = defaultFont = ( 'Menlo' if sys.platform == 'darwin' else 'Courier' if sys.platform in ['win32', 'cygwin'] else 'DejaVu Sans Mono') self.defaultFontSize = defaultFontSize = 13 self.editorBox = gui.vBox(self, box="Editor", spacing=4) self.splitCanvas.addWidget(self.editorBox) darkMode = QApplication.instance().property('darkMode') scheme_name = 'Dark' if darkMode else 'Light' syntax_highlighting_scheme = SYNTAX_HIGHLIGHTING_STYLES[scheme_name] self.pygments_style_class = make_pygments_style(scheme_name) eFont = QFont(defaultFont) eFont.setPointSize(defaultFontSize) # Fake Signature self.func_sig = func_sig = FunctionSignature( self.editorBox, syntax_highlighting_scheme, eFont) # Editor editor = PythonEditor(self) editor.setFont(eFont) editor.setup_completer_appearance((300, 180), eFont) # Fake return return_stmt = ReturnStatement(self.editorBox, syntax_highlighting_scheme, eFont) self.return_stmt = return_stmt # Match indentation textEditBox = QWidget(self.editorBox) textEditBox.setLayout(QHBoxLayout()) char_4_width = QFontMetrics(eFont).horizontalAdvance('0000') @editor.viewport_margins_updated.connect def _(width): func_sig.setIndent(width) textEditMargin = max(0, round(char_4_width - width)) return_stmt.setIndent(textEditMargin + width) textEditBox.layout().setContentsMargins(textEditMargin, 0, 0, 0) self.text = editor textEditBox.layout().addWidget(editor) self.editorBox.layout().addWidget(func_sig) self.editorBox.layout().addWidget(textEditBox) self.editorBox.layout().addWidget(return_stmt) self.editorBox.setAlignment(Qt.AlignVCenter) self.text.setTabStopWidth(4) self.text.modificationChanged[bool].connect(self.onModificationChanged) # Controls self.editor_controls = gui.vBox(self.controlArea, box='Preferences') self.vim_box = gui.hBox(self.editor_controls, spacing=20) self.vim_indicator = VimIndicator(self.vim_box) vim_sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) vim_sp.setRetainSizeWhenHidden(True) self.vim_indicator.setSizePolicy(vim_sp) def enable_vim_mode(): editor.vimModeEnabled = self.vimModeEnabled self.vim_indicator.setVisible(self.vimModeEnabled) enable_vim_mode() gui.checkBox(self.vim_box, self, 'vimModeEnabled', 'Vim mode', tooltip="Only for the coolest.", callback=enable_vim_mode) self.vim_box.layout().addWidget(self.vim_indicator) @editor.vimModeIndicationChanged.connect def _(color, text): self.vim_indicator.indicator_color = color self.vim_indicator.indicator_text = text self.vim_indicator.update() # Library self.libraryListSource = [] self._cachedDocuments = {} self.libraryList = itemmodels.PyListModel( [], self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.libraryList.wrap(self.libraryListSource) self.controlBox = gui.vBox(self.controlArea, 'Library') self.controlBox.layout().setSpacing(1) self.libraryView = QListView( editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)) self.libraryView.setItemDelegate(ScriptItemDelegate(self)) self.libraryView.setModel(self.libraryList) self.libraryView.selectionModel().selectionChanged.connect( self.onSelectedScriptChanged) self.controlBox.layout().addWidget(self.libraryView) w = itemmodels.ModelActionsWidget() self.addNewScriptAction = action = QAction("+", self) action.setToolTip("Add a new script to the library") action.triggered.connect(self.onAddScript) w.addAction(action) action = QAction(unicodedata.lookup("MINUS SIGN"), self) action.setToolTip("Remove script from library") action.triggered.connect(self.onRemoveScript) w.addAction(action) action = QAction("Update", self) action.setToolTip("Save changes in the editor to library") action.setShortcut(QKeySequence(QKeySequence.Save)) action.triggered.connect(self.commitChangesToLibrary) w.addAction(action) action = QAction("More", self, toolTip="More actions") new_from_file = QAction("Import Script from File", self) save_to_file = QAction("Save Selected Script to File", self) restore_saved = QAction("Undo Changes to Selected Script", self) save_to_file.setShortcut(QKeySequence(QKeySequence.SaveAs)) new_from_file.triggered.connect(self.onAddScriptFromFile) save_to_file.triggered.connect(self.saveScript) restore_saved.triggered.connect(self.restoreSaved) menu = QMenu(w) menu.addAction(new_from_file) menu.addAction(save_to_file) menu.addAction(restore_saved) action.setMenu(menu) button = w.addAction(action) button.setPopupMode(QToolButton.InstantPopup) w.layout().setSpacing(1) self.controlBox.layout().addWidget(w) self.execute_button = gui.button(self.buttonsArea, self, 'Run', callback=self.commit) self.run_action = QAction("Run script", self, triggered=self.commit, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R)) self.addAction(self.run_action) self.saveAction = action = QAction("&Save", self.text) action.setToolTip("Save script to file") action.setShortcut(QKeySequence(QKeySequence.Save)) action.setShortcutContext(Qt.WidgetWithChildrenShortcut) action.triggered.connect(self.saveScript) self.consoleBox = gui.vBox(self.splitCanvas, 'Console') self.console = PythonConsole({}, self) self.consoleBox.layout().addWidget(self.console) self.console.document().setDefaultFont(QFont(defaultFont)) self.consoleBox.setAlignment(Qt.AlignBottom) self.splitCanvas.setSizes([2, 1]) self.controlArea.layout().addStretch(10) self._restoreState() self.settingsAboutToBePacked.connect(self._saveState) def sizeHint(self) -> QSize: return super().sizeHint().expandedTo(QSize(800, 600)) def _restoreState(self): self.libraryListSource = [ Script.fromdict(s) for s in self.scriptLibrary ] self.libraryList.wrap(self.libraryListSource) select_row(self.libraryView, self.currentScriptIndex) if self.scriptText is not None: current = self.text.toPlainText() # do not mark scripts as modified if self.scriptText != current: self.text.document().setPlainText(self.scriptText) if self.splitterState is not None: self.splitCanvas.restoreState(QByteArray(self.splitterState)) def _saveState(self): self.scriptLibrary = [s.asdict() for s in self.libraryListSource] self.scriptText = self.text.toPlainText() self.splitterState = bytes(self.splitCanvas.saveState()) def set_input(self, index, obj, signal): dic = getattr(self, signal) dic[index] = obj def insert_input(self, index, obj, signal): dic = getattr(self, signal) dic.insert(index, obj) def remove_input(self, index, signal): dic = getattr(self, signal) dic.pop(index) @Inputs.data def set_data(self, index, data): self.set_input(index, data, "data") @Inputs.data.insert def insert_data(self, index, data): self.insert_input(index, data, "data") @Inputs.data.remove def remove_data(self, index): self.remove_input(index, "data") @Inputs.learner def set_learner(self, index, learner): self.set_input(index, learner, "learner") @Inputs.learner.insert def insert_learner(self, index, learner): self.insert_input(index, learner, "learner") @Inputs.learner.remove def remove_learner(self, index): self.remove_input(index, "learner") @Inputs.classifier def set_classifier(self, index, classifier): self.set_input(index, classifier, "classifier") @Inputs.classifier.insert def insert_classifier(self, index, classifier): self.insert_input(index, classifier, "classifier") @Inputs.classifier.remove def remove_classifier(self, index): self.remove_input(index, "classifier") @Inputs.object def set_object(self, index, object): self.set_input(index, object, "object") @Inputs.object.insert def insert_object(self, index, object): self.insert_input(index, object, "object") @Inputs.object.remove def remove_object(self, index): self.remove_input(index, "object") def handleNewSignals(self): # update fake signature labels self.func_sig.update_signal_text( {n: len(getattr(self, n)) for n in self.signal_names}) self.commit() def selectedScriptIndex(self): rows = self.libraryView.selectionModel().selectedRows() if rows: return [i.row() for i in rows][0] else: return None def setSelectedScript(self, index): select_row(self.libraryView, index) def onAddScript(self, *_): self.libraryList.append( Script("New script", self.text.toPlainText(), 0)) self.setSelectedScript(len(self.libraryList) - 1) def onAddScriptFromFile(self, *_): filename, _ = QFileDialog.getOpenFileName( self, 'Open Python Script', os.path.expanduser("~/"), 'Python files (*.py)\nAll files(*.*)') if filename: name = os.path.basename(filename) with tokenize.open(filename) as f: contents = f.read() self.libraryList.append(Script(name, contents, 0, filename)) self.setSelectedScript(len(self.libraryList) - 1) def onRemoveScript(self, *_): index = self.selectedScriptIndex() if index is not None: del self.libraryList[index] select_row(self.libraryView, max(index - 1, 0)) def onSaveScriptToFile(self, *_): index = self.selectedScriptIndex() if index is not None: self.saveScript() def onSelectedScriptChanged(self, selected, _deselected): index = [i.row() for i in selected.indexes()] if index: current = index[0] if current >= len(self.libraryList): self.addNewScriptAction.trigger() return self.text.setDocument(self.documentForScript(current)) self.currentScriptIndex = current def documentForScript(self, script=0): if not isinstance(script, Script): script = self.libraryList[script] if script not in self._cachedDocuments: doc = QTextDocument(self) doc.setDocumentLayout(QPlainTextDocumentLayout(doc)) doc.setPlainText(script.script) doc.setDefaultFont(QFont(self.defaultFont)) doc.highlighter = PygmentsHighlighter(doc) doc.highlighter.set_style(self.pygments_style_class) doc.setDefaultFont( QFont(self.defaultFont, pointSize=self.defaultFontSize)) doc.modificationChanged[bool].connect(self.onModificationChanged) doc.setModified(False) self._cachedDocuments[script] = doc return self._cachedDocuments[script] def commitChangesToLibrary(self, *_): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].script = self.text.toPlainText() self.text.document().setModified(False) self.libraryList.emitDataChanged(index) def onModificationChanged(self, modified): index = self.selectedScriptIndex() if index is not None: self.libraryList[index].flags = Script.Modified if modified else 0 self.libraryList.emitDataChanged(index) def restoreSaved(self): index = self.selectedScriptIndex() if index is not None: self.text.document().setPlainText(self.libraryList[index].script) self.text.document().setModified(False) def saveScript(self): index = self.selectedScriptIndex() if index is not None: script = self.libraryList[index] filename = script.filename else: filename = os.path.expanduser("~/") filename, _ = QFileDialog.getSaveFileName( self, 'Save Python Script', filename, 'Python files (*.py)\nAll files(*.*)') if filename: fn = "" head, tail = os.path.splitext(filename) if not tail: fn = head + ".py" else: fn = filename f = open(fn, 'w') f.write(self.text.toPlainText()) f.close() def initial_locals_state(self): d = {} for name in self.signal_names: value = getattr(self, name) all_values = list(value) one_value = all_values[0] if len(all_values) == 1 else None d["in_" + name + "s"] = all_values d["in_" + name] = one_value return d def commit(self): self.Error.clear() lcls = self.initial_locals_state() lcls["_script"] = str(self.text.toPlainText()) self.console.updateLocals(lcls) self.console.write("\nRunning script:\n") self.console.push("exec(_script)") self.console.new_prompt(sys.ps1) for signal in self.signal_names: out_var = self.console.locals.get("out_" + signal) signal_type = getattr(self.Outputs, signal).type if not isinstance(out_var, signal_type) and out_var is not None: self.Error.add_message( signal, "'{}' has to be an instance of '{}'.".format( signal, signal_type.__name__)) getattr(self.Error, signal)() out_var = None getattr(self.Outputs, signal).send(out_var) def keyPressEvent(self, e): if e.matches(QKeySequence.InsertLineSeparator): # run on Shift+Enter, Ctrl+Enter self.run_action.trigger() e.accept() else: super().keyPressEvent(e) def dragEnterEvent(self, event): # pylint: disable=no-self-use urls = event.mimeData().urls() if urls: # try reading the file as text c = read_file_content(urls[0].toLocalFile(), limit=1000) if c is not None: event.acceptProposedAction() @classmethod def migrate_settings(cls, settings, version): if version is not None and version < 2: scripts = settings.pop("libraryListSource") # type: List[Script] library = [ dict(name=s.name, script=s.script, filename=s.filename) for s in scripts ] # type: List[_ScriptData] settings["scriptLibrary"] = library def onDeleteWidget(self): self.text.terminate() super().onDeleteWidget()