1268 lines
43 KiB
Python
1268 lines
43 KiB
Python
"""
|
|
This module contains the base code editor widget.
|
|
"""
|
|
from __future__ import print_function
|
|
import os
|
|
import sys
|
|
try:
|
|
from future.builtins import str, super
|
|
except:
|
|
# not availabe on python 3.2 (but not needed)
|
|
pass
|
|
import logging
|
|
import platform
|
|
from pyqode.core import icons
|
|
from pyqode.core.cache import Cache
|
|
from pyqode.core.api.utils import DelayJobRunner, TextHelper
|
|
from pyqode.core.dialogs.goto import DlgGotoLine
|
|
from pyqode.core.managers import BackendManager
|
|
from pyqode.core.managers import FileManager
|
|
from pyqode.core.managers import ModesManager
|
|
from pyqode.core.managers import TextDecorationsManager
|
|
from pyqode.core.managers import PanelsManager
|
|
# ensure pyqode resource have been imported and are ready to be used.
|
|
from pyqode.core._forms import pyqode_core_rc
|
|
from pyqode.qt import QtWidgets, QtCore, QtGui
|
|
|
|
|
|
def _logger():
|
|
""" Returns module's logger """
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
class CodeEdit(QtWidgets.QPlainTextEdit):
|
|
"""
|
|
The editor widget is a simple extension to QPlainTextEdit.
|
|
|
|
It adds a few utility signals/methods and introduces the concepts of
|
|
**Managers, Modes and Panels**.
|
|
|
|
A **mode/panel** is an editor extension that, once added to a CodeEdit
|
|
instance, may modify its behaviour and appearance:
|
|
|
|
* **Modes** are simple objects which connect to the editor signals to
|
|
append new behaviours (such as automatic indentation, code completion,
|
|
syntax checking,...)
|
|
|
|
* **Panels** are the combination of a **Mode** and a **QWidget**.
|
|
They are displayed in the CodeEdit's content margins.
|
|
|
|
When you install a Panel on a CodeEdit, you can choose to install it in
|
|
one of the four following zones:
|
|
|
|
.. image:: _static/editor_widget.png
|
|
:align: center
|
|
:width: 600
|
|
:height: 450
|
|
|
|
A **manager** is an object that literally manage a specific aspect of
|
|
:class:`pyqode.core.api.CodeEdit`. There are managers to manage the list of
|
|
modes/panels, to open/save file and to control the backend:
|
|
|
|
- :attr:`pyqode.core.api.CodeEdit.file`:
|
|
File manager. Use it to open/save files or access the opened file
|
|
attribute.
|
|
- :attr:`pyqode.core.api.CodeEdit.backend`:
|
|
Backend manager. Use it to start/stop the backend or send a work
|
|
request.
|
|
- :attr:`pyqode.core.api.CodeEdit.modes`:
|
|
Modes manager. Use it to append/remove modes on the editor.
|
|
- :attr:`pyqode.core.api.CodeEdit.panels`:
|
|
Modes manager. Use it to append/remove panels on the editor.
|
|
|
|
Starting from version 2.1, CodeEdit defines the
|
|
:attr:`pyqode.core.api.CodeEdit.mimetypes` class attribute that can be used
|
|
by IDE to determine which editor to use for a given mime type. This
|
|
property is a list of supported mimetypes. An empty list means the
|
|
CodeEdit is generic. **Code editors specialised for a specific language
|
|
should define the mime types they support!**
|
|
"""
|
|
#: Paint hook
|
|
painted = QtCore.Signal(QtGui.QPaintEvent)
|
|
#: Signal emitted when a new text is set on the widget
|
|
new_text_set = QtCore.Signal()
|
|
#: Signal emitted when the text is saved to file
|
|
text_saved = QtCore.Signal(str)
|
|
#: Signal emitted before the text is saved to file
|
|
text_saving = QtCore.Signal(str)
|
|
#: Signal emitted when the dirty state changed
|
|
dirty_changed = QtCore.Signal(bool)
|
|
#: Signal emitted when a key is pressed
|
|
key_pressed = QtCore.Signal(QtGui.QKeyEvent)
|
|
#: Signal emitted when a key is released
|
|
key_released = QtCore.Signal(QtGui.QKeyEvent)
|
|
#: Signal emitted when a mouse button is pressed
|
|
mouse_pressed = QtCore.Signal(QtGui.QMouseEvent)
|
|
#: Signal emitted when a mouse button is released
|
|
mouse_released = QtCore.Signal(QtGui.QMouseEvent)
|
|
#: Signal emitted when a mouse double click event occured
|
|
mouse_double_clicked = QtCore.Signal(QtGui.QMouseEvent)
|
|
#: Signal emitted on a wheel event
|
|
mouse_wheel_activated = QtCore.Signal(QtGui.QWheelEvent)
|
|
#: Signal emitted at the end of the key_pressed event
|
|
post_key_pressed = QtCore.Signal(QtGui.QKeyEvent)
|
|
#: Signal emitted when focusInEvent is is called
|
|
focused_in = QtCore.Signal(QtGui.QFocusEvent)
|
|
#: Signal emitted when the mouse_moved
|
|
mouse_moved = QtCore.Signal(QtGui.QMouseEvent)
|
|
#: Signal emitted when the user press the TAB key
|
|
indent_requested = QtCore.Signal()
|
|
#: Signal emitted when the user press the BACK-TAB (Shift+TAB) key
|
|
unindent_requested = QtCore.Signal()
|
|
|
|
#: Store the list of mimetypes associated with the editor, for
|
|
#: specialised editors.
|
|
mimetypes = []
|
|
|
|
_DEFAULT_FONT = 'Source Code Pro' if sys.platform != 'darwin' else 'Monaco'
|
|
|
|
@property
|
|
def use_spaces_instead_of_tabs(self):
|
|
""" Use spaces instead of tabulations. Default is True. """
|
|
return self._use_spaces_instead_of_tabs
|
|
|
|
@use_spaces_instead_of_tabs.setter
|
|
def use_spaces_instead_of_tabs(self, value):
|
|
self._use_spaces_instead_of_tabs = value
|
|
for c in self.clones:
|
|
c.use_spaces_instead_of_tabs = value
|
|
|
|
@property
|
|
def tab_length(self):
|
|
""" Tab length, number of spaces. """
|
|
return self._tab_length
|
|
|
|
@tab_length.setter
|
|
def tab_length(self, value):
|
|
self._tab_length = value
|
|
for c in self.clones:
|
|
c.tab_length = value
|
|
|
|
@property
|
|
def save_on_focus_out(self):
|
|
"""
|
|
Automatically saves editor content on focus out.
|
|
|
|
Default is False.
|
|
"""
|
|
return self._save_on_focus_out
|
|
|
|
@save_on_focus_out.setter
|
|
def save_on_focus_out(self, value):
|
|
self._save_on_focus_out = value
|
|
for c in self.clones:
|
|
c.save_on_focus_out = value
|
|
|
|
@property
|
|
def show_whitespaces(self):
|
|
"""
|
|
Shows/Hides virtual white spaces.
|
|
"""
|
|
return self._show_whitespaces
|
|
|
|
@show_whitespaces.setter
|
|
def show_whitespaces(self, value):
|
|
if self._show_whitespaces != value:
|
|
self._show_whitespaces = value
|
|
self._set_whitespaces_flags(value)
|
|
for c in self.clones:
|
|
c.show_whitespaces = value
|
|
self.rehighlight()
|
|
|
|
@property
|
|
def font_name(self):
|
|
"""
|
|
The editor font family name.
|
|
"""
|
|
return self._font_family
|
|
|
|
@font_name.setter
|
|
def font_name(self, value):
|
|
if value == "":
|
|
value = self._DEFAULT_FONT
|
|
self._font_family = value
|
|
self._reset_stylesheet()
|
|
for c in self.clones:
|
|
c.font_name = value
|
|
|
|
@property
|
|
def zoom_level(self):
|
|
"""
|
|
Gets/Sets the editor zoom level.
|
|
|
|
The zoom level is a value that is added to the current editor font
|
|
size. Negative values are used to zoom out the editor, positive values
|
|
are used to zoom in the editor.
|
|
"""
|
|
return self._zoom_level
|
|
|
|
@zoom_level.setter
|
|
def zoom_level(self, value):
|
|
self._zoom_level = value
|
|
|
|
@property
|
|
def font_size(self):
|
|
"""
|
|
The font point size.
|
|
|
|
.. note:: Please, **never use setFontPointSize/setFontFamily functions
|
|
directly** as the values you define there will be overwritten as
|
|
soon as the user zoom the editor or as soon as a stylesheet
|
|
property has changed.
|
|
"""
|
|
return self._font_size
|
|
|
|
@font_size.setter
|
|
def font_size(self, value):
|
|
self._font_size = value
|
|
self._reset_stylesheet()
|
|
for c in self.clones:
|
|
c.font_size = value
|
|
|
|
@property
|
|
def background(self):
|
|
"""
|
|
The editor background color (QColor)
|
|
"""
|
|
return self._background
|
|
|
|
@background.setter
|
|
def background(self, value):
|
|
self._background = value
|
|
self._reset_stylesheet()
|
|
for c in self.clones:
|
|
c.background = value
|
|
|
|
@property
|
|
def foreground(self):
|
|
"""
|
|
The editor foreground color (QColor)
|
|
"""
|
|
return self._foreground
|
|
|
|
@foreground.setter
|
|
def foreground(self, value):
|
|
self._foreground = value
|
|
self._reset_stylesheet()
|
|
for c in self.clones:
|
|
c.foreground = value
|
|
|
|
@property
|
|
def whitespaces_foreground(self):
|
|
"""
|
|
The editor white spaces' foreground color. White spaces are highlighted
|
|
by the syntax highlighter. You should call rehighlight to update their
|
|
color. This is not done automatically to prevent multiple, useless
|
|
call to ``rehighlight`` which can take some time on big files.
|
|
"""
|
|
return self._whitespaces_foreground
|
|
|
|
@whitespaces_foreground.setter
|
|
def whitespaces_foreground(self, value):
|
|
self._whitespaces_foreground = value
|
|
for c in self.clones:
|
|
c.whitespaces_foreground = value
|
|
|
|
@property
|
|
def selection_background(self):
|
|
"""
|
|
The editor selection's background color.
|
|
"""
|
|
return self._sel_background
|
|
|
|
@selection_background.setter
|
|
def selection_background(self, value):
|
|
self._sel_background = value
|
|
self._reset_stylesheet()
|
|
for c in self.clones:
|
|
c.selection_background = value
|
|
|
|
@property
|
|
def selection_foreground(self):
|
|
"""
|
|
The editor selection's foreground color.
|
|
"""
|
|
return self._sel_foreground
|
|
|
|
@selection_foreground.setter
|
|
def selection_foreground(self, value):
|
|
self._sel_foreground = value
|
|
for c in self.clones:
|
|
c.selection_foreground = value
|
|
|
|
@property
|
|
def word_separators(self):
|
|
"""
|
|
The list of word separators used by the code completion mode
|
|
and the word clicked mode.
|
|
"""
|
|
return self._word_separators
|
|
|
|
@word_separators.setter
|
|
def word_separators(self, value):
|
|
self._word_separators = value
|
|
for c in self.clones:
|
|
c._word_separators = value
|
|
|
|
@property
|
|
def dirty(self):
|
|
"""
|
|
Tells whethere the content of editor has been modified.
|
|
|
|
(this is just a shortcut to QTextDocument.isModified
|
|
|
|
:type: bool
|
|
"""
|
|
return self.document().isModified()
|
|
|
|
@property
|
|
def visible_blocks(self):
|
|
"""
|
|
Returns the list of visible blocks.
|
|
|
|
Each element in the list is a tuple made up of the line top position,
|
|
the line number and the QTextBlock itself.
|
|
|
|
:return: A list of tuple(top_position, line_number, block)
|
|
:rtype: List of tuple(int, int, QtWidgets.QTextBlock)
|
|
"""
|
|
return self._visible_blocks
|
|
|
|
@property
|
|
def file(self):
|
|
"""
|
|
Returns a reference to the :class:`pyqode.core.managers.FileManager`
|
|
used to open/save file on the editor
|
|
"""
|
|
return self._file
|
|
|
|
@file.setter
|
|
def file(self, file_manager):
|
|
"""
|
|
Sets a custom file manager.
|
|
|
|
:param file_manager: custom file manager instance.
|
|
"""
|
|
self._file = file_manager
|
|
|
|
@property
|
|
def backend(self):
|
|
"""
|
|
Returns a reference to the :class:`pyqode.core.managers.BackendManager`
|
|
used to control the backend process.
|
|
"""
|
|
return self._backend
|
|
|
|
@property
|
|
def modes(self):
|
|
"""
|
|
Returns a reference to the :class:`pyqode.core.managers.ModesManager`
|
|
used to manage the collection of installed modes.
|
|
"""
|
|
return self._modes
|
|
|
|
@property
|
|
def panels(self):
|
|
"""
|
|
Returns a reference to the :class:`pyqode.core.managers.PanelsManager`
|
|
used to manage the collection of installed panels
|
|
"""
|
|
return self._panels
|
|
|
|
@property
|
|
def decorations(self):
|
|
"""
|
|
Returns a reference to the
|
|
:class:`pyqode.core.managers.TextDecorationManager` used to manage the
|
|
list of :class:`pyqode.core.api.TextDecoration`
|
|
"""
|
|
return self._decorations
|
|
|
|
@property
|
|
def syntax_highlighter(self):
|
|
"""
|
|
Returns a reference to the syntax highlighter mode currently used to
|
|
highlight the editor content.
|
|
|
|
:return: :class:`pyqode.core.api.SyntaxHighlighter`
|
|
"""
|
|
for mode in self.modes:
|
|
if hasattr(mode, 'highlightBlock'):
|
|
return mode
|
|
return None
|
|
|
|
@property
|
|
def show_context_menu(self):
|
|
"""
|
|
Specifies whether we should display the context menu or not.
|
|
|
|
Default is True
|
|
"""
|
|
return self._show_ctx_mnu
|
|
|
|
@show_context_menu.setter
|
|
def show_context_menu(self, value):
|
|
self._show_ctx_mnu = value
|
|
|
|
def __init__(self, parent=None, create_default_actions=True):
|
|
"""
|
|
:param parent: Parent widget
|
|
|
|
:param create_default_actions: True to create the action for the
|
|
standard shortcuts (copy, paste, delete, undo, redo,...).
|
|
Non-standard actions will always get created. If you would like
|
|
to prevent the context menu from showing, just set the
|
|
:attr:`show_menu_enabled` to False.
|
|
"""
|
|
super(CodeEdit, self).__init__(parent)
|
|
self.installEventFilter(self)
|
|
self.clones = []
|
|
self._show_ctx_mnu = True
|
|
self._default_font_size = 10
|
|
self._backend = BackendManager(self)
|
|
self._file = FileManager(self)
|
|
self._modes = ModesManager(self)
|
|
self._panels = PanelsManager(self)
|
|
self._decorations = TextDecorationsManager(self)
|
|
self.document().modificationChanged.connect(self._emit_dirty_changed)
|
|
|
|
self._word_separators = [
|
|
'~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
|
|
'}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
|
|
']', '\\', '\n', '\t', '=', '-', ' '
|
|
]
|
|
self._save_on_focus_out = False
|
|
self._use_spaces_instead_of_tabs = True
|
|
self._whitespaces_foreground = None
|
|
self._sel_background = None
|
|
self._show_whitespaces = False
|
|
self._foreground = None
|
|
self._sel_foreground = None
|
|
self._tab_length = 4
|
|
self._zoom_level = 0
|
|
self._font_size = 10
|
|
self._background = None
|
|
QtGui.QFontDatabase.addApplicationFont(
|
|
':/fonts/rc/SourceCodePro-Regular.ttf')
|
|
QtGui.QFontDatabase.addApplicationFont(
|
|
':/fonts/rc/SourceCodePro-Bold.ttf')
|
|
self._font_family = self._DEFAULT_FONT
|
|
self._mimetypes = []
|
|
|
|
# Flags/Working variables
|
|
self._last_mouse_pos = QtCore.QPoint(0, 0)
|
|
self._modified_lines = set()
|
|
self._cleaning = False
|
|
self._visible_blocks = []
|
|
self._tooltips_runner = DelayJobRunner(delay=700)
|
|
self._prev_tooltip_block_nbr = -1
|
|
self._original_text = ""
|
|
|
|
self._dirty = False
|
|
|
|
# setup context menu
|
|
self._actions = []
|
|
self._menus = []
|
|
self._init_actions(create_default_actions)
|
|
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self._show_context_menu)
|
|
self._mnu = None # bug with PySide (github #63)
|
|
|
|
# init settings and styles from global settings/style modules
|
|
self._init_settings()
|
|
self._init_style()
|
|
|
|
# connect slots
|
|
self.textChanged.connect(self._on_text_changed)
|
|
self.blockCountChanged.connect(self.update)
|
|
self.cursorPositionChanged.connect(self.update)
|
|
self.selectionChanged.connect(self.update)
|
|
|
|
self.setMouseTracking(True)
|
|
self.setCenterOnScroll(True)
|
|
self.setLineWrapMode(self.NoWrap)
|
|
|
|
def __repr__(self):
|
|
return '%s(path=%r)' % (self.__class__.__name__, self.file.path)
|
|
|
|
def split(self):
|
|
"""
|
|
Split the code editor widget, return a clone of the widget ready to
|
|
be used (and synchronised with its original).
|
|
|
|
Splitting the widget is done in 2 steps:
|
|
- first we clone the widget, you can override ``clone`` if your
|
|
widget needs additional arguments.
|
|
|
|
- then we link the two text document and disable some modes on the
|
|
cloned instance (such as the watcher mode).
|
|
"""
|
|
# cache cursor position so that the clone open at the current cursor
|
|
# pos
|
|
l, c = TextHelper(self).cursor_position()
|
|
clone = self.clone()
|
|
self.link(clone)
|
|
TextHelper(clone).goto_line(l, c)
|
|
self.clones.append(clone)
|
|
return clone
|
|
|
|
def clone(self):
|
|
"""
|
|
Clone ourselves, return an instance of the same class, using the
|
|
default QWidget constructor.
|
|
"""
|
|
clone = self.__class__(parent=self.parent())
|
|
return clone
|
|
|
|
def link(self, clone):
|
|
"""
|
|
Links the clone with its original. We copy the file manager infos
|
|
(path, mimetype, ...) and setup the clone text document as reference
|
|
to our text document.
|
|
|
|
:param clone: clone to link.
|
|
"""
|
|
clone.file._path = self.file.path
|
|
clone.file._encoding = self.file.encoding
|
|
clone.file._mimetype = self.file.mimetype
|
|
clone.setDocument(self.document())
|
|
for original_mode, mode in zip(list(self.modes), list(clone.modes)):
|
|
mode.enabled = original_mode.enabled
|
|
mode.clone_settings(original_mode)
|
|
for original_panel, panel in zip(
|
|
list(self.panels), list(clone.panels)):
|
|
panel.enabled = original_panel.isEnabled()
|
|
panel.clone_settings(original_panel)
|
|
if not original_panel.isVisible():
|
|
panel.setVisible(False)
|
|
clone.use_spaces_instead_of_tabs = self.use_spaces_instead_of_tabs
|
|
clone.tab_length = self.tab_length
|
|
clone.save_on_focus_out = self.save_on_focus_out
|
|
clone.show_whitespaces = self.show_whitespaces
|
|
clone.font_name = self.font_name
|
|
clone.font_size = self.font_size
|
|
clone.zoom_level = self.zoom_level
|
|
clone.background = self.background
|
|
clone.foreground = self.foreground
|
|
clone.whitespaces_foreground = self.whitespaces_foreground
|
|
clone.selection_background = self.selection_background
|
|
clone.selection_foreground = self.selection_foreground
|
|
clone.word_separators = self.word_separators
|
|
clone.file.clone_settings(self.file)
|
|
|
|
def close(self, clear=True):
|
|
"""
|
|
Closes the editor, stops the backend and removes any installed
|
|
mode/panel.
|
|
|
|
This is also where we cache the cursor position.
|
|
|
|
:param clear: True to clear the editor content before closing.
|
|
"""
|
|
self.decorations.clear()
|
|
self.modes.clear()
|
|
self.panels.clear()
|
|
self.backend.stop()
|
|
Cache().set_cursor_position(
|
|
self.file.path, self.textCursor().position())
|
|
super(CodeEdit, self).close()
|
|
_logger().debug('closed')
|
|
|
|
def set_mouse_cursor(self, cursor):
|
|
"""
|
|
Changes the viewport's cursor
|
|
|
|
:param cursor: the mouse cursor to set.
|
|
:type cursor: QtWidgets.QCursor
|
|
"""
|
|
self.viewport().setCursor(cursor)
|
|
|
|
def show_tooltip(self, pos, tooltip, _sender_deco=None):
|
|
"""
|
|
Show a tool tip at the specified position
|
|
|
|
:param pos: Tooltip position
|
|
:param tooltip: Tooltip text
|
|
|
|
:param _sender_deco: TextDecoration which is the sender of the show
|
|
tooltip request. (for internal use only).
|
|
"""
|
|
if _sender_deco is not None and _sender_deco not in self.decorations:
|
|
return
|
|
QtWidgets.QToolTip.showText(pos, tooltip[0: 1024], self)
|
|
|
|
def setPlainText(self, txt, mime_type, encoding):
|
|
"""
|
|
Extends setPlainText to force the user to setup an encoding and a
|
|
mime type.
|
|
|
|
Emits the new_text_set signal.
|
|
|
|
:param txt: The new text to set.
|
|
:param mime_type: Associated mimetype. Setting the mime will update the
|
|
pygments lexer.
|
|
:param encoding: text encoding
|
|
"""
|
|
self.file.mimetype = mime_type
|
|
self.file._encoding = encoding
|
|
self._original_text = txt
|
|
self._modified_lines.clear()
|
|
import time
|
|
t = time.time()
|
|
super(CodeEdit, self).setPlainText(txt)
|
|
_logger().debug('setPlainText duration: %fs' % (time.time() - t))
|
|
self.new_text_set.emit()
|
|
self.redoAvailable.emit(False)
|
|
self.undoAvailable.emit(False)
|
|
|
|
def add_action(self, action):
|
|
"""
|
|
Adds an action to the editor's context menu.
|
|
|
|
:param action: QtWidgets.QAction
|
|
"""
|
|
self._actions.append(action)
|
|
action.setShortcutContext(QtCore.Qt.WidgetShortcut)
|
|
super(CodeEdit, self).addAction(action)
|
|
|
|
def insert_action(self, action, prev_action):
|
|
"""
|
|
Inserts an action to the editor's context menu
|
|
|
|
:param action: action to insert
|
|
:param prev_action: the action after which the new action must be
|
|
inserted or the insert index
|
|
"""
|
|
if isinstance(prev_action, QtWidgets.QAction):
|
|
index = self._actions.index(prev_action)
|
|
else:
|
|
index = prev_action
|
|
action.setShortcutContext(QtCore.Qt.WidgetShortcut)
|
|
self._actions.insert(index, action)
|
|
|
|
def actions(self):
|
|
"""
|
|
Returns the list of actions/sepqrators of the context menu.
|
|
|
|
"""
|
|
return self._actions
|
|
|
|
def add_separator(self):
|
|
"""
|
|
Adds a sepqrator to the editor's context menu.
|
|
|
|
:return: The sepator that has been added.
|
|
:rtype: QtWidgets.QAction
|
|
"""
|
|
action = QtWidgets.QAction(self)
|
|
action.setSeparator(True)
|
|
self._actions.append(action)
|
|
self.addAction(action)
|
|
return action
|
|
|
|
def remove_action(self, action):
|
|
"""
|
|
Removes an action/separator from the editor's context menu.
|
|
|
|
:param action: Action/seprator to remove.
|
|
"""
|
|
try:
|
|
self._actions.remove(action)
|
|
except ValueError:
|
|
pass
|
|
self.removeAction(action)
|
|
|
|
def add_menu(self, menu):
|
|
"""
|
|
Adds a sub-menu to the editor context menu.
|
|
|
|
Menu are put at the bottom of the context menu.
|
|
|
|
.. note:: to add a menu in the middle of the context menu, you can
|
|
always add its menuAction().
|
|
|
|
:param menu: menu to add
|
|
"""
|
|
self._menus.append(menu)
|
|
for action in menu.actions():
|
|
action.setShortcutContext(QtCore.Qt.WidgetShortcut)
|
|
self.addActions(menu.actions())
|
|
|
|
def remove_menu(self, menu):
|
|
"""
|
|
Removes a sub-menu from the context menu.
|
|
:param menu: Sub-menu to remove.
|
|
"""
|
|
self._menus.remove(menu)
|
|
for action in menu.actions():
|
|
self.removeAction(action)
|
|
|
|
@QtCore.Slot()
|
|
def delete(self):
|
|
""" Deletes the selected text """
|
|
self.textCursor().removeSelectedText()
|
|
|
|
@QtCore.Slot()
|
|
def goto_line(self):
|
|
"""
|
|
Shows the *go to line dialog* and go to the selected line.
|
|
"""
|
|
helper = TextHelper(self)
|
|
line, result = DlgGotoLine.get_line(
|
|
self, helper.current_line_nbr(), helper.line_count())
|
|
if not result:
|
|
return
|
|
return helper.goto_line(line, move=True)
|
|
|
|
@QtCore.Slot()
|
|
def rehighlight(self):
|
|
"""
|
|
Calls ``rehighlight`` on the installed syntax highlighter mode.
|
|
"""
|
|
if self.syntax_highlighter:
|
|
self.syntax_highlighter.rehighlight()
|
|
|
|
@QtCore.Slot()
|
|
def reset_zoom(self):
|
|
"""
|
|
Resets the zoom level.
|
|
"""
|
|
self._zoom_level = 0
|
|
self._reset_stylesheet()
|
|
|
|
@QtCore.Slot()
|
|
def zoom_in(self, increment=1):
|
|
"""
|
|
Zooms in the editor (makes the font bigger).
|
|
|
|
:param increment: zoom level increment. Default is 1.
|
|
"""
|
|
self.zoom_level += increment
|
|
TextHelper(self).mark_whole_doc_dirty()
|
|
self._reset_stylesheet()
|
|
|
|
@QtCore.Slot()
|
|
def zoom_out(self, decrement=1):
|
|
"""
|
|
Zooms out the editor (makes the font smaller).
|
|
|
|
:param decrement: zoom level decrement. Default is 1. The value is
|
|
given as an absolute value.
|
|
"""
|
|
self.zoom_level -= decrement
|
|
# make sure font size remains > 0
|
|
if self.font_size + self.zoom_level <= 0:
|
|
self.zoom_level = -self._font_size + 1
|
|
TextHelper(self).mark_whole_doc_dirty()
|
|
self._reset_stylesheet()
|
|
|
|
@QtCore.Slot()
|
|
def duplicate_line(self):
|
|
"""
|
|
Duplicates the line under the cursor. If multiple lines are selected,
|
|
only the last one is duplicated.
|
|
"""
|
|
cursor = self.textCursor()
|
|
assert isinstance(cursor, QtGui.QTextCursor)
|
|
has_selection = True
|
|
if not cursor.hasSelection():
|
|
cursor.select(cursor.LineUnderCursor)
|
|
has_selection = False
|
|
line = cursor.selectedText()
|
|
line = '\n'.join(line.split('\u2029'))
|
|
end = cursor.selectionEnd()
|
|
cursor.setPosition(end)
|
|
cursor.beginEditBlock()
|
|
cursor.insertText('\n')
|
|
cursor.insertText(line)
|
|
cursor.endEditBlock()
|
|
if has_selection:
|
|
pos = cursor.position()
|
|
cursor.setPosition(end + 1)
|
|
cursor.setPosition(pos, cursor.KeepAnchor)
|
|
self.setTextCursor(cursor)
|
|
|
|
@QtCore.Slot()
|
|
def indent(self):
|
|
"""
|
|
Indents the text cursor or the selection.
|
|
|
|
Emits the :attr:`pyqode.core.api.CodeEdit.indent_requested`
|
|
signal, the :class:`pyqode.core.modes.IndenterMode` will
|
|
perform the actual indentation.
|
|
"""
|
|
self.indent_requested.emit()
|
|
|
|
@QtCore.Slot()
|
|
def un_indent(self):
|
|
"""
|
|
Un-indents the text cursor or the selection.
|
|
|
|
Emits the :attr:`pyqode.core.api.CodeEdit.unindent_requested`
|
|
signal, the :class:`pyqode.core.modes.IndenterMode` will
|
|
perform the actual un-indentation.
|
|
"""
|
|
self.unindent_requested.emit()
|
|
|
|
def eventFilter(self, obj, event):
|
|
if obj == self and event.type() == QtCore.QEvent.KeyPress:
|
|
if event.key() == QtCore.Qt.Key_X and \
|
|
int(event.modifiers()) == QtCore.Qt.ControlModifier:
|
|
self.cut()
|
|
return True
|
|
if event.key() == QtCore.Qt.Key_C and \
|
|
int(event.modifiers()) == QtCore.Qt.ControlModifier:
|
|
self.copy()
|
|
return True
|
|
return False
|
|
|
|
def cut(self):
|
|
tc = self.textCursor()
|
|
helper = TextHelper(self)
|
|
tc.beginEditBlock()
|
|
no_selection = False
|
|
if not helper.current_line_text().strip():
|
|
tc.deleteChar()
|
|
else:
|
|
if not self.textCursor().hasSelection():
|
|
no_selection = True
|
|
TextHelper(self).select_whole_line()
|
|
super(CodeEdit, self).cut()
|
|
if no_selection:
|
|
tc.deleteChar()
|
|
tc.endEditBlock()
|
|
self.setTextCursor(tc)
|
|
|
|
def copy(self):
|
|
if not self.textCursor().hasSelection():
|
|
TextHelper(self).select_whole_line()
|
|
super(CodeEdit, self).copy()
|
|
|
|
def resizeEvent(self, e):
|
|
"""
|
|
Overrides resize event to resize the editor's panels.
|
|
|
|
:param e: resize event
|
|
"""
|
|
super(CodeEdit, self).resizeEvent(e)
|
|
self.panels.resize()
|
|
|
|
def paintEvent(self, e):
|
|
"""
|
|
Overrides paint event to update the list of visible blocks and emit
|
|
the painted event.
|
|
|
|
:param e: paint event
|
|
"""
|
|
self._update_visible_blocks(e)
|
|
super(CodeEdit, self).paintEvent(e)
|
|
self.painted.emit(e)
|
|
|
|
def keyPressEvent(self, event):
|
|
"""
|
|
Overrides the keyPressEvent to emit the key_pressed signal.
|
|
|
|
Also takes care of indenting and handling smarter home key.
|
|
|
|
:param event: QKeyEvent
|
|
"""
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.key_pressed.emit(event)
|
|
state = event.isAccepted()
|
|
if not event.isAccepted():
|
|
if event.key() == QtCore.Qt.Key_Tab:
|
|
self.indent()
|
|
event.accept()
|
|
elif event.key() == QtCore.Qt.Key_Backtab:
|
|
self.un_indent()
|
|
event.accept()
|
|
elif event.key() == QtCore.Qt.Key_Home and \
|
|
int(event.modifiers()) & QtCore.Qt.ControlModifier == 0:
|
|
self._do_home_key(
|
|
event, int(event.modifiers()) & QtCore.Qt.ShiftModifier)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).keyPressEvent(event)
|
|
new_state = event.isAccepted()
|
|
event.setAccepted(state)
|
|
self.post_key_pressed.emit(event)
|
|
event.setAccepted(new_state)
|
|
|
|
def keyReleaseEvent(self, event):
|
|
"""
|
|
Overrides keyReleaseEvent to emit the key_released signal.
|
|
|
|
:param event: QKeyEvent
|
|
"""
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.key_released.emit(event)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).keyReleaseEvent(event)
|
|
|
|
def mouseDoubleClickEvent(self, event):
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.mouse_double_clicked.emit(event)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).mouseDoubleClickEvent(event)
|
|
|
|
def focusInEvent(self, event):
|
|
"""
|
|
Overrides focusInEvent to emits the focused_in signal
|
|
|
|
:param event: QFocusEvent
|
|
"""
|
|
# fix a visual bug if the editor was resized while being hidden (
|
|
# e.g. a dock # widget has been resized and some editors were in a
|
|
# tab widget. Non visible editor have a visual bug where horizontal
|
|
# scroll bar range
|
|
#
|
|
TextHelper(self).mark_whole_doc_dirty()
|
|
s = self.size()
|
|
s.setWidth(s.width() + 1)
|
|
self.resizeEvent(QtGui.QResizeEvent(self.size(), s))
|
|
|
|
self.focused_in.emit(event)
|
|
super(CodeEdit, self).focusInEvent(event)
|
|
self.repaint()
|
|
|
|
def focusOutEvent(self, event):
|
|
# Saves content if save_on_focus_out is True.
|
|
if self.save_on_focus_out and self.dirty and self.file.path:
|
|
self.file.save()
|
|
super(CodeEdit, self).focusOutEvent(event)
|
|
|
|
def mousePressEvent(self, event):
|
|
"""
|
|
Overrides mousePressEvent to emits mouse_pressed signal
|
|
|
|
:param event: QMouseEvent
|
|
"""
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.mouse_pressed.emit(event)
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
cursor = self.cursorForPosition(event.pos())
|
|
for sel in self.decorations:
|
|
if sel.cursor.blockNumber() == cursor.blockNumber():
|
|
if sel.contains_cursor(cursor):
|
|
sel.signals.clicked.emit(sel)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).mousePressEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
"""
|
|
Emits mouse_released signal.
|
|
|
|
:param event: QMouseEvent
|
|
"""
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.mouse_released.emit(event)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).mouseReleaseEvent(event)
|
|
|
|
def wheelEvent(self, event):
|
|
"""
|
|
Emits the mouse_wheel_activated signal.
|
|
|
|
:param event: QMouseEvent
|
|
"""
|
|
initial_state = event.isAccepted()
|
|
event.ignore()
|
|
self.mouse_wheel_activated.emit(event)
|
|
if not event.isAccepted():
|
|
event.setAccepted(initial_state)
|
|
super(CodeEdit, self).wheelEvent(event)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
"""
|
|
Overrides mouseMovedEvent to display any decoration tooltip and emits
|
|
the mouse_moved event.
|
|
|
|
:param event: QMouseEvent
|
|
"""
|
|
cursor = self.cursorForPosition(event.pos())
|
|
self._last_mouse_pos = event.pos()
|
|
block_found = False
|
|
for sel in self.decorations:
|
|
if sel.contains_cursor(cursor) and sel.tooltip:
|
|
if (self._prev_tooltip_block_nbr != cursor.blockNumber() or
|
|
not QtWidgets.QToolTip.isVisible()):
|
|
pos = event.pos()
|
|
# add left margin
|
|
pos.setX(pos.x() + self.panels.margin_size())
|
|
# add top margin
|
|
pos.setY(pos.y() + self.panels.margin_size(0))
|
|
self._tooltips_runner.request_job(
|
|
self.show_tooltip,
|
|
self.mapToGlobal(pos), sel.tooltip[0: 1024], sel)
|
|
self._prev_tooltip_block_nbr = cursor.blockNumber()
|
|
block_found = True
|
|
break
|
|
if not block_found and self._prev_tooltip_block_nbr != -1:
|
|
QtWidgets.QToolTip.hideText()
|
|
self._prev_tooltip_block_nbr = -1
|
|
self._tooltips_runner.cancel_requests()
|
|
self.mouse_moved.emit(event)
|
|
super(CodeEdit, self).mouseMoveEvent(event)
|
|
|
|
def showEvent(self, event):
|
|
""" Overrides showEvent to update the viewport margins """
|
|
super(CodeEdit, self).showEvent(event)
|
|
_logger().debug('show event: %r' % self)
|
|
self.panels.refresh()
|
|
|
|
def get_context_menu(self):
|
|
"""
|
|
Gets the editor context menu.
|
|
|
|
:return: QMenu
|
|
"""
|
|
mnu = QtWidgets.QMenu()
|
|
mnu.addActions(self._actions)
|
|
mnu.addSeparator()
|
|
for menu in self._menus:
|
|
mnu.addMenu(menu)
|
|
return mnu
|
|
|
|
def _show_context_menu(self, point):
|
|
""" Shows the context menu """
|
|
tc = self.textCursor()
|
|
nc = self.cursorForPosition(point)
|
|
if not nc.position() in range(tc.selectionStart(), tc.selectionEnd()):
|
|
self.setTextCursor(nc)
|
|
self._mnu = self.get_context_menu()
|
|
if len(self._mnu.actions()) > 1 and self.show_context_menu:
|
|
self._mnu.popup(self.mapToGlobal(point))
|
|
|
|
def _set_whitespaces_flags(self, show):
|
|
""" Sets show white spaces flag """
|
|
doc = self.document()
|
|
options = doc.defaultTextOption()
|
|
if show:
|
|
options.setFlags(options.flags() |
|
|
QtGui.QTextOption.ShowTabsAndSpaces)
|
|
else:
|
|
options.setFlags(
|
|
options.flags() & ~QtGui.QTextOption.ShowTabsAndSpaces)
|
|
doc.setDefaultTextOption(options)
|
|
|
|
def _init_actions(self, create_standard_actions):
|
|
""" Init context menu action """
|
|
if create_standard_actions:
|
|
# Undo
|
|
action = QtWidgets.QAction('Undo', self)
|
|
action.setShortcut(QtGui.QKeySequence.Undo)
|
|
action.setIcon(icons.icon(
|
|
'edit-undo', ':/pyqode-icons/rc/edit-undo.png', 'fa.undo'))
|
|
action.triggered.connect(self.undo)
|
|
self.undoAvailable.connect(action.setEnabled)
|
|
self.add_action(action)
|
|
self.action_undo = action
|
|
# Redo
|
|
action = QtWidgets.QAction('Redo', self)
|
|
action.setShortcut(QtGui.QKeySequence.Redo)
|
|
action.setIcon(icons.icon(
|
|
'edit-redo', ':/pyqode-icons/rc/edit-redo.png', 'fa.repeat'))
|
|
action.triggered.connect(self.redo)
|
|
self.redoAvailable.connect(action.setEnabled)
|
|
self.add_action(action)
|
|
self.action_redo = action
|
|
# separator
|
|
self.add_separator()
|
|
# Copy
|
|
action = QtWidgets.QAction('Copy', self)
|
|
action.setShortcut(QtGui.QKeySequence.Copy)
|
|
action.setIcon(icons.icon(
|
|
'edit-copy', ':/pyqode-icons/rc/edit-copy.png', 'fa.copy'))
|
|
action.triggered.connect(self.copy)
|
|
self.copyAvailable.connect(action.setEnabled)
|
|
self.add_action(action)
|
|
self.action_copy = action
|
|
# cut
|
|
action = QtWidgets.QAction('Cut', self)
|
|
action.setShortcut(QtGui.QKeySequence.Cut)
|
|
action.setIcon(icons.icon(
|
|
'edit-cut', ':/pyqode-icons/rc/edit-cut.png', 'fa.cut'))
|
|
action.triggered.connect(self.cut)
|
|
self.copyAvailable.connect(action.setEnabled)
|
|
self.add_action(action)
|
|
self.action_cut = action
|
|
# paste
|
|
action = QtWidgets.QAction('Paste', self)
|
|
action.setShortcut(QtGui.QKeySequence.Paste)
|
|
action.setIcon(icons.icon(
|
|
'edit-paste', ':/pyqode-icons/rc/edit-paste.png',
|
|
'fa.paste'))
|
|
action.triggered.connect(self.paste)
|
|
self.add_action(action)
|
|
self.action_paste = action
|
|
# duplicate line
|
|
action = QtWidgets.QAction('Duplicate line', self)
|
|
action.setShortcut('Ctrl+D')
|
|
action.triggered.connect(self.duplicate_line)
|
|
self.add_action(action)
|
|
self.action_duplicate_line = action
|
|
# select all
|
|
action = QtWidgets.QAction('Select all', self)
|
|
action.setShortcut(QtGui.QKeySequence.SelectAll)
|
|
action.triggered.connect(self.selectAll)
|
|
self.action_select_all = action
|
|
self.addAction(self.action_select_all)
|
|
if create_standard_actions:
|
|
# delete
|
|
action = QtWidgets.QAction('Delete', self)
|
|
action.setShortcut(QtGui.QKeySequence.Delete)
|
|
action.setIcon(icons.icon(
|
|
'edit-delete', ':/pyqode-icons/rc/edit-delete.png',
|
|
'fa.remove'))
|
|
action.triggered.connect(self.delete)
|
|
self.add_action(action)
|
|
self.action_delete = action
|
|
self.add_separator()
|
|
self.add_action(self.action_select_all)
|
|
# separator
|
|
self.add_separator()
|
|
# indent
|
|
action = QtWidgets.QAction('Indent', self)
|
|
action.setShortcut('Tab')
|
|
action.setIcon(icons.icon(
|
|
'format-indent-more',
|
|
':/pyqode-icons/rc/format-indent-more.png', 'fa.indent'))
|
|
action.triggered.connect(self.indent)
|
|
self.add_action(action)
|
|
self.action_indent = action
|
|
# unindent
|
|
action = QtWidgets.QAction('Un-indent', self)
|
|
action.setShortcut('Shift+Tab')
|
|
action.setIcon(icons.icon(
|
|
'format-indent-less',
|
|
':/pyqode-icons/rc/format-indent-less.png', 'fa.dedent'))
|
|
action.triggered.connect(self.un_indent)
|
|
self.add_action(action)
|
|
self.action_un_indent = action
|
|
# separator
|
|
self.add_separator()
|
|
# goto
|
|
action = QtWidgets.QAction('Go to line', self)
|
|
action.setShortcut('Ctrl+G')
|
|
action.setIcon(icons.icon(
|
|
'go-jump', ':/pyqode-icons/rc/goto-line.png', 'fa.share'))
|
|
action.triggered.connect(self.goto_line)
|
|
self.add_action(action)
|
|
self.action_goto_line = action
|
|
|
|
def _init_settings(self):
|
|
""" Init setting """
|
|
self._show_whitespaces = False
|
|
self._tab_length = 4
|
|
self._use_spaces_instead_of_tabs = True
|
|
self.setTabStopWidth(self._tab_length *
|
|
self.fontMetrics().width(" "))
|
|
self._set_whitespaces_flags(self._show_whitespaces)
|
|
|
|
def _init_style(self):
|
|
""" Inits style options """
|
|
self._background = QtGui.QColor('white')
|
|
self._foreground = QtGui.QColor('black')
|
|
self._whitespaces_foreground = QtGui.QColor('light gray')
|
|
app = QtWidgets.QApplication.instance()
|
|
self._sel_background = app.palette().highlight().color()
|
|
self._sel_foreground = app.palette().highlightedText().color()
|
|
self._font_size = 10
|
|
self.font_name = ""
|
|
|
|
def _update_visible_blocks(self, *args):
|
|
""" Updates the list of visible blocks """
|
|
self._visible_blocks[:] = []
|
|
block = self.firstVisibleBlock()
|
|
block_nbr = block.blockNumber()
|
|
top = int(self.blockBoundingGeometry(block).translated(
|
|
self.contentOffset()).top())
|
|
bottom = top + int(self.blockBoundingRect(block).height())
|
|
ebottom_top = 0
|
|
ebottom_bottom = self.height()
|
|
while block.isValid():
|
|
visible = (top >= ebottom_top and bottom <= ebottom_bottom)
|
|
if not visible:
|
|
break
|
|
if block.isVisible():
|
|
self._visible_blocks.append((top, block_nbr, block))
|
|
block = block.next()
|
|
top = bottom
|
|
bottom = top + int(self.blockBoundingRect(block).height())
|
|
block_nbr = block.blockNumber()
|
|
|
|
def _on_text_changed(self):
|
|
""" Adjust dirty flag depending on editor's content """
|
|
if not self._cleaning:
|
|
ln = TextHelper(self).cursor_position()[0]
|
|
self._modified_lines.add(ln)
|
|
|
|
def _reset_stylesheet(self):
|
|
""" Resets stylesheet"""
|
|
self.setFont(QtGui.QFont(self._font_family,
|
|
self._font_size + self._zoom_level))
|
|
flg_stylesheet = hasattr(self, '_flg_stylesheet')
|
|
if QtWidgets.QApplication.instance().styleSheet() or flg_stylesheet:
|
|
self._flg_stylesheet = True
|
|
# On Window, if the application once had a stylesheet, we must
|
|
# keep on using a stylesheet otherwise strange colors appear
|
|
# see https://github.com/OpenCobolIDE/OpenCobolIDE/issues/65
|
|
# Also happen on plasma 5
|
|
try:
|
|
plasma = os.environ['DESKTOP_SESSION'] == 'plasma'
|
|
except KeyError:
|
|
plasma = False
|
|
if sys.platform == 'win32' or plasma:
|
|
self.setStyleSheet('''QPlainTextEdit
|
|
{
|
|
background-color: %s;
|
|
color: %s;
|
|
}
|
|
''' % (self.background.name(), self.foreground.name()))
|
|
else:
|
|
# on linux/osx we just have to set an empty stylesheet to
|
|
# cancel any previous stylesheet and still keep a correct
|
|
# style for scrollbars
|
|
self.setStyleSheet('')
|
|
else:
|
|
p = self.palette()
|
|
p.setColor(QtGui.QPalette.Base, self.background)
|
|
p.setColor(QtGui.QPalette.Text, self.foreground)
|
|
p.setColor(QtGui.QPalette.Highlight,
|
|
self.selection_background)
|
|
p.setColor(QtGui.QPalette.HighlightedText,
|
|
self.selection_foreground)
|
|
self.setPalette(p)
|
|
self.repaint()
|
|
|
|
def _do_home_key(self, event=None, select=False):
|
|
""" Performs home key action """
|
|
# get nb char to first significative char
|
|
delta = (self.textCursor().positionInBlock() -
|
|
TextHelper(self).line_indent())
|
|
cursor = self.textCursor()
|
|
move = QtGui.QTextCursor.MoveAnchor
|
|
if select:
|
|
move = QtGui.QTextCursor.KeepAnchor
|
|
if delta > 0:
|
|
cursor.movePosition(QtGui.QTextCursor.Left, move, delta)
|
|
else:
|
|
cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move)
|
|
self.setTextCursor(cursor)
|
|
if event:
|
|
event.accept()
|
|
|
|
def _emit_dirty_changed(self, state):
|
|
self.dirty_changed.emit(state)
|
|
for c in self.clones:
|
|
c.dirty_changed.emit(state)
|