# -*- coding: utf-8 -*-
#
# Copyright © 2009-2011 Pierre Raybaut
# Licensed under the terms of the MIT License
# (see spyderlib/__init__.py for details)
"""Editor Widget"""
# pylint: disable=C0103
# pylint: disable=R0903
# pylint: disable=R0911
# pylint: disable=R0201
from __future__ import print_function
from spyderlib.qt import is_pyqt46
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QMenu, QFont,
QAction, QApplication, QWidget, QHBoxLayout,
QLabel, QKeySequence, QMainWindow,
QSplitter, QListWidget, QListWidgetItem,
QDialog, QLineEdit)
from spyderlib.qt.QtCore import (SIGNAL, Qt, QFileInfo, QThread, QObject,
QByteArray, QSize, QPoint, QTimer, Slot)
from spyderlib.qt.compat import getsavefilename
import os
import sys
import os.path as osp
# Local imports
from spyderlib.utils import encoding, sourcecode, codeanalysis
from spyderlib.utils import introspection
from spyderlib.baseconfig import _, DEBUG, STDOUT, STDERR
from spyderlib.config import EDIT_FILTERS, EDIT_EXT, get_filter, EDIT_FILETYPES
from spyderlib.guiconfig import create_shortcut, new_shortcut
from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions,
mimedata2url, get_filetype_icon,
create_toolbutton)
from spyderlib.widgets.tabs import BaseTabs
from spyderlib.widgets.findreplace import FindReplace
from spyderlib.widgets.editortools import OutlineExplorerWidget
from spyderlib.widgets.status import (ReadWriteStatus, EOLStatus,
EncodingStatus, CursorPositionStatus)
from spyderlib.widgets.sourcecode import syntaxhighlighters, codeeditor
from spyderlib.widgets.sourcecode.base import TextEditBaseWidget #analysis:ignore
from spyderlib.widgets.sourcecode.codeeditor import Printer #analysis:ignore
from spyderlib.widgets.sourcecode.codeeditor import get_file_language
from spyderlib.py3compat import to_text_string, qbytearray_to_str, u
DEBUG_EDITOR = DEBUG >= 3
class FileListDialog(QDialog):
def __init__(self, parent, tabs, fullpath_sorting):
QDialog.__init__(self, parent)
# Destroying the C++ object right after closing the dialog box,
# otherwise it may be garbage-collected in another QThread
# (e.g. the editor's analysis thread in Spyder), thus leading to
# a segmentation fault on UNIX or an application crash on Windows
self.setAttribute(Qt.WA_DeleteOnClose)
self.indexes = None
self.setWindowIcon(get_icon('filelist.png'))
self.setWindowTitle(_("File list management"))
self.setModal(True)
flabel = QLabel(_("Filter:"))
self.edit = QLineEdit(self)
self.connect(self.edit, SIGNAL("returnPressed()"), self.edit_file)
self.connect(self.edit, SIGNAL("textChanged(QString)"),
lambda text: self.synchronize(0))
fhint = QLabel(_("(press Enter to edit file)"))
edit_layout = QHBoxLayout()
edit_layout.addWidget(flabel)
edit_layout.addWidget(self.edit)
edit_layout.addWidget(fhint)
self.listwidget = QListWidget(self)
self.listwidget.setResizeMode(QListWidget.Adjust)
self.connect(self.listwidget, SIGNAL("itemSelectionChanged()"),
self.item_selection_changed)
self.connect(self.listwidget, SIGNAL("itemActivated(QListWidgetItem*)"),
self.edit_file)
btn_layout = QHBoxLayout()
edit_btn = create_toolbutton(self, icon=get_icon('edit.png'),
text=_("&Edit file"), autoraise=False,
triggered=self.edit_file, text_beside_icon=True)
edit_btn.setMinimumHeight(28)
btn_layout.addWidget(edit_btn)
btn_layout.addStretch()
btn_layout.addSpacing(150)
btn_layout.addStretch()
close_btn = create_toolbutton(self, text=_("&Close file"),
icon=get_icon("fileclose.png"),
autoraise=False, text_beside_icon=True,
triggered=lambda: self.emit(SIGNAL("close_file(int)"),
self.indexes[self.listwidget.currentRow()]))
close_btn.setMinimumHeight(28)
btn_layout.addWidget(close_btn)
hint = QLabel(_("Hint: press Alt to show accelerators"))
hint.setAlignment(Qt.AlignCenter)
vlayout = QVBoxLayout()
vlayout.addLayout(edit_layout)
vlayout.addWidget(self.listwidget)
vlayout.addLayout(btn_layout)
vlayout.addWidget(hint)
self.setLayout(vlayout)
self.tabs = tabs
self.fullpath_sorting = fullpath_sorting
self.buttons = (edit_btn, close_btn)
def edit_file(self):
row = self.listwidget.currentRow()
if self.listwidget.count() > 0 and row >= 0:
self.emit(SIGNAL("edit_file(int)"), self.indexes[row])
self.accept()
def item_selection_changed(self):
for btn in self.buttons:
btn.setEnabled(self.listwidget.currentRow() >= 0)
def synchronize(self, stack_index):
count = self.tabs.count()
if count == 0:
self.accept()
return
self.listwidget.setTextElideMode(Qt.ElideMiddle
if self.fullpath_sorting
else Qt.ElideRight)
current_row = self.listwidget.currentRow()
if current_row >= 0:
current_text = to_text_string(self.listwidget.currentItem().text())
else:
current_text = ""
self.listwidget.clear()
self.indexes = []
new_current_index = stack_index
filter_text = to_text_string(self.edit.text())
lw_index = 0
for index in range(count):
text = to_text_string(self.tabs.tabText(index))
if len(filter_text) == 0 or filter_text in text:
if text == current_text:
new_current_index = lw_index
lw_index += 1
item = QListWidgetItem(self.tabs.tabIcon(index),
text, self.listwidget)
item.setSizeHint(QSize(0, 25))
self.listwidget.addItem(item)
self.indexes.append(index)
if new_current_index < self.listwidget.count():
self.listwidget.setCurrentRow(new_current_index)
for btn in self.buttons:
btn.setEnabled(lw_index > 0)
class AnalysisThread(QThread):
"""Analysis thread"""
def __init__(self, parent, checker, source_code):
super(AnalysisThread, self).__init__(parent)
self.checker = checker
self.results = None
self.source_code = source_code
def run(self):
"""Run analysis"""
try:
self.results = self.checker(self.source_code)
except Exception:
if DEBUG_EDITOR:
import traceback
traceback.print_exc(file=STDERR)
class ThreadManager(QObject):
"""Analysis thread manager"""
def __init__(self, parent, max_simultaneous_threads=2):
super(ThreadManager, self).__init__(parent)
self.max_simultaneous_threads = max_simultaneous_threads
self.started_threads = {}
self.pending_threads = []
self.end_callbacks = {}
def close_threads(self, parent):
"""Close threads associated to parent_id"""
if DEBUG_EDITOR:
print("Call to 'close_threads'", file=STDOUT)
if parent is None:
# Closing all threads
self.pending_threads = []
threadlist = []
for threads in list(self.started_threads.values()):
threadlist += threads
else:
parent_id = id(parent)
self.pending_threads = [(_th, _id) for (_th, _id)
in self.pending_threads
if _id != parent_id]
threadlist = self.started_threads.get(parent_id, [])
for thread in threadlist:
if DEBUG_EDITOR:
print("Waiting for thread %r to finish" % thread, file=STDOUT)
while thread.isRunning():
# We can't terminate thread safely, so we simply wait...
QApplication.processEvents()
def close_all_threads(self):
"""Close all threads"""
if DEBUG_EDITOR:
print("Call to 'close_all_threads'", file=STDOUT)
self.close_threads(None)
def add_thread(self, checker, end_callback, source_code, parent):
"""Add thread to queue"""
parent_id = id(parent)
thread = AnalysisThread(self, checker, source_code)
self.end_callbacks[id(thread)] = end_callback
self.pending_threads.append((thread, parent_id))
if DEBUG_EDITOR:
print("Added thread %r to queue" % thread, file=STDOUT)
QTimer.singleShot(50, self.update_queue)
def update_queue(self):
"""Update queue"""
started = 0
for parent_id, threadlist in list(self.started_threads.items()):
still_running = []
for thread in threadlist:
if thread.isFinished():
end_callback = self.end_callbacks.pop(id(thread))
if thread.results is not None:
# The thread was executed successfully
end_callback(thread.results)
thread.setParent(None)
thread = None
else:
still_running.append(thread)
started += 1
threadlist = None
if still_running:
self.started_threads[parent_id] = still_running
else:
self.started_threads.pop(parent_id)
if DEBUG_EDITOR:
print("Updating queue:", file=STDOUT)
print(" started:", started, file=STDOUT)
print(" pending:", len(self.pending_threads), file=STDOUT)
if self.pending_threads and started < self.max_simultaneous_threads:
thread, parent_id = self.pending_threads.pop(0)
self.connect(thread, SIGNAL('finished()'), self.update_queue)
threadlist = self.started_threads.get(parent_id, [])
self.started_threads[parent_id] = threadlist+[thread]
if DEBUG_EDITOR:
print("===>starting:", thread, file=STDOUT)
thread.start()
class FileInfo(QObject):
"""File properties"""
def __init__(self, filename, encoding, editor, new, threadmanager,
introspection_plugin):
QObject.__init__(self)
self.threadmanager = threadmanager
self.filename = filename
self.newly_created = new
self.default = False # Default untitled file
self.encoding = encoding
self.editor = editor
self.path = []
self.classes = (filename, None, None)
self.analysis_results = []
self.todo_results = []
self.lastmodified = QFileInfo(filename).lastModified()
self.connect(self.editor, SIGNAL('textChanged()'),
self.text_changed)
self.connect(self.editor, SIGNAL('breakpoints_changed()'),
self.breakpoints_changed)
self.pyflakes_results = None
self.pep8_results = None
def text_changed(self):
"""Editor's text has changed"""
self.default = False
self.emit(SIGNAL('text_changed_at(QString,int)'),
self.filename, self.editor.get_position('cursor'))
def get_source_code(self):
"""Return associated editor source code"""
return to_text_string(self.editor.toPlainText())
def run_code_analysis(self, run_pyflakes, run_pep8):
"""Run code analysis"""
run_pyflakes = run_pyflakes and codeanalysis.is_pyflakes_installed()
run_pep8 = run_pep8 and\
codeanalysis.get_checker_executable('pep8') is not None
self.pyflakes_results = []
self.pep8_results = []
if self.editor.is_python():
enc = self.encoding.replace('-guessed', '').replace('-bom', '')
source_code = self.get_source_code().encode(enc)
if run_pyflakes:
self.pyflakes_results = None
if run_pep8:
self.pep8_results = None
if run_pyflakes:
self.threadmanager.add_thread(codeanalysis.check_with_pyflakes,
self.pyflakes_analysis_finished,
source_code, self)
if run_pep8:
self.threadmanager.add_thread(codeanalysis.check_with_pep8,
self.pep8_analysis_finished,
source_code, self)
def pyflakes_analysis_finished(self, results):
"""Pyflakes code analysis thread has finished"""
self.pyflakes_results = results
if self.pep8_results is not None:
self.code_analysis_finished()
def pep8_analysis_finished(self, results):
"""Pep8 code analysis thread has finished"""
self.pep8_results = results
if self.pyflakes_results is not None:
self.code_analysis_finished()
def code_analysis_finished(self):
"""Code analysis thread has finished"""
self.set_analysis_results(self.pyflakes_results+self.pep8_results)
self.emit(SIGNAL('analysis_results_changed()'))
def set_analysis_results(self, results):
"""Set analysis results and update warning markers in editor"""
self.analysis_results = results
self.editor.process_code_analysis(results)
def cleanup_analysis_results(self):
"""Clean-up analysis results"""
self.analysis_results = []
self.editor.cleanup_code_analysis()
def run_todo_finder(self):
"""Run TODO finder"""
if self.editor.is_python():
self.threadmanager.add_thread(codeanalysis.find_tasks,
self.todo_finished,
self.get_source_code(), self)
def todo_finished(self, results):
"""Code analysis thread has finished"""
self.set_todo_results(results)
self.emit(SIGNAL('todo_results_changed()'))
def set_todo_results(self, results):
"""Set TODO results and update markers in editor"""
self.todo_results = results
self.editor.process_todo(results)
def cleanup_todo_results(self):
"""Clean-up TODO finder results"""
self.todo_results = []
def breakpoints_changed(self):
"""Breakpoint list has changed"""
breakpoints = self.editor.get_breakpoints()
if self.editor.breakpoints != breakpoints:
self.editor.breakpoints = breakpoints
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
self.filename, repr(breakpoints))
class EditorStack(QWidget):
def __init__(self, parent, actions):
QWidget.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.threadmanager = ThreadManager(self)
self.newwindow_action = None
self.horsplit_action = None
self.versplit_action = None
self.close_action = None
self.__get_split_actions()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
self.menu = None
self.filelist_dlg = None
# self.filelist_btn = None
# self.previous_btn = None
# self.next_btn = None
self.tabs = None
self.stack_history = []
self.setup_editorstack(parent, layout)
self.find_widget = None
self.data = []
filelist_action = create_action(self, _("File list management"),
icon=get_icon('filelist.png'),
triggered=self.open_filelistdialog)
copy_to_cb_action = create_action(self, _("Copy path to clipboard"),
icon="editcopy.png",
triggered=lambda:
QApplication.clipboard().setText(self.get_current_filename()))
self.menu_actions = actions+[None, filelist_action, copy_to_cb_action]
self.outlineexplorer = None
self.inspector = None
self.unregister_callback = None
self.is_closable = False
self.new_action = None
self.open_action = None
self.save_action = None
self.revert_action = None
self.tempfile_path = None
self.title = _("Editor")
self.pyflakes_enabled = True
self.pep8_enabled = False
self.todolist_enabled = True
self.realtime_analysis_enabled = False
self.is_analysis_done = False
self.linenumbers_enabled = True
self.blanks_enabled = False
self.edgeline_enabled = True
self.edgeline_column = 79
self.codecompletion_auto_enabled = True
self.codecompletion_case_enabled = False
self.codecompletion_enter_enabled = False
self.calltips_enabled = True
self.go_to_definition_enabled = True
self.close_parentheses_enabled = True
self.close_quotes_enabled = True
self.add_colons_enabled = True
self.auto_unindent_enabled = True
self.indent_chars = " "*4
self.tab_stop_width = 40
self.inspector_enabled = False
self.default_font = None
self.wrap_enabled = False
self.tabmode_enabled = False
self.intelligent_backspace_enabled = True
self.highlight_current_line_enabled = False
self.highlight_current_cell_enabled = False
self.occurence_highlighting_enabled = True
self.checkeolchars_enabled = True
self.always_remove_trailing_spaces = False
self.fullpath_sorting_enabled = None
self.focus_to_editor = True
self.set_fullpath_sorting_enabled(False)
ccs = 'Spyder'
if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES:
ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0]
self.color_scheme = ccs
self.introspector = introspection.PluginManager(self)
self.introspector.send_to_inspector.connect(self.send_to_inspector)
self.introspector.edit_goto.connect(
lambda fname, lineno, name:
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
fname, lineno, name))
self.__file_status_flag = False
# Real-time code analysis
self.analysis_timer = QTimer(self)
self.analysis_timer.setSingleShot(True)
self.analysis_timer.setInterval(2000)
self.connect(self.analysis_timer, SIGNAL("timeout()"),
self.analyze_script)
# Accepting drops
self.setAcceptDrops(True)
# Local shortcuts
self.shortcuts = self.create_shortcuts()
def create_shortcuts(self):
"""Create local shortcuts"""
# Configurable shortcuts
inspect = create_shortcut(self.inspect_current_object, context='Editor',
name='Inspect current object', parent=self)
breakpoint = create_shortcut(self.set_or_clear_breakpoint,
context='Editor', name='Breakpoint',
parent=self)
cbreakpoint = create_shortcut(self.set_or_edit_conditional_breakpoint,
context='Editor',
name='Conditional breakpoint',
parent=self)
gotoline = create_shortcut(self.go_to_line, context='Editor',
name='Go to line', parent=self)
filelist = create_shortcut(self.open_filelistdialog, context='Editor',
name='File list management', parent=self)
tab = create_shortcut(self.go_to_previous_file, context='Editor',
name='Go to previous file', parent=self)
tabshift = create_shortcut(self.go_to_next_file, context='Editor',
name='Go to next file', parent=self)
# Fixed shortcuts
new_shortcut(QKeySequence.ZoomIn, self,
lambda: self.emit(SIGNAL('zoom_in()')))
new_shortcut("Ctrl+=", self, lambda: self.emit(SIGNAL('zoom_in()')))
new_shortcut(QKeySequence.ZoomOut, self,
lambda: self.emit(SIGNAL('zoom_out()')))
new_shortcut("Ctrl+0", self, lambda: self.emit(SIGNAL('zoom_reset()')))
new_shortcut("Ctrl+W", self, self.close_file)
new_shortcut("Ctrl+F4", self, self.close_file)
# Return configurable ones
return [inspect, breakpoint, cbreakpoint, gotoline, filelist, tab,
tabshift]
def get_shortcut_data(self):
"""
Returns shortcut data, a list of tuples (shortcut, text, default)
shortcut (QShortcut or QAction instance)
text (string): action/shortcut description
default (string): default key sequence
"""
return [sc.data for sc in self.shortcuts]
def setup_editorstack(self, parent, layout):
"""Setup editorstack's layout"""
menu_btn = create_toolbutton(self, icon=get_icon("tooloptions.png"),
tip=_("Options"))
self.menu = QMenu(self)
menu_btn.setMenu(self.menu)
menu_btn.setPopupMode(menu_btn.InstantPopup)
self.connect(self.menu, SIGNAL("aboutToShow()"), self.__setup_menu)
# self.filelist_btn = create_toolbutton(self,
# icon=get_icon('filelist.png'),
# tip=_("File list management"),
# triggered=self.open_filelistdialog)
#
# self.previous_btn = create_toolbutton(self,
# icon=get_icon('previous.png'),
# tip=_("Previous file"),
# triggered=self.go_to_previous_file)
#
# self.next_btn = create_toolbutton(self,
# icon=get_icon('next.png'),
# tip=_("Next file"),
# triggered=self.go_to_next_file)
# Optional tabs
# corner_widgets = {Qt.TopRightCorner: [self.previous_btn,
# self.filelist_btn, self.next_btn,
# 5, menu_btn]}
corner_widgets = {Qt.TopRightCorner: [menu_btn]}
self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True,
corner_widgets=corner_widgets)
self.tabs.tabBar().setObjectName('plugin-tab')
self.tabs.set_close_function(self.close_file)
if hasattr(self.tabs, 'setDocumentMode') \
and not sys.platform == 'darwin':
# Don't set document mode to true on OSX because it generates
# a crash when the editor is detached from the main window
# Fixes Issue 561
self.tabs.setDocumentMode(True)
self.connect(self.tabs, SIGNAL('currentChanged(int)'),
self.current_changed)
if sys.platform == 'darwin':
tab_container = QWidget()
tab_container.setObjectName('tab-container')
tab_layout = QHBoxLayout(tab_container)
tab_layout.setContentsMargins(0, 0, 0, 0)
tab_layout.addWidget(self.tabs)
layout.addWidget(tab_container)
else:
layout.addWidget(self.tabs)
def add_corner_widgets_to_tabbar(self, widgets):
self.tabs.add_corner_widgets(widgets)
def closeEvent(self, event):
self.threadmanager.close_all_threads()
self.disconnect(self.analysis_timer, SIGNAL("timeout()"),
self.analyze_script)
QWidget.closeEvent(self, event)
if is_pyqt46:
self.emit(SIGNAL('destroyed()'))
def clone_editor_from(self, other_finfo, set_current):
fname = other_finfo.filename
enc = other_finfo.encoding
new = other_finfo.newly_created
finfo = self.create_new_editor(fname, enc, "",
set_current=set_current, new=new,
cloned_from=other_finfo.editor)
finfo.set_analysis_results(other_finfo.analysis_results)
finfo.set_todo_results(other_finfo.todo_results)
return finfo.editor
def clone_from(self, other):
"""Clone EditorStack from other instance"""
for other_finfo in other.data:
self.clone_editor_from(other_finfo, set_current=True)
self.set_stack_index(other.get_stack_index())
def open_filelistdialog(self):
"""Open file list management dialog box"""
self.filelist_dlg = dlg = FileListDialog(self, self.tabs,
self.fullpath_sorting_enabled)
self.connect(dlg, SIGNAL("edit_file(int)"), self.set_stack_index)
self.connect(dlg, SIGNAL("close_file(int)"), self.close_file)
dlg.synchronize(self.get_stack_index())
dlg.exec_()
self.filelist_dlg = None
def update_filelistdialog(self):
"""Synchronize file list dialog box with editor widget tabs"""
if self.filelist_dlg is not None:
self.filelist_dlg.synchronize(self.get_stack_index())
def go_to_line(self):
"""Go to line dialog"""
if self.data:
self.get_current_editor().exec_gotolinedialog()
def set_or_clear_breakpoint(self):
"""Set/clear breakpoint"""
if self.data:
editor = self.get_current_editor()
editor.add_remove_breakpoint()
def set_or_edit_conditional_breakpoint(self):
"""Set conditional breakpoint"""
if self.data:
editor = self.get_current_editor()
editor.add_remove_breakpoint(edit_condition=True)
def inspect_current_object(self):
"""Inspect current object in Object Inspector"""
if self.introspector:
editor = self.get_current_editor()
position = editor.get_position('cursor')
self.inspector.switch_to_editor_source()
self.introspector.show_object_info(position, auto=False)
else:
text = self.get_current_editor().get_current_object()
if text:
self.send_to_inspector(text, force=True)
#------ Editor Widget Settings
def set_closable(self, state):
"""Parent widget must handle the closable state"""
self.is_closable = state
def set_io_actions(self, new_action, open_action,
save_action, revert_action):
self.new_action = new_action
self.open_action = open_action
self.save_action = save_action
self.revert_action = revert_action
def set_find_widget(self, find_widget):
self.find_widget = find_widget
def set_outlineexplorer(self, outlineexplorer):
self.outlineexplorer = outlineexplorer
self.connect(self.outlineexplorer,
SIGNAL("outlineexplorer_is_visible()"),
self._refresh_outlineexplorer)
def initialize_outlineexplorer(self):
"""This method is called separately from 'set_oulineexplorer' to avoid
doing unnecessary updates when there are multiple editor windows"""
for index in range(self.get_stack_count()):
if index != self.get_stack_index():
self._refresh_outlineexplorer(index=index)
def add_outlineexplorer_button(self, editor_plugin):
oe_btn = create_toolbutton(editor_plugin)
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
self.add_corner_widgets_to_tabbar([5, oe_btn])
def set_inspector(self, inspector):
self.inspector = inspector
def set_tempfile_path(self, path):
self.tempfile_path = path
def set_title(self, text):
self.title = text
def __update_editor_margins(self, editor):
editor.setup_margins(linenumbers=self.linenumbers_enabled,
markers=self.has_markers())
def __codeanalysis_settings_changed(self, current_finfo):
if self.data:
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
for finfo in self.data:
self.__update_editor_margins(finfo.editor)
finfo.cleanup_analysis_results()
if (run_pyflakes or run_pep8) and current_finfo is not None:
if current_finfo is not finfo:
finfo.run_code_analysis(run_pyflakes, run_pep8)
def set_pyflakes_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'code_analysis/pyflakes')
self.pyflakes_enabled = state
self.__codeanalysis_settings_changed(current_finfo)
def set_pep8_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'code_analysis/pep8')
self.pep8_enabled = state
self.__codeanalysis_settings_changed(current_finfo)
def has_markers(self):
"""Return True if this editorstack has a marker margin for TODOs or
code analysis"""
return self.todolist_enabled or self.pyflakes_enabled\
or self.pep8_enabled
def set_todolist_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'todo_list')
self.todolist_enabled = state
if self.data:
for finfo in self.data:
self.__update_editor_margins(finfo.editor)
finfo.cleanup_todo_results()
if state and current_finfo is not None:
if current_finfo is not finfo:
finfo.run_todo_finder()
def set_realtime_analysis_enabled(self, state):
self.realtime_analysis_enabled = state
def set_realtime_analysis_timeout(self, timeout):
self.analysis_timer.setInterval(timeout)
def set_linenumbers_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'line_numbers')
self.linenumbers_enabled = state
if self.data:
for finfo in self.data:
self.__update_editor_margins(finfo.editor)
def set_blanks_enabled(self, state):
self.blanks_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_blanks_enabled(state)
def set_edgeline_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'edge_line')
self.edgeline_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_edge_line_enabled(state)
def set_edgeline_column(self, column):
# CONF.get(self.CONF_SECTION, 'edge_line_column')
self.edgeline_column = column
if self.data:
for finfo in self.data:
finfo.editor.set_edge_line_column(column)
def set_codecompletion_auto_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'codecompletion_auto')
self.codecompletion_auto_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_codecompletion_auto(state)
def set_codecompletion_case_enabled(self, state):
self.codecompletion_case_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_codecompletion_case(state)
def set_codecompletion_enter_enabled(self, state):
self.codecompletion_enter_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_codecompletion_enter(state)
def set_calltips_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'calltips')
self.calltips_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_calltips(state)
def set_go_to_definition_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'go_to_definition')
self.go_to_definition_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_go_to_definition_enabled(state)
def set_close_parentheses_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'close_parentheses')
self.close_parentheses_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_close_parentheses_enabled(state)
def set_close_quotes_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'close_quotes')
self.close_quotes_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_close_quotes_enabled(state)
def set_add_colons_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'add_colons')
self.add_colons_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_add_colons_enabled(state)
def set_auto_unindent_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'auto_unindent')
self.auto_unindent_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_auto_unindent_enabled(state)
def set_indent_chars(self, indent_chars):
# CONF.get(self.CONF_SECTION, 'indent_chars')
indent_chars = indent_chars[1:-1] # removing the leading/ending '*'
self.indent_chars = indent_chars
if self.data:
for finfo in self.data:
finfo.editor.set_indent_chars(indent_chars)
def set_tab_stop_width(self, tab_stop_width):
# CONF.get(self.CONF_SECTION, 'tab_stop_width')
self.tab_stop_width = tab_stop_width
if self.data:
for finfo in self.data:
finfo.editor.setTabStopWidth(tab_stop_width)
def set_inspector_enabled(self, state):
self.inspector_enabled = state
def set_default_font(self, font, color_scheme=None):
# get_font(self.CONF_SECTION)
self.default_font = font
if color_scheme is not None:
self.color_scheme = color_scheme
if self.data:
for finfo in self.data:
finfo.editor.set_font(font, color_scheme)
def set_color_scheme(self, color_scheme):
self.color_scheme = color_scheme
if self.data:
for finfo in self.data:
finfo.editor.set_color_scheme(color_scheme)
def set_wrap_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'wrap')
self.wrap_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_wrap_mode(state)
def set_tabmode_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'tab_always_indent')
self.tabmode_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_tab_mode(state)
def set_intelligent_backspace_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'intelligent_backspace')
self.intelligent_backspace_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_intelligent_backspace(state)
def set_occurence_highlighting_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'occurence_highlighting')
self.occurence_highlighting_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_occurence_highlighting(state)
def set_occurence_highlighting_timeout(self, timeout):
# CONF.get(self.CONF_SECTION, 'occurence_highlighting/timeout')
self.occurence_highlighting_timeout = timeout
if self.data:
for finfo in self.data:
finfo.editor.set_occurence_timeout(timeout)
def set_highlight_current_line_enabled(self, state):
self.highlight_current_line_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_highlight_current_line(state)
def set_highlight_current_cell_enabled(self, state):
self.highlight_current_cell_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_highlight_current_cell(state)
def set_checkeolchars_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'check_eol_chars')
self.checkeolchars_enabled = state
def set_fullpath_sorting_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'fullpath_sorting')
self.fullpath_sorting_enabled = state
if self.data:
finfo = self.data[self.get_stack_index()]
self.data.sort(key=self.__get_sorting_func())
new_index = self.data.index(finfo)
self.__repopulate_stack()
self.set_stack_index(new_index)
def set_always_remove_trailing_spaces(self, state):
# CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces')
self.always_remove_trailing_spaces = state
def set_focus_to_editor(self, state):
self.focus_to_editor = state
#------ Stacked widget management
def get_stack_index(self):
return self.tabs.currentIndex()
def get_current_finfo(self):
if self.data:
return self.data[self.get_stack_index()]
def get_current_editor(self):
return self.tabs.currentWidget()
def get_stack_count(self):
return self.tabs.count()
def set_stack_index(self, index):
self.tabs.setCurrentIndex(index)
def set_tabbar_visible(self, state):
self.tabs.tabBar().setVisible(state)
def remove_from_data(self, index):
self.tabs.blockSignals(True)
self.tabs.removeTab(index)
self.data.pop(index)
self.tabs.blockSignals(False)
self.update_actions()
self.update_filelistdialog()
def __modified_readonly_title(self, title, is_modified, is_readonly):
if is_modified is not None and is_modified:
title += "*"
if is_readonly is not None and is_readonly:
title = "(%s)" % title
return title
def get_tab_text(self, filename, is_modified=None, is_readonly=None):
"""Return tab title"""
return self.__modified_readonly_title(osp.basename(filename),
is_modified, is_readonly)
def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
"""Return tab menu title"""
if self.fullpath_sorting_enabled:
text = filename
else:
text = u("%s — %s")
text = self.__modified_readonly_title(text,
is_modified, is_readonly)
if self.tempfile_path is not None\
and filename == encoding.to_unicode_from_fs(self.tempfile_path):
temp_file_str = to_text_string(_("Temporary file"))
if self.fullpath_sorting_enabled:
return "%s (%s)" % (text, temp_file_str)
else:
return text % (temp_file_str, self.tempfile_path)
else:
if self.fullpath_sorting_enabled:
return text
else:
return text % (osp.basename(filename), osp.dirname(filename))
def __get_sorting_func(self):
if self.fullpath_sorting_enabled:
return lambda item: osp.join(osp.dirname(item.filename),
'_'+osp.basename(item.filename))
else:
return lambda item: osp.basename(item.filename)
def add_to_data(self, finfo, set_current):
self.data.append(finfo)
self.data.sort(key=self.__get_sorting_func())
index = self.data.index(finfo)
fname, editor = finfo.filename, finfo.editor
self.tabs.insertTab(index, editor, get_filetype_icon(fname),
self.get_tab_text(fname))
self.set_stack_title(index, False)
if set_current:
self.set_stack_index(index)
self.current_changed(index)
self.update_actions()
self.update_filelistdialog()
def __repopulate_stack(self):
self.tabs.blockSignals(True)
self.tabs.clear()
for finfo in self.data:
icon = get_filetype_icon(finfo.filename)
if finfo.newly_created:
is_modified = True
else:
is_modified = None
tab_text = self.get_tab_text(finfo.filename, is_modified)
tab_tip = self.get_tab_tip(finfo.filename)
index = self.tabs.addTab(finfo.editor, icon, tab_text)
self.tabs.setTabToolTip(index, tab_tip)
self.tabs.blockSignals(False)
self.update_filelistdialog()
def rename_in_data(self, index, new_filename):
finfo = self.data[index]
if osp.splitext(finfo.filename)[1] != osp.splitext(new_filename)[1]:
# File type has changed!
txt = to_text_string(finfo.editor.get_text_with_eol())
language = get_file_language(new_filename, txt)
finfo.editor.set_language(language)
set_new_index = index == self.get_stack_index()
current_fname = self.get_current_filename()
finfo.filename = new_filename
self.data.sort(key=self.__get_sorting_func())
new_index = self.data.index(finfo)
self.__repopulate_stack()
if set_new_index:
self.set_stack_index(new_index)
else:
# Fixes Issue 1287
self.set_current_filename(current_fname)
if self.outlineexplorer is not None:
self.outlineexplorer.file_renamed(finfo.editor, finfo.filename)
return new_index
def set_stack_title(self, index, is_modified):
finfo = self.data[index]
fname = finfo.filename
is_modified = (is_modified or finfo.newly_created) and not finfo.default
is_readonly = finfo.editor.isReadOnly()
tab_text = self.get_tab_text(fname, is_modified, is_readonly)
tab_tip = self.get_tab_tip(fname, is_modified, is_readonly)
self.tabs.setTabText(index, tab_text)
self.tabs.setTabToolTip(index, tab_tip)
#------ Context menu
def __setup_menu(self):
"""Setup tab context menu before showing it"""
self.menu.clear()
if self.data:
actions = self.menu_actions
else:
actions = (self.new_action, self.open_action)
self.setFocus() # --> Editor.__get_focus_editortabwidget
add_actions(self.menu, list(actions)+self.__get_split_actions())
self.close_action.setEnabled(self.is_closable)
#------ Hor/Ver splitting
def __get_split_actions(self):
# New window
self.newwindow_action = create_action(self, _("New window"),
icon="newwindow.png", tip=_("Create a new editor window"),
triggered=lambda: self.emit(SIGNAL("create_new_window()")))
# Splitting
self.versplit_action = create_action(self, _("Split vertically"),
icon="versplit.png",
tip=_("Split vertically this editor window"),
triggered=lambda: self.emit(SIGNAL("split_vertically()")))
self.horsplit_action = create_action(self, _("Split horizontally"),
icon="horsplit.png",
tip=_("Split horizontally this editor window"),
triggered=lambda: self.emit(SIGNAL("split_horizontally()")))
self.close_action = create_action(self, _("Close this panel"),
icon="close_panel.png", triggered=self.close)
return [None, self.newwindow_action, None,
self.versplit_action, self.horsplit_action, self.close_action]
def reset_orientation(self):
self.horsplit_action.setEnabled(True)
self.versplit_action.setEnabled(True)
def set_orientation(self, orientation):
self.horsplit_action.setEnabled(orientation == Qt.Horizontal)
self.versplit_action.setEnabled(orientation == Qt.Vertical)
def update_actions(self):
state = self.get_stack_count() > 0
self.horsplit_action.setEnabled(state)
self.versplit_action.setEnabled(state)
#------ Accessors
def get_current_filename(self):
if self.data:
return self.data[self.get_stack_index()].filename
def has_filename(self, filename):
fixpath = lambda path: osp.normcase(osp.realpath(path))
for index, finfo in enumerate(self.data):
if fixpath(filename) == fixpath(finfo.filename):
return index
def set_current_filename(self, filename):
"""Set current filename and return the associated editor instance"""
index = self.has_filename(filename)
if index is not None:
self.set_stack_index(index)
editor = self.data[index].editor
editor.setFocus()
return editor
def is_file_opened(self, filename=None):
if filename is None:
# Is there any file opened?
return len(self.data) > 0
else:
return self.has_filename(filename)
#------ Close file, tabwidget...
def close_file(self, index=None, force=False):
"""Close file (index=None -> close current file)
Keep current file index unchanged (if current file
that is being closed)"""
current_index = self.get_stack_index()
count = self.get_stack_count()
if index is None:
if count > 0:
index = current_index
else:
self.find_widget.set_editor(None)
return
new_index = None
if count > 1:
if current_index == index:
new_index = self._get_previous_file_index()
else:
new_index = current_index
is_ok = force or self.save_if_changed(cancelable=True, index=index)
if is_ok:
finfo = self.data[index]
self.threadmanager.close_threads(finfo)
# Removing editor reference from outline explorer settings:
if self.outlineexplorer is not None:
self.outlineexplorer.remove_editor(finfo.editor)
self.remove_from_data(index)
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms
# (see Issue 1094, Issue 1098)
self.emit(SIGNAL('close_file(QString,int)'), str(id(self)), index)
if not self.data and self.is_closable:
# editortabwidget is empty: removing it
# (if it's not the first editortabwidget)
self.close()
self.emit(SIGNAL('opened_files_list_changed()'))
self.emit(SIGNAL('update_code_analysis_actions()'))
self._refresh_outlineexplorer()
self.emit(SIGNAL('refresh_file_dependent_actions()'))
self.emit(SIGNAL('update_plugin_title()'))
editor = self.get_current_editor()
if editor:
editor.setFocus()
if new_index is not None:
if index < new_index:
new_index -= 1
self.set_stack_index(new_index)
if self.get_stack_count() == 0:
self.emit(SIGNAL('sig_new_file()'))
return False
return is_ok
def close_all_files(self):
"""Close all opened scripts"""
while self.close_file():
pass
#------ Save
def save_if_changed(self, cancelable=False, index=None):
"""Ask user to save file if modified"""
if index is None:
indexes = list(range(self.get_stack_count()))
else:
indexes = [index]
buttons = QMessageBox.Yes | QMessageBox.No
if cancelable:
buttons |= QMessageBox.Cancel
unsaved_nb = 0
for index in indexes:
if self.data[index].editor.document().isModified():
unsaved_nb += 1
if not unsaved_nb:
# No file to save
return True
if unsaved_nb > 1:
buttons |= QMessageBox.YesAll | QMessageBox.NoAll
yes_all = False
for index in indexes:
self.set_stack_index(index)
finfo = self.data[index]
if finfo.filename == self.tempfile_path or yes_all:
if not self.save():
return False
elif finfo.editor.document().isModified():
answer = QMessageBox.question(self, self.title,
_("%s has been modified."
"
Do you want to save changes?"
) % osp.basename(finfo.filename),
buttons)
if answer == QMessageBox.Yes:
if not self.save():
return False
elif answer == QMessageBox.YesAll:
if not self.save():
return False
yes_all = True
elif answer == QMessageBox.NoAll:
return True
elif answer == QMessageBox.Cancel:
return False
return True
def save(self, index=None, force=False):
"""Save file"""
if index is None:
# Save the currently edited file
if not self.get_stack_count():
return
index = self.get_stack_index()
finfo = self.data[index]
if not finfo.editor.document().isModified() and not force:
return True
if not osp.isfile(finfo.filename) and not force:
# File has not been saved yet
return self.save_as(index=index)
if self.always_remove_trailing_spaces:
self.remove_trailing_spaces(index)
txt = to_text_string(finfo.editor.get_text_with_eol())
try:
finfo.encoding = encoding.write(txt, finfo.filename,
finfo.encoding)
finfo.newly_created = False
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms
# (see Issue 1094, Issue 1098)
self.emit(SIGNAL('file_saved(QString,int,QString)'),
str(id(self)), index, finfo.filename)
finfo.editor.document().setModified(False)
self.modification_changed(index=index)
self.analyze_script(index)
self.introspector.validate()
#XXX CodeEditor-only: re-scan the whole text to rebuild outline
# explorer data from scratch (could be optimized because
# rehighlighting text means searching for all syntax coloring
# patterns instead of only searching for class/def patterns which
# would be sufficient for outline explorer data.
finfo.editor.rehighlight()
self._refresh_outlineexplorer(index)
return True
except EnvironmentError as error:
QMessageBox.critical(self, _("Save"),
_("Unable to save script '%s'"
"
Error message:
%s"
) % (osp.basename(finfo.filename),
str(error)))
return False
def file_saved_in_other_editorstack(self, index, filename):
"""
File was just saved in another editorstack, let's synchronize!
This avoid file to be automatically reloaded
Filename is passed in case file was just saved as another name
"""
finfo = self.data[index]
finfo.newly_created = False
finfo.filename = to_text_string(filename)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
def select_savename(self, original_filename):
selectedfilter = get_filter(EDIT_FILETYPES,
osp.splitext(original_filename)[1])
self.emit(SIGNAL('redirect_stdio(bool)'), False)
filename, _selfilter = getsavefilename(self, _("Save Python script"),
original_filename, EDIT_FILTERS, selectedfilter)
self.emit(SIGNAL('redirect_stdio(bool)'), True)
if filename:
return osp.normpath(filename)
def save_as(self, index=None):
"""Save file as..."""
if index is None:
# Save the currently edited file
index = self.get_stack_index()
finfo = self.data[index]
filename = self.select_savename(finfo.filename)
if filename:
ao_index = self.has_filename(filename)
# Note: ao_index == index --> saving an untitled file
if ao_index and ao_index != index:
if not self.close_file(ao_index):
return
if ao_index < index:
index -= 1
new_index = self.rename_in_data(index, new_filename=filename)
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms
# (see Issue 1094, Issue 1098)
self.emit(SIGNAL('file_renamed_in_data(QString,int,QString)'),
str(id(self)), index, filename)
ok = self.save(index=new_index, force=True)
self.refresh(new_index)
self.set_stack_index(new_index)
return ok
else:
return False
def save_all(self):
"""Save all opened files"""
folders = set()
for index in range(self.get_stack_count()):
if self.data[index].editor.document().isModified():
folders.add(osp.dirname(self.data[index].filename))
self.save(index)
#------ Update UI
def start_stop_analysis_timer(self):
self.is_analysis_done = False
if self.realtime_analysis_enabled:
self.analysis_timer.stop()
self.analysis_timer.start()
def analyze_script(self, index=None):
"""Analyze current script with pyflakes + find todos"""
if self.is_analysis_done:
return
if index is None:
index = self.get_stack_index()
if self.data:
finfo = self.data[index]
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
if run_pyflakes or run_pep8:
finfo.run_code_analysis(run_pyflakes, run_pep8)
if self.todolist_enabled:
finfo.run_todo_finder()
self.is_analysis_done = True
def set_analysis_results(self, index, analysis_results):
"""Synchronize analysis results between editorstacks"""
self.data[index].set_analysis_results(analysis_results)
def get_analysis_results(self):
if self.data:
return self.data[self.get_stack_index()].analysis_results
def set_todo_results(self, index, todo_results):
"""Synchronize todo results between editorstacks"""
self.data[index].set_todo_results(todo_results)
def get_todo_results(self):
if self.data:
return self.data[self.get_stack_index()].todo_results
def current_changed(self, index):
"""Stack index has changed"""
# count = self.get_stack_count()
# for btn in (self.filelist_btn, self.previous_btn, self.next_btn):
# btn.setEnabled(count > 1)
editor = self.get_current_editor()
if index != -1:
editor.setFocus()
if DEBUG_EDITOR:
print("setfocusto:", editor, file=STDOUT)
else:
self.emit(SIGNAL('reset_statusbar()'))
self.emit(SIGNAL('opened_files_list_changed()'))
# Index history management
id_list = [id(self.tabs.widget(_i))
for _i in range(self.tabs.count())]
for _id in self.stack_history[:]:
if _id not in id_list:
self.stack_history.pop(self.stack_history.index(_id))
current_id = id(self.tabs.widget(index))
while current_id in self.stack_history:
self.stack_history.pop(self.stack_history.index(current_id))
self.stack_history.append(current_id)
if DEBUG_EDITOR:
print("current_changed:", index, self.data[index].editor, end=' ', file=STDOUT)
print(self.data[index].editor.get_document_id(), file=STDOUT)
self.emit(SIGNAL('update_plugin_title()'))
if editor is not None:
self.emit(SIGNAL('current_file_changed(QString,int)'),
self.data[index].filename, editor.get_position('cursor'))
def _get_previous_file_index(self):
if len(self.stack_history) > 1:
last = len(self.stack_history)-1
w_id = self.stack_history.pop(last)
self.stack_history.insert(0, w_id)
last_id = self.stack_history[last]
for _i in range(self.tabs.count()):
if id(self.tabs.widget(_i)) == last_id:
return _i
def go_to_previous_file(self):
"""Ctrl+Tab"""
prev_index = self._get_previous_file_index()
if prev_index is not None:
self.set_stack_index(prev_index)
elif len(self.stack_history) == 0 and self.get_stack_count():
self.stack_history = [id(self.tabs.currentWidget())]
def go_to_next_file(self):
"""Ctrl+Shift+Tab"""
if len(self.stack_history) > 1:
last = len(self.stack_history)-1
w_id = self.stack_history.pop(0)
self.stack_history.append(w_id)
last_id = self.stack_history[last]
for _i in range(self.tabs.count()):
if id(self.tabs.widget(_i)) == last_id:
self.set_stack_index(_i)
break
elif len(self.stack_history) == 0 and self.get_stack_count():
self.stack_history = [id(self.tabs.currentWidget())]
def focus_changed(self):
"""Editor focus has changed"""
fwidget = QApplication.focusWidget()
for finfo in self.data:
if fwidget is finfo.editor:
self.refresh()
self.emit(SIGNAL("editor_focus_changed()"))
def _refresh_outlineexplorer(self, index=None, update=True, clear=False):
"""Refresh outline explorer panel"""
oe = self.outlineexplorer
if oe is None:
return
if index is None:
index = self.get_stack_index()
enable = False
if self.data:
finfo = self.data[index]
if finfo.editor.is_python():
enable = True
oe.setEnabled(True)
oe.set_current_editor(finfo.editor, finfo.filename,
update=update, clear=clear)
if not enable:
oe.setEnabled(False)
def __refresh_statusbar(self, index):
"""Refreshing statusbar widgets"""
finfo = self.data[index]
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
# Refresh cursor position status:
line, index = finfo.editor.get_cursor_line_column()
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
line, index)
def __refresh_readonly(self, index):
finfo = self.data[index]
read_only = not QFileInfo(finfo.filename).isWritable()
if not osp.isfile(finfo.filename):
# This is an 'untitledX.py' file (newly created)
read_only = False
finfo.editor.setReadOnly(read_only)
self.emit(SIGNAL('readonly_changed(bool)'), read_only)
def __check_file_status(self, index):
"""Check if file has been changed in any way outside Spyder:
1. removed, moved or renamed outside Spyder
2. modified outside Spyder"""
if self.__file_status_flag:
# Avoid infinite loop: when the QMessageBox.question pops, it
# gets focus and then give it back to the CodeEditor instance,
# triggering a refresh cycle which calls this method
return
self.__file_status_flag = True
finfo = self.data[index]
name = osp.basename(finfo.filename)
if finfo.newly_created:
# File was just created (not yet saved): do nothing
# (do not return because of the clean-up at the end of the method)
pass
elif not osp.isfile(finfo.filename):
# File doesn't exist (removed, moved or offline):
answer = QMessageBox.warning(self, self.title,
_("%s is unavailable "
"(this file may have been removed, moved "
"or renamed outside Spyder)."
"
Do you want to close it?") % name,
QMessageBox.Yes | QMessageBox.No)
if answer == QMessageBox.Yes:
self.close_file(index)
else:
finfo.newly_created = True
finfo.editor.document().setModified(True)
self.modification_changed(index=index)
else:
# Else, testing if it has been modified elsewhere:
lastm = QFileInfo(finfo.filename).lastModified()
if to_text_string(lastm.toString()) \
!= to_text_string(finfo.lastmodified.toString()):
if finfo.editor.document().isModified():
answer = QMessageBox.question(self,
self.title,
_("%s has been modified outside Spyder."
"
Do you want to reload it and lose all "
"your changes?") % name,
QMessageBox.Yes | QMessageBox.No)
if answer == QMessageBox.Yes:
self.reload(index)
else:
finfo.lastmodified = lastm
else:
self.reload(index)
# Finally, resetting temporary flag:
self.__file_status_flag = False
def refresh(self, index=None):
"""Refresh tabwidget"""
if index is None:
index = self.get_stack_index()
# Set current editor
if self.get_stack_count():
index = self.get_stack_index()
finfo = self.data[index]
editor = finfo.editor
editor.setFocus()
self._refresh_outlineexplorer(index, update=False)
self.emit(SIGNAL('update_code_analysis_actions()'))
self.__refresh_statusbar(index)
self.__refresh_readonly(index)
self.__check_file_status(index)
self.emit(SIGNAL('update_plugin_title()'))
else:
editor = None
# Update the modification-state-dependent parameters
self.modification_changed()
# Update FindReplace binding
self.find_widget.set_editor(editor, refresh=False)
def modification_changed(self, state=None, index=None, editor_id=None):
"""
Current editor's modification state has changed
--> change tab title depending on new modification state
--> enable/disable save/save all actions
"""
if editor_id is not None:
for index, _finfo in enumerate(self.data):
if id(_finfo.editor) == editor_id:
break
# This must be done before refreshing save/save all actions:
# (otherwise Save/Save all actions will always be enabled)
self.emit(SIGNAL('opened_files_list_changed()'))
# --
if index is None:
index = self.get_stack_index()
if index == -1:
return
finfo = self.data[index]
if state is None:
state = finfo.editor.document().isModified()
self.set_stack_title(index, state)
# Toggle save/save all actions state
self.save_action.setEnabled(state)
self.emit(SIGNAL('refresh_save_all_action()'))
# Refreshing eol mode
eol_chars = finfo.editor.get_line_separator()
os_name = sourcecode.get_os_name_from_eol_chars(eol_chars)
self.emit(SIGNAL('refresh_eol_chars(QString)'), os_name)
#------ Load, reload
def reload(self, index):
"""Reload file from disk"""
finfo = self.data[index]
txt, finfo.encoding = encoding.read(finfo.filename)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
position = finfo.editor.get_position('cursor')
finfo.editor.set_text(txt)
finfo.editor.document().setModified(False)
finfo.editor.set_cursor_position(position)
self.introspector.validate()
#XXX CodeEditor-only: re-scan the whole text to rebuild outline
# explorer data from scratch (could be optimized because
# rehighlighting text means searching for all syntax coloring
# patterns instead of only searching for class/def patterns which
# would be sufficient for outline explorer data.
finfo.editor.rehighlight()
self._refresh_outlineexplorer(index)
def revert(self):
"""Revert file from disk"""
index = self.get_stack_index()
finfo = self.data[index]
filename = finfo.filename
if finfo.editor.document().isModified():
answer = QMessageBox.warning(self, self.title,
_("All changes to %s will be lost."
"
Do you want to revert file from disk?"
) % osp.basename(filename),
QMessageBox.Yes|QMessageBox.No)
if answer != QMessageBox.Yes:
return
self.reload(index)
def create_new_editor(self, fname, enc, txt, set_current, new=False,
cloned_from=None):
"""
Create a new editor instance
Returns finfo object (instead of editor as in previous releases)
"""
editor = codeeditor.CodeEditor(self)
introspector = self.introspector
self.connect(editor, SIGNAL("get_completions(bool)"),
introspector.get_completions)
self.connect(editor, SIGNAL("show_object_info(int)"),
introspector.show_object_info)
self.connect(editor, SIGNAL("go_to_definition(int)"),
introspector.go_to_definition)
finfo = FileInfo(fname, enc, editor, new, self.threadmanager,
self.introspector)
self.add_to_data(finfo, set_current)
self.connect(finfo, SIGNAL(
"send_to_inspector(QString,QString,QString,QString,bool)"),
self.send_to_inspector)
self.connect(finfo, SIGNAL('analysis_results_changed()'),
lambda: self.emit(SIGNAL('analysis_results_changed()')))
self.connect(finfo, SIGNAL('todo_results_changed()'),
lambda: self.emit(SIGNAL('todo_results_changed()')))
self.connect(finfo, SIGNAL("edit_goto(QString,int,QString)"),
lambda fname, lineno, name:
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
fname, lineno, name))
self.connect(finfo, SIGNAL("save_breakpoints(QString,QString)"),
lambda s1, s2:
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
s1, s2))
self.connect(editor, SIGNAL("run_selection()"), self.run_selection)
self.connect(editor, SIGNAL("run_cell()"), self.run_cell)
self.connect(editor, SIGNAL('run_cell_and_advance()'),
self.run_cell_and_advance)
editor.sig_new_file.connect(lambda s: self.parent().plugin.new(text=s))
language = get_file_language(fname, txt)
editor.setup_editor(
linenumbers=self.linenumbers_enabled,
show_blanks=self.blanks_enabled,
edge_line=self.edgeline_enabled,
edge_line_column=self.edgeline_column, language=language,
markers=self.has_markers(), font=self.default_font,
color_scheme=self.color_scheme,
wrap=self.wrap_enabled, tab_mode=self.tabmode_enabled,
intelligent_backspace=self.intelligent_backspace_enabled,
highlight_current_line=self.highlight_current_line_enabled,
highlight_current_cell=self.highlight_current_cell_enabled,
occurence_highlighting=self.occurence_highlighting_enabled,
occurence_timeout=self.occurence_highlighting_timeout,
codecompletion_auto=self.codecompletion_auto_enabled,
codecompletion_case=self.codecompletion_case_enabled,
codecompletion_enter=self.codecompletion_enter_enabled,
calltips=self.calltips_enabled,
go_to_definition=self.go_to_definition_enabled,
close_parentheses=self.close_parentheses_enabled,
close_quotes=self.close_quotes_enabled,
add_colons=self.add_colons_enabled,
auto_unindent=self.auto_unindent_enabled,
indent_chars=self.indent_chars,
tab_stop_width=self.tab_stop_width,
cloned_from=cloned_from)
if cloned_from is None:
editor.set_text(txt)
editor.document().setModified(False)
self.connect(finfo, SIGNAL('text_changed_at(QString,int)'),
lambda fname, position:
self.emit(SIGNAL('text_changed_at(QString,int)'),
fname, position))
self.connect(editor, SIGNAL('cursorPositionChanged(int,int)'),
self.editor_cursor_position_changed)
self.connect(editor, SIGNAL('textChanged()'),
self.start_stop_analysis_timer)
self.connect(editor, SIGNAL('modificationChanged(bool)'),
lambda state: self.modification_changed(state,
editor_id=id(editor)))
self.connect(editor, SIGNAL("focus_in()"), self.focus_changed)
self.connect(editor, SIGNAL('zoom_in()'),
lambda: self.emit(SIGNAL('zoom_in()')))
self.connect(editor, SIGNAL('zoom_out()'),
lambda: self.emit(SIGNAL('zoom_out()')))
self.connect(editor, SIGNAL('zoom_reset()'),
lambda: self.emit(SIGNAL('zoom_reset()')))
if self.outlineexplorer is not None:
# Removing editor reference from outline explorer settings:
self.connect(editor, SIGNAL("destroyed()"),
lambda obj=editor:
self.outlineexplorer.remove_editor(obj))
self.find_widget.set_editor(editor)
self.emit(SIGNAL('refresh_file_dependent_actions()'))
self.modification_changed(index=self.data.index(finfo))
return finfo
def editor_cursor_position_changed(self, line, index):
"""Cursor position of one of the editor in the stack has changed"""
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
line, index)
def send_to_inspector(self, qstr1, qstr2=None, qstr3=None,
qstr4=None, force=False):
"""qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text"""
if not force and not self.inspector_enabled:
return
if self.inspector is not None \
and (force or self.inspector.dockwidget.isVisible()):
# ObjectInspector widget exists and is visible
if qstr4 is None:
self.inspector.set_object_text(qstr1, ignore_unknown=True,
force_refresh=force)
else:
objtxt = to_text_string(qstr1)
name = objtxt.split('.')[-1]
argspec = to_text_string(qstr2)
note = to_text_string(qstr3)
docstring = to_text_string(qstr4)
doc = {'obj_text': objtxt, 'name': name, 'argspec': argspec,
'note': note, 'docstring': docstring}
self.inspector.set_editor_doc(doc, force_refresh=force)
editor = self.get_current_editor()
editor.setFocus()
def new(self, filename, encoding, text, default_content=False):
"""
Create new filename with *encoding* and *text*
"""
finfo = self.create_new_editor(filename, encoding, text,
set_current=False, new=True)
finfo.editor.set_cursor_position('eof')
finfo.editor.insert_text(os.linesep)
if default_content:
finfo.default = True
finfo.editor.document().setModified(False)
return finfo
def load(self, filename, set_current=True):
"""
Load filename, create an editor instance and return it
*Warning* This is loading file, creating editor but not executing
the source code analysis -- the analysis must be done by the editor
plugin (in case multiple editorstack instances are handled)
"""
filename = osp.abspath(to_text_string(filename))
self.emit(SIGNAL('starting_long_process(QString)'),
_("Loading %s...") % filename)
text, enc = encoding.read(filename)
finfo = self.create_new_editor(filename, enc, text, set_current)
index = self.data.index(finfo)
self._refresh_outlineexplorer(index, update=True)
self.emit(SIGNAL('ending_long_process(QString)'), "")
if self.isVisible() and self.checkeolchars_enabled \
and sourcecode.has_mixed_eol_chars(text):
name = osp.basename(filename)
QMessageBox.warning(self, self.title,
_("%s contains mixed end-of-line "
"characters.
Spyder will fix this "
"automatically.") % name,
QMessageBox.Ok)
self.set_os_eol_chars(index)
self.is_analysis_done = False
return finfo
def set_os_eol_chars(self, index=None):
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
eol_chars = sourcecode.get_eol_chars_from_os_name(os.name)
finfo.editor.set_eol_chars(eol_chars)
finfo.editor.document().setModified(True)
def remove_trailing_spaces(self, index=None):
"""Remove trailing spaces"""
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
finfo.editor.remove_trailing_spaces()
def fix_indentation(self, index=None):
"""Replace tab characters by spaces"""
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
finfo.editor.fix_indentation()
#------ Run
def run_selection(self):
"""Run selected text or current line in console"""
text = self.get_current_editor().get_selection_as_executable_code()
if not text:
line = self.get_current_editor().get_current_line()
text = line.lstrip()
self.emit(SIGNAL('exec_in_extconsole(QString,bool)'), text,
self.focus_to_editor)
def run_cell(self):
"""Run current cell"""
text = self.get_current_editor().get_cell_as_executable_code()
finfo = self.get_current_finfo()
if finfo.editor.is_python() and text:
self.emit(SIGNAL('exec_in_extconsole(QString,bool)'),
text, self.focus_to_editor)
def run_cell_and_advance(self):
"""Run current cell and advance to the next one"""
self.run_cell()
if self.focus_to_editor:
self.get_current_editor().go_to_next_cell()
else:
term = QApplication.focusWidget()
self.get_current_editor().go_to_next_cell()
term.setFocus()
#------ Drag and drop
def dragEnterEvent(self, event):
"""Reimplement Qt method
Inform Qt about the types of data that the widget accepts"""
source = event.mimeData()
# The second check is necessary on Windows, where source.hasUrls()
# can return True but source.urls() is []
if source.hasUrls() and source.urls():
if mimedata2url(source, extlist=EDIT_EXT):
event.acceptProposedAction()
else:
all_urls = mimedata2url(source)
text = [encoding.is_text_file(url) for url in all_urls]
if any(text):
event.acceptProposedAction()
else:
event.ignore()
elif source.hasText():
event.acceptProposedAction()
elif os.name == 'nt':
# This covers cases like dragging from compressed files,
# which can be opened by the Editor if they are plain
# text, but doesn't come with url info.
# Fixes Issue 2032
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
"""Reimplement Qt method
Unpack dropped data and handle it"""
source = event.mimeData()
if source.hasUrls():
files = mimedata2url(source)
files = [f for f in files if encoding.is_text_file(f)]
supported_files = mimedata2url(source, extlist=EDIT_EXT)
files = set(files or []) | set(supported_files or [])
for fname in files:
self.emit(SIGNAL('plugin_load(QString)'), fname)
elif source.hasText():
editor = self.get_current_editor()
if editor is not None:
editor.insert_text( source.text() )
event.acceptProposedAction()
class EditorSplitter(QSplitter):
def __init__(self, parent, plugin, menu_actions, first=False,
register_editorstack_cb=None, unregister_editorstack_cb=None):
QSplitter.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setChildrenCollapsible(False)
self.toolbar_list = None
self.menu_list = None
self.plugin = plugin
if register_editorstack_cb is None:
register_editorstack_cb = self.plugin.register_editorstack
self.register_editorstack_cb = register_editorstack_cb
if unregister_editorstack_cb is None:
unregister_editorstack_cb = self.plugin.unregister_editorstack
self.unregister_editorstack_cb = unregister_editorstack_cb
self.menu_actions = menu_actions
self.editorstack = EditorStack(self, menu_actions)
self.register_editorstack_cb(self.editorstack)
if not first:
self.plugin.clone_editorstack(editorstack=self.editorstack)
self.connect(self.editorstack, SIGNAL("destroyed()"),
lambda: self.editorstack_closed())
self.connect(self.editorstack, SIGNAL("split_vertically()"),
lambda: self.split(orientation=Qt.Vertical))
self.connect(self.editorstack, SIGNAL("split_horizontally()"),
lambda: self.split(orientation=Qt.Horizontal))
self.addWidget(self.editorstack)
def closeEvent(self, event):
QSplitter.closeEvent(self, event)
if is_pyqt46:
self.emit(SIGNAL('destroyed()'))
def __give_focus_to_remaining_editor(self):
focus_widget = self.plugin.get_focus_widget()
if focus_widget is not None:
focus_widget.setFocus()
def editorstack_closed(self):
if DEBUG_EDITOR:
print("method 'editorstack_closed':", file=STDOUT)
print(" self :", self, file=STDOUT)
# print >>STDOUT, " sender:", self.sender()
self.unregister_editorstack_cb(self.editorstack)
self.editorstack = None
try:
close_splitter = self.count() == 1
except RuntimeError:
# editorsplitter has been destroyed (happens when closing a
# EditorMainWindow instance)
return
if close_splitter:
# editorstack just closed was the last widget in this QSplitter
self.close()
return
self.__give_focus_to_remaining_editor()
def editorsplitter_closed(self):
if DEBUG_EDITOR:
print("method 'editorsplitter_closed':", file=STDOUT)
print(" self :", self, file=STDOUT)
# print >>STDOUT, " sender:", self.sender()
try:
close_splitter = self.count() == 1 and self.editorstack is None
except RuntimeError:
# editorsplitter has been destroyed (happens when closing a
# EditorMainWindow instance)
return
if close_splitter:
# editorsplitter just closed was the last widget in this QSplitter
self.close()
return
elif self.count() == 2 and self.editorstack:
# back to the initial state: a single editorstack instance,
# as a single widget in this QSplitter: orientation may be changed
self.editorstack.reset_orientation()
self.__give_focus_to_remaining_editor()
def split(self, orientation=Qt.Vertical):
self.setOrientation(orientation)
self.editorstack.set_orientation(orientation)
editorsplitter = EditorSplitter(self.parent(), self.plugin,
self.menu_actions,
register_editorstack_cb=self.register_editorstack_cb,
unregister_editorstack_cb=self.unregister_editorstack_cb)
self.addWidget(editorsplitter)
self.connect(editorsplitter, SIGNAL("destroyed()"),
lambda: self.editorsplitter_closed())
current_editor = editorsplitter.editorstack.get_current_editor()
if current_editor is not None:
current_editor.setFocus()
def iter_editorstacks(self):
editorstacks = [(self.widget(0), self.orientation())]
if self.count() > 1:
editorsplitter = self.widget(1)
editorstacks += editorsplitter.iter_editorstacks()
return editorstacks
def get_layout_settings(self):
"""Return layout state"""
splitsettings = []
for editorstack, orientation in self.iter_editorstacks():
clines = [finfo.editor.get_cursor_line_number()
for finfo in editorstack.data]
cfname = editorstack.get_current_filename()
splitsettings.append((orientation == Qt.Vertical, cfname, clines))
return dict(hexstate=qbytearray_to_str(self.saveState()),
sizes=self.sizes(), splitsettings=splitsettings)
def set_layout_settings(self, settings):
"""Restore layout state"""
splitsettings = settings.get('splitsettings')
if splitsettings is None:
return
splitter = self
editor = None
for index, (is_vertical, cfname, clines) in enumerate(splitsettings):
if index > 0:
splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal)
splitter = splitter.widget(1)
editorstack = splitter.widget(0)
for index, finfo in enumerate(editorstack.data):
editor = finfo.editor
editor.go_to_line(clines[index])
editorstack.set_current_filename(cfname)
hexstate = settings.get('hexstate')
if hexstate is not None:
self.restoreState( QByteArray().fromHex(str(hexstate)) )
sizes = settings.get('sizes')
if sizes is not None:
self.setSizes(sizes)
if editor is not None:
editor.clearFocus()
editor.setFocus()
class EditorWidget(QSplitter):
def __init__(self, parent, plugin, menu_actions, show_fullpath,
fullpath_sorting, show_all_files, show_comments):
QSplitter.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
statusbar = parent.statusBar() # Create a status bar
self.readwrite_status = ReadWriteStatus(self, statusbar)
self.eol_status = EOLStatus(self, statusbar)
self.encoding_status = EncodingStatus(self, statusbar)
self.cursorpos_status = CursorPositionStatus(self, statusbar)
self.editorstacks = []
self.plugin = plugin
self.find_widget = FindReplace(self, enable_replace=True)
self.plugin.register_widget_shortcuts("Editor", self.find_widget)
self.find_widget.hide()
self.outlineexplorer = OutlineExplorerWidget(self,
show_fullpath=show_fullpath,
fullpath_sorting=fullpath_sorting,
show_all_files=show_all_files,
show_comments=show_comments)
self.connect(self.outlineexplorer,
SIGNAL("edit_goto(QString,int,QString)"),
lambda filenames, goto, word:
plugin.load(filenames=filenames, goto=goto, word=word,
editorwindow=self.parent()))
editor_widgets = QWidget(self)
editor_layout = QVBoxLayout()
editor_layout.setContentsMargins(0, 0, 0, 0)
editor_widgets.setLayout(editor_layout)
editorsplitter = EditorSplitter(self, plugin, menu_actions,
register_editorstack_cb=self.register_editorstack,
unregister_editorstack_cb=self.unregister_editorstack)
self.editorsplitter = editorsplitter
editor_layout.addWidget(editorsplitter)
editor_layout.addWidget(self.find_widget)
splitter = QSplitter(self)
splitter.setContentsMargins(0, 0, 0, 0)
splitter.addWidget(editor_widgets)
splitter.addWidget(self.outlineexplorer)
splitter.setStretchFactor(0, 5)
splitter.setStretchFactor(1, 1)
# Refreshing outline explorer
editorsplitter.editorstack.initialize_outlineexplorer()
def register_editorstack(self, editorstack):
self.editorstacks.append(editorstack)
if DEBUG_EDITOR:
print("EditorWidget.register_editorstack:", editorstack, file=STDOUT)
self.__print_editorstacks()
self.plugin.last_focus_editorstack[self.parent()] = editorstack
editorstack.set_closable( len(self.editorstacks) > 1 )
editorstack.set_outlineexplorer(self.outlineexplorer)
editorstack.set_find_widget(self.find_widget)
self.connect(editorstack, SIGNAL('reset_statusbar()'),
self.readwrite_status.hide)
self.connect(editorstack, SIGNAL('reset_statusbar()'),
self.encoding_status.hide)
self.connect(editorstack, SIGNAL('reset_statusbar()'),
self.cursorpos_status.hide)
self.connect(editorstack, SIGNAL('readonly_changed(bool)'),
self.readwrite_status.readonly_changed)
self.connect(editorstack, SIGNAL('encoding_changed(QString)'),
self.encoding_status.encoding_changed)
self.connect(editorstack,
SIGNAL('editor_cursor_position_changed(int,int)'),
self.cursorpos_status.cursor_position_changed)
self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'),
self.eol_status.eol_changed)
self.plugin.register_editorstack(editorstack)
oe_btn = create_toolbutton(self)
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
def __print_editorstacks(self):
print("%d editorstack(s) in editorwidget:" \
% len(self.editorstacks), file=STDOUT)
for edst in self.editorstacks:
print(" ", edst, file=STDOUT)
def unregister_editorstack(self, editorstack):
if DEBUG_EDITOR:
print("EditorWidget.unregister_editorstack:", editorstack, file=STDOUT)
self.plugin.unregister_editorstack(editorstack)
self.editorstacks.pop(self.editorstacks.index(editorstack))
if DEBUG_EDITOR:
self.__print_editorstacks()
class EditorMainWindow(QMainWindow):
def __init__(self, plugin, menu_actions, toolbar_list, menu_list,
show_fullpath, fullpath_sorting, show_all_files,
show_comments):
QMainWindow.__init__(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.window_size = None
self.editorwidget = EditorWidget(self, plugin, menu_actions,
show_fullpath, fullpath_sorting,
show_all_files, show_comments)
self.setCentralWidget(self.editorwidget)
# Give focus to current editor to update/show all status bar widgets
editorstack = self.editorwidget.editorsplitter.editorstack
editor = editorstack.get_current_editor()
if editor is not None:
editor.setFocus()
self.setWindowTitle("Spyder - %s" % plugin.windowTitle())
self.setWindowIcon(plugin.windowIcon())
if toolbar_list:
toolbars = []
for title, actions in toolbar_list:
toolbar = self.addToolBar(title)
toolbar.setObjectName(str(id(toolbar)))
add_actions(toolbar, actions)
toolbars.append(toolbar)
if menu_list:
quit_action = create_action(self, _("Close window"),
icon="close_panel.png",
tip=_("Close this window"),
triggered=self.close)
menus = []
for index, (title, actions) in enumerate(menu_list):
menu = self.menuBar().addMenu(title)
if index == 0:
# File menu
add_actions(menu, actions+[None, quit_action])
else:
add_actions(menu, actions)
menus.append(menu)
def resizeEvent(self, event):
"""Reimplement Qt method"""
if not self.isMaximized() and not self.isFullScreen():
self.window_size = self.size()
QMainWindow.resizeEvent(self, event)
def closeEvent(self, event):
"""Reimplement Qt method"""
QMainWindow.closeEvent(self, event)
if is_pyqt46:
self.emit(SIGNAL('destroyed()'))
for editorstack in self.editorwidget.editorstacks[:]:
if DEBUG_EDITOR:
print("--> destroy_editorstack:", editorstack, file=STDOUT)
editorstack.emit(SIGNAL('destroyed()'))
def get_layout_settings(self):
"""Return layout state"""
splitsettings = self.editorwidget.editorsplitter.get_layout_settings()
return dict(size=(self.window_size.width(), self.window_size.height()),
pos=(self.pos().x(), self.pos().y()),
is_maximized=self.isMaximized(),
is_fullscreen=self.isFullScreen(),
hexstate=qbytearray_to_str(self.saveState()),
splitsettings=splitsettings)
def set_layout_settings(self, settings):
"""Restore layout state"""
size = settings.get('size')
if size is not None:
self.resize( QSize(*size) )
self.window_size = self.size()
pos = settings.get('pos')
if pos is not None:
self.move( QPoint(*pos) )
hexstate = settings.get('hexstate')
if hexstate is not None:
self.restoreState( QByteArray().fromHex(str(hexstate)) )
if settings.get('is_maximized'):
self.setWindowState(Qt.WindowMaximized)
if settings.get('is_fullscreen'):
self.setWindowState(Qt.WindowFullScreen)
splitsettings = settings.get('splitsettings')
if splitsettings is not None:
self.editorwidget.editorsplitter.set_layout_settings(splitsettings)
class EditorPluginExample(QSplitter):
def __init__(self):
QSplitter.__init__(self)
menu_actions = []
self.editorstacks = []
self.editorwindows = []
self.last_focus_editorstack = {} # fake
self.find_widget = FindReplace(self, enable_replace=True)
self.outlineexplorer = OutlineExplorerWidget(self, show_fullpath=False,
show_all_files=False)
self.connect(self.outlineexplorer,
SIGNAL("edit_goto(QString,int,QString)"),
self.go_to_file)
editor_widgets = QWidget(self)
editor_layout = QVBoxLayout()
editor_layout.setContentsMargins(0, 0, 0, 0)
editor_widgets.setLayout(editor_layout)
editor_layout.addWidget(EditorSplitter(self, self, menu_actions,
first=True))
editor_layout.addWidget(self.find_widget)
self.setContentsMargins(0, 0, 0, 0)
self.addWidget(editor_widgets)
self.addWidget(self.outlineexplorer)
self.setStretchFactor(0, 5)
self.setStretchFactor(1, 1)
self.menu_actions = menu_actions
self.toolbar_list = None
self.menu_list = None
self.setup_window([], [])
def go_to_file(self, fname, lineno, text):
editorstack = self.editorstacks[0]
editorstack.set_current_filename(to_text_string(fname))
editor = editorstack.get_current_editor()
editor.go_to_line(lineno, word=text)
def closeEvent(self, event):
for win in self.editorwindows[:]:
win.close()
if DEBUG_EDITOR:
print(len(self.editorwindows), ":", self.editorwindows, file=STDOUT)
print(len(self.editorstacks), ":", self.editorstacks, file=STDOUT)
event.accept()
def load(self, fname):
QApplication.processEvents()
editorstack = self.editorstacks[0]
editorstack.load(fname)
editorstack.analyze_script()
def register_editorstack(self, editorstack):
if DEBUG_EDITOR:
print("FakePlugin.register_editorstack:", editorstack, file=STDOUT)
self.editorstacks.append(editorstack)
if self.isAncestorOf(editorstack):
# editorstack is a child of the Editor plugin
editorstack.set_fullpath_sorting_enabled(True)
editorstack.set_closable( len(self.editorstacks) > 1 )
editorstack.set_outlineexplorer(self.outlineexplorer)
editorstack.set_find_widget(self.find_widget)
oe_btn = create_toolbutton(self)
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
action = QAction(self)
editorstack.set_io_actions(action, action, action, action)
font = QFont("Courier New")
font.setPointSize(10)
editorstack.set_default_font(font, color_scheme='Spyder')
self.connect(editorstack, SIGNAL('close_file(QString,int)'),
self.close_file_in_all_editorstacks)
self.connect(editorstack, SIGNAL('file_saved(QString,int,QString)'),
self.file_saved_in_editorstack)
self.connect(editorstack,
SIGNAL('file_renamed_in_data(QString,int,QString)'),
self.file_renamed_in_data_in_editorstack)
self.connect(editorstack, SIGNAL("create_new_window()"),
self.create_new_window)
self.connect(editorstack, SIGNAL('plugin_load(QString)'),
self.load)
def unregister_editorstack(self, editorstack):
if DEBUG_EDITOR:
print("FakePlugin.unregister_editorstack:", editorstack, file=STDOUT)
self.editorstacks.pop(self.editorstacks.index(editorstack))
def clone_editorstack(self, editorstack):
editorstack.clone_from(self.editorstacks[0])
def setup_window(self, toolbar_list, menu_list):
self.toolbar_list = toolbar_list
self.menu_list = menu_list
def create_new_window(self):
window = EditorMainWindow(self, self.menu_actions,
self.toolbar_list, self.menu_list,
show_fullpath=False, fullpath_sorting=True,
show_all_files=False, show_comments=True)
window.resize(self.size())
window.show()
self.register_editorwindow(window)
self.connect(window, SIGNAL("destroyed()"),
lambda win=window: self.unregister_editorwindow(win))
def register_editorwindow(self, window):
if DEBUG_EDITOR:
print("register_editorwindowQObject*:", window, file=STDOUT)
self.editorwindows.append(window)
def unregister_editorwindow(self, window):
if DEBUG_EDITOR:
print("unregister_editorwindow:", window, file=STDOUT)
self.editorwindows.pop(self.editorwindows.index(window))
def get_focus_widget(self):
pass
@Slot(int, int)
def close_file_in_all_editorstacks(self, editorstack_id_str, index):
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.blockSignals(True)
editorstack.close_file(index, force=True)
editorstack.blockSignals(False)
# This method is never called in this plugin example. It's here only
# to show how to use the file_saved signal (see above).
@Slot(int, int)
def file_saved_in_editorstack(self, editorstack_id_str, index, filename):
"""A file was saved in editorstack, this notifies others"""
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.file_saved_in_other_editorstack(index, filename)
# This method is never called in this plugin example. It's here only
# to show how to use the file_saved signal (see above).
@Slot(int, int)
def file_renamed_in_data_in_editorstack(self, editorstack_id_str,
index, filename):
"""A file was renamed in data in editorstack, this notifies others"""
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.rename_in_data(index, filename)
def register_widget_shortcuts(self, context, widget):
"""Fake!"""
pass
def test():
from spyderlib.utils.qthelpers import qapplication
app = qapplication()
test = EditorPluginExample()
test.resize(900, 700)
test.show()
import time
t0 = time.time()
test.load(__file__)
test.load("explorer.py")
test.load("dicteditor.py")
test.load("sourcecode/codeeditor.py")
test.load("../spyder.py")
print("Elapsed time: %.3f s" % (time.time()-t0))
sys.exit(app.exec_())
if __name__ == "__main__":
test()