From c1b11a0085c780ac3d5e3400a31ad939b376ede3 Mon Sep 17 00:00:00 2001 From: Jordan Wiens Date: Wed, 2 Sep 2020 14:18:07 -0400 Subject: update snippets from latest plugin --- python/examples/snippets/LICENSE | 4 +- python/examples/snippets/QCodeEditor.py | 379 ++++++++++++++++++++++++++++ python/examples/snippets/README.md | 20 +- python/examples/snippets/__init__.py | 113 +++++++-- python/examples/snippets/media/snippets.gif | Bin 0 -> 4105422 bytes python/examples/snippets/plugin.json | 24 +- 6 files changed, 482 insertions(+), 58 deletions(-) create mode 100644 python/examples/snippets/QCodeEditor.py create mode 100644 python/examples/snippets/media/snippets.gif (limited to 'python/examples/snippets') diff --git a/python/examples/snippets/LICENSE b/python/examples/snippets/LICENSE index c9f5cb2c..edd8f6bc 100644 --- a/python/examples/snippets/LICENSE +++ b/python/examples/snippets/LICENSE @@ -1,7 +1,7 @@ -Copyright (c) 2019-2020 Vector 35 Inc +Copyright (c) 2019 Vector 35 Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/python/examples/snippets/QCodeEditor.py b/python/examples/snippets/QCodeEditor.py new file mode 100644 index 00000000..7b0c0e09 --- /dev/null +++ b/python/examples/snippets/QCodeEditor.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +''' +Licensed under the terms of the MIT License +https://github.com/luchko/QCodeEditor +@author: Ivan Luchko (luchko.ivan@gmail.com) + +Python Highlighting added by: +https://github.com/unihernandez22/QCodeEditor +@author: unihernandez22 + +Adapted to Binary Ninja by: +@author: Jordan Wiens (https://github.com/psifertex) + +Integrating syntax highlighting from: +https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting +Released under the Modified BSD License: http://directory.fsf.org/wiki/License:BSD_3Clause + +Note that this will not be merged back to the parent repositories as it's been +modified to be heavily dependent on the BN theme system. +''' + +from PySide2.QtCore import Qt, QRect, QRegExp +from PySide2.QtWidgets import QWidget, QTextEdit, QPlainTextEdit +from PySide2.QtGui import (QPainter, QFont, QSyntaxHighlighter, QTextFormat, QTextCharFormat) +from binaryninjaui import (getMonospaceFont, getThemeColor, ThemeColor) + + +def format(color, style=''): + """Return a QTextCharFormat with the given attributes.""" + _color = eval('getThemeColor(ThemeColor.%s)' % color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + return _format + +STYLES = { + 'keyword': format('StackVariableColor'), + 'operator': format('TokenHighlightColor'), + 'brace': format('LinearDisassemblySeparatorColor'), + 'defclass': format('DataSymbolColor'), + 'string': format('StringColor'), + 'string2': format('TypeNameColor'), + 'comment': format('AnnotationColor', 'italic'), + 'self': format('KeywordColor', 'italic'), + 'numbers': format('NumberColor'), + 'numberbar': getThemeColor(ThemeColor.BackgroundHighlightDarkColor), + 'blockselected': getThemeColor(ThemeColor.TokenHighlightColor), + 'blocknormal': getThemeColor(ThemeColor.TokenSelectionColor) +} + +class PythonHighlighter (QSyntaxHighlighter): + """Syntax highlighter for the Python language. + """ + # Python keywords + keywords = [ + 'and', 'assert', 'break', 'class', 'continue', 'def', + 'del', 'elif', 'else', 'except', 'exec', 'finally', + 'for', 'from', 'global', 'if', 'import', 'in', + 'is', 'lambda', 'not', 'or', 'pass', 'print', + 'raise', 'return', 'try', 'while', 'yield', + 'None', 'True', 'False', + ] + + # Python operators + operators = [ + '=', + # Comparison + '==', '!=', '<', '<=', '>', '>=', + # Arithmetic + '\+', '-', '\*', '/', '//', '\%', '\*\*', + # In-place + '\+=', '-=', '\*=', '/=', '\%=', + # Bitwise + '\^', '\|', '\&', '\~', '>>', '<<', + ] + + # Python braces + braces = [ + '\{', '\}', '\(', '\)', '\[', '\]', + ] + def __init__(self, document): + QSyntaxHighlighter.__init__(self, document) + + # Multi-line strings (expression, flag, style) + # FIXME: The triple-quotes in these two lines will mess up the + # syntax highlighting from this point onward + self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) + self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + + rules = [] + + # Keyword, operator, and brace rules + rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + for w in PythonHighlighter.keywords] + rules += [(r'%s' % o, 0, STYLES['operator']) + for o in PythonHighlighter.operators] + rules += [(r'%s' % b, 0, STYLES['brace']) + for b in PythonHighlighter.braces] + + # All other rules + rules += [ + # 'self' + (r'\bself\b', 0, STYLES['self']), + + # Double-quoted string, possibly containing escape sequences + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + # Single-quoted string, possibly containing escape sequences + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + + # 'def' followed by an identifier + (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + # 'class' followed by an identifier + (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + + # From '#' until a newline + (r'#[^\n]*', 0, STYLES['comment']), + + # Numeric literals + (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + ] + + # Build a QRegExp for each pattern + self.rules = [(QRegExp(pat), index, fmt) + for (pat, index, fmt) in rules] + + + def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + # Do other syntax formatting + for expression, nth, format in self.rules: + index = expression.indexIn(text, 0) + + while index >= 0: + # We actually want the index of the nth match + index = expression.pos(nth) + length = len(expression.cap(nth)) + self.setFormat(index, length, format) + index = expression.indexIn(text, index + length) + + self.setCurrentBlockState(0) + + # Do multi-line strings + in_multiline = self.match_multiline(text, *self.tri_single) + if not in_multiline: + in_multiline = self.match_multiline(text, *self.tri_double) + + + def match_multiline(self, text, delimiter, in_state, style): + """Do highlighting of multi-line strings. ``delimiter`` should be a + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and + ``in_state`` should be a unique integer to represent the corresponding + state changes when inside those strings. Returns True if we're still + inside a multi-line string when this function is finished. + """ + # If inside triple-single quotes, start at 0 + if self.previousBlockState() == in_state: + start = 0 + add = 0 + # Otherwise, look for the delimiter on this line + else: + start = delimiter.indexIn(text) + # Move past this match + add = delimiter.matchedLength() + + # As long as there's a delimiter match on this line... + while start >= 0: + # Look for the ending delimiter + end = delimiter.indexIn(text, start + add) + # Ending delimiter on this line? + if end >= add: + length = end - start + add + delimiter.matchedLength() + self.setCurrentBlockState(0) + # No; multi-line string + else: + self.setCurrentBlockState(in_state) + length = len(text) - start + add + # Apply formatting + self.setFormat(start, length, style) + # Look for the next match + start = delimiter.indexIn(text, start + length) + + # Return True if still inside a multi-line string, False otherwise + if self.currentBlockState() == in_state: + return True + else: + return False + + +class QCodeEditor(QPlainTextEdit): + ''' + QCodeEditor inherited from QPlainTextEdit providing: + + numberBar - set by DISPLAY_LINE_NUMBERS flag equals True + curent line highligthing - set by HIGHLIGHT_CURRENT_LINE flag equals True + setting up QSyntaxHighlighter + + references: + https://john.nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/ + http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html + + ''' + class NumberBar(QWidget): + '''class that deifnes textEditor numberBar''' + + def __init__(self, editor): + QWidget.__init__(self, editor) + + self.editor = editor + self.editor.blockCountChanged.connect(self.updateWidth) + self.editor.updateRequest.connect(self.updateContents) + self.font = QFont() + self.numberBarColor = STYLES["numberbar"] + + def paintEvent(self, event): + + painter = QPainter(self) + painter.fillRect(event.rect(), self.numberBarColor) + + block = self.editor.firstVisibleBlock() + + # Iterate over all visible text blocks in the document. + while block.isValid(): + blockNumber = block.blockNumber() + block_top = self.editor.blockBoundingGeometry(block).translated(self.editor.contentOffset()).top() + + # Check if the position of the block is out side of the visible area. + if not block.isVisible() or block_top >= event.rect().bottom(): + break + + # We want the line number for the selected line to be bold. + if blockNumber == self.editor.textCursor().blockNumber(): + self.font.setBold(True) + painter.setPen(STYLES["blockselected"]) + else: + self.font.setBold(False) + painter.setPen(STYLES["blocknormal"]) + painter.setFont(self.font) + + # Draw the line number right justified at the position of the line. + paint_rect = QRect(0, block_top, self.width(), self.editor.fontMetrics().height()) + painter.drawText(paint_rect, Qt.AlignLeft, str(blockNumber+1)) + + block = block.next() + + painter.end() + + QWidget.paintEvent(self, event) + + def getWidth(self): + count = self.editor.blockCount() + width = self.fontMetrics().width(str(count)) + 10 + return width + + def updateWidth(self): + width = self.getWidth() + if self.width() != width: + self.setFixedWidth(width) + self.editor.setViewportMargins(width, 0, 0, 0); + + def updateContents(self, rect, scroll): + if scroll: + self.scroll(0, scroll) + else: + self.update(0, rect.y(), self.width(), rect.height()) + + if rect.contains(self.editor.viewport().rect()): + fontSize = self.editor.currentCharFormat().font().pointSize() + self.font.setPointSize(fontSize) + self.font.setStyle(QFont.StyleNormal) + self.updateWidth() + + + def __init__(self, DISPLAY_LINE_NUMBERS=True, HIGHLIGHT_CURRENT_LINE=True, + SyntaxHighlighter=None, *args): + ''' + Parameters + ---------- + DISPLAY_LINE_NUMBERS : bool + switch on/off the presence of the lines number bar + HIGHLIGHT_CURRENT_LINE : bool + switch on/off the current line highliting + SyntaxHighlighter : QSyntaxHighlighter + should be inherited from QSyntaxHighlighter + + ''' + super(QCodeEditor, self).__init__() + + self.setFont(QFont("Ubuntu Mono", 11)) + self.setLineWrapMode(QPlainTextEdit.NoWrap) + + self.DISPLAY_LINE_NUMBERS = DISPLAY_LINE_NUMBERS + + if DISPLAY_LINE_NUMBERS: + self.number_bar = self.NumberBar(self) + + if HIGHLIGHT_CURRENT_LINE: + self.currentLineNumber = None + self.currentLineColor = STYLES['currentLine'] + self.cursorPositionChanged.connect(self.highligtCurrentLine) + + if SyntaxHighlighter is not None: # add highlighter to textdocument + self.highlighter = SyntaxHighlighter(self.document()) + + def resizeEvent(self, *e): + '''overload resizeEvent handler''' + + if self.DISPLAY_LINE_NUMBERS: # resize number_bar widget + cr = self.contentsRect() + rec = QRect(cr.left(), cr.top(), self.number_bar.getWidth(), cr.height()) + self.number_bar.setGeometry(rec) + + QPlainTextEdit.resizeEvent(self, *e) + + def highligtCurrentLine(self): + newCurrentLineNumber = self.textCursor().blockNumber() + if newCurrentLineNumber != self.currentLineNumber: + self.currentLineNumber = newCurrentLineNumber + hi_selection = QTextEdit.ExtraSelection() + hi_selection.format.setBackground(self.currentLineColor) + hi_selection.format.setProperty(QTextFormat.FullWidthSelection, True) + hi_selection.cursor = self.textCursor() + hi_selection.cursor.clearSelection() + self.setExtraSelections([hi_selection]) + +############################################################################## + +if __name__ == '__main__': + + # TESTING + + def run_test(): + + from PySide2.QtGui import QApplication + import sys + + app = QApplication([]) + + editor = QCodeEditor(DISPLAY_LINE_NUMBERS=True, + HIGHLIGHT_CURRENT_LINE=True, + SyntaxHighlighter=PythonHighlighter) + +# text = ''' +# +# +# 1.0 0.0 0.0 +# 0.0 1.0 0.0 +# +# +# +# +# +# +# +# +# ''' + text = """\ +def hello(text): + print(text) + +hello('Hello World') + +# Comment""" + editor.setPlainText(text) + editor.resize(400,250) + editor.show() + + sys.exit(app.exec_()) + + + run_test() diff --git a/python/examples/snippets/README.md b/python/examples/snippets/README.md index f2b24889..00274c3a 100644 --- a/python/examples/snippets/README.md +++ b/python/examples/snippets/README.md @@ -1,18 +1,17 @@ -# Snippet UI Plugin (v1.1) +# Snippet UI Plugin (v1.5) Author: **Vector 35 Inc** -_Example UI plugin demonstrating how to create a snippet manager that allows for quick one-liners to be bound to hotkeys._ -## Description: -# Snippet UI Plugin (v1.0 alpha) -Author: **Vector 35 Inc** +_Powerful code-editing plugin for writing and managing python code-snippets with syntax highlightingd, hotkey binding and other features_ + +## Description: -_Example UI plugin demonstrating how to create a snippet manager that allows for quick one-liners to be bound to hotkeys._ +The snippet editor started as a simple example UI plugin to demonstrate new features available to UI plugins. It has turned into a functionally useful plugin in its own right. The snippet editor allows you to write small bits of code that might not be big enough to warrant the effort of a full plugin but are longer enough that you don't want to retype them every time in the python-console! -![](./media/snippets.gif) +As an added bonus, all snippets are added to the snippets menu and hot-keys can be associated with them as they make use of the action system. All action-system items are also available through the command-palette (CTL/CMD-p). -## Description: +![](https://github.com/Vector35/snippets/blob/master/media/snippets.gif?raw=true) -This plugin is dual purpose -- first, it demonstrates the new UI plugin interface available for third-party plugins, and secondly it implements the often-requested functionality of adding a snippet editor. This is particularly useful for binding commonly used snippets of python code from the ScriptingConsole to a hotkey to be able to trigger them more easily. +. ## Installation Instructions @@ -28,11 +27,12 @@ no special instructions, package manager is recommended ### Windows no special instructions, package manager is recommended + ## Minimum Version This plugin requires the following minimum version of Binary Ninja: - * 1401 +* 1528 diff --git a/python/examples/snippets/__init__.py b/python/examples/snippets/__init__.py index 5fbae0d9..f751f303 100644 --- a/python/examples/snippets/__init__.py +++ b/python/examples/snippets/__init__.py @@ -3,18 +3,21 @@ import sys import os import re +import codecs from PySide2.QtWidgets import (QLineEdit, QPushButton, QApplication, QTextEdit, QWidget, - QVBoxLayout, QHBoxLayout, QDialog, QFileSystemModel, QTreeView, QLabel, QSplitter, - QInputDialog, QMessageBox, QHeaderView, QMenu, QAction, QKeySequenceEdit, - QPlainTextEdit) -from PySide2.QtCore import (QDir, QObject, Qt, QFileInfo, QItemSelectionModel, QSettings) -from PySide2.QtGui import (QFont, QFontMetrics, QDesktopServices, QKeySequence) + QVBoxLayout, QHBoxLayout, QDialog, QFileSystemModel, QTreeView, QLabel, QSplitter, + QInputDialog, QMessageBox, QHeaderView, QMenu, QAction, QKeySequenceEdit, + QPlainTextEdit) +from PySide2.QtCore import (QDir, QObject, Qt, QFileInfo, QItemSelectionModel, QSettings, QUrl) +from PySide2.QtGui import (QFont, QFontMetrics, QDesktopServices, QKeySequence, QIcon) from binaryninja import user_plugin_path from binaryninja.plugin import PluginCommand, MainThreadActionHandler from binaryninja.mainthread import execute_on_main_thread from binaryninja.log import (log_error, log_debug) -from binaryninjaui import (getMonospaceFont, UIAction, UIActionHandler, Menu) +from binaryninjaui import (getMonospaceFont, UIAction, UIActionHandler, Menu, DockHandler, + getThemeColor, ThemeColor) import numbers +from .QCodeEditor import QCodeEditor, PythonHighlighter snippetPath = os.path.realpath(os.path.join(user_plugin_path(), "..", "snippets")) try: @@ -32,9 +35,10 @@ def includeWalk(dir, includeExt): filePaths.append(os.path.join(root, f)) return filePaths + def loadSnippetFromFile(snippetPath): try: - snippetText = open(snippetPath, 'r').readlines() + snippetText = codecs.open(snippetPath, 'r', "utf-8").readlines() except: return ("", "", "") if (len(snippetText) < 3): @@ -48,8 +52,30 @@ def loadSnippetFromFile(snippetPath): ''.join(snippetText[2:]) ) + +def actionFromSnippet(snippetName, snippetDescription): + if not snippetDescription: + shortName = os.path.basename(snippetName) + if shortName.endswith('.py'): + shortName = shortName[:-3] + return "Snippets\\" + shortName + else: + return "Snippets\\" + snippetDescription + + def executeSnippet(code, context): snippetGlobals = {} + if context.binaryView == None: + dock = DockHandler.getActiveDockHandler() + if not dock: + log_error("Snippet triggered with no context and no dock handler. This should not happen. Please report reproduction steps if possible.") + return + viewFrame = dock.getViewFrame() + if not viewFrame: + log_error("Snippet triggered with no context and no view frame. Snippets require at least one open binary.") + return + viewInterface = viewFrame.getCurrentViewInterface() + context.binaryView = viewInterface.getData() snippetGlobals['current_view'] = context.binaryView snippetGlobals['bv'] = context.binaryView if not context.function: @@ -91,27 +117,34 @@ def executeSnippet(code, context): if snippetGlobals['current_address'] != context.address: context.binaryView.file.navigate(context.binaryView.file.view, snippetGlobals['current_address']) + def makeSnippetFunction(code): return lambda context: executeSnippet(code, context) class Snippets(QDialog): - def __init__(self, parent=None): + def __init__(self, context, parent=None): super(Snippets, self).__init__(parent) # Create widgets self.setWindowModality(Qt.ApplicationModal) self.title = QLabel(self.tr("Snippet Editor")) - self.saveButton = QPushButton(self.tr("Save")) + self.saveButton = QPushButton(self.tr("&Save")) + self.saveButton.setShortcut(QKeySequence(self.tr("Ctrl+S"))) + self.runButton = QPushButton(self.tr("&Run")) + self.runButton.setShortcut(QKeySequence(self.tr("Ctrl+R"))) self.closeButton = QPushButton(self.tr("Close")) self.clearHotkeyButton = QPushButton(self.tr("Clear Hotkey")) self.setWindowTitle(self.title.text()) - self.newFolderButton = QPushButton("New Folder") + #self.newFolderButton = QPushButton("New Folder") + self.browseButton = QPushButton("Browse Snippets") + self.browseButton.setIcon(QIcon.fromTheme("edit-undo")) self.deleteSnippetButton = QPushButton("Delete") self.newSnippetButton = QPushButton("New Snippet") - self.edit = QPlainTextEdit() + self.edit = QCodeEditor(HIGHLIGHT_CURRENT_LINE=False, SyntaxHighlighter=PythonHighlighter) self.edit.setPlaceholderText("python code") self.resetting = False self.columns = 3 + self.context = context self.keySequenceEdit = QKeySequenceEdit(self) self.currentHotkey = QKeySequence() @@ -145,7 +178,8 @@ class Snippets(QDialog): treeLayout = QVBoxLayout() treeLayout.addWidget(self.tree) treeButtons = QHBoxLayout() - treeButtons.addWidget(self.newFolderButton) + #treeButtons.addWidget(self.newFolderButton) + treeButtons.addWidget(self.browseButton) treeButtons.addWidget(self.newSnippetButton) treeButtons.addWidget(self.deleteSnippetButton) treeLayout.addLayout(treeButtons) @@ -158,6 +192,7 @@ class Snippets(QDialog): buttons.addWidget(self.keySequenceEdit) buttons.addWidget(self.currentHotkeyLabel) buttons.addWidget(self.closeButton) + buttons.addWidget(self.runButton) buttons.addWidget(self.saveButton) description = QHBoxLayout() @@ -196,11 +231,13 @@ class Snippets(QDialog): # Add signals self.saveButton.clicked.connect(self.save) self.closeButton.clicked.connect(self.close) + self.runButton.clicked.connect(self.run) self.clearHotkeyButton.clicked.connect(self.clearHotkey) self.tree.selectionModel().selectionChanged.connect(self.selectFile) self.newSnippetButton.clicked.connect(self.newFileDialog) self.deleteSnippetButton.clicked.connect(self.deleteSnippet) - self.newFolderButton.clicked.connect(self.newFolder) + #self.newFolderButton.clicked.connect(self.newFolder) + self.browseButton.clicked.connect(self.browseSnippets) if self.settings.contains("ui/snippeteditor/selected"): selectedName = self.settings.value("ui/snippeteditor/selected") @@ -229,17 +266,14 @@ class Snippets(QDialog): for snippet in includeWalk(snippetPath, ".py"): snippetKeys = None (snippetDescription, snippetKeys, snippetCode) = loadSnippetFromFile(snippet) - if not snippetDescription: - actionText = "Snippets\\" + os.path.basename(snippet).rstrip(".py") - else: - actionText = "Snippets\\" + snippetDescription + actionText = actionFromSnippet(snippet, snippetDescription) if snippetCode: if snippetKeys == None: UIAction.registerAction(actionText) else: UIAction.registerAction(actionText, snippetKeys) UIActionHandler.globalActions().bindAction(actionText, UIAction(makeSnippetFunction(snippetCode))) - Menu.mainMenu("Tools").addAction(actionText, actionText) + Menu.mainMenu("Tools").addAction(actionText, "Snippets") def clearSelection(self): self.keySequenceEdit.clear() @@ -247,7 +281,8 @@ class Snippets(QDialog): self.currentHotkeyLabel.setText("") self.currentFileLabel.setText("") self.snippetDescription.setText("") - self.edit.setPlainText("") + self.edit.clear() + self.tree.clearSelection() self.currentFile = "" def reject(self): @@ -259,6 +294,10 @@ class Snippets(QDialog): return self.accept() + def browseSnippets(self): + url = QUrl.fromLocalFile(snippetPath) + QDesktopServices.openUrl(url); + def newFolder(self): (folderName, ok) = QInputDialog.getText(self, self.tr("Folder Name"), self.tr("Folder Name: ")) if ok and folderName: @@ -273,11 +312,16 @@ class Snippets(QDialog): if (self.resetting): self.resetting = False return + if len(new.indexes()) == 0: + self.clearSelection() + self.currentFile = "" + self.readOnly(True) + return newSelection = self.files.filePath(new.indexes()[0]) self.settings.setValue("ui/snippeteditor/selected", newSelection) if QFileInfo(newSelection).isDir(): self.readOnly(True) - self.tree.clearSelection() + self.clearSelection() self.currentFile = "" return @@ -351,7 +395,28 @@ class Snippets(QDialog): def save(self): log_debug("Saving snippet %s" % self.currentFile) - outputSnippet = open(self.currentFile, "w") + outputSnippet = codecs.open(self.currentFile, "w", "utf-8") + outputSnippet.write("#" + self.snippetDescription.text() + "\n") + outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n") + outputSnippet.write(self.edit.toPlainText()) + outputSnippet.close() + self.registerAllSnippets() + + def run(self): + if self.context == None: + log_warn("Cannot run snippets outside of the UI at this time.") + return + if self.snippetChanged(): + question = QMessageBox.question(self, self.tr("Confirm"), self.tr("You have unsaved changes, must save first. Save?")) + if (question == QMessageBox.StandardButton.No): + return + else: + self.save() + actionText = actionFromSnippet(self.currentFile, self.snippetDescription.text()) + UIActionHandler.globalActions().executeAction(actionText, self.context) + + log_debug("Saving snippet %s" % self.currentFile) + outputSnippet = codecs.open(self.currentFile, "w", "utf-8") outputSnippet.write("#" + self.snippetDescription.text() + "\n") outputSnippet.write("#" + self.keySequenceEdit.keySequence().toString() + "\n") outputSnippet.write(self.edit.toPlainText()) @@ -361,13 +426,15 @@ class Snippets(QDialog): def clearHotkey(self): self.keySequenceEdit.clear() + def launchPlugin(context): - snippets = Snippets() + snippets = Snippets(context) snippets.exec_() + if __name__ == '__main__': app = QApplication(sys.argv) - snippets = Snippets() + snippets = Snippets(None) snippets.show() sys.exit(app.exec_()) else: diff --git a/python/examples/snippets/media/snippets.gif b/python/examples/snippets/media/snippets.gif new file mode 100644 index 00000000..08ea5766 Binary files /dev/null and b/python/examples/snippets/media/snippets.gif differ diff --git a/python/examples/snippets/plugin.json b/python/examples/snippets/plugin.json index d2e4ebdf..27517a26 100644 --- a/python/examples/snippets/plugin.json +++ b/python/examples/snippets/plugin.json @@ -1,23 +1 @@ -{ - "pluginmetadataversion": 2, - "name": "Snippet UI Plugin", - "type": ["ui"], - "api": ["python2", "python3"], - "description": "Example UI plugin demonstrating how to create a snippet manager that allows for quick one-liners to be bound to hotkeys.", - "longdescription": "# Snippet UI Plugin (v1.0 alpha)\n\nAuthor: **Vector 35 Inc**\n\n_Example UI plugin demonstrating how to create a snippet manager that allows for quick one-liners to be bound to hotkeys._\n\n![](./media/snippets.gif)\n\n## Description:\n\nThis plugin is dual purpose -- first, it demonstrates the new UI plugin interface available for third-party plugins, and secondly it implements the often-requested functionality of adding a snippet editor. This is particularly useful for binding commonly used snippets of python code from the ScriptingConsole to a hotkey to be able to trigger them more easily.", - "license": { - "name": "MIT", - "text": "Copyright (c) 2019 Vector 35 Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." - }, - "platforms" : ["Darwin", "Linux", "Windows"], - "installinstructions" : { - "Darwin" : "no special instructions, package manager is recommended", - "Linux" : "no special instructions, package manager is recommended", - "Windows" : "no special instructions, package manager is recommended" - }, - "dependencies": { - }, - "version": "1.1", - "author": "Vector 35 Inc", - "minimumbinaryninjaversion": 1401 -} +{"pluginmetadataversion": 2, "name": "Snippet UI Plugin", "type": ["ui"], "api": ["python2", "python3"], "description": "Powerful code-editing plugin for writing and managing python code-snippets with syntax highlightingd, hotkey binding and other features", "longdescription": "The snippet editor started as a simple example UI plugin to demonstrate new features available to UI plugins. It has turned into a functionally useful plugin in its own right. The snippet editor allows you to write small bits of code that might not be big enough to warrant the effort of a full plugin but are longer enough that you don't want to retype them every time in the python-console!\n\nAs an added bonus, all snippets are added to the snippets menu and hot-keys can be associated with them as they make use of the action system. All action-system items are also available through the command-palette (CTL/CMD-p).\n\n![](https://github.com/Vector35/snippets/blob/master/media/snippets.gif?raw=true)\n\n.", "license": {"name": "MIT", "text": "Copyright (c) 2019 Vector 35 Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."}, "platforms": ["Darwin", "Linux", "Windows"], "installinstructions": {"Darwin": "no special instructions, package manager is recommended", "Linux": "no special instructions, package manager is recommended", "Windows": "no special instructions, package manager is recommended"}, "dependencies": {}, "version": "1.5", "author": "Vector 35 Inc", "minimumbinaryninjaversion": 1528} \ No newline at end of file -- cgit v1.3.1