class LoaderList(QtWidgets.QListView): #WIP name emit_text = QtCore.Signal(str) emit_tab = QtCore.Signal(dict) def __init__(self): super(LoaderList, self).__init__() _model = QtGui.QStandardItemModel() self._model = QtGui.QStandardItemModel() self.setModel(self._model) def __setitem__(self, name, value): item = QtGui.QStandardItem(name) item.setData(value, role=QtCore.Qt.UserRole + 1) self._model.appendRow(item) def selectionChanged(self, selected, deselected): #print selected, deselected for index in selected.indexes(): item = self._model.item(index.row(), index.column()) #print item a = item.data(QtCore.Qt.UserRole + 1) print a #self.emit_text.emit(text) self.emit_tab.emit(a) #if index.column() == 0: super(LoaderList, self).selectionChanged(selected, deselected)
def set_style(self, theme): self.styles = { feature: self.format(*style) for feature, style in theme.items() } self.tri_single = (QtCore.QRegExp("'''"), 1, self.styles['comment']) self.tri_double = (QtCore.QRegExp('"""'), 2, self.styles['comment'])
class EditLine(QtWidgets.QLineEdit): """ Base class for search/replace widgets. Emits signals on certain keystrokes and sets defaults common to both. """ escape_signal = QtCore.Signal() ctrl_enter_signal = QtCore.Signal() def __init__(self, editor): super(EditLine, self).__init__(editor) self.editor = editor font = QtGui.QFont(constants.DEFAULT_FONT) font.setPointSize(10) self.setFont(font) def keyPressEvent(self, event): esc = QtCore.Qt.Key.Key_Escape if event.key() == esc: self.editor.setFocus(QtCore.Qt.MouseFocusReason) self.escape_signal.emit() return enter_keys = [QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter] enter = event.key() in enter_keys ctrl = event.modifiers() == QtCore.Qt.ControlModifier if ctrl and enter: self.ctrl_enter_signal.emit() super(EditLine, self).keyPressEvent(event)
class Signal(QtCore.QObject): s = QtCore.Signal(str) e = QtCore.Signal() receivers = [] def customEvent(self, event): pass # from _fnpython import stderrRedirector, outputRedirector # try: # outputRedirector(event.text) # except: # pass for func in self.receivers: func(text=event.text)
def set_list(self, stringlist): """ Sets the list of completions. """ qslm = QtCore.QStringListModel() qslm.setStringList(stringlist) self.setModel(qslm)
def sizeHint(self): self.ensurePolished() width = self.style().pixelMetric( QtWidgets.QStyle.PM_TabCloseIndicatorWidth, None, self) height = self.style().pixelMetric( QtWidgets.QStyle.PM_TabCloseIndicatorHeight, None, self) return QtCore.QSize(width, height)
def sizeHint(self, option, index): text = index.data() fontmetrics = option.fontMetrics try: get_width = fontmetrics.horizontalAdvance except AttributeError: # python 2.7 & earlier versions of PySide2 get_width = fontmetrics.width width = get_width(text) column_width = self.column_width chars_per_line = 250 while (width > column_width) and (chars_per_line > 10): width = get_width(text, chars_per_line) chars_per_line -= 10 num_lines = math.ceil(len(text) / chars_per_line) + 0.5 height = fontmetrics.height() height *= num_lines size = QtCore.QSize() size.setWidth(width) size.setHeight(height) # FIXME: the below causes recursion. we do want to figure out a way # to have the row height adjusted though. this just isn't it yet. # index.model().setData(index, size, QtCore.Qt.SizeHintRole) # index.model().dataChanged.emit(index, index) return size
class Redirect(QtCore.QObject): signal = QtCore.Signal(str, object) def __init__(self, stream): super(Redirect, self).__init__() self.stream = stream self.queue = Queue(maxsize=2000) self.SERedirect = lambda x: None # self.receivers = [] for a in dir(stream): try: getattr(self, a) except AttributeError: attr = getattr(stream, a) setattr(self, a, attr) def write(self, text): queue = self.queue receivers = self.receivers('2signal') if not receivers: queue.put(text) else: # if queue.empty(): # queue = None # for func in receivers: # func(text=text, queue=queue) self.signal.emit(text, queue) self.stream.write(text) self.SERedirect(text)
class FileTree(QtWidgets.QTreeView): path_signal = QtCore.Signal(str) def __init__(self, path): super(FileTree, self).__init__() self.set_model(path) def set_model(self, path): model = QtWidgets.QFileSystemModel() model.setRootPath(path) model.setNameFilterDisables(False) model.setNameFilters(['*.py', '*.txt', '*.md']) self.setModel(model) RTC = QtWidgets.QHeaderView.ResizeToContents self.header().setResizeMode(RTC) self.setRootIndex(model.index(path)) def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: super(FileTree, self).mousePressEvent(event) if event.button() == QtCore.Qt.RightButton: menu = QtWidgets.QMenu() menu.addAction('New', 'print "does nothing"') menu.addAction('Delete', 'print "does nothing"') cursor = QtGui.QCursor() pos = cursor.pos() menu.exec_(pos) def selectionChanged(self, selected, deselected): index_sel = selected.indexes()[0] item = self.model().filePath(index_sel) self.path_signal.emit(item)
def move_to_bottom(self): geo = self.parent().geometry() centre = geo.center() x = centre.x() - (self.width() / 2) y = geo.bottom() - 70 pos = QtCore.QPoint(x, y) pos = self.parent().mapToGlobal(pos) self.move(pos)
def setup_save_timer(self, interval=500): """ Initialise the autosave timer. :param interval: autosave interval in milliseconds :type interval: int """ self.autosave_timer = QtCore.QTimer() self.autosave_timer.setSingleShot(True) self.autosave_timer.setInterval(interval)
def start_timer(self): """ Starts timer for self.check_update_globals """ self.timer = QtCore.QTimer() self.timer.setInterval(100) self.timer.timeout.connect(self.check_update_globals) self.timer.start()
def pythonKnobEdit(): if nuke.thisKnob().name() == 'showPanel': # TODO: is there a 'knob added' knobchanged? node = nuke.thisNode() global timer timer = QtCore.QTimer() timer.setSingleShot(True) timer.setInterval(10) timer.timeout.connect(partial(addTextKnobs, node)) timer.start()
def anim_popup_bar(popup_bar): anim = QtCore.QPropertyAnimation( popup_bar, 'maximumHeight' ) anim.setStartValue(0) anim.setEndValue(46) anim.setDuration(400) anim.start() anim_popup_bar.anim = anim
def __init__(self): super(Terminal, self).__init__() self.setReadOnly(True) self.setObjectName('Terminal') self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.queue = Queue() self.timer = QtCore.QTimer() self.timer.timeout.connect(self.check_queue) self.timer.setInterval(10) self.timer.start() self.interval = time.time()
def animate_popup(self, start=0, end=46): """ FIXME: This works in the prototype, but not here yet. Perhaps because it's called via a few signals, or something to do with the parent object being the tabeditor while the class is instantiated on the tabeditor's parent class, pythoneditor. """ anim = QtCore.QPropertyAnimation(self.popup_bar, 'maximumHeight') self._anim = anim anim.setStartValue(start) anim.setEndValue(end) anim.setDuration(400) anim.start()
class Signal(QtCore.QObject): s = QtCore.Signal(str) receivers = {'<stdout>': [], '<stderr>': []} def customEvent(self, event): for func in self.receivers[event.print_type]: func(text=event.text, queue=event.queue) if event.text is None: return if event.print_type == '<stdout>': sys.outputRedirector(event.text) elif event.print_type == '<stderr>': sys.stderrRedirector(event.text)
def setup_new_tab_btn(self): """ Adds a new tab [+] button to the right of the tabs. """ widget = QtWidgets.QWidget() widget.setObjectName('Tab_Widget_New_Button') self.insertTab(0, widget, '') nb = self.new_btn = QtWidgets.QToolButton() nb.setMinimumSize(QtCore.QSize(50, 10)) nb.setText('+') # you could set an icon instead of text nb.setAutoRaise(True) nb.clicked.connect(self.new_tab) tabBar = self.tabBar() tabBar.setTabButton(0, QtWidgets.QTabBar.RightSide, nb) tabBar.setTabEnabled(0, False)
class Editor(QtWidgets.QPlainTextEdit): block_key_press = False key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent) def __init__(self): super(Editor, self).__init__() self._completer = Completer(self) def keyPressEvent(self, event): self.block_key_press = False self.key_pressed_signal.emit(event) if self.block_key_press: return super(Editor, self).keyPressEvent(event)
def move_tab_close_button(self, pos): i = self.tabAt(pos) rect = self.tabRect(i) btn = self.tab_close_button x = rect.right() - btn.width() - 2 y = rect.center().y() - (btn.height() / 2) if i != self.currentIndex(): y += 2 btn_pos = QtCore.QPoint(x, y) btn.move(btn_pos) btn.raise_() if not self.tab_only_rect().contains(btn_pos): btn.hide() elif not self.tab_pressed: btn.show()
def make_rules(self): # rules rules = [] # function args/kwargs TODO: find correct regex pattern for separate # args and kwargs (words) between parentheses # rules += [('(?:def \w+\()([^)]+)', 1, self.styles['args'])] class_regex = r'(?:class\s+\w+\()([a-zA-Z\.]+)(?:\))' rules += [(class_regex, 1, self.styles['inherited'])] rules += [(r'\b%s\b' % i, 0, self.styles['arguments']) for i in self.arguments] rules += [(r'\b%s\b' % i, 0, self.styles['keyword']) for i in self.keywords] rules += [(i, 0, self.styles['keyword']) for i in self.operatorKeywords] rules += [(r'\b%s\b' % i, 0, self.styles['numbers']) for i in self.truthy] rules += [(r'\b%s\b' % i, 0, self.styles['instantiators']) for i in self.instantiators] rules += [(r'\b%s\b' % i, 0, self.styles['exceptions']) for i in self.exceptions + self.types] rules += [ # function names (r'(?:def\s+|)(\w+)(?:\()', 1, self.styles['function_names']), # class names (r'(?:class\s+)(\w+)(?:\()', 1, self.styles['class_names']), # methods (r'(?:\.)([a-zA-Z\.]+)(?:\()', 1, self.styles['methods']), # decorators (r'(?:@)(\w+)', 1, self.styles['function_names']), # string formatters (r'([rfb])(?:\'|\")', 0, self.styles['formatters']), # integers (r'\b[0-9]+\b', 0, self.styles['numbers']), # Double-quoted string, possibly containing escape sequences (r'"[^"\\]*(\\.[^"\\]*)*"', 0, self.styles['string']), # Single-quoted string, possibly containing escape sequences (r"'[^'\\]*(\\.[^'\\]*)*'", 0, self.styles['string']), ] # Build a QRegExp for each pattern self.rules = [(QtCore.QRegExp(pat), index, fmt) for (pat, index, fmt) in rules]
class ItemWordWrap(QtWidgets.QStyledItemDelegate): update_viewport = QtCore.Signal() def __init__(self, parent=None): super(ItemWordWrap, self).__init__(parent=parent) self.column_width = 100 @QtCore.Slot(int, int, int) def handle_column_resized(self, logicalIndex, oldSize, newSize): if logicalIndex == 2: self.column_width = newSize self.update_viewport.emit() def sizeHint(self, option, index): text = index.data() fontmetrics = option.fontMetrics try: get_width = fontmetrics.horizontalAdvance except AttributeError: # python 2.7 & earlier versions of PySide2 get_width = fontmetrics.width width = get_width(text) column_width = self.column_width chars_per_line = 250 while (width > column_width) and (chars_per_line > 10): width = get_width(text, chars_per_line) chars_per_line -= 10 num_lines = math.ceil(len(text) / chars_per_line) + 0.5 height = fontmetrics.height() height *= num_lines size = QtCore.QSize() size.setWidth(width) size.setHeight(height) # FIXME: the below causes recursion. we do want to figure out a way # to have the row height adjusted though. this just isn't it yet. # index.model().setData(index, size, QtCore.Qt.SizeHintRole) # index.model().dataChanged.emit(index, index) return size
class Terminal(QtWidgets.QPlainTextEdit): """ Output text display widget """ link_activated = QtCore.Signal(str) def __init__(self): super(Terminal, self).__init__() self.setObjectName('Terminal') self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.setReadOnly(True) self.setup() self.destroyed.connect(self.stop) font = QtGui.QFont(DEFAULT_FONT) font.setPointSize(10) self.setFont(font) @QtCore.Slot(str) def receive(self, text): try: textCursor = self.textCursor() if bool(textCursor): self.moveCursor(QtGui.QTextCursor.End) except Exception: pass self.insertPlainText(text) def stop(self): sys.stdout.reset() sys.stderr.reset() def setup(self): """ Checks for an existing stream wrapper for sys.stdout and connects to it. If not present, creates a new one. TODO: The FnRedirect sys.stdout is always active. With a singleton object on a thread, that reads off this stream, we can make it available to Python Editor even before opening the panel. """ if hasattr(sys.stdout, '_signal'): self.speaker = sys.stdout._signal else: self.speaker = streams.Speaker() sys.stdout = streams.SESysStdOut(sys.stdout, self.speaker) sys.stderr = streams.SESysStdErr(sys.stderr, self.speaker) self.speaker.emitter.connect(self.receive) def mousePressEvent(self, e): if not hasattr(self, 'anchorAt'): # pyqt doesn't use anchorAt return super(Terminal, self).mousePressEvent(e) if (e.button() == QtCore.Qt.LeftButton): clickedAnchor = self.anchorAt(e.pos()) if clickedAnchor: self.link_activated.emit(clickedAnchor) super(Terminal, self).mousePressEvent(e)
class ShortcutHandler(QtCore.QObject): """ Shortcut Manager with custom signals. :param editor: required `QPlainTextEdit` or `Editor` class. :param tabeditor: optional `QWidget` or `TabEditor` :param terminal: optional `QPlainTextEdit` or `Terminal` class. """ def __init__( self, editor=None, tabeditor=None, terminal=None, ): super(ShortcutHandler, self).__init__() self.setObjectName('ShortcutHandler') self._installed = False if editor is None: raise Exception(""" A text editor is necessary for this class. """.strip()) self.editor = editor if tabeditor is not None: self.tabeditor = tabeditor self.tabs = tabeditor.tabs parent_widget = tabeditor else: parent_widget = editor if terminal is not None: self.terminal = terminal self.parent_widget = parent_widget self.setParent(parent_widget) self.shortcut_dict = {} self.register_shortcuts() self.connect_signals() def connect_signals(self): """ Connects the current editor's signals to this class """ self.editor.focus_in_signal.connect(self.install_event_filter, QtCore.Qt.DirectConnection) self.editor.focus_out_signal.connect(self.remove_event_filter, QtCore.Qt.DirectConnection) def install_event_filter(self): if self._installed: return app = QtWidgets.QApplication.instance() QtCore.QCoreApplication.installEventFilter(app, self) self._installed = True def remove_event_filter(self): app = QtWidgets.QApplication.instance() QtCore.QCoreApplication.removeEventFilter(app, self) self._installed = False def eventFilter(self, obj, event): try: if not self.editor.isVisible(): self.remove_event_filter() return False except RuntimeError: self.remove_event_filter() return False if not self.editor.hasFocus(): self.remove_event_filter() return False if QtCore is None: return False if not hasattr(event, 'type'): return False if event.type() == QtCore.QEvent.Shortcut: if isinstance(obj, QtWidgets.QAction): combo = obj.shortcut() shortcut = combo.toString() action = self.shortcut_dict.get(shortcut) if action is None: return False action.trigger() return True if event.type() == QtCore.QEvent.KeyPress: # only let the editor receive keypress overrides if obj == self.editor: return self.handle_keypress(event) return False QtCore.Slot(QtGui.QKeyEvent) def handle_keypress(self, event): app = QtWidgets.QApplication held = app.keyboardModifiers() if (event.isAutoRepeat() and held == QtCore.Qt.NoModifier): return False key = event.key() if key in [ QtCore.Qt.Key_Control, QtCore.Qt.Key_Shift, QtCore.Qt.Key_Alt, QtCore.Qt.Key_AltGr, QtCore.Qt.Key_Meta, ]: return False # is it a Tab after a dot? if key == QtCore.Qt.Key_Tab: cursor = self.editor.textCursor() cursor.select(cursor.LineUnderCursor) text = cursor.selectedText() if text.endswith('.'): # allow autocompletion to handle this return False # try with event.text() for things # like " and { which appear as # shift+2 and shift+[ respectively action = self.shortcut_dict.get(event.text()) single_key = (action is not None) if not single_key: combo = key_to_sequence(key) shortcut = combo.toString() action = self.shortcut_dict.get(shortcut) if action is None: return False # need some way for the key to be # recognised, for example in wrap_text e = self.editor e.last_key_pressed = event.text() action.trigger() e.shortcut_overrode_keyevent = True if single_key: # it's a single key. let the # autocomplete do its thing e.post_key_pressed_signal.emit(event) return True def register_shortcuts(self, action_dict=None): """ Use the shortcut register to apply shortcuts to actions that exist on the widget. """ if action_dict is None: a = actions.load_actions_from_json action_dict = a() widgacts = action_dict.items() for widget_name, widget_actions in widgacts: if not hasattr(self, widget_name): continue widget = getattr(self, widget_name) if widget is None: continue acts = widget_actions.items() for action_name, attributes in acts: shortcuts = attributes['Shortcuts'] if len(shortcuts) == 0: continue for action in widget.actions(): if action.text() != action_name: continue break else: continue key_seqs = [] for shortcut in shortcuts: key_seq = QtGui.QKeySequence(shortcut) # convert to unicode again # to make sure the format # stays the same s = key_seq.toString() self.shortcut_dict[s] = action key_seqs.append(key_seq) action.setShortcuts(key_seqs) action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
class EditTabs(QtWidgets.QTabWidget): """ QTabWidget containing Editor QPlainTextEdit widgets. """ reset_tab_signal = QtCore.Signal() closed_tab_signal = QtCore.Signal(object) tab_switched_signal = QtCore.Signal(int, int, bool) contents_saved_signal = QtCore.Signal(object) tab_moved_signal = QtCore.Signal(object, int) def __init__(self): QtWidgets.QTabWidget.__init__(self) self.setTabBar(TabBar(self)) self.setTabsClosable(True) self.user_cancelled_tab_close = False self.setTabShape(QtWidgets.QTabWidget.Rounded) self.tab_count = 0 self.current_index = 0 tabBar = self.tabBar() tabBar.setMovable(True) tabBar.tabMoved.connect(self.tab_restrict_move, QtCore.Qt.DirectConnection) self.setup_new_tab_btn() self.tabCloseRequested.connect(self.close_tab) self.reset_tab_signal.connect(self.reset_tabs) self.currentChanged.connect(self.widget_changed) self.setStyleSheet("QTabBar::tab { height: 24px; }") # add tab list button self.corner_button = QtWidgets.QPushButton(':') self.corner_button.setFixedSize(24, 24) self.corner_button.setStyleSheet("border: 5px solid black") self.corner_button.clicked.connect(self.show_tab_menu) self.setCornerWidget(self.corner_button, corner=QtCore.Qt.TopRightCorner) # currently not in use @property def editor(self): widget = self.currentWidget() if widget.objectName() != 'Editor': raise Exception('Current Widget is not an Editor') return widget # currently not in use @editor.setter def editor(self, widget): if widget.objectName() != 'Editor': raise Exception('Current Widget is not an Editor') self.setCurrentWidget(widget) def show_tab_menu(self): """ Show a list of tabs and go to the tab clicked. """ menu = QtWidgets.QMenu() from functools import partial for i in range(self.count()): tab_name = self.tabText(i) if not tab_name.strip(): button = self.tabBar().tabButton(i, QtWidgets.QTabBar.LeftSide) if not isinstance(button, TabButton): continue tab_name = button.text() action = partial(self.setCurrentIndex, i) menu.addAction(tab_name, action) menu.exec_(QtGui.QCursor().pos()) @QtCore.Slot(int, int) def tab_restrict_move(self, from_index, to_index): """ Prevents tabs from being moved beyond the + new tab button. """ if from_index >= self.count()-1: self.tabBar().moveTab(to_index, from_index) return for index in from_index, to_index: widget = self.widget(index) widget.tab_index = index if hasattr(widget, 'name'): self.tab_moved_signal.emit(widget, index) def setup_new_tab_btn(self): """ Adds a new tab [+] button to the right of the tabs. """ widget = QtWidgets.QWidget() widget.setObjectName('Tab_Widget_New_Button') self.insertTab(0, widget, '') nb = self.new_btn = QtWidgets.QToolButton() nb.setMinimumSize(QtCore.QSize(50, 10)) nb.setText('+') # you could set an icon instead of text nb.setAutoRaise(True) nb.clicked.connect(self.new_tab) tabBar = self.tabBar() tabBar.setTabButton(0, QtWidgets.QTabBar.RightSide, nb) tabBar.setTabEnabled(0, False) @QtCore.Slot(str) def new_tab(self, tab_name=None, init_features=True): """ Creates a new tab. """ count = self.count() index = 0 if count == 0 else count - 1 editor = EDITOR.Editor(handle_shortcuts=False, init_features=init_features) if (tab_name is None or not tab_name): tab_name = 'Tab {0}'.format(index) editor.name = tab_name editor.tab_index = index self.insertTab(index, editor, tab_name ) self.setCurrentIndex(index) # relay the contents saved signal editor.contents_saved_signal.connect(self.contents_saved_signal) self.tab_count = self.count() self.current_index = self.currentIndex() editor.setFocus() return editor def close_current_tab(self): """ Closes the active tab. Called via shortcut key. """ _index = self.currentIndex() self.tabCloseRequested.emit(_index) def close_tab(self, index): """ Remove current tab if tab count is greater than 3 (so that the last tab left open is not the new button tab, although a better solution here is to open a new tab if the only tab left open is the 'new tab' tab). Also emits a close signal which is used by the autosave to determine if an editor's contents need saving. """ if self.count() < 3: return _index = self.currentIndex() editor = self.widget(index) if editor.objectName() == 'Tab_Widget_New_Button': return self.closed_tab_signal.emit(editor) # the below attribute may be altered # by a slot connected with DirectConnection if self.user_cancelled_tab_close: return editor.deleteLater() tab_on_left = (index < _index) last_tab_before_end = (_index == self.count()-2) self.removeTab(index) if tab_on_left or last_tab_before_end: _index -= 1 self.setCurrentIndex(_index) self.tab_count = self.count() def reset_tabs(self): for index in reversed(range(self.count())): widget = self.widget(index) if widget is None: continue if widget.objectName() == 'Editor': self.removeTab(index) self.new_tab() @QtCore.Slot(int) def widget_changed(self, index): """ Emits tab_switched_signal with current widget. """ tabremoved = self.count() < self.tab_count previous = self.current_index current = self.currentIndex() self.tab_switched_signal.emit(previous, current, tabremoved) self.current_index = self.currentIndex() self.tab_count = self.count() def addTab(self, *args): """ Insurance override to point to insertTab. args may be: const QString &text const QIcon &icon, const QString &text TODO: Needs testing. """ debug('Warning. addTab override feature needs testing.') if len(args) == 1: label, = args return self.insertTab(self.count(), None, label) elif len(args) == 2: icon, label = args return self.insertTab(self.count(), None, icon, label) else: return super(EditTabs, self).addTab(*args, **kwargs) def insertTab(self, *args): """ Override overloaded method which can receive the following params: int index, QWidget widget, QString label int index, QWidget widget, QIcon icon, QString label """ super(EditTabs, self).insertTab(*args) if len(args) == 3: index, widget, label = args elif len(args) == 4: index, widget, icon, label = args else: return if not isinstance(widget, EDITOR.Editor): return if not hasattr(self, 'tab_button_list'): # Note: button removed from set when tab removed. self.tab_button_list = set([]) button = TabButton(label, widget) self.tab_button_list.add(button) self.setTabText(index, '') self.tabBar().setTabButton(index, QtWidgets.QTabBar.LeftSide, button) def removeTab(self, index): """ Override QTabWidget method in order to also remove any buttons stored on the class. As this is the sole method for removing tabs uring a session, this should be sufficient to cleanup the list and prevent memory leaks. """ button = self.tabBar().tabButton(index, QtWidgets.QTabBar.LeftSide) if button in self.tab_button_list: self.tab_button_list.remove(button) super(EditTabs, self).removeTab(index) def setTabText(self, index, label): """ Override QTabWidget setTabText so that if the tab has a button, the text will be set on the button instead of the tab. """ button = self.tabBar().tabButton(index, QtWidgets.QTabBar.LeftSide) if isinstance(button, TabButton): return button.setText(label) super(EditTabs, self).setTabText(index, label)
def sizeHint(self): return QtCore.QSize(self.lineNumberAreaWidth(), 0)
def resizeLineNo(self): cr = self.editor.contentsRect() rect = QtCore.QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()) self.setGeometry(rect)
class Speaker(QtCore.QObject): """ Used to relay sys stdout, stderr, stdin """ emitter = QtCore.Signal(str)
class Editor(QtWidgets.QPlainTextEdit): """ Code Editor widget. Extends QPlainTextEdit to provide: - Line Number Area - Syntax Highlighting - Autocompletion (of Python code) - Shortcuts for code editing - New Context Menu - Signals for connecting the Editor to other UI elements. - Unique identifier to match Editor widget to file storage. """ wrap_types = [ '\'', '"', '[', ']', '(', ')', '{', '}', ] wrap_signal = QtCore.Signal(str) uuid_signal = QtCore.Signal(str) return_signal = QtCore.Signal(QtGui.QKeyEvent) focus_in_signal = QtCore.Signal(QtGui.QFocusEvent) post_key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent) wheel_signal = QtCore.Signal(QtGui.QWheelEvent) key_pressed_signal = QtCore.Signal(QtGui.QKeyEvent) # key_release_signal = QtCore.Signal(QtGui.QKeyEvent) shortcut_signal = QtCore.Signal(QtGui.QKeyEvent) resize_signal = QtCore.Signal(QtGui.QResizeEvent) context_menu_signal = QtCore.Signal(QtWidgets.QMenu) tab_signal = QtCore.Signal() home_key_signal = QtCore.Signal() ctrl_x_signal = QtCore.Signal() ctrl_n_signal = QtCore.Signal() ctrl_w_signal = QtCore.Signal() ctrl_s_signal = QtCore.Signal() ctrl_c_signal = QtCore.Signal() ctrl_enter_signal = QtCore.Signal() end_key_ctrl_alt_signal = QtCore.Signal() home_key_ctrl_alt_signal = QtCore.Signal() relay_clear_output_signal = QtCore.Signal() editingFinished = QtCore.Signal() text_changed_signal = QtCore.Signal() def __init__(self, handle_shortcuts=True, uid=None, init_features=True): super(Editor, self).__init__() self.setObjectName('Editor') self.setAcceptDrops(True) font = QtGui.QFont(DEFAULT_FONT) font.setPointSize(10) self.setFont(font) self.setMouseTracking(True) self.setStyleSheet(""" QToolTip { color: #F6F6F6; background-color: rgb(45, 42, 46); } """) self.shortcut_overrode_keyevent = False if uid is None: uid = str(uuid.uuid4()) self._uuid = uid self._changed = False self.wait_for_autocomplete = False self._handle_shortcuts = handle_shortcuts self._features_initialised = False self.emit_text_changed = True self.textChanged.connect(self._handle_textChanged) linenumberarea.LineNumberArea(self) if init_features: self.init_features() def init_features(self): """ Initialise custom Editor features. """ if self._features_initialised: return self._features_initialised = True # QSyntaxHighlighter causes textChanged to be emitted, which we don't want. self.emit_text_changed = False syntaxhighlighter.Highlight(self.document()) def set_text_changed_enabled(): self.emit_text_changed = True QtCore.QTimer.singleShot(0, set_text_changed_enabled) # self.emit_text_changed = True self.contextmenu = contextmenu.ContextMenu(self) # TOOD: add a new autocompleter that uses DirectConnection. self.wait_for_autocomplete = True self.autocomplete = autocompletion.AutoCompleter(self) if self._handle_shortcuts: sch = shortcuts.ShortcutHandler(editor=self, use_tabs=False) sch.clear_output_signal.connect(self.relay_clear_output_signal) self.shortcuteditor = shortcuteditor.ShortcutEditor(sch) self.selectionChanged.connect(self.highlight_same_words) def _handle_textChanged(self): self._changed = True # emit custom textChanged when desired. if self.emit_text_changed: self.text_changed_signal.emit() def setTextChanged(self, state=True): self._changed = state def highlight_same_words(self): """ Highlights other matching words in document when full word selected. TODO: implement this! """ textCursor = self.textCursor() if not textCursor.hasSelection(): return """ text = textCursor.selection().toPlainText() textCursor.select(QtGui.QTextCursor.WordUnderCursor) word = textCursor.selection().toPlainText() print(text, word) if text == word: print(word) """ def setPlainText(self, text): """ Override original method to prevent textChanged signal being emitted. WARNING: textCursor can still be used to setPlainText. """ self.emit_text_changed = False super(Editor, self).setPlainText(text) self.emit_text_changed = True def insertPlainText(self, text): """ Override original method to prevent textChanged signal being emitted. """ self.emit_text_changed = False super(Editor, self).insertPlainText(text) self.emit_text_changed = True def appendPlainText(self, text): """ Override original method to prevent textChanged signal being emitted. """ self.emit_text_changed = False super(Editor, self).appendPlainText(text) self.emit_text_changed = True def focusInEvent(self, event): """ Emit a signal when focusing in a window. When there used to be an editor per tab, this would work well to check that the tab's contents had not been changed. Now, we'll also want to signal from the tab switched signal. """ FR = QtCore.Qt.FocusReason ignored_reasons = [FR.PopupFocusReason, FR.MouseFocusReason] if event.reason() not in ignored_reasons: self.focus_in_signal.emit(event) super(Editor, self).focusInEvent(event) def focusOutEvent(self, event): if self._changed: self.editingFinished.emit() super(Editor, self).focusOutEvent(event) def resizeEvent(self, event): """ Emit signal on resize so that the LineNumberArea has a chance to update. """ super(Editor, self).resizeEvent(event) self.resize_signal.emit(event) def keyPressEvent(self, event): """ Emit signals for key events that QShortcut cannot override. """ # will this be enough to give focus back to the # script editor or rest of the application? if not self.hasFocus(): event.ignore() return # self.wait_for_autocomplete = True # # QtCore.Qt.DirectConnection # self.key_pressed_signal.emit(event) # self.autocomplete_overrode_keyevent = False if self.wait_for_autocomplete: # TODO: Connect (in autocomplete) using # QtCore.Qt.DirectConnection to work synchronously self.key_pressed_signal.emit(event) return self.shortcut_overrode_keyevent = False self.shortcut_signal.emit(event) if self.shortcut_overrode_keyevent: return if event.modifiers() == QtCore.Qt.NoModifier: # Tab Key if event.key() == QtCore.Qt.Key_Tab: return self.tab_signal.emit() # Enter/Return Key if event.key() == QtCore.Qt.Key_Return: return self.return_signal.emit(event) # Ctrl+Enter/Return Key if (event.key() == QtCore.Qt.Key_Return and event.modifiers() == CTRL): return self.ctrl_enter_signal.emit() # Surround selected text in brackets or quotes if (event.text() in self.wrap_types and self.textCursor().hasSelection()): return self.wrap_signal.emit(event.text()) if event.key() == QtCore.Qt.Key_Home: # Ctrl+Alt+Home if event.modifiers() == CTRL_ALT: self.home_key_ctrl_alt_signal.emit() # Home elif event.modifiers() == QtCore.Qt.NoModifier: return self.home_key_signal.emit() # Ctrl+Alt+End if (event.key() == QtCore.Qt.Key_End and event.modifiers() == CTRL_ALT): self.end_key_ctrl_alt_signal.emit() # Ctrl+X if (event.key() == QtCore.Qt.Key_X and event.modifiers() == CTRL): self.ctrl_x_signal.emit() self.text_changed_signal.emit() # Ctrl+N if (event.key() == QtCore.Qt.Key_N and event.modifiers() == CTRL): self.ctrl_n_signal.emit() # Ctrl+W if (event.key() == QtCore.Qt.Key_W and event.modifiers() == CTRL): self.ctrl_w_signal.emit() # Ctrl+S if (event.key() == QtCore.Qt.Key_S and event.modifiers() == CTRL): self.ctrl_s_signal.emit() # Ctrl+C if (event.key() == QtCore.Qt.Key_C and event.modifiers() == CTRL): self.ctrl_c_signal.emit() super(Editor, self).keyPressEvent(event) self.post_key_pressed_signal.emit(event) def keyReleaseEvent(self, event): if not isinstance(self, Editor): # when the key released is F5 (reload app) return self.wait_for_autocomplete = True # self.key_release_signal.emit(event) super(Editor, self).keyReleaseEvent(event) def contextMenuEvent(self, event): """ Creates a standard context menu and emits it for futher changes and execution elsewhere. """ menu = self.createStandardContextMenu() self.context_menu_signal.emit(menu) def dragEnterEvent(self, e): mimeData = e.mimeData() if mimeData.hasUrls: e.accept() else: super(Editor, self).dragEnterEvent(e) # prevent mimedata from being displayed unless alt is held app = QtWidgets.QApplication.instance() if app.keyboardModifiers() != QtCore.Qt.AltModifier: return # let's see what the data contains, at least! # maybe restrict this to non-known formats... for f in mimeData.formats(): data = str(mimeData.data(f)).replace(b'\0', b'') data = data.replace(b'\x12', b'') print(f, data) def dragMoveEvent(self, e): # prevent mimedata from being displayed unless alt is held app = QtWidgets.QApplication.instance() if app.keyboardModifiers() != QtCore.Qt.AltModifier: super(Editor, self).dragMoveEvent(e) return if e.mimeData().hasUrls: e.accept() else: super(Editor, self).dragMoveEvent(e) def dropEvent(self, e): """ TODO: e.ignore() files and send to edittabs to create new tab instead? """ mimeData = e.mimeData() if (mimeData.hasUrls and mimeData.urls()): urls = mimeData.urls() text_list = [] for url in urls: path = url.toLocalFile() with open(path, 'r') as f: text_list.append(f.read()) self.textCursor().insertText('\n'.join(text_list)) else: super(Editor, self).dropEvent(e) def wheelEvent(self, event): """ Restore focus and, if ctrl held, emit signal """ self.setFocus(QtCore.Qt.MouseFocusReason) vertical = QtCore.Qt.Orientation.Vertical is_vertical = (event.orientation() == vertical) is_ctrl = (event.modifiers() == CTRL) if is_ctrl and is_vertical: return self.wheel_signal.emit(event) super(Editor, self).wheelEvent(event) def insertFromMimeData(self, mimeData): """ Override to emit text_changed_signal (which triggers autosave) when text is pasted or dragged in. """ self.text_changed_signal.emit() super(Editor, self).insertFromMimeData(mimeData) """ # Great idea, needs testing
class ShortcutHandler(QtCore.QObject): """ Shortcut Manager with custom signals. """ clear_output_signal = QtCore.Signal() exec_text_signal = QtCore.Signal() def __init__(self, parent_widget, use_tabs=True): """ :param use_tabs: If False, the parent_widget is the QPlainTextEdit (Editor) widget. If True, apply shortcuts to the QTabBar as well as the Editor. """ super(ShortcutHandler, self).__init__() self.setObjectName('ShortcutHandler') self.setParent(parent_widget) self.parent_widget = parent_widget self.use_tabs = use_tabs if use_tabs: self.tabs = parent_widget.tabs self.editor = parent_widget.editor else: self.editor = parent_widget self.connect_signals() self.install_shortcuts() def connect_signals(self): """ Connects the current editor's signals to this class """ editor = self.editor pairs = [ (editor.tab_signal, self.tab_handler), (editor.return_signal, self.return_handler), (editor.wrap_signal, self.wrap_text), (editor.home_key_ctrl_alt_signal, self.move_to_top), (editor.end_key_ctrl_alt_signal, self.move_to_bottom), (editor.ctrl_x_signal, self.cut_line), (editor.ctrl_c_signal, self.copy_block_or_selection), (editor.ctrl_s_signal, self.save), (editor.home_key_signal, self.jump_to_start), (editor.wheel_signal, self.wheel_zoom), (editor.ctrl_enter_signal, self.exec_selected_text), ] self._connections = [] for signal, slot in pairs: name, _, handle = connect(editor, signal, slot) self._connections.append((name, slot)) def install_shortcuts(self): """ Maps shortcuts on the QPlainTextEdit widget to methods on this class. """ def notimp(msg): return partial(self.notimplemented, msg) editor_shortcuts = { 'Ctrl+B': self.exec_current_line, 'Ctrl+Shift+Return': self.new_line_above, 'Ctrl+Alt+Return': self.new_line_below, 'Ctrl+Shift+D': self.duplicate_lines, 'Ctrl+Shift+T': self.print_type, 'Ctrl+Shift+F': self.search_input, 'Ctrl+H': self.print_help, 'Ctrl+L': self.select_lines, 'Ctrl+J': self.join_lines, 'Ctrl+/': self.comment_toggle, 'Ctrl+]': self.indent, 'Ctrl+[': self.unindent, 'Shift+Tab': self.unindent, 'Ctrl+=': self.zoom_in, 'Ctrl++': self.zoom_in, 'Ctrl+-': self.zoom_out, 'Ctrl+Shift+K': self.delete_lines, 'Ctrl+D': self.select_word, 'Ctrl+M': self.hop_brackets, 'Ctrl+Shift+M': self.select_between_brackets, 'Ctrl+Shift+Delete': self.delete_to_end_of_line, 'Ctrl+Shift+Backspace': self.delete_to_start_of_line, 'Ctrl+Shift+Up': self.move_blocks_up, 'Ctrl+Shift+Down': self.move_blocks_down, 'Ctrl+G': notimp('goto'), 'Ctrl+P': notimp('palette'), 'Ctrl+C': self.copy_block_or_selection, 'Ctrl+E': self.open_module_file, # 'Ctrl+Shift+Alt+Up': notimp('duplicate cursor up'), # 'Ctrl+Shift+Alt+Down': notimp('duplicate cursor down'), } if self.use_tabs: editor_shortcuts['Ctrl+S'] = self.save editor_shortcuts[QtCore.Qt.Key_F5] = self.parent_widget.parent( ).parent().parent().reload_package terminal_shortcuts = { 'Ctrl+Backspace': self.clear_output_signal.emit, } editor_shortcuts.update(terminal_shortcuts) if self.use_tabs: tab_shortcuts = { 'Ctrl+T': self.tabs.new_tab, 'Ctrl+Shift+N': self.tabs.new_tab, 'Ctrl+Shift+W': self.tabs.remove_current_tab, 'Ctrl+Tab': self.next_tab, 'Ctrl+Shift+Tab': self.previous_tab, # 'Ctrl+Shift+T': notimp('reopen previous tab'), } editor_shortcuts.update(tab_shortcuts) def doc(f): return f.func_doc if hasattr(f, 'func_doc') else f.__doc__ self.shortcut_dict = { key: doc(func) for key, func in editor_shortcuts.items() } signal_dict = { 'Tab': self.tab_handler.__doc__, 'Return/Enter': self.return_handler.__doc__, r'\' " ( ) [ ] \{ \}': self.wrap_text.__doc__, 'Ctrl+Alt+Home': self.move_to_top.__doc__, 'Ctrl+Alt+End': self.move_to_bottom.__doc__, 'Ctrl+X': self.cut_line.__doc__, 'Home': self.jump_to_start.__doc__, 'Ctrl+Mouse Wheel': self.wheel_zoom.__doc__, 'Ctrl+Backspace': '\n' + ' ' * 8 + 'Clear Output Terminal\n', } self.shortcut_dict.update(signal_dict) def add_action(action, widget, shortcut, func): """ Add action to widget with a shortcut that triggers the given function. :action: QtWidgets.QAction :widget: QtWidgets.QWidget :shortcut: str (e.g. 'Ctrl+S') or Qt Key :func: a callable that gets executed when triggering the action. """ key_seq = QtGui.QKeySequence(shortcut) a.setShortcut(key_seq) a.setShortcutContext(QtCore.Qt.WidgetShortcut) a.triggered.connect(func) widget.addAction(a) for shortcut, func in editor_shortcuts.items(): a = QtWidgets.QAction(self.editor) add_action(a, self.editor, shortcut, func) if self.use_tabs: terminal = self.parent_widget.parent().parent().terminal for shortcut, func in terminal_shortcuts.items(): a = QtWidgets.QAction(terminal) add_action(a, terminal, shortcut, func) for shortcut, func in tab_shortcuts.items(): a = QtWidgets.QAction(self.tabs) add_action(a, self.tabs, shortcut, func) def notimplemented(self, text): """ Development reminders to implement features """ raise NotImplementedError(text) def get_selected_blocks(self, ignoreEmpty=True): """ Utility method for getting lines in selection. """ textCursor = self.editor.textCursor() doc = self.editor.document() start = textCursor.selectionStart() end = textCursor.selectionEnd() # get line numbers blockNumbers = set( [doc.findBlock(b).blockNumber() for b in range(start, end)]) pos = textCursor.position() blockNumbers |= set([doc.findBlock(pos).blockNumber()]) def isEmpty(b): return doc.findBlockByNumber(b).text().strip() != '' blocks = [] for b in blockNumbers: bn = doc.findBlockByNumber(b) if not ignoreEmpty: blocks.append(bn) elif isEmpty(b): blocks.append(bn) return blocks def save(self): actions.save_action(self.tabs, self.editor) def open_module_file(self): textCursor = self.editor.textCursor() text = textCursor.selection().toPlainText() if not text.strip(): return obj = actions.get_subobject(text) actions.open_module_file(obj) def offset_for_traceback(self, text=None): """ Offset text using newlines to get proper line ref in tracebacks. """ textCursor = self.editor.textCursor() if text is None: text = textCursor.selection().toPlainText() selection_offset = textCursor.selectionStart() doc = self.editor.document() block_num = doc.findBlock(selection_offset).blockNumber() text = '\n' * block_num + text return text def exec_text(self, text, whole_text): """ Execute whatever text is passed into this function. :text: the actual text to be executed :whole_text: the whole text for context and full traceback """ self.exec_text_signal.emit() error_line_numbers = execute.mainexec(text, whole_text) if error_line_numbers is None: return else: self.highlight_errored_lines(error_line_numbers) def exec_selected_text(self): """ If text is selected, call exec on that text. If no text is selected, it will execute all text within boundaries demarcated by the symbols #&& """ textCursor = self.editor.textCursor() whole_text = self.editor.toPlainText() if textCursor.hasSelection(): text = self.offset_for_traceback() return self.exec_text(text, whole_text) if not '\n#&&' in whole_text: text = whole_text whole_text = '\n' + whole_text return self.exec_text(text, whole_text) text = whole_text whole_text = '\n' + whole_text pos = textCursor.position() text_before = text[:pos] text_after = text[pos:] symbol_pos = text_before.rfind('#&&') text_before = text_before.split('\n#&&')[-1] text_after = text_after.split('\n#&&')[0] text = text_before + text_after doc = self.editor.document() block_num = doc.findBlock(symbol_pos).blockNumber() text = '\n' * block_num + text self.exec_text(text, whole_text) def exec_current_line(self): """ Calls exec with the text of the line the cursor is on. Calls lstrip on current line text to allow exec of indented text. """ textCursor = self.editor.textCursor() whole_text = self.editor.toPlainText() if textCursor.hasSelection(): return self.exec_selected_text() textCursor.select(QtGui.QTextCursor.BlockUnderCursor) text = textCursor.selection().toPlainText().lstrip() text = self.offset_for_traceback(text=text) whole_text = '\n' + whole_text error_line_numbers = execute.mainexec(text, whole_text, verbosity=1) if error_line_numbers is None: return else: self.highlight_errored_lines(error_line_numbers) def highlight_errored_lines(self, error_line_numbers): """ Draw a red background on any lines that caused an error. """ extraSelections = [] cursor = self.editor.textCursor() doc = self.editor.document() for lineno in error_line_numbers: selection = QtWidgets.QTextEdit.ExtraSelection() lineColor = QtGui.QColor.fromRgbF(0.8, 0.1, 0, 0.2) selection.format.setBackground(lineColor) selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True) block = doc.findBlockByLineNumber(lineno - 1) cursor.setPosition(block.position()) selection.cursor = cursor selection.cursor.clearSelection() extraSelections.append(selection) self.editor.setExtraSelections(extraSelections) @QtCore.Slot(QtGui.QKeyEvent) def return_handler(self, event): """ New line with auto-indentation. """ return self.indent_next_line() def indent_next_line(self): """ Match next line indentation to current line If ':' is character in cursor position and current line contains non-whitespace characters, add an extra four spaces. """ textCursor = self.editor.textCursor() text = textCursor.block().text() indentCount = len(text) - len(text.lstrip(' ')) doc = self.editor.document() if doc.characterAt(textCursor.position() - 1) == ':': indentCount = indentCount + 4 insertion = '\n' + ' ' * indentCount if len(text.strip()) == 0: insertion = '\n' if not self.editor.wait_for_autocomplete: textCursor.insertText(insertion) self.editor.setTextCursor(textCursor) return True @QtCore.Slot() def cut_line(self): """ If no text selected, cut whole current line to clipboard. """ textCursor = self.editor.textCursor() if textCursor.hasSelection(): return textCursor.select(QtGui.QTextCursor.LineUnderCursor) text = textCursor.selectedText() textCursor.insertText('') QtGui.QClipboard().setText(text) def new_line_above(self): """ Inserts new line above current. """ textCursor = self.editor.textCursor() line = textCursor.block().text() indentCount = len(line) - len(line.lstrip(' ')) indent = ' ' * indentCount textCursor.movePosition(textCursor.StartOfLine) self.editor.setTextCursor(textCursor) textCursor.insertText(indent + '\n') self.editor.moveCursor(textCursor.Left) def new_line_below(self): """ Inserts new line below current. """ textCursor = self.editor.textCursor() line = textCursor.block().text() indentCount = len(line) - len(line.lstrip(' ')) indent = ' ' * indentCount textCursor.movePosition(textCursor.EndOfLine) self.editor.setTextCursor(textCursor) textCursor.insertText('\n' + indent) @QtCore.Slot() def tab_handler(self): """ Indents selected text. If no text is selected, adds four spaces. """ textCursor = self.editor.textCursor() if textCursor.hasSelection(): self.indent() else: self.tab_space() def indent(self): """ Indent Selected Text """ blocks = self.get_selected_blocks() for block in blocks: cursor = QtGui.QTextCursor(block) cursor.movePosition(QtGui.QTextCursor.StartOfLine) cursor.insertText(' ') def unindent(self): """ Unindent Selected Text TODO: Maintain original selection and cursor position. """ blocks = self.get_selected_blocks(ignoreEmpty=False) for block in blocks: cursor = QtGui.QTextCursor(block) cursor.select(QtGui.QTextCursor.LineUnderCursor) lineText = cursor.selectedText() if lineText.startswith(' '): newText = str(lineText[:4]).replace(' ', '') + lineText[4:] cursor.insertText(newText) def tab_space(self): """ Insert spaces instead of tabs """ self.editor.insertPlainText(' ') def next_tab(self): """ Switch to the next tab. """ if hasattr(self, 'tabs'): next_index = self.tabs.currentIndex() + 1 if next_index <= self.tabs.count(): self.tabs.setCurrentIndex(next_index) def previous_tab(self): """ Switch to the next tab. """ if hasattr(self, 'tabs'): self.tabs.setCurrentIndex(self.tabs.currentIndex() - 1) def jump_to_start(self): """ Jump to first character in line. If at first character, jump to start of line. """ textCursor = self.editor.textCursor() init_pos = textCursor.position() textCursor.select(QtGui.QTextCursor.LineUnderCursor) text = textCursor.selection().toPlainText() textCursor.movePosition(QtGui.QTextCursor.StartOfLine) pos = textCursor.position() offset = len(text) - len(text.lstrip()) new_pos = pos + offset if new_pos != init_pos: textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) self.editor.setTextCursor(textCursor) def comment_toggle(self): """ Toggles commenting out selected lines, or lines with cursor. """ blocks = self.get_selected_blocks() # iterate through lines in doc commenting or uncommenting # based on whether everything is commented or not commentAllOut = any([ not str(block.text()).lstrip().startswith('#') for block in blocks ]) if commentAllOut: for block in blocks: cursor = QtGui.QTextCursor(block) cursor.select(QtGui.QTextCursor.LineUnderCursor) selectedText = cursor.selectedText() right_split = len(selectedText.lstrip()) count = len(selectedText) split_index = count - right_split split_text = selectedText[split_index:] newText = ' ' * split_index + '#' + split_text cursor.insertText(newText) else: for block in blocks: cursor = QtGui.QTextCursor(block) cursor.select(QtGui.QTextCursor.LineUnderCursor) selectedText = cursor.selectedText() newText = str(selectedText).replace('#', '', 1) cursor.insertText(newText) @QtCore.Slot(str) def wrap_text(self, key): """ Wrap selected text in brackets or quotes of type "key". """ if key in ['\'', '"']: key_in = key key_out = key elif key in ['[', ']']: key_in = '[' key_out = ']' elif key in ['(', ')']: key_in = '(' key_out = ')' elif key in ['{', '}']: key_in = '{' key_out = '}' # elif key in ['<', '>']: # key_in = '<' # key_out = '>' textCursor = self.editor.textCursor() text = key_in + textCursor.selectedText() + key_out textCursor.insertText(text) def select_lines(self): """ Sets current lines selected and moves cursor to beginning of next line. """ textCursor = self.editor.textCursor() start = textCursor.selectionStart() end = textCursor.selectionEnd() textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.EndOfLine) new_end = textCursor.position() + 1 if new_end >= self.editor.document().characterCount(): new_end = new_end - 1 textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.StartOfLine) textCursor.setPosition(new_end, QtGui.QTextCursor.KeepAnchor) self.editor.setTextCursor(textCursor) def join_lines(self): """ Joins current line(s) with next by deleting the newline at the end of the current line(s). """ textCursor = self.editor.textCursor() blocks = self.get_selected_blocks(ignoreEmpty=False) if len(blocks) > 1: text = textCursor.selectedText() text = ' '.join(ln.strip() for ln in text.splitlines()) textCursor.insertText(text) else: block = textCursor.block() text = block.text() next_line = block.next().text().strip() new_text = text + ' ' + next_line textCursor.select(QtGui.QTextCursor.LineUnderCursor) textCursor.movePosition(QtGui.QTextCursor.EndOfLine) new_pos = textCursor.position() + 1 if new_pos >= self.editor.document().characterCount(): return textCursor.setPosition(new_pos, QtGui.QTextCursor.KeepAnchor) textCursor.insertText('') textCursor.select(QtGui.QTextCursor.LineUnderCursor) textCursor.insertText(new_text) self.editor.setTextCursor(textCursor) def delete_lines(self): """ Deletes the contents of the current line(s). """ textCursor = self.editor.textCursor() start = textCursor.selectionStart() end = textCursor.selectionEnd() textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.StartOfLine) new_start = textCursor.position() textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.EndOfLine) new_end = textCursor.position() textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor) if textCursor.selectedText() == '': textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor) next_line = new_end + 1 if 0 < next_line >= self.editor.document().characterCount(): next_line = next_line - 2 if next_line == -1: return textCursor.setPosition(next_line, QtGui.QTextCursor.KeepAnchor) textCursor.insertText('') def copy_block_or_selection(self): """ If there's no text selected, copy the current block. """ textCursor = self.editor.textCursor() selection = textCursor.selection() text = selection.toPlainText() if not text: textCursor.select(QtGui.QTextCursor.BlockUnderCursor) selection = textCursor.selection() text = selection.toPlainText() QtGui.QClipboard().setText(text) def select_word(self): """ Selects the word under cursor if no selection. If selection, selects next occurence of the same word. TODO: 1 )could optionally highlight all occurences of the word and iterate to the next selection. 2) Would be nice if extra selections could be made editable. 3) Wrap around. """ textCursor = self.editor.textCursor() if not textCursor.hasSelection(): textCursor.select(QtGui.QTextCursor.WordUnderCursor) return self.editor.setTextCursor(textCursor) text = textCursor.selection().toPlainText() start_pos = textCursor.selectionStart() end_pos = textCursor.selectionEnd() word_len = abs(end_pos - start_pos) whole_text = self.editor.toPlainText() second_half = whole_text[end_pos:] next_pos = second_half.find(text) if next_pos == -1: return next_start = next_pos + start_pos + word_len next_end = next_start + word_len textCursor.setPosition(next_start, QtGui.QTextCursor.MoveAnchor) textCursor.setPosition(next_end, QtGui.QTextCursor.KeepAnchor) self.editor.setTextCursor(textCursor) extraSelections = [] selection = QtWidgets.QTextEdit.ExtraSelection() lineColor = QtGui.QColor.fromRgbF(1, 1, 1, 0.3) selection.format.setBackground(lineColor) selection.cursor = self.editor.textCursor() selection.cursor.setPosition(start_pos, QtGui.QTextCursor.MoveAnchor) selection.cursor.setPosition(end_pos, QtGui.QTextCursor.KeepAnchor) extraSelections.append(selection) self.editor.setExtraSelections(extraSelections) def hop_brackets(self): """ Jump to closest bracket, starting with closing bracket. """ textCursor = self.editor.textCursor() pos = textCursor.position() whole_text = self.editor.toPlainText() first_half = whole_text[:pos] second_half = whole_text[pos:] first_pos = first_half.rfind('(') second_pos = second_half.find(')') first_pos = first_pos + 1 second_pos = second_pos + pos new_pos = first_pos if whole_text[pos] == ')' else second_pos textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) self.editor.setTextCursor(textCursor) def select_between_brackets(self): """ Selects text between [] {} () TODO: implement [] and {} """ textCursor = self.editor.textCursor() pos = textCursor.position() whole_text = self.editor.toPlainText() first_half = whole_text[:pos] second_half = whole_text[pos:] first_pos = first_half.rfind('(') second_pos = second_half.find(')') first_pos = first_pos + 1 second_pos = second_pos + pos textCursor.setPosition(first_pos, QtGui.QTextCursor.MoveAnchor) textCursor.setPosition(second_pos, QtGui.QTextCursor.KeepAnchor) self.editor.setTextCursor(textCursor) def search_input(self): """ Very basic search dialog. TODO: Create a QAction/util for this as it is also accessed through the right-click menu. """ getText = QtWidgets.QInputDialog.getText dialog = getText( self.editor, 'Search', '', ) text, ok = dialog if not ok: return textCursor = self.editor.textCursor() original_pos = textCursor.position() # start the search from the beginning of the document textCursor.setPosition(0, QtGui.QTextCursor.MoveAnchor) document = self.editor.document() cursor = document.find(text, textCursor) pos = cursor.position() if pos != -1: self.editor.setTextCursor(cursor) def duplicate_lines(self): """ Duplicates the current line or selected text downwards. """ textCursor = self.editor.textCursor() if textCursor.hasSelection(): selected_text = textCursor.selectedText() for i in range(2): textCursor.insertText(selected_text) self.editor.setTextCursor(textCursor) else: textCursor.movePosition(QtGui.QTextCursor.EndOfLine) end_pos = textCursor.position() textCursor.movePosition(QtGui.QTextCursor.StartOfLine) textCursor.setPosition(end_pos, QtGui.QTextCursor.KeepAnchor) selected_text = textCursor.selectedText() textCursor.insertText(selected_text + '\n' + selected_text) def delete_to_end_of_line(self): """ Deletes characters from cursor position to end of line. """ textCursor = self.editor.textCursor() pos = textCursor.position() textCursor.movePosition(QtGui.QTextCursor.EndOfLine) textCursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor) textCursor.insertText('') def delete_to_start_of_line(self): """ Deletes characters from cursor position to start of line. """ textCursor = self.editor.textCursor() pos = textCursor.position() textCursor.movePosition(QtGui.QTextCursor.StartOfLine) textCursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor) textCursor.insertText('') def print_help(self): """ Prints documentation for selected object """ cursor = self.editor.textCursor() selection = cursor.selection() text = selection.toPlainText().strip() if not text: return obj = __main__.__dict__.get(text) if obj is not None: print(obj.__doc__) elif text: exec('help(' + text + ')', __main__.__dict__) def print_type(self): """ Prints type for selected object """ cursor = self.editor.textCursor() selection = cursor.selection() text = selection.toPlainText().strip() if not text: return obj = __main__.__dict__.get(text) if obj is not None: print(type(obj)) elif text: exec('print(type(' + text + '))', __main__.__dict__) def zoom_in(self): """ Zooms in by changing the font size. """ font = self.editor.font() size = font.pointSize() new_size = size + 1 font.setPointSize(new_size) self.editor.setFont(font) def zoom_out(self): """ Zooms out by changing the font size. """ font = self.editor.font() size = font.pointSize() new_size = size - 1 if size > 1 else 1 font.setPointSize(new_size) self.editor.setFont(font) def wheel_zoom(self, event): """ Zooms by changing the font size according to the wheel zoom delta. """ font = self.editor.font() size = font.pointSize() delta = event.delta() amount = int(delta / 10) if delta > 1 or delta < -1 else delta new_size = size + amount new_size = new_size if new_size > 0 else 1 font.setPointSize(new_size) self.editor.setFont(font) def move_blocks_up(self): """ Moves selected blocks upwards. """ restoreSelection = False textCursor = self.editor.textCursor() if textCursor.hasSelection(): restoreSelection = True start = textCursor.selectionStart() end = textCursor.selectionEnd() selection_length = end - start textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.StartOfBlock) new_start = textCursor.position() textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.EndOfBlock) start_offset = start - new_start if new_start == 0: return textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor) selectedText = textCursor.selectedText() textCursor.insertText('') textCursor.deletePreviousChar() textCursor.movePosition(QtGui.QTextCursor.StartOfBlock) pos = textCursor.position() textCursor.insertText(selectedText + '\n') textCursor.setPosition(pos, QtGui.QTextCursor.MoveAnchor) if restoreSelection: moved_start = textCursor.position() + start_offset textCursor.setPosition(moved_start, QtGui.QTextCursor.MoveAnchor) moved_end = textCursor.position() + selection_length textCursor.setPosition(moved_end, QtGui.QTextCursor.KeepAnchor) else: new_pos = pos + start_offset textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) self.editor.setTextCursor(textCursor) def move_blocks_down(self): """ Moves selected blocks downwards. """ restoreSelection = False textCursor = self.editor.textCursor() if textCursor.hasSelection(): restoreSelection = True start = textCursor.selectionStart() end = textCursor.selectionEnd() selection_length = end - start textCursor.setPosition(start, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.StartOfBlock) new_start = textCursor.position() textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor) textCursor.movePosition(QtGui.QTextCursor.EndOfBlock) new_end = textCursor.position() if new_end + 1 >= self.editor.document().characterCount(): return end_offset = new_end - end textCursor.setPosition(new_start, QtGui.QTextCursor.KeepAnchor) selectedText = textCursor.selectedText() textCursor.insertText('') textCursor.deleteChar() textCursor.movePosition(QtGui.QTextCursor.EndOfBlock) textCursor.insertText('\n' + selectedText) if restoreSelection: moved_end = textCursor.position() - end_offset textCursor.setPosition(moved_end, QtGui.QTextCursor.MoveAnchor) moved_start = moved_end - selection_length textCursor.setPosition(moved_start, QtGui.QTextCursor.KeepAnchor) else: pos = textCursor.position() new_pos = pos - end_offset textCursor.setPosition(new_pos, QtGui.QTextCursor.MoveAnchor) self.editor.setTextCursor(textCursor) def move_to_top(self): """ Move selection or line if no selection to top of document. """ textCursor = self.editor.textCursor() if not textCursor.hasSelection(): textCursor.select(QtGui.QTextCursor.LineUnderCursor) text = textCursor.selectedText() textCursor.insertText('') textCursor.setPosition(0, QtGui.QTextCursor.MoveAnchor) textCursor.insertText(text) self.editor.setTextCursor(textCursor) def move_to_bottom(self): """ Move selection or line if no selection to bottom of document. """ textCursor = self.editor.textCursor() if not textCursor.hasSelection(): textCursor.select(QtGui.QTextCursor.LineUnderCursor) text = textCursor.selectedText() textCursor.insertText('') end = len(self.editor.toPlainText()) textCursor.setPosition(end, QtGui.QTextCursor.MoveAnchor) textCursor.insertText(text) self.editor.setTextCursor(textCursor)