1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2011 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
9
# pylint: disable=C0103
10
# pylint: disable=R0903
11
# pylint: disable=R0911
12
# pylint: disable=R0201
14
from spyderlib.qt import is_pyqt46
15
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QMenu, QFont,
16
QAction, QApplication, QWidget, QHBoxLayout,
17
QLabel, QKeySequence, QShortcut, QMainWindow,
18
QSplitter, QListWidget, QListWidgetItem,
20
from spyderlib.qt.QtCore import (SIGNAL, Qt, QFileInfo, QThread, QObject,
21
QByteArray, QSize, QPoint, QTimer)
22
from spyderlib.qt.compat import getsavefilename
30
from spyderlib.utils import encoding, sourcecode, programs, codeanalysis
31
from spyderlib.utils.dochelpers import getsignaturesfromtext
32
from spyderlib.utils.module_completion import moduleCompletion
33
from spyderlib.baseconfig import _, DEBUG, STDOUT, STDERR
34
from spyderlib.config import EDIT_FILTERS, EDIT_EXT
35
from spyderlib.guiconfig import get_icon
36
from spyderlib.utils.qthelpers import (create_action, add_actions,
37
mimedata2url, get_filetype_icon,
39
from spyderlib.widgets.tabs import BaseTabs
40
from spyderlib.widgets.findreplace import FindReplace
41
from spyderlib.widgets.editortools import OutlineExplorerWidget
42
from spyderlib.widgets.status import (ReadWriteStatus, EOLStatus,
43
EncodingStatus, CursorPositionStatus)
44
from spyderlib.widgets.sourcecode import syntaxhighlighters, codeeditor
45
from spyderlib.widgets.sourcecode.base import TextEditBaseWidget #analysis:ignore
46
from spyderlib.widgets.sourcecode.codeeditor import Printer #analysis:ignore
47
from spyderlib.widgets.sourcecode.codeeditor import get_file_language
50
class FileListDialog(QDialog):
51
def __init__(self, parent, tabs, fullpath_sorting):
52
QDialog.__init__(self, parent)
54
# Destroying the C++ object right after closing the dialog box,
55
# otherwise it may be garbage-collected in another QThread
56
# (e.g. the editor's analysis thread in Spyder), thus leading to
57
# a segmentation fault on UNIX or an application crash on Windows
58
self.setAttribute(Qt.WA_DeleteOnClose)
62
self.setWindowIcon(get_icon('filelist.png'))
63
self.setWindowTitle(_("File list management"))
67
flabel = QLabel(_("Filter:"))
68
self.edit = QLineEdit(self)
69
self.connect(self.edit, SIGNAL("returnPressed()"), self.edit_file)
70
self.connect(self.edit, SIGNAL("textChanged(QString)"),
71
lambda text: self.synchronize(0))
72
fhint = QLabel(_("(press <b>Enter</b> to edit file)"))
73
edit_layout = QHBoxLayout()
74
edit_layout.addWidget(flabel)
75
edit_layout.addWidget(self.edit)
76
edit_layout.addWidget(fhint)
78
self.listwidget = QListWidget(self)
79
self.listwidget.setResizeMode(QListWidget.Adjust)
80
self.connect(self.listwidget, SIGNAL("itemSelectionChanged()"),
81
self.item_selection_changed)
82
self.connect(self.listwidget, SIGNAL("itemActivated(QListWidgetItem*)"),
85
btn_layout = QHBoxLayout()
86
edit_btn = create_toolbutton(self, icon=get_icon('edit.png'),
87
text=_("&Edit file"), autoraise=False,
88
triggered=self.edit_file, text_beside_icon=True)
89
edit_btn.setMinimumHeight(28)
90
btn_layout.addWidget(edit_btn)
92
btn_layout.addStretch()
93
btn_layout.addSpacing(150)
94
btn_layout.addStretch()
96
close_btn = create_toolbutton(self, text=_("&Close file"),
97
icon=get_icon("fileclose.png"),
98
autoraise=False, text_beside_icon=True,
99
triggered=lambda: self.emit(SIGNAL("close_file(int)"),
100
self.indexes[self.listwidget.currentRow()]))
101
close_btn.setMinimumHeight(28)
102
btn_layout.addWidget(close_btn)
104
hint = QLabel(_("Hint: press <b>Alt</b> to show accelerators"))
105
hint.setAlignment(Qt.AlignCenter)
107
vlayout = QVBoxLayout()
108
vlayout.addLayout(edit_layout)
109
vlayout.addWidget(self.listwidget)
110
vlayout.addLayout(btn_layout)
111
vlayout.addWidget(hint)
112
self.setLayout(vlayout)
115
self.fullpath_sorting = fullpath_sorting
116
self.buttons = (edit_btn, close_btn)
119
row = self.listwidget.currentRow()
120
if self.listwidget.count() > 0 and row >= 0:
121
self.emit(SIGNAL("edit_file(int)"), self.indexes[row])
124
def item_selection_changed(self):
125
for btn in self.buttons:
126
btn.setEnabled(self.listwidget.currentRow() >= 0)
128
def synchronize(self, stack_index):
129
count = self.tabs.count()
133
self.listwidget.setTextElideMode(Qt.ElideMiddle
134
if self.fullpath_sorting
136
current_row = self.listwidget.currentRow()
138
current_text = unicode(self.listwidget.currentItem().text())
141
self.listwidget.clear()
143
new_current_index = stack_index
144
filter_text = unicode(self.edit.text())
146
for index in range(count):
147
text = unicode(self.tabs.tabText(index))
148
if len(filter_text) == 0 or filter_text in text:
149
if text == current_text:
150
new_current_index = lw_index
152
item = QListWidgetItem(self.tabs.tabIcon(index),
153
text, self.listwidget)
154
item.setSizeHint(QSize(0, 25))
155
self.listwidget.addItem(item)
156
self.indexes.append(index)
157
if new_current_index < self.listwidget.count():
158
self.listwidget.setCurrentRow(new_current_index)
159
for btn in self.buttons:
160
btn.setEnabled(lw_index > 0)
163
class AnalysisThread(QThread):
164
"""Analysis thread"""
165
def __init__(self, parent, checker, source_code):
166
super(AnalysisThread, self).__init__(parent)
167
self.checker = checker
169
self.source_code = source_code
174
self.results = self.checker(self.source_code)
178
traceback.print_exc(file=STDERR)
180
class ThreadManager(QObject):
181
"""Analysis thread manager"""
182
def __init__(self, parent, max_simultaneous_threads=2):
183
super(ThreadManager, self).__init__(parent)
184
self.max_simultaneous_threads = max_simultaneous_threads
185
self.started_threads = {}
186
self.pending_threads = []
187
self.end_callbacks = {}
189
def close_threads(self, parent):
190
"""Close threads associated to parent_id"""
192
print >>STDOUT, "Call to 'close_threads'"
194
# Closing all threads
195
self.pending_threads = []
197
for threads in self.started_threads.values():
198
threadlist += threads
200
parent_id = id(parent)
201
self.pending_threads = [(_th, _id) for (_th, _id)
202
in self.pending_threads
204
threadlist = self.started_threads.get(parent_id, [])
205
for thread in threadlist:
207
print >>STDOUT, "Waiting for thread %r to finish" % thread
208
while thread.isRunning():
209
# We can't terminate thread safely, so we simply wait...
210
QApplication.processEvents()
212
def close_all_threads(self):
213
"""Close all threads"""
215
print >>STDOUT, "Call to 'close_all_threads'"
216
self.close_threads(None)
218
def add_thread(self, checker, end_callback, source_code, parent):
219
"""Add thread to queue"""
220
parent_id = id(parent)
221
thread = AnalysisThread(self, checker, source_code)
222
self.end_callbacks[id(thread)] = end_callback
223
self.pending_threads.append((thread, parent_id))
225
print >>STDOUT, "Added thread %r to queue" % thread
226
QTimer.singleShot(50, self.update_queue)
228
def update_queue(self):
231
for parent_id, threadlist in self.started_threads.items():
233
for thread in threadlist:
234
if thread.isFinished():
235
end_callback = self.end_callbacks.pop(id(thread))
236
if thread.results is not None:
237
# The thread was executed successfully
238
end_callback(thread.results)
239
thread.setParent(None)
242
still_running.append(thread)
246
self.started_threads[parent_id] = still_running
248
self.started_threads.pop(parent_id)
250
print >>STDOUT, "Updating queue:"
251
print >>STDOUT, " started:", started
252
print >>STDOUT, " pending:", len(self.pending_threads)
253
if self.pending_threads and started < self.max_simultaneous_threads:
254
thread, parent_id = self.pending_threads.pop(0)
255
self.connect(thread, SIGNAL('finished()'), self.update_queue)
256
threadlist = self.started_threads.get(parent_id, [])
257
self.started_threads[parent_id] = threadlist+[thread]
259
print >>STDOUT, "===>starting:", thread
263
class FileInfo(QObject):
264
"""File properties"""
265
def __init__(self, filename, encoding, editor, new, threadmanager):
266
QObject.__init__(self)
267
self.threadmanager = threadmanager
268
self.filename = filename
269
self.newly_created = new
270
self.encoding = encoding
272
self.rope_project = codeeditor.get_rope_project()
273
self.classes = (filename, None, None)
274
self.analysis_results = []
275
self.todo_results = []
276
self.lastmodified = QFileInfo(filename).lastModified()
278
self.connect(editor, SIGNAL('trigger_code_completion(bool)'),
279
self.trigger_code_completion)
280
self.connect(editor, SIGNAL('trigger_calltip(int)'),
281
self.trigger_calltip)
282
self.connect(editor, SIGNAL("go_to_definition(int)"),
283
self.go_to_definition)
285
self.connect(editor, SIGNAL('textChanged()'),
288
self.connect(editor, SIGNAL('breakpoints_changed()'),
289
self.breakpoints_changed)
291
self.pyflakes_results = None
292
self.pep8_results = None
294
def text_changed(self):
295
"""Editor's text has changed"""
296
self.emit(SIGNAL('text_changed_at(QString,int)'),
297
self.filename, self.editor.get_position('cursor'))
299
def trigger_code_completion(self, automatic):
300
"""Trigger code completion"""
301
source_code = unicode(self.editor.toPlainText())
302
offset = self.editor.get_position('cursor')
303
text = self.editor.get_text('sol', 'cursor')
305
if text.startswith('import '):
306
comp_list = moduleCompletion(text)
307
words = text.split(' ')
309
words = words[-1].split(',')
311
self.editor.show_completion_list(comp_list,
312
completion_text=words[-1],
315
elif text.startswith('from '):
316
comp_list = moduleCompletion(text)
317
words = text.split(' ')
319
words = words[:-2] + words[-1].split('(')
321
words = words[:-2] + words[-1].split(',')
322
self.editor.show_completion_list(comp_list,
323
completion_text=words[-1],
327
textlist = self.rope_project.get_completion_list(source_code,
331
completion_text = re.split(r"[^a-zA-Z0-9_]", text)[-1]
332
if text.lstrip().startswith('#') and text.endswith('.'):
335
self.editor.show_completion_list(textlist, completion_text,
339
def trigger_calltip(self, position, auto=True):
340
"""Trigger calltip"""
341
# auto is True means that trigger_calltip was called automatically,
342
# i.e. the user has just entered an opening parenthesis -- in that
343
# case, we don't want to force the object inspector to be visible,
344
# to avoid polluting the window layout
345
source_code = unicode(self.editor.toPlainText())
348
textlist = self.rope_project.get_calltip_text(source_code, offset,
354
cts, doc_text = textlist
355
cts = cts.replace('.__init__', '')
356
parpos = cts.find('(')
358
obj_fullname = cts[:parpos]
359
obj_name = obj_fullname.split('.')[-1]
360
cts = cts.replace(obj_fullname, obj_name)
363
# Either inspected object has no argument, or it's
364
# a builtin or an extension -- in this last case
365
# the following attempt may succeed:
366
signatures = getsignaturesfromtext(doc_text, obj_name)
368
obj_fullname = codeeditor.get_primary_at(source_code, offset)
369
if obj_fullname and not obj_fullname.startswith('self.') and doc_text:
371
argspec_st = signatures[0].find('(')
372
argspec = signatures[0][argspec_st:]
373
module_end = obj_fullname.rfind('.')
374
module = obj_fullname[:module_end]
375
note = 'Present in %s module' % module
380
"send_to_inspector(QString,QString,QString,QString,bool)"),
381
obj_fullname, argspec, note, doc_text, not auto)
383
signatures = ['<b>'+s.replace('(', '(</b>'
384
).replace(')', '<b>)</b>')
386
self.editor.show_calltip(obj_fullname, '<br>'.join(signatures),
387
at_position=position)
389
def go_to_definition(self, position):
390
"""Go to definition"""
391
source_code = unicode(self.editor.toPlainText())
393
fname, lineno = self.rope_project.get_definition_location(source_code,
394
offset, self.filename)
395
if fname is not None and lineno is not None:
396
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
399
def get_source_code(self):
400
"""Return associated editor source code"""
401
return unicode(self.editor.toPlainText()).encode('utf-8')
403
def run_code_analysis(self, run_pyflakes, run_pep8):
404
"""Run code analysis"""
405
run_pyflakes = run_pyflakes and codeanalysis.is_pyflakes_installed()
406
run_pep8 = run_pep8 and\
407
codeanalysis.get_checker_executable('pep8') is not None
408
self.pyflakes_results = []
409
self.pep8_results = []
410
if self.editor.is_python():
411
source_code = self.get_source_code()
413
self.pyflakes_results = None
415
self.pep8_results = None
417
self.threadmanager.add_thread(codeanalysis.check_with_pyflakes,
418
self.pyflakes_analysis_finished,
421
self.threadmanager.add_thread(codeanalysis.check_with_pep8,
422
self.pep8_analysis_finished,
425
def pyflakes_analysis_finished(self, results):
426
"""Pyflakes code analysis thread has finished"""
427
self.pyflakes_results = results
428
if self.pep8_results is not None:
429
self.code_analysis_finished()
431
def pep8_analysis_finished(self, results):
432
"""Pep8 code analysis thread has finished"""
433
self.pep8_results = results
434
if self.pyflakes_results is not None:
435
self.code_analysis_finished()
437
def code_analysis_finished(self):
438
"""Code analysis thread has finished"""
439
self.set_analysis_results(self.pyflakes_results+self.pep8_results)
440
self.emit(SIGNAL('analysis_results_changed()'))
442
def set_analysis_results(self, results):
443
"""Set analysis results and update warning markers in editor"""
444
self.analysis_results = results
445
self.editor.process_code_analysis(results)
447
def cleanup_analysis_results(self):
448
"""Clean-up analysis results"""
449
self.analysis_results = []
450
self.editor.cleanup_code_analysis()
452
def run_todo_finder(self):
453
"""Run TODO finder"""
454
if self.editor.is_python():
455
self.threadmanager.add_thread(codeanalysis.find_tasks,
457
self.get_source_code(), self)
459
def todo_finished(self, results):
460
"""Code analysis thread has finished"""
461
self.set_todo_results(results)
462
self.emit(SIGNAL('todo_results_changed()'))
464
def set_todo_results(self, results):
465
"""Set TODO results and update markers in editor"""
466
self.todo_results = results
467
self.editor.process_todo(results)
469
def cleanup_todo_results(self):
470
"""Clean-up TODO finder results"""
471
self.todo_results = []
473
def breakpoints_changed(self):
474
"""Breakpoint list has changed"""
475
breakpoints = self.editor.get_breakpoints()
476
if self.editor.breakpoints != breakpoints:
477
self.editor.breakpoints = breakpoints
478
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
479
self.filename, repr(breakpoints))
482
class EditorStack(QWidget):
483
def __init__(self, parent, actions):
484
QWidget.__init__(self, parent)
486
self.setAttribute(Qt.WA_DeleteOnClose)
488
self.threadmanager = ThreadManager(self)
490
self.newwindow_action = None
491
self.horsplit_action = None
492
self.versplit_action = None
493
self.close_action = None
494
self.__get_split_actions()
496
layout = QVBoxLayout()
497
layout.setContentsMargins(0, 0, 0, 0)
498
self.setLayout(layout)
501
self.filelist_dlg = None
502
# self.filelist_btn = None
503
# self.previous_btn = None
504
# self.next_btn = None
507
self.stack_history = []
509
self.setup_editorstack(parent, layout)
511
self.find_widget = None
515
filelist_action = create_action(self, _("File list management"),
516
icon=get_icon('filelist.png'),
517
triggered=self.open_filelistdialog)
518
copy_to_cb_action = create_action(self, _("Copy path to clipboard"),
521
QApplication.clipboard().setText(self.get_current_filename()))
522
self.menu_actions = actions+[None, filelist_action, copy_to_cb_action]
523
self.outlineexplorer = None
524
self.inspector = None
525
self.unregister_callback = None
526
self.is_closable = False
527
self.new_action = None
528
self.open_action = None
529
self.save_action = None
530
self.revert_action = None
531
self.tempfile_path = None
532
self.title = _("Editor")
533
self.pyflakes_enabled = True
534
self.pep8_enabled = False
535
self.todolist_enabled = True
536
self.realtime_analysis_enabled = False
537
self.is_analysis_done = False
538
self.linenumbers_enabled = True
539
self.edgeline_enabled = True
540
self.edgeline_column = 79
541
self.outlineexplorer_enabled = True
542
self.codecompletion_auto_enabled = True
543
self.codecompletion_case_enabled = False
544
self.codecompletion_single_enabled = False
545
self.codecompletion_enter_enabled = False
546
self.calltips_enabled = True
547
self.go_to_definition_enabled = True
548
self.close_parentheses_enabled = True
549
self.close_quotes_enabled = True
550
self.add_colons_enabled = True
551
self.auto_unindent_enabled = True
552
self.indent_chars = " "*4
553
self.tab_stop_width = 40
554
self.inspector_enabled = False
555
self.default_font = None
556
self.wrap_enabled = False
557
self.tabmode_enabled = False
558
self.intelligent_backspace_enabled = True
559
self.highlight_current_line_enabled = False
560
self.occurence_highlighting_enabled = True
561
self.checkeolchars_enabled = True
562
self.always_remove_trailing_spaces = False
563
self.fullpath_sorting_enabled = None
564
self.set_fullpath_sorting_enabled(False)
566
if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES:
567
ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0]
568
self.color_scheme = ccs
570
self.__file_status_flag = False
572
# Real-time code analysis
573
self.analysis_timer = QTimer(self)
574
self.analysis_timer.setSingleShot(True)
575
self.analysis_timer.setInterval(2000)
576
self.connect(self.analysis_timer, SIGNAL("timeout()"),
580
self.setAcceptDrops(True)
583
self.inspectsc = QShortcut(QKeySequence("Ctrl+I"), self,
584
self.inspect_current_object)
585
self.inspectsc.setContext(Qt.WidgetWithChildrenShortcut)
586
self.breakpointsc = QShortcut(QKeySequence("F12"), self,
587
self.set_or_clear_breakpoint)
588
self.breakpointsc.setContext(Qt.WidgetWithChildrenShortcut)
589
self.cbreakpointsc = QShortcut(QKeySequence("Shift+F12"), self,
590
self.set_or_edit_conditional_breakpoint)
591
self.cbreakpointsc.setContext(Qt.WidgetWithChildrenShortcut)
592
self.gotolinesc = QShortcut(QKeySequence("Ctrl+L"), self,
594
self.gotolinesc.setContext(Qt.WidgetWithChildrenShortcut)
595
self.filelistsc = QShortcut(QKeySequence("Ctrl+E"), self,
596
self.open_filelistdialog)
597
self.filelistsc.setContext(Qt.WidgetWithChildrenShortcut)
598
self.tabsc = QShortcut(QKeySequence("Ctrl+Tab"), self,
599
self.go_to_previous_file)
600
self.tabsc.setContext(Qt.WidgetWithChildrenShortcut)
601
self.closesc = QShortcut(QKeySequence("Ctrl+F4"), self,
603
self.closesc.setContext(Qt.WidgetWithChildrenShortcut)
604
self.tabshiftsc = QShortcut(QKeySequence("Ctrl+Shift+Tab"), self,
605
self.go_to_next_file)
606
self.tabshiftsc.setContext(Qt.WidgetWithChildrenShortcut)
608
def get_shortcut_data(self):
610
Returns shortcut data, a list of tuples (shortcut, text, default)
611
shortcut (QShortcut or QAction instance)
612
text (string): action/shortcut description
613
default (string): default key sequence
616
(self.inspectsc, "Inspect current object", "Ctrl+I"),
617
(self.breakpointsc, "Breakpoint", "F12"),
618
(self.cbreakpointsc, "Conditional breakpoint", "Shift+F12"),
619
(self.gotolinesc, "Go to line", "Ctrl+L"),
620
(self.filelistsc, "File list management", "Ctrl+E"),
623
def setup_editorstack(self, parent, layout):
624
"""Setup editorstack's layout"""
625
menu_btn = create_toolbutton(self, icon=get_icon("tooloptions.png"),
627
self.menu = QMenu(self)
628
menu_btn.setMenu(self.menu)
629
menu_btn.setPopupMode(menu_btn.InstantPopup)
630
self.connect(self.menu, SIGNAL("aboutToShow()"), self.__setup_menu)
632
# self.filelist_btn = create_toolbutton(self,
633
# icon=get_icon('filelist.png'),
634
# tip=_("File list management"),
635
# triggered=self.open_filelistdialog)
637
# self.previous_btn = create_toolbutton(self,
638
# icon=get_icon('previous.png'),
639
# tip=_("Previous file"),
640
# triggered=self.go_to_previous_file)
642
# self.next_btn = create_toolbutton(self,
643
# icon=get_icon('next.png'),
644
# tip=_("Next file"),
645
# triggered=self.go_to_next_file)
648
# corner_widgets = {Qt.TopRightCorner: [self.previous_btn,
649
# self.filelist_btn, self.next_btn,
651
corner_widgets = {Qt.TopRightCorner: [menu_btn]}
652
self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True,
653
corner_widgets=corner_widgets)
654
self.tabs.set_close_function(self.close_file)
655
if hasattr(self.tabs, 'setDocumentMode') \
656
and not sys.platform == 'darwin':
657
# Don't set document mode to true on OSX because it generates
658
# a crash when the editor is detached from the main window
660
self.tabs.setDocumentMode(True)
661
self.connect(self.tabs, SIGNAL('currentChanged(int)'),
662
self.current_changed)
663
layout.addWidget(self.tabs)
665
def add_corner_widgets_to_tabbar(self, widgets):
666
self.tabs.add_corner_widgets(widgets)
668
def closeEvent(self, event):
669
self.threadmanager.close_all_threads()
670
self.disconnect(self.analysis_timer, SIGNAL("timeout()"),
672
QWidget.closeEvent(self, event)
674
self.emit(SIGNAL('destroyed()'))
676
def clone_editor_from(self, other_finfo, set_current):
677
fname = other_finfo.filename
678
enc = other_finfo.encoding
679
new = other_finfo.newly_created
680
finfo = self.create_new_editor(fname, enc, "",
681
set_current=set_current, new=new,
682
cloned_from=other_finfo.editor)
683
finfo.set_analysis_results(other_finfo.analysis_results)
684
finfo.set_todo_results(other_finfo.todo_results)
687
def clone_from(self, other):
688
"""Clone EditorStack from other instance"""
689
for other_finfo in other.data:
690
self.clone_editor_from(other_finfo, set_current=True)
691
self.set_stack_index(other.get_stack_index())
693
def open_filelistdialog(self):
694
"""Open file list management dialog box"""
695
self.filelist_dlg = dlg = FileListDialog(self, self.tabs,
696
self.fullpath_sorting_enabled)
697
self.connect(dlg, SIGNAL("edit_file(int)"), self.set_stack_index)
698
self.connect(dlg, SIGNAL("close_file(int)"), self.close_file)
699
dlg.synchronize(self.get_stack_index())
701
self.filelist_dlg = None
703
def update_filelistdialog(self):
704
"""Synchronize file list dialog box with editor widget tabs"""
705
if self.filelist_dlg is not None:
706
self.filelist_dlg.synchronize(self.get_stack_index())
708
def go_to_line(self):
709
"""Go to line dialog"""
711
self.get_current_editor().exec_gotolinedialog()
713
def set_or_clear_breakpoint(self):
714
"""Set/clear breakpoint"""
716
editor = self.get_current_editor()
717
editor.add_remove_breakpoint()
719
def set_or_edit_conditional_breakpoint(self):
720
"""Set conditional breakpoint"""
722
editor = self.get_current_editor()
723
editor.add_remove_breakpoint(edit_condition=True)
725
def inspect_current_object(self):
726
"""Inspect current object in Object Inspector"""
727
if programs.is_module_installed('rope'):
728
editor = self.get_current_editor()
729
position = editor.get_position('cursor')
730
finfo = self.get_current_finfo()
731
finfo.trigger_calltip(position, auto=False)
733
text = self.get_current_editor().get_current_object()
735
self.send_to_inspector(text, force=True)
738
#------ Editor Widget Settings
739
def set_closable(self, state):
740
"""Parent widget must handle the closable state"""
741
self.is_closable = state
743
def set_io_actions(self, new_action, open_action,
744
save_action, revert_action):
745
self.new_action = new_action
746
self.open_action = open_action
747
self.save_action = save_action
748
self.revert_action = revert_action
750
def set_find_widget(self, find_widget):
751
self.find_widget = find_widget
753
def set_outlineexplorer(self, outlineexplorer):
754
self.outlineexplorer = outlineexplorer
755
self.outlineexplorer_enabled = True
756
self.connect(self.outlineexplorer,
757
SIGNAL("outlineexplorer_is_visible()"),
758
self._refresh_outlineexplorer)
760
def add_outlineexplorer_button(self, editor_plugin):
761
oe_btn = create_toolbutton(editor_plugin)
762
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
763
self.add_corner_widgets_to_tabbar([5, oe_btn])
765
def set_inspector(self, inspector):
766
self.inspector = inspector
768
def set_tempfile_path(self, path):
769
self.tempfile_path = path
771
def set_title(self, text):
774
def __update_editor_margins(self, editor):
775
editor.setup_margins(linenumbers=self.linenumbers_enabled,
776
markers=self.has_markers())
778
def __codeanalysis_settings_changed(self, current_finfo):
780
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
781
for finfo in self.data:
782
self.__update_editor_margins(finfo.editor)
783
finfo.cleanup_analysis_results()
784
if (run_pyflakes or run_pep8) and current_finfo is not None:
785
if current_finfo is not finfo:
786
finfo.run_code_analysis(run_pyflakes, run_pep8)
788
def set_pyflakes_enabled(self, state, current_finfo=None):
789
# CONF.get(self.CONF_SECTION, 'code_analysis/pyflakes')
790
self.pyflakes_enabled = state
791
self.__codeanalysis_settings_changed(current_finfo)
793
def set_pep8_enabled(self, state, current_finfo=None):
794
# CONF.get(self.CONF_SECTION, 'code_analysis/pep8')
795
self.pep8_enabled = state
796
self.__codeanalysis_settings_changed(current_finfo)
798
def has_markers(self):
799
"""Return True if this editorstack has a marker margin for TODOs or
801
return self.todolist_enabled or self.pyflakes_enabled\
804
def set_todolist_enabled(self, state, current_finfo=None):
805
# CONF.get(self.CONF_SECTION, 'todo_list')
806
self.todolist_enabled = state
808
for finfo in self.data:
809
self.__update_editor_margins(finfo.editor)
810
finfo.cleanup_todo_results()
811
if state and current_finfo is not None:
812
if current_finfo is not finfo:
813
finfo.run_todo_finder()
815
def set_realtime_analysis_enabled(self, state):
816
self.realtime_analysis_enabled = state
818
def set_realtime_analysis_timeout(self, timeout):
819
self.analysis_timer.setInterval(timeout)
821
def set_linenumbers_enabled(self, state, current_finfo=None):
822
# CONF.get(self.CONF_SECTION, 'line_numbers')
823
self.linenumbers_enabled = state
825
for finfo in self.data:
826
self.__update_editor_margins(finfo.editor)
828
def set_edgeline_enabled(self, state):
829
# CONF.get(self.CONF_SECTION, 'edge_line')
830
self.edgeline_enabled = state
832
for finfo in self.data:
833
finfo.editor.set_edge_line_enabled(state)
835
def set_edgeline_column(self, column):
836
# CONF.get(self.CONF_SECTION, 'edge_line_column')
837
self.edgeline_column = column
839
for finfo in self.data:
840
finfo.editor.set_edge_line_column(column)
842
def set_codecompletion_auto_enabled(self, state):
843
# CONF.get(self.CONF_SECTION, 'codecompletion_auto')
844
self.codecompletion_auto_enabled = state
846
for finfo in self.data:
847
finfo.editor.set_codecompletion_auto(state)
849
def set_codecompletion_case_enabled(self, state):
850
self.codecompletion_case_enabled = state
852
for finfo in self.data:
853
finfo.editor.set_codecompletion_case(state)
855
def set_codecompletion_single_enabled(self, state):
856
self.codecompletion_single_enabled = state
858
for finfo in self.data:
859
finfo.editor.set_codecompletion_single(state)
861
def set_codecompletion_enter_enabled(self, state):
862
self.codecompletion_enter_enabled = state
864
for finfo in self.data:
865
finfo.editor.set_codecompletion_enter(state)
867
def set_calltips_enabled(self, state):
868
# CONF.get(self.CONF_SECTION, 'calltips')
869
self.calltips_enabled = state
871
for finfo in self.data:
872
finfo.editor.set_calltips(state)
874
def set_go_to_definition_enabled(self, state):
875
# CONF.get(self.CONF_SECTION, 'go_to_definition')
876
self.go_to_definition_enabled = state
878
for finfo in self.data:
879
finfo.editor.set_go_to_definition_enabled(state)
881
def set_close_parentheses_enabled(self, state):
882
# CONF.get(self.CONF_SECTION, 'close_parentheses')
883
self.close_parentheses_enabled = state
885
for finfo in self.data:
886
finfo.editor.set_close_parentheses_enabled(state)
888
def set_close_quotes_enabled(self, state):
889
# CONF.get(self.CONF_SECTION, 'close_quotes')
890
self.close_quotes_enabled = state
892
for finfo in self.data:
893
finfo.editor.set_close_quotes_enabled(state)
895
def set_add_colons_enabled(self, state):
896
# CONF.get(self.CONF_SECTION, 'add_colons')
897
self.add_colons_enabled = state
899
for finfo in self.data:
900
finfo.editor.set_add_colons_enabled(state)
902
def set_auto_unindent_enabled(self, state):
903
# CONF.get(self.CONF_SECTION, 'auto_unindent')
904
self.auto_unindent_enabled = state
906
for finfo in self.data:
907
finfo.editor.set_auto_unindent_enabled(state)
909
def set_indent_chars(self, indent_chars):
910
# CONF.get(self.CONF_SECTION, 'indent_chars')
911
indent_chars = indent_chars[1:-1] # removing the leading/ending '*'
912
self.indent_chars = indent_chars
914
for finfo in self.data:
915
finfo.editor.set_indent_chars(indent_chars)
917
def set_tab_stop_width(self, tab_stop_width):
918
# CONF.get(self.CONF_SECTION, 'tab_stop_width')
919
self.tab_stop_width = tab_stop_width
921
for finfo in self.data:
922
finfo.editor.setTabStopWidth(tab_stop_width)
924
def set_inspector_enabled(self, state):
925
self.inspector_enabled = state
927
def set_outlineexplorer_enabled(self, state):
928
# CONF.get(self.CONF_SECTION, 'outline_explorer')
929
self.outlineexplorer_enabled = state
931
def set_default_font(self, font, color_scheme=None):
932
# get_font(self.CONF_SECTION)
933
self.default_font = font
934
if color_scheme is not None:
935
self.color_scheme = color_scheme
937
for finfo in self.data:
938
finfo.editor.set_font(font, color_scheme)
940
def set_color_scheme(self, color_scheme):
941
self.color_scheme = color_scheme
943
for finfo in self.data:
944
finfo.editor.set_color_scheme(color_scheme)
946
def set_wrap_enabled(self, state):
947
# CONF.get(self.CONF_SECTION, 'wrap')
948
self.wrap_enabled = state
950
for finfo in self.data:
951
finfo.editor.toggle_wrap_mode(state)
953
def set_tabmode_enabled(self, state):
954
# CONF.get(self.CONF_SECTION, 'tab_always_indent')
955
self.tabmode_enabled = state
957
for finfo in self.data:
958
finfo.editor.set_tab_mode(state)
960
def set_intelligent_backspace_enabled(self, state):
961
# CONF.get(self.CONF_SECTION, 'intelligent_backspace')
962
self.intelligent_backspace_enabled = state
964
for finfo in self.data:
965
finfo.editor.toggle_intelligent_backspace(state)
967
def set_occurence_highlighting_enabled(self, state):
968
# CONF.get(self.CONF_SECTION, 'occurence_highlighting')
969
self.occurence_highlighting_enabled = state
971
for finfo in self.data:
972
finfo.editor.set_occurence_highlighting(state)
974
def set_occurence_highlighting_timeout(self, timeout):
975
# CONF.get(self.CONF_SECTION, 'occurence_highlighting/timeout')
976
self.occurence_highlighting_timeout = timeout
978
for finfo in self.data:
979
finfo.editor.set_occurence_timeout(timeout)
981
def set_highlight_current_line_enabled(self, state):
982
self.highlight_current_line_enabled = state
984
for finfo in self.data:
985
finfo.editor.set_highlight_current_line(state)
987
def set_checkeolchars_enabled(self, state):
988
# CONF.get(self.CONF_SECTION, 'check_eol_chars')
989
self.checkeolchars_enabled = state
991
def set_fullpath_sorting_enabled(self, state):
992
# CONF.get(self.CONF_SECTION, 'fullpath_sorting')
993
self.fullpath_sorting_enabled = state
995
finfo = self.data[self.get_stack_index()]
996
self.data.sort(key=self.__get_sorting_func())
997
new_index = self.data.index(finfo)
998
self.__repopulate_stack()
999
self.set_stack_index(new_index)
1001
def set_always_remove_trailing_spaces(self, state):
1002
# CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces')
1003
self.always_remove_trailing_spaces = state
1006
#------ Stacked widget management
1007
def get_stack_index(self):
1008
return self.tabs.currentIndex()
1010
def get_current_finfo(self):
1012
return self.data[self.get_stack_index()]
1014
def get_current_editor(self):
1015
return self.tabs.currentWidget()
1017
def get_stack_count(self):
1018
return self.tabs.count()
1020
def set_stack_index(self, index):
1021
self.tabs.setCurrentIndex(index)
1023
def set_tabbar_visible(self, state):
1024
self.tabs.tabBar().setVisible(state)
1026
def remove_from_data(self, index):
1027
self.tabs.blockSignals(True)
1028
self.tabs.removeTab(index)
1029
self.data.pop(index)
1030
self.tabs.blockSignals(False)
1031
self.update_actions()
1032
self.update_filelistdialog()
1034
def __modified_readonly_title(self, title, is_modified, is_readonly):
1035
if is_modified is not None and is_modified:
1037
if is_readonly is not None and is_readonly:
1038
title = "(%s)" % title
1041
def get_tab_text(self, filename, is_modified=None, is_readonly=None):
1042
"""Return tab title"""
1043
return self.__modified_readonly_title(osp.basename(filename),
1044
is_modified, is_readonly)
1046
def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
1047
"""Return tab menu title"""
1048
if self.fullpath_sorting_enabled:
1052
text = self.__modified_readonly_title(text,
1053
is_modified, is_readonly)
1054
if self.tempfile_path is not None\
1055
and filename == encoding.to_unicode_from_fs(self.tempfile_path):
1056
temp_file_str = unicode(_("Temporary file"))
1057
if self.fullpath_sorting_enabled:
1058
return "%s (%s)" % (text, temp_file_str)
1060
return text % (temp_file_str, self.tempfile_path)
1062
if self.fullpath_sorting_enabled:
1065
return text % (osp.basename(filename), osp.dirname(filename))
1067
def __get_sorting_func(self):
1068
if self.fullpath_sorting_enabled:
1069
return lambda item: osp.join(osp.dirname(item.filename),
1070
'_'+osp.basename(item.filename))
1072
return lambda item: osp.basename(item.filename)
1074
def add_to_data(self, finfo, set_current):
1075
self.data.append(finfo)
1076
self.data.sort(key=self.__get_sorting_func())
1077
index = self.data.index(finfo)
1078
fname, editor = finfo.filename, finfo.editor
1079
self.tabs.insertTab(index, editor, get_filetype_icon(fname),
1080
self.get_tab_text(fname))
1081
self.set_stack_title(index, False)
1083
self.set_stack_index(index)
1084
self.current_changed(index)
1085
self.update_actions()
1086
self.update_filelistdialog()
1088
def __repopulate_stack(self):
1089
self.tabs.blockSignals(True)
1091
for finfo in self.data:
1092
icon = get_filetype_icon(finfo.filename)
1093
tab_text = self.get_tab_text(finfo.filename)
1094
tab_tip = self.get_tab_tip(finfo.filename)
1095
index = self.tabs.addTab(finfo.editor, icon, tab_text)
1096
self.tabs.setTabToolTip(index, tab_tip)
1097
self.tabs.blockSignals(False)
1098
self.update_filelistdialog()
1100
def rename_in_data(self, index, new_filename):
1101
finfo = self.data[index]
1102
if osp.splitext(finfo.filename)[1] != osp.splitext(new_filename)[1]:
1103
# File type has changed!
1104
txt = unicode(finfo.editor.get_text_with_eol())
1105
language = get_file_language(new_filename, txt)
1106
finfo.editor.set_language(language)
1107
set_new_index = index == self.get_stack_index()
1108
finfo.filename = new_filename
1109
self.data.sort(key=self.__get_sorting_func())
1110
new_index = self.data.index(finfo)
1111
self.__repopulate_stack()
1113
self.set_stack_index(new_index)
1114
if self.outlineexplorer is not None:
1115
self.outlineexplorer.file_renamed(finfo.editor, finfo.filename)
1118
def set_stack_title(self, index, is_modified):
1119
finfo = self.data[index]
1120
fname = finfo.filename
1121
is_readonly = finfo.editor.isReadOnly()
1122
tab_text = self.get_tab_text(fname, is_modified, is_readonly)
1123
tab_tip = self.get_tab_tip(fname, is_modified, is_readonly)
1124
self.tabs.setTabText(index, tab_text)
1125
self.tabs.setTabToolTip(index, tab_tip)
1128
#------ Context menu
1129
def __setup_menu(self):
1130
"""Setup tab context menu before showing it"""
1133
actions = self.menu_actions
1135
actions = (self.new_action, self.open_action)
1136
self.setFocus() # --> Editor.__get_focus_editortabwidget
1137
add_actions(self.menu, list(actions)+self.__get_split_actions())
1138
self.close_action.setEnabled(self.is_closable)
1141
#------ Hor/Ver splitting
1142
def __get_split_actions(self):
1144
self.newwindow_action = create_action(self, _("New window"),
1145
icon="newwindow.png", tip=_("Create a new editor window"),
1146
triggered=lambda: self.emit(SIGNAL("create_new_window()")))
1148
self.versplit_action = create_action(self, _("Split vertically"),
1149
icon="versplit.png",
1150
tip=_("Split vertically this editor window"),
1151
triggered=lambda: self.emit(SIGNAL("split_vertically()")))
1152
self.horsplit_action = create_action(self, _("Split horizontally"),
1153
icon="horsplit.png",
1154
tip=_("Split horizontally this editor window"),
1155
triggered=lambda: self.emit(SIGNAL("split_horizontally()")))
1156
self.close_action = create_action(self, _("Close this panel"),
1157
icon="close_panel.png", triggered=self.close)
1158
return [None, self.newwindow_action, None,
1159
self.versplit_action, self.horsplit_action, self.close_action]
1161
def reset_orientation(self):
1162
self.horsplit_action.setEnabled(True)
1163
self.versplit_action.setEnabled(True)
1165
def set_orientation(self, orientation):
1166
self.horsplit_action.setEnabled(orientation == Qt.Horizontal)
1167
self.versplit_action.setEnabled(orientation == Qt.Vertical)
1169
def update_actions(self):
1170
state = self.get_stack_count() > 0
1171
self.horsplit_action.setEnabled(state)
1172
self.versplit_action.setEnabled(state)
1176
def get_current_filename(self):
1178
return self.data[self.get_stack_index()].filename
1180
def has_filename(self, filename):
1181
fixpath = lambda path: osp.normcase(osp.realpath(path))
1182
for index, finfo in enumerate(self.data):
1183
if fixpath(filename) == fixpath(finfo.filename):
1186
def set_current_filename(self, filename):
1187
"""Set current filename and return the associated editor instance"""
1188
index = self.has_filename(filename)
1189
if index is not None:
1190
self.set_stack_index(index)
1191
editor = self.data[index].editor
1195
def is_file_opened(self, filename=None):
1196
if filename is None:
1197
# Is there any file opened?
1198
return len(self.data) > 0
1200
return self.has_filename(filename)
1203
#------ Close file, tabwidget...
1204
def close_file(self, index=None, force=False):
1205
"""Close file (index=None -> close current file)
1206
Keep current file index unchanged (if current file
1207
that is being closed)"""
1208
current_index = self.get_stack_index()
1209
count = self.get_stack_count()
1212
index = current_index
1214
self.find_widget.set_editor(None)
1218
if current_index == index:
1219
new_index = self._get_previous_file_index()
1221
new_index = current_index
1222
is_ok = force or self.save_if_changed(cancelable=True, index=index)
1224
finfo = self.data[index]
1225
self.threadmanager.close_threads(finfo)
1226
# Removing editor reference from outline explorer settings:
1227
if self.outlineexplorer is not None:
1228
self.outlineexplorer.remove_editor(finfo.editor)
1230
self.remove_from_data(index)
1232
# We pass self object ID as a QString, because otherwise it would
1233
# depend on the platform: long for 64bit, int for 32bit. Replacing
1234
# by long all the time is not working on some 32bit platforms
1235
# (see Issue 1094, Issue 1098)
1236
self.emit(SIGNAL('close_file(QString,int)'), str(id(self)), index)
1238
if not self.data and self.is_closable:
1239
# editortabwidget is empty: removing it
1240
# (if it's not the first editortabwidget)
1242
self.emit(SIGNAL('opened_files_list_changed()'))
1243
self.emit(SIGNAL('update_code_analysis_actions()'))
1244
self._refresh_outlineexplorer()
1245
self.emit(SIGNAL('refresh_file_dependent_actions()'))
1246
self.emit(SIGNAL('update_plugin_title()'))
1248
if new_index is not None:
1249
if index < new_index:
1251
self.set_stack_index(new_index)
1254
def close_all_files(self):
1255
"""Close all opened scripts"""
1256
while self.close_file():
1261
def save_if_changed(self, cancelable=False, index=None):
1262
"""Ask user to save file if modified"""
1264
indexes = range(self.get_stack_count())
1267
buttons = QMessageBox.Yes | QMessageBox.No
1269
buttons |= QMessageBox.Cancel
1271
for index in indexes:
1272
if self.data[index].editor.document().isModified():
1278
buttons |= QMessageBox.YesAll | QMessageBox.NoAll
1280
for index in indexes:
1281
self.set_stack_index(index)
1282
finfo = self.data[index]
1283
if finfo.filename == self.tempfile_path or yes_all:
1286
elif finfo.editor.document().isModified():
1287
answer = QMessageBox.question(self, self.title,
1288
_("<b>%s</b> has been modified."
1289
"<br>Do you want to save changes?"
1290
) % osp.basename(finfo.filename),
1292
if answer == QMessageBox.Yes:
1295
elif answer == QMessageBox.YesAll:
1299
elif answer == QMessageBox.NoAll:
1301
elif answer == QMessageBox.Cancel:
1305
def save(self, index=None, force=False):
1308
# Save the currently edited file
1309
if not self.get_stack_count():
1311
index = self.get_stack_index()
1313
finfo = self.data[index]
1314
if not finfo.editor.document().isModified() and not force:
1316
if not osp.isfile(finfo.filename) and not force:
1317
# File has not been saved yet
1318
return self.save_as(index=index)
1319
if self.always_remove_trailing_spaces:
1320
self.remove_trailing_spaces(index)
1321
txt = unicode(finfo.editor.get_text_with_eol())
1323
finfo.encoding = encoding.write(txt, finfo.filename,
1325
finfo.newly_created = False
1326
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
1327
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1329
# We pass self object ID as a QString, because otherwise it would
1330
# depend on the platform: long for 64bit, int for 32bit. Replacing
1331
# by long all the time is not working on some 32bit platforms
1332
# (see Issue 1094, Issue 1098)
1333
self.emit(SIGNAL('file_saved(QString,int,QString)'),
1334
str(id(self)), index, finfo.filename)
1336
finfo.editor.document().setModified(False)
1337
self.modification_changed(index=index)
1338
self.analyze_script(index)
1339
codeeditor.validate_rope_project()
1341
#XXX CodeEditor-only: re-scan the whole text to rebuild outline
1342
# explorer data from scratch (could be optimized because
1343
# rehighlighting text means searching for all syntax coloring
1344
# patterns instead of only searching for class/def patterns which
1345
# would be sufficient for outline explorer data.
1346
finfo.editor.rehighlight()
1348
self._refresh_outlineexplorer(index)
1350
except EnvironmentError, error:
1351
QMessageBox.critical(self, _("Save"),
1352
_("<b>Unable to save script '%s'</b>"
1353
"<br><br>Error message:<br>%s"
1354
) % (osp.basename(finfo.filename),
1358
def file_saved_in_other_editorstack(self, index, filename):
1360
File was just saved in another editorstack, let's synchronize!
1361
This avoid file to be automatically reloaded
1363
Filename is passed in case file was just saved as another name
1365
finfo = self.data[index]
1366
finfo.newly_created = False
1367
finfo.filename = unicode(filename)
1368
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1370
def select_savename(self, original_filename):
1371
self.emit(SIGNAL('redirect_stdio(bool)'), False)
1372
filename, _selfilter = getsavefilename(self, _("Save Python script"),
1373
original_filename, EDIT_FILTERS)
1374
self.emit(SIGNAL('redirect_stdio(bool)'), True)
1376
return osp.normpath(filename)
1378
def save_as(self, index=None):
1379
"""Save file as..."""
1381
# Save the currently edited file
1382
index = self.get_stack_index()
1383
finfo = self.data[index]
1384
filename = self.select_savename(finfo.filename)
1386
ao_index = self.has_filename(filename)
1387
# Note: ao_index == index --> saving an untitled file
1388
if ao_index and ao_index != index:
1389
if not self.close_file(ao_index):
1391
if ao_index < index:
1393
new_index = self.rename_in_data(index, new_filename=filename)
1394
ok = self.save(index=new_index, force=True)
1395
self.refresh(new_index)
1396
self.set_stack_index(new_index)
1402
"""Save all opened files"""
1404
for index in range(self.get_stack_count()):
1405
if self.data[index].editor.document().isModified():
1406
folders.add(osp.dirname(self.data[index].filename))
1410
def start_stop_analysis_timer(self):
1411
self.is_analysis_done = False
1412
if self.realtime_analysis_enabled:
1413
self.analysis_timer.stop()
1414
self.analysis_timer.start()
1416
def analyze_script(self, index=None):
1417
"""Analyze current script with pyflakes + find todos"""
1418
if self.is_analysis_done:
1421
index = self.get_stack_index()
1423
finfo = self.data[index]
1424
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
1425
if run_pyflakes or run_pep8:
1426
finfo.run_code_analysis(run_pyflakes, run_pep8)
1427
if self.todolist_enabled:
1428
finfo.run_todo_finder()
1429
self.is_analysis_done = True
1431
def set_analysis_results(self, index, analysis_results):
1432
"""Synchronize analysis results between editorstacks"""
1433
self.data[index].set_analysis_results(analysis_results)
1435
def get_analysis_results(self):
1437
return self.data[self.get_stack_index()].analysis_results
1439
def set_todo_results(self, index, todo_results):
1440
"""Synchronize todo results between editorstacks"""
1441
self.data[index].set_todo_results(todo_results)
1443
def get_todo_results(self):
1445
return self.data[self.get_stack_index()].todo_results
1447
def current_changed(self, index):
1448
"""Stack index has changed"""
1449
# count = self.get_stack_count()
1450
# for btn in (self.filelist_btn, self.previous_btn, self.next_btn):
1451
# btn.setEnabled(count > 1)
1453
editor = self.get_current_editor()
1457
print >>STDOUT, "setfocusto:", editor
1459
self.emit(SIGNAL('reset_statusbar()'))
1460
self.emit(SIGNAL('opened_files_list_changed()'))
1462
# Index history management
1463
id_list = [id(self.tabs.widget(_i))
1464
for _i in range(self.tabs.count())]
1465
for _id in self.stack_history[:]:
1466
if _id not in id_list:
1467
self.stack_history.pop(self.stack_history.index(_id))
1468
current_id = id(self.tabs.widget(index))
1469
while current_id in self.stack_history:
1470
self.stack_history.pop(self.stack_history.index(current_id))
1471
self.stack_history.append(current_id)
1473
print >>STDOUT, "current_changed:", index, self.data[index].editor,
1474
print >>STDOUT, self.data[index].editor.get_document_id()
1476
self.emit(SIGNAL('update_plugin_title()'))
1477
if editor is not None:
1478
self.emit(SIGNAL('current_file_changed(QString,int)'),
1479
self.data[index].filename, editor.get_position('cursor'))
1481
def _get_previous_file_index(self):
1482
if len(self.stack_history) > 1:
1483
last = len(self.stack_history)-1
1484
w_id = self.stack_history.pop(last)
1485
self.stack_history.insert(0, w_id)
1486
last_id = self.stack_history[last]
1487
for _i in range(self.tabs.count()):
1488
if id(self.tabs.widget(_i)) == last_id:
1491
def go_to_previous_file(self):
1493
prev_index = self._get_previous_file_index()
1494
if prev_index is not None:
1495
self.set_stack_index(prev_index)
1496
elif len(self.stack_history) == 0 and self.get_stack_count():
1497
self.stack_history = [id(self.tabs.currentWidget())]
1499
def go_to_next_file(self):
1500
"""Ctrl+Shift+Tab"""
1501
if len(self.stack_history) > 1:
1502
last = len(self.stack_history)-1
1503
w_id = self.stack_history.pop(0)
1504
self.stack_history.append(w_id)
1505
last_id = self.stack_history[last]
1506
for _i in range(self.tabs.count()):
1507
if id(self.tabs.widget(_i)) == last_id:
1508
self.set_stack_index(_i)
1510
elif len(self.stack_history) == 0 and self.get_stack_count():
1511
self.stack_history = [id(self.tabs.currentWidget())]
1513
def focus_changed(self):
1514
"""Editor focus has changed"""
1515
fwidget = QApplication.focusWidget()
1516
for finfo in self.data:
1517
if fwidget is finfo.editor:
1519
self.emit(SIGNAL("editor_focus_changed()"))
1521
def _refresh_outlineexplorer(self, index=None, update=True, clear=False):
1522
"""Refresh outline explorer panel"""
1523
oe = self.outlineexplorer
1527
index = self.get_stack_index()
1530
finfo = self.data[index]
1531
# oe_visible: if outline explorer is not visible, maybe the whole
1532
# GUI is not visible (Spyder is starting up) -> in this case,
1533
# it is necessary to update the outline explorer
1534
oe_visible = oe.isVisible() or not self.isVisible()
1535
if self.outlineexplorer_enabled and finfo.editor.is_python() \
1539
oe.set_current_editor(finfo.editor, finfo.filename,
1540
update=update, clear=clear)
1542
oe.setEnabled(False)
1544
def __refresh_statusbar(self, index):
1545
"""Refreshing statusbar widgets"""
1546
finfo = self.data[index]
1547
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
1548
# Refresh cursor position status:
1549
line, index = finfo.editor.get_cursor_line_column()
1550
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
1553
def __refresh_readonly(self, index):
1554
finfo = self.data[index]
1555
read_only = not QFileInfo(finfo.filename).isWritable()
1556
if not osp.isfile(finfo.filename):
1557
# This is an 'untitledX.py' file (newly created)
1559
finfo.editor.setReadOnly(read_only)
1560
self.emit(SIGNAL('readonly_changed(bool)'), read_only)
1562
def __check_file_status(self, index):
1563
"""Check if file has been changed in any way outside Spyder:
1564
1. removed, moved or renamed outside Spyder
1565
2. modified outside Spyder"""
1566
if self.__file_status_flag:
1567
# Avoid infinite loop: when the QMessageBox.question pops, it
1568
# gets focus and then give it back to the CodeEditor instance,
1569
# triggering a refresh cycle which calls this method
1571
self.__file_status_flag = True
1573
finfo = self.data[index]
1574
if finfo.newly_created:
1576
name = osp.basename(finfo.filename)
1577
# First, testing if file still exists (removed, moved or offline):
1578
if not osp.isfile(finfo.filename):
1579
answer = QMessageBox.warning(self, self.title,
1580
_("<b>%s</b> is unavailable "
1581
"(this file may have been removed, moved "
1582
"or renamed outside Spyder)."
1583
"<br>Do you want to close it?") % name,
1584
QMessageBox.Yes | QMessageBox.No)
1585
if answer == QMessageBox.Yes:
1586
self.close_file(index)
1588
finfo.newly_created = True
1589
finfo.editor.document().setModified(True)
1590
self.modification_changed(index=index)
1592
# Else, testing if it has been modified elsewhere:
1593
lastm = QFileInfo(finfo.filename).lastModified()
1594
if unicode(lastm.toString()) \
1595
!= unicode(finfo.lastmodified.toString()):
1596
if finfo.editor.document().isModified():
1597
answer = QMessageBox.question(self,
1599
_("<b>%s</b> has been modified outside Spyder."
1600
"<br>Do you want to reload it and loose all "
1601
"your changes?") % name,
1602
QMessageBox.Yes | QMessageBox.No)
1603
if answer == QMessageBox.Yes:
1606
finfo.lastmodified = lastm
1610
# Finally, resetting temporary flag:
1611
self.__file_status_flag = False
1613
def refresh(self, index=None):
1614
"""Refresh tabwidget"""
1616
index = self.get_stack_index()
1617
# Set current editor
1618
if self.get_stack_count():
1619
index = self.get_stack_index()
1620
finfo = self.data[index]
1621
editor = finfo.editor
1623
self._refresh_outlineexplorer(index, update=False)
1624
self.emit(SIGNAL('update_code_analysis_actions()'))
1625
self.__refresh_statusbar(index)
1626
self.__refresh_readonly(index)
1627
self.__check_file_status(index)
1628
self.emit(SIGNAL('update_plugin_title()'))
1631
# Update the modification-state-dependent parameters
1632
self.modification_changed()
1633
# Update FindReplace binding
1634
self.find_widget.set_editor(editor, refresh=False)
1636
def modification_changed(self, state=None, index=None, editor_id=None):
1638
Current editor's modification state has changed
1639
--> change tab title depending on new modification state
1640
--> enable/disable save/save all actions
1642
if editor_id is not None:
1643
for index, _finfo in enumerate(self.data):
1644
if id(_finfo.editor) == editor_id:
1646
# This must be done before refreshing save/save all actions:
1647
# (otherwise Save/Save all actions will always be enabled)
1648
self.emit(SIGNAL('opened_files_list_changed()'))
1651
index = self.get_stack_index()
1654
finfo = self.data[index]
1656
state = finfo.editor.document().isModified()
1657
self.set_stack_title(index, state)
1658
# Toggle save/save all actions state
1659
self.save_action.setEnabled(state)
1660
self.emit(SIGNAL('refresh_save_all_action()'))
1661
# Refreshing eol mode
1662
eol_chars = finfo.editor.get_line_separator()
1663
os_name = sourcecode.get_os_name_from_eol_chars(eol_chars)
1664
self.emit(SIGNAL('refresh_eol_chars(QString)'), os_name)
1667
#------ Load, reload
1668
def reload(self, index):
1669
"""Reload file from disk"""
1670
finfo = self.data[index]
1671
txt, finfo.encoding = encoding.read(finfo.filename)
1672
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1673
position = finfo.editor.get_position('cursor')
1674
finfo.editor.set_text(txt)
1675
finfo.editor.document().setModified(False)
1676
finfo.editor.set_cursor_position(position)
1677
codeeditor.validate_rope_project()
1678
self._refresh_outlineexplorer(index, update=True, clear=True)
1681
"""Revert file from disk"""
1682
index = self.get_stack_index()
1683
finfo = self.data[index]
1684
filename = finfo.filename
1685
if finfo.editor.document().isModified():
1686
answer = QMessageBox.warning(self, self.title,
1687
_("All changes to <b>%s</b> will be lost."
1688
"<br>Do you want to revert file from disk?"
1689
) % osp.basename(filename),
1690
QMessageBox.Yes|QMessageBox.No)
1691
if answer != QMessageBox.Yes:
1695
def create_new_editor(self, fname, enc, txt,
1696
set_current, new=False, cloned_from=None):
1698
Create a new editor instance
1699
Returns finfo object (instead of editor as in previous releases)
1701
editor = codeeditor.CodeEditor(self)
1702
finfo = FileInfo(fname, enc, editor, new, self.threadmanager)
1703
self.add_to_data(finfo, set_current)
1704
self.connect(finfo, SIGNAL(
1705
"send_to_inspector(QString,QString,QString,QString,bool)"),
1706
self.send_to_inspector)
1707
self.connect(finfo, SIGNAL('analysis_results_changed()'),
1708
lambda: self.emit(SIGNAL('analysis_results_changed()')))
1709
self.connect(finfo, SIGNAL('todo_results_changed()'),
1710
lambda: self.emit(SIGNAL('todo_results_changed()')))
1711
self.connect(finfo, SIGNAL("edit_goto(QString,int,QString)"),
1712
lambda fname, lineno, name:
1713
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
1714
fname, lineno, name))
1715
self.connect(finfo, SIGNAL("save_breakpoints(QString,QString)"),
1717
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
1719
language = get_file_language(fname, txt)
1720
editor.setup_editor(
1721
linenumbers=self.linenumbers_enabled,
1722
edge_line=self.edgeline_enabled,
1723
edge_line_column=self.edgeline_column, language=language,
1724
markers=self.has_markers(), font=self.default_font,
1725
color_scheme=self.color_scheme,
1726
wrap=self.wrap_enabled, tab_mode=self.tabmode_enabled,
1727
intelligent_backspace=self.intelligent_backspace_enabled,
1728
highlight_current_line=self.highlight_current_line_enabled,
1729
occurence_highlighting=self.occurence_highlighting_enabled,
1730
codecompletion_auto=self.codecompletion_auto_enabled,
1731
codecompletion_case=self.codecompletion_case_enabled,
1732
codecompletion_single=self.codecompletion_single_enabled,
1733
codecompletion_enter=self.codecompletion_enter_enabled,
1734
calltips=self.calltips_enabled,
1735
go_to_definition=self.go_to_definition_enabled,
1736
close_parentheses=self.close_parentheses_enabled,
1737
close_quotes=self.close_quotes_enabled,
1738
add_colons=self.add_colons_enabled,
1739
auto_unindent=self.auto_unindent_enabled,
1740
indent_chars=self.indent_chars,
1741
tab_stop_width=self.tab_stop_width,
1742
cloned_from=cloned_from)
1743
if cloned_from is None:
1744
editor.set_text(txt)
1745
editor.document().setModified(False)
1746
self.connect(finfo, SIGNAL('text_changed_at(QString,int)'),
1747
lambda fname, position:
1748
self.emit(SIGNAL('text_changed_at(QString,int)'),
1750
self.connect(editor, SIGNAL('cursorPositionChanged(int,int)'),
1751
self.editor_cursor_position_changed)
1752
self.connect(editor, SIGNAL('textChanged()'),
1753
self.start_stop_analysis_timer)
1754
self.connect(editor, SIGNAL('modificationChanged(bool)'),
1755
lambda state: self.modification_changed(state,
1756
editor_id=id(editor)))
1757
self.connect(editor, SIGNAL("focus_in()"), self.focus_changed)
1758
if self.outlineexplorer is not None:
1759
# Removing editor reference from outline explorer settings:
1760
self.connect(editor, SIGNAL("destroyed()"),
1762
self.outlineexplorer.remove_editor(obj))
1764
self.find_widget.set_editor(editor)
1766
self.emit(SIGNAL('refresh_file_dependent_actions()'))
1767
self.modification_changed(index=self.data.index(finfo))
1771
def editor_cursor_position_changed(self, line, index):
1772
"""Cursor position of one of the editor in the stack has changed"""
1773
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
1776
def send_to_inspector(self, qstr1, qstr2=None, qstr3=None,
1777
qstr4=None, force=False):
1778
"""qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text"""
1779
if not force and not self.inspector_enabled:
1781
if self.inspector is not None \
1782
and (force or self.inspector.dockwidget.isVisible()):
1783
# ObjectInspector widget exists and is visible
1785
self.inspector.set_object_text(qstr1, ignore_unknown=True,
1786
force_refresh=force)
1788
objtxt = unicode(qstr1)
1789
title = objtxt.split('.')[-1]
1790
argspec = unicode(qstr2)
1791
note = unicode(qstr3)
1792
doc = unicode(qstr4)
1793
text = {'obj_text': objtxt, 'title': title, 'argspec': argspec,
1794
'note': note, 'doc': doc}
1795
self.inspector.set_rope_doc(text, force_refresh=force)
1796
editor = self.get_current_editor()
1799
def new(self, filename, encoding, text):
1801
Create new filename with *encoding* and *text*
1803
finfo = self.create_new_editor(filename, encoding, text,
1804
set_current=False, new=True)
1805
finfo.editor.set_cursor_position('eof')
1806
finfo.editor.insert_text(os.linesep)
1809
def load(self, filename, set_current=True):
1811
Load filename, create an editor instance and return it
1812
*Warning* This is loading file, creating editor but not executing
1813
the source code analysis -- the analysis must be done by the editor
1814
plugin (in case multiple editorstack instances are handled)
1816
filename = osp.abspath(unicode(filename))
1817
self.emit(SIGNAL('starting_long_process(QString)'),
1818
_("Loading %s...") % filename)
1819
text, enc = encoding.read(filename)
1820
finfo = self.create_new_editor(filename, enc, text, set_current)
1821
index = self.data.index(finfo)
1822
self._refresh_outlineexplorer(index, update=True)
1823
self.emit(SIGNAL('ending_long_process(QString)'), "")
1824
if self.isVisible() and self.checkeolchars_enabled \
1825
and sourcecode.has_mixed_eol_chars(text):
1826
name = osp.basename(filename)
1827
QMessageBox.warning(self, self.title,
1828
_("<b>%s</b> contains mixed end-of-line "
1829
"characters.<br>Spyder will fix this "
1830
"automatically.") % name,
1832
self.set_os_eol_chars(index)
1833
self.is_analysis_done = False
1836
def set_os_eol_chars(self, index=None):
1838
index = self.get_stack_index()
1839
finfo = self.data[index]
1840
eol_chars = sourcecode.get_eol_chars_from_os_name(os.name)
1841
finfo.editor.set_eol_chars(eol_chars)
1842
finfo.editor.document().setModified(True)
1844
def remove_trailing_spaces(self, index=None):
1845
"""Remove trailing spaces"""
1847
index = self.get_stack_index()
1848
finfo = self.data[index]
1849
finfo.editor.remove_trailing_spaces()
1851
def fix_indentation(self, index=None):
1852
"""Replace tab characters by spaces"""
1854
index = self.get_stack_index()
1855
finfo = self.data[index]
1856
finfo.editor.fix_indentation()
1859
def run_selection_or_block(self):
1861
Run selected text in console and set focus to console
1862
*or*, if there is no selection,
1863
Run current block of lines in console and go to next block
1865
self.emit(SIGNAL('external_console_execute_lines(QString)'),
1866
self.get_current_editor().get_executable_text())
1868
#------ Drag and drop
1869
def dragEnterEvent(self, event):
1870
"""Reimplement Qt method
1871
Inform Qt about the types of data that the widget accepts"""
1872
source = event.mimeData()
1873
if source.hasUrls():
1874
if mimedata2url(source, extlist=EDIT_EXT):
1875
event.acceptProposedAction()
1878
elif source.hasText():
1879
event.acceptProposedAction()
1883
def dropEvent(self, event):
1884
"""Reimplement Qt method
1885
Unpack dropped data and handle it"""
1886
source = event.mimeData()
1887
if source.hasUrls():
1888
files = mimedata2url(source, extlist=EDIT_EXT)
1891
self.emit(SIGNAL('plugin_load(QString)'), fname)
1892
elif source.hasText():
1893
editor = self.get_current_editor()
1894
if editor is not None:
1895
editor.insert_text( source.text() )
1896
event.acceptProposedAction()
1899
class EditorSplitter(QSplitter):
1900
def __init__(self, parent, plugin, menu_actions, first=False,
1901
register_editorstack_cb=None, unregister_editorstack_cb=None):
1902
QSplitter.__init__(self, parent)
1903
self.setAttribute(Qt.WA_DeleteOnClose)
1904
self.setChildrenCollapsible(False)
1906
self.toolbar_list = None
1907
self.menu_list = None
1909
self.plugin = plugin
1911
if register_editorstack_cb is None:
1912
register_editorstack_cb = self.plugin.register_editorstack
1913
self.register_editorstack_cb = register_editorstack_cb
1914
if unregister_editorstack_cb is None:
1915
unregister_editorstack_cb = self.plugin.unregister_editorstack
1916
self.unregister_editorstack_cb = unregister_editorstack_cb
1918
self.menu_actions = menu_actions
1919
self.editorstack = EditorStack(self, menu_actions)
1920
self.register_editorstack_cb(self.editorstack)
1922
self.plugin.clone_editorstack(editorstack=self.editorstack)
1923
self.connect(self.editorstack, SIGNAL("destroyed()"),
1924
lambda: self.editorstack_closed())
1925
self.connect(self.editorstack, SIGNAL("split_vertically()"),
1926
lambda: self.split(orientation=Qt.Vertical))
1927
self.connect(self.editorstack, SIGNAL("split_horizontally()"),
1928
lambda: self.split(orientation=Qt.Horizontal))
1929
self.addWidget(self.editorstack)
1931
def closeEvent(self, event):
1932
QSplitter.closeEvent(self, event)
1934
self.emit(SIGNAL('destroyed()'))
1936
def __give_focus_to_remaining_editor(self):
1937
focus_widget = self.plugin.get_focus_widget()
1938
if focus_widget is not None:
1939
focus_widget.setFocus()
1941
def editorstack_closed(self):
1943
print >>STDOUT, "method 'editorstack_closed':"
1944
print >>STDOUT, " self :", self
1945
# print >>STDOUT, " sender:", self.sender()
1946
self.unregister_editorstack_cb(self.editorstack)
1947
self.editorstack = None
1949
close_splitter = self.count() == 1
1950
except RuntimeError:
1951
# editorsplitter has been destroyed (happens when closing a
1952
# EditorMainWindow instance)
1955
# editorstack just closed was the last widget in this QSplitter
1958
self.__give_focus_to_remaining_editor()
1960
def editorsplitter_closed(self):
1962
print >>STDOUT, "method 'editorsplitter_closed':"
1963
print >>STDOUT, " self :", self
1964
# print >>STDOUT, " sender:", self.sender()
1966
close_splitter = self.count() == 1 and self.editorstack is None
1967
except RuntimeError:
1968
# editorsplitter has been destroyed (happens when closing a
1969
# EditorMainWindow instance)
1972
# editorsplitter just closed was the last widget in this QSplitter
1975
elif self.count() == 2 and self.editorstack:
1976
# back to the initial state: a single editorstack instance,
1977
# as a single widget in this QSplitter: orientation may be changed
1978
self.editorstack.reset_orientation()
1979
self.__give_focus_to_remaining_editor()
1981
def split(self, orientation=Qt.Vertical):
1982
self.setOrientation(orientation)
1983
self.editorstack.set_orientation(orientation)
1984
editorsplitter = EditorSplitter(self.parent(), self.plugin,
1986
register_editorstack_cb=self.register_editorstack_cb,
1987
unregister_editorstack_cb=self.unregister_editorstack_cb)
1988
self.addWidget(editorsplitter)
1989
self.connect(editorsplitter, SIGNAL("destroyed()"),
1990
lambda: self.editorsplitter_closed())
1991
current_editor = editorsplitter.editorstack.get_current_editor()
1992
if current_editor is not None:
1993
current_editor.setFocus()
1995
def iter_editorstacks(self):
1996
editorstacks = [(self.widget(0), self.orientation())]
1997
if self.count() > 1:
1998
editorsplitter = self.widget(1)
1999
editorstacks += editorsplitter.iter_editorstacks()
2002
def get_layout_settings(self):
2003
"""Return layout state"""
2005
for editorstack, orientation in self.iter_editorstacks():
2006
clines = [finfo.editor.get_cursor_line_number()
2007
for finfo in editorstack.data]
2008
cfname = editorstack.get_current_filename()
2009
splitsettings.append((orientation == Qt.Vertical, cfname, clines))
2010
return dict(hexstate=str(self.saveState().toHex()),
2011
sizes=self.sizes(), splitsettings=splitsettings)
2013
def set_layout_settings(self, settings):
2014
"""Restore layout state"""
2015
splitsettings = settings.get('splitsettings')
2016
if splitsettings is None:
2020
for index, (is_vertical, cfname, clines) in enumerate(splitsettings):
2022
splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal)
2023
splitter = splitter.widget(1)
2024
editorstack = splitter.widget(0)
2025
for index, finfo in enumerate(editorstack.data):
2026
editor = finfo.editor
2027
editor.go_to_line(clines[index])
2028
editorstack.set_current_filename(cfname)
2029
hexstate = settings.get('hexstate')
2030
if hexstate is not None:
2031
self.restoreState( QByteArray().fromHex(str(hexstate)) )
2032
sizes = settings.get('sizes')
2033
if sizes is not None:
2034
self.setSizes(sizes)
2035
if editor is not None:
2040
class EditorWidget(QSplitter):
2041
def __init__(self, parent, plugin, menu_actions, show_fullpath,
2042
fullpath_sorting, show_all_files, show_comments):
2043
QSplitter.__init__(self, parent)
2044
self.setAttribute(Qt.WA_DeleteOnClose)
2046
statusbar = parent.statusBar() # Create a status bar
2047
self.readwrite_status = ReadWriteStatus(self, statusbar)
2048
self.eol_status = EOLStatus(self, statusbar)
2049
self.encoding_status = EncodingStatus(self, statusbar)
2050
self.cursorpos_status = CursorPositionStatus(self, statusbar)
2052
self.editorstacks = []
2054
self.plugin = plugin
2056
self.find_widget = FindReplace(self, enable_replace=True)
2057
self.plugin.register_widget_shortcuts("Editor", self.find_widget)
2058
self.find_widget.hide()
2059
self.outlineexplorer = OutlineExplorerWidget(self,
2060
show_fullpath=show_fullpath,
2061
fullpath_sorting=fullpath_sorting,
2062
show_all_files=show_all_files,
2063
show_comments=show_comments)
2064
self.connect(self.outlineexplorer,
2065
SIGNAL("edit_goto(QString,int,QString)"),
2066
lambda filenames, goto, word:
2067
plugin.load(filenames=filenames, goto=goto, word=word,
2068
editorwindow=self.parent()))
2070
editor_widgets = QWidget(self)
2071
editor_layout = QVBoxLayout()
2072
editor_layout.setContentsMargins(0, 0, 0, 0)
2073
editor_widgets.setLayout(editor_layout)
2074
editorsplitter = EditorSplitter(self, plugin, menu_actions,
2075
register_editorstack_cb=self.register_editorstack,
2076
unregister_editorstack_cb=self.unregister_editorstack)
2077
self.editorsplitter = editorsplitter
2078
editor_layout.addWidget(editorsplitter)
2079
editor_layout.addWidget(self.find_widget)
2081
splitter = QSplitter(self)
2082
splitter.setContentsMargins(0, 0, 0, 0)
2083
splitter.addWidget(editor_widgets)
2084
splitter.addWidget(self.outlineexplorer)
2085
splitter.setStretchFactor(0, 5)
2086
splitter.setStretchFactor(1, 1)
2088
# Refreshing outline explorer
2089
for index in range(editorsplitter.editorstack.get_stack_count()):
2090
editorsplitter.editorstack._refresh_outlineexplorer(index,
2093
def register_editorstack(self, editorstack):
2094
self.editorstacks.append(editorstack)
2096
print >>STDOUT, "EditorWidget.register_editorstack:", editorstack
2097
self.__print_editorstacks()
2098
self.plugin.last_focus_editorstack[self.parent()] = editorstack
2099
editorstack.set_closable( len(self.editorstacks) > 1 )
2100
editorstack.set_outlineexplorer(self.outlineexplorer)
2101
editorstack.set_find_widget(self.find_widget)
2102
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2103
self.readwrite_status.hide)
2104
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2105
self.encoding_status.hide)
2106
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2107
self.cursorpos_status.hide)
2108
self.connect(editorstack, SIGNAL('readonly_changed(bool)'),
2109
self.readwrite_status.readonly_changed)
2110
self.connect(editorstack, SIGNAL('encoding_changed(QString)'),
2111
self.encoding_status.encoding_changed)
2112
self.connect(editorstack,
2113
SIGNAL('editor_cursor_position_changed(int,int)'),
2114
self.cursorpos_status.cursor_position_changed)
2115
self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'),
2116
self.eol_status.eol_changed)
2117
self.plugin.register_editorstack(editorstack)
2118
oe_btn = create_toolbutton(self)
2119
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
2120
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
2122
def __print_editorstacks(self):
2123
print >>STDOUT, "%d editorstack(s) in editorwidget:" \
2124
% len(self.editorstacks)
2125
for edst in self.editorstacks:
2126
print >>STDOUT, " ", edst
2128
def unregister_editorstack(self, editorstack):
2130
print >>STDOUT, "EditorWidget.unregister_editorstack:", editorstack
2131
self.plugin.unregister_editorstack(editorstack)
2132
self.editorstacks.pop(self.editorstacks.index(editorstack))
2134
self.__print_editorstacks()
2137
class EditorMainWindow(QMainWindow):
2138
def __init__(self, plugin, menu_actions, toolbar_list, menu_list,
2139
show_fullpath, fullpath_sorting, show_all_files,
2141
QMainWindow.__init__(self)
2142
self.setAttribute(Qt.WA_DeleteOnClose)
2144
self.window_size = None
2146
self.editorwidget = EditorWidget(self, plugin, menu_actions,
2147
show_fullpath, fullpath_sorting,
2148
show_all_files, show_comments)
2149
self.setCentralWidget(self.editorwidget)
2151
# Give focus to current editor to update/show all status bar widgets
2152
editorstack = self.editorwidget.editorsplitter.editorstack
2153
editor = editorstack.get_current_editor()
2154
if editor is not None:
2157
self.setWindowTitle("Spyder - %s" % plugin.windowTitle())
2158
self.setWindowIcon(plugin.windowIcon())
2162
for title, actions in toolbar_list:
2163
toolbar = self.addToolBar(title)
2164
toolbar.setObjectName(str(id(toolbar)))
2165
add_actions(toolbar, actions)
2166
toolbars.append(toolbar)
2168
quit_action = create_action(self, _("Close window"),
2169
icon="close_panel.png",
2170
tip=_("Close this window"),
2171
triggered=self.close)
2173
for index, (title, actions) in enumerate(menu_list):
2174
menu = self.menuBar().addMenu(title)
2177
add_actions(menu, actions+[None, quit_action])
2179
add_actions(menu, actions)
2182
def resizeEvent(self, event):
2183
"""Reimplement Qt method"""
2184
if not self.isMaximized() and not self.isFullScreen():
2185
self.window_size = self.size()
2186
QMainWindow.resizeEvent(self, event)
2188
def closeEvent(self, event):
2189
"""Reimplement Qt method"""
2190
QMainWindow.closeEvent(self, event)
2192
self.emit(SIGNAL('destroyed()'))
2193
for editorstack in self.editorwidget.editorstacks[:]:
2195
print >>STDOUT, "--> destroy_editorstack:", editorstack
2196
editorstack.emit(SIGNAL('destroyed()'))
2198
def get_layout_settings(self):
2199
"""Return layout state"""
2200
splitsettings = self.editorwidget.editorsplitter.get_layout_settings()
2201
return dict(size=(self.window_size.width(), self.window_size.height()),
2202
pos=(self.pos().x(), self.pos().y()),
2203
is_maximized=self.isMaximized(),
2204
is_fullscreen=self.isFullScreen(),
2205
hexstate=str(self.saveState().toHex()),
2206
splitsettings=splitsettings)
2208
def set_layout_settings(self, settings):
2209
"""Restore layout state"""
2210
size = settings.get('size')
2211
if size is not None:
2212
self.resize( QSize(*size) )
2213
self.window_size = self.size()
2214
pos = settings.get('pos')
2216
self.move( QPoint(*pos) )
2217
hexstate = settings.get('hexstate')
2218
if hexstate is not None:
2219
self.restoreState( QByteArray().fromHex(str(hexstate)) )
2220
if settings.get('is_maximized'):
2221
self.setWindowState(Qt.WindowMaximized)
2222
if settings.get('is_fullscreen'):
2223
self.setWindowState(Qt.WindowFullScreen)
2224
splitsettings = settings.get('splitsettings')
2225
if splitsettings is not None:
2226
self.editorwidget.editorsplitter.set_layout_settings(splitsettings)
2229
class EditorPluginExample(QSplitter):
2231
QSplitter.__init__(self)
2235
self.editorstacks = []
2236
self.editorwindows = []
2238
self.last_focus_editorstack = {} # fake
2240
self.find_widget = FindReplace(self, enable_replace=True)
2241
self.outlineexplorer = OutlineExplorerWidget(self, show_fullpath=False,
2242
show_all_files=False)
2243
self.connect(self.outlineexplorer,
2244
SIGNAL("edit_goto(QString,int,QString)"),
2247
editor_widgets = QWidget(self)
2248
editor_layout = QVBoxLayout()
2249
editor_layout.setContentsMargins(0, 0, 0, 0)
2250
editor_widgets.setLayout(editor_layout)
2251
editor_layout.addWidget(EditorSplitter(self, self, menu_actions,
2253
editor_layout.addWidget(self.find_widget)
2255
self.setContentsMargins(0, 0, 0, 0)
2256
self.addWidget(editor_widgets)
2257
self.addWidget(self.outlineexplorer)
2259
self.setStretchFactor(0, 5)
2260
self.setStretchFactor(1, 1)
2262
self.menu_actions = menu_actions
2263
self.toolbar_list = None
2264
self.menu_list = None
2265
self.setup_window([], [])
2267
def go_to_file(self, fname, lineno, text):
2268
editorstack = self.editorstacks[0]
2269
editorstack.set_current_filename(unicode(fname))
2270
editor = editorstack.get_current_editor()
2271
editor.go_to_line(lineno, word=text)
2273
def closeEvent(self, event):
2274
for win in self.editorwindows[:]:
2277
print >>STDOUT, len(self.editorwindows), ":", self.editorwindows
2278
print >>STDOUT, len(self.editorstacks), ":", self.editorstacks
2282
def load(self, fname):
2283
QApplication.processEvents()
2284
editorstack = self.editorstacks[0]
2285
editorstack.load(fname)
2286
editorstack.analyze_script()
2288
def register_editorstack(self, editorstack):
2290
print >>STDOUT, "FakePlugin.register_editorstack:", editorstack
2291
self.editorstacks.append(editorstack)
2292
if self.isAncestorOf(editorstack):
2293
# editorstack is a child of the Editor plugin
2294
editorstack.set_fullpath_sorting_enabled(True)
2295
editorstack.set_closable( len(self.editorstacks) > 1 )
2296
editorstack.set_outlineexplorer(self.outlineexplorer)
2297
editorstack.set_find_widget(self.find_widget)
2298
oe_btn = create_toolbutton(self)
2299
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
2300
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
2302
action = QAction(self)
2303
editorstack.set_io_actions(action, action, action, action)
2304
font = QFont("Courier New")
2305
font.setPointSize(10)
2306
editorstack.set_default_font(font, color_scheme='Spyder')
2307
self.connect(editorstack, SIGNAL('close_file(QString,int)'),
2308
self.close_file_in_all_editorstacks)
2309
self.connect(editorstack, SIGNAL("create_new_window()"),
2310
self.create_new_window)
2311
self.connect(editorstack, SIGNAL('plugin_load(QString)'),
2314
def unregister_editorstack(self, editorstack):
2316
print >>STDOUT, "FakePlugin.unregister_editorstack:", editorstack
2317
self.editorstacks.pop(self.editorstacks.index(editorstack))
2319
def clone_editorstack(self, editorstack):
2320
editorstack.clone_from(self.editorstacks[0])
2322
def setup_window(self, toolbar_list, menu_list):
2323
self.toolbar_list = toolbar_list
2324
self.menu_list = menu_list
2326
def create_new_window(self):
2327
window = EditorMainWindow(self, self.menu_actions,
2328
self.toolbar_list, self.menu_list,
2329
show_fullpath=False, fullpath_sorting=True,
2330
show_all_files=False, show_comments=True)
2331
window.resize(self.size())
2333
self.register_editorwindow(window)
2334
self.connect(window, SIGNAL("destroyed()"),
2335
lambda win=window: self.unregister_editorwindow(win))
2337
def register_editorwindow(self, window):
2339
print >>STDOUT, "register_editorwindowQObject*:", window
2340
self.editorwindows.append(window)
2342
def unregister_editorwindow(self, window):
2344
print >>STDOUT, "unregister_editorwindow:", window
2345
self.editorwindows.pop(self.editorwindows.index(window))
2347
def get_focus_widget(self):
2350
def close_file_in_all_editorstacks(self, editorstack_id_str, index):
2351
for editorstack in self.editorstacks:
2352
if str(id(editorstack)) != editorstack_id_str:
2353
editorstack.blockSignals(True)
2354
editorstack.close_file(index, force=True)
2355
editorstack.blockSignals(False)
2357
def register_widget_shortcuts(self, context, widget):
2362
from spyderlib.utils.qthelpers import qapplication
2363
app = qapplication()
2364
test = EditorPluginExample()
2365
test.resize(900, 700)
2370
test.load("explorer.py")
2371
test.load("dicteditor.py")
2372
test.load("sourcecode/codeeditor.py")
2373
test.load("../spyder.py")
2374
print "Elapsed time: %.3f s" % (time.time()-t0)
2375
sys.exit(app.exec_())
2377
if __name__ == "__main__":
1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2011 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
9
# pylint: disable=C0103
10
# pylint: disable=R0903
11
# pylint: disable=R0911
12
# pylint: disable=R0201
14
from spyderlib.qt import is_pyqt46
15
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QMenu, QFont,
16
QAction, QApplication, QWidget, QHBoxLayout,
17
QLabel, QKeySequence, QShortcut, QMainWindow,
18
QSplitter, QListWidget, QListWidgetItem,
20
from spyderlib.qt.QtCore import (SIGNAL, Qt, QFileInfo, QThread, QObject,
21
QByteArray, QSize, QPoint, QTimer, Slot)
22
from spyderlib.qt.compat import getsavefilename
30
from spyderlib.utils import encoding, sourcecode, programs, codeanalysis
31
from spyderlib.utils.dochelpers import getsignaturesfromtext
32
from spyderlib.utils.module_completion import (module_completion,
33
get_preferred_submodules)
34
from spyderlib.baseconfig import _, DEBUG, STDOUT, STDERR
35
from spyderlib.config import EDIT_FILTERS, EDIT_EXT
36
from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions,
37
mimedata2url, get_filetype_icon,
39
from spyderlib.widgets.tabs import BaseTabs
40
from spyderlib.widgets.findreplace import FindReplace
41
from spyderlib.widgets.editortools import OutlineExplorerWidget
42
from spyderlib.widgets.status import (ReadWriteStatus, EOLStatus,
43
EncodingStatus, CursorPositionStatus)
44
from spyderlib.widgets.sourcecode import syntaxhighlighters, codeeditor
45
from spyderlib.widgets.sourcecode.base import TextEditBaseWidget #analysis:ignore
46
from spyderlib.widgets.sourcecode.codeeditor import Printer #analysis:ignore
47
from spyderlib.widgets.sourcecode.codeeditor import get_file_language
50
class FileListDialog(QDialog):
51
def __init__(self, parent, tabs, fullpath_sorting):
52
QDialog.__init__(self, parent)
54
# Destroying the C++ object right after closing the dialog box,
55
# otherwise it may be garbage-collected in another QThread
56
# (e.g. the editor's analysis thread in Spyder), thus leading to
57
# a segmentation fault on UNIX or an application crash on Windows
58
self.setAttribute(Qt.WA_DeleteOnClose)
62
self.setWindowIcon(get_icon('filelist.png'))
63
self.setWindowTitle(_("File list management"))
67
flabel = QLabel(_("Filter:"))
68
self.edit = QLineEdit(self)
69
self.connect(self.edit, SIGNAL("returnPressed()"), self.edit_file)
70
self.connect(self.edit, SIGNAL("textChanged(QString)"),
71
lambda text: self.synchronize(0))
72
fhint = QLabel(_("(press <b>Enter</b> to edit file)"))
73
edit_layout = QHBoxLayout()
74
edit_layout.addWidget(flabel)
75
edit_layout.addWidget(self.edit)
76
edit_layout.addWidget(fhint)
78
self.listwidget = QListWidget(self)
79
self.listwidget.setResizeMode(QListWidget.Adjust)
80
self.connect(self.listwidget, SIGNAL("itemSelectionChanged()"),
81
self.item_selection_changed)
82
self.connect(self.listwidget, SIGNAL("itemActivated(QListWidgetItem*)"),
85
btn_layout = QHBoxLayout()
86
edit_btn = create_toolbutton(self, icon=get_icon('edit.png'),
87
text=_("&Edit file"), autoraise=False,
88
triggered=self.edit_file, text_beside_icon=True)
89
edit_btn.setMinimumHeight(28)
90
btn_layout.addWidget(edit_btn)
92
btn_layout.addStretch()
93
btn_layout.addSpacing(150)
94
btn_layout.addStretch()
96
close_btn = create_toolbutton(self, text=_("&Close file"),
97
icon=get_icon("fileclose.png"),
98
autoraise=False, text_beside_icon=True,
99
triggered=lambda: self.emit(SIGNAL("close_file(int)"),
100
self.indexes[self.listwidget.currentRow()]))
101
close_btn.setMinimumHeight(28)
102
btn_layout.addWidget(close_btn)
104
hint = QLabel(_("Hint: press <b>Alt</b> to show accelerators"))
105
hint.setAlignment(Qt.AlignCenter)
107
vlayout = QVBoxLayout()
108
vlayout.addLayout(edit_layout)
109
vlayout.addWidget(self.listwidget)
110
vlayout.addLayout(btn_layout)
111
vlayout.addWidget(hint)
112
self.setLayout(vlayout)
115
self.fullpath_sorting = fullpath_sorting
116
self.buttons = (edit_btn, close_btn)
119
row = self.listwidget.currentRow()
120
if self.listwidget.count() > 0 and row >= 0:
121
self.emit(SIGNAL("edit_file(int)"), self.indexes[row])
124
def item_selection_changed(self):
125
for btn in self.buttons:
126
btn.setEnabled(self.listwidget.currentRow() >= 0)
128
def synchronize(self, stack_index):
129
count = self.tabs.count()
133
self.listwidget.setTextElideMode(Qt.ElideMiddle
134
if self.fullpath_sorting
136
current_row = self.listwidget.currentRow()
138
current_text = unicode(self.listwidget.currentItem().text())
141
self.listwidget.clear()
143
new_current_index = stack_index
144
filter_text = unicode(self.edit.text())
146
for index in range(count):
147
text = unicode(self.tabs.tabText(index))
148
if len(filter_text) == 0 or filter_text in text:
149
if text == current_text:
150
new_current_index = lw_index
152
item = QListWidgetItem(self.tabs.tabIcon(index),
153
text, self.listwidget)
154
item.setSizeHint(QSize(0, 25))
155
self.listwidget.addItem(item)
156
self.indexes.append(index)
157
if new_current_index < self.listwidget.count():
158
self.listwidget.setCurrentRow(new_current_index)
159
for btn in self.buttons:
160
btn.setEnabled(lw_index > 0)
163
class AnalysisThread(QThread):
164
"""Analysis thread"""
165
def __init__(self, parent, checker, source_code):
166
super(AnalysisThread, self).__init__(parent)
167
self.checker = checker
169
self.source_code = source_code
174
self.results = self.checker(self.source_code)
178
traceback.print_exc(file=STDERR)
181
class GetSubmodulesThread(QThread):
183
A thread to generate a list of submodules to be passed to Rope
184
extension_modules preference
187
super(GetSubmodulesThread, self).__init__()
191
self.submods = get_preferred_submodules()
192
self.emit(SIGNAL('submods_ready()'))
195
class ThreadManager(QObject):
196
"""Analysis thread manager"""
197
def __init__(self, parent, max_simultaneous_threads=2):
198
super(ThreadManager, self).__init__(parent)
199
self.max_simultaneous_threads = max_simultaneous_threads
200
self.started_threads = {}
201
self.pending_threads = []
202
self.end_callbacks = {}
204
def close_threads(self, parent):
205
"""Close threads associated to parent_id"""
207
print >>STDOUT, "Call to 'close_threads'"
209
# Closing all threads
210
self.pending_threads = []
212
for threads in self.started_threads.values():
213
threadlist += threads
215
parent_id = id(parent)
216
self.pending_threads = [(_th, _id) for (_th, _id)
217
in self.pending_threads
219
threadlist = self.started_threads.get(parent_id, [])
220
for thread in threadlist:
222
print >>STDOUT, "Waiting for thread %r to finish" % thread
223
while thread.isRunning():
224
# We can't terminate thread safely, so we simply wait...
225
QApplication.processEvents()
227
def close_all_threads(self):
228
"""Close all threads"""
230
print >>STDOUT, "Call to 'close_all_threads'"
231
self.close_threads(None)
233
def add_thread(self, checker, end_callback, source_code, parent):
234
"""Add thread to queue"""
235
parent_id = id(parent)
236
thread = AnalysisThread(self, checker, source_code)
237
self.end_callbacks[id(thread)] = end_callback
238
self.pending_threads.append((thread, parent_id))
240
print >>STDOUT, "Added thread %r to queue" % thread
241
QTimer.singleShot(50, self.update_queue)
243
def update_queue(self):
246
for parent_id, threadlist in self.started_threads.items():
248
for thread in threadlist:
249
if thread.isFinished():
250
end_callback = self.end_callbacks.pop(id(thread))
251
if thread.results is not None:
252
# The thread was executed successfully
253
end_callback(thread.results)
254
thread.setParent(None)
257
still_running.append(thread)
261
self.started_threads[parent_id] = still_running
263
self.started_threads.pop(parent_id)
265
print >>STDOUT, "Updating queue:"
266
print >>STDOUT, " started:", started
267
print >>STDOUT, " pending:", len(self.pending_threads)
268
if self.pending_threads and started < self.max_simultaneous_threads:
269
thread, parent_id = self.pending_threads.pop(0)
270
self.connect(thread, SIGNAL('finished()'), self.update_queue)
271
threadlist = self.started_threads.get(parent_id, [])
272
self.started_threads[parent_id] = threadlist+[thread]
274
print >>STDOUT, "===>starting:", thread
278
class FileInfo(QObject):
279
"""File properties"""
280
def __init__(self, filename, encoding, editor, new, threadmanager):
281
QObject.__init__(self)
282
self.threadmanager = threadmanager
283
self.filename = filename
284
self.newly_created = new
285
self.encoding = encoding
288
self.submods_thread = GetSubmodulesThread()
289
self.rope_project = codeeditor.get_rope_project()
290
self.classes = (filename, None, None)
291
self.analysis_results = []
292
self.todo_results = []
293
self.lastmodified = QFileInfo(filename).lastModified()
295
self.submods_thread.start()
296
self.connect(editor, SIGNAL('trigger_code_completion(bool)'),
297
self.trigger_code_completion)
298
self.connect(editor, SIGNAL('trigger_calltip_and_doc_rendering(int)'),
299
self.trigger_calltip_and_doc_rendering)
300
self.connect(editor, SIGNAL("go_to_definition(int)"),
301
self.go_to_definition)
303
self.connect(editor, SIGNAL('textChanged()'),
306
self.connect(editor, SIGNAL('breakpoints_changed()'),
307
self.breakpoints_changed)
308
self.connect(self.submods_thread, SIGNAL('submods_ready()'),
309
self.update_extension_modules)
311
self.pyflakes_results = None
312
self.pep8_results = None
314
def update_extension_modules(self):
315
self.rope_project.set_pref('extension_modules',
316
self.submods_thread.submods)
318
def text_changed(self):
319
"""Editor's text has changed"""
320
self.emit(SIGNAL('text_changed_at(QString,int)'),
321
self.filename, self.editor.get_position('cursor'))
323
def trigger_code_completion(self, automatic):
324
"""Trigger code completion"""
325
source_code = unicode(self.editor.toPlainText())
326
offset = self.editor.get_position('cursor')
327
text = self.editor.get_text('sol', 'cursor')
329
if text.lstrip().startswith('import '):
331
comp_list = module_completion(text, self.path)
332
words = text.split(' ')
334
words = words[-1].split(',')
336
self.editor.show_completion_list(comp_list,
337
completion_text=words[-1],
340
elif text.lstrip().startswith('from '):
342
comp_list = module_completion(text, self.path)
343
words = text.split(' ')
345
words = words[:-2] + words[-1].split('(')
347
words = words[:-2] + words[-1].split(',')
348
self.editor.show_completion_list(comp_list,
349
completion_text=words[-1],
353
textlist = self.rope_project.get_completion_list(source_code,
357
completion_text = re.split(r"[^a-zA-Z0-9_]", text)[-1]
358
if text.lstrip().startswith('#') and text.endswith('.'):
361
self.editor.show_completion_list(textlist, completion_text,
365
def trigger_calltip_and_doc_rendering(self, position, auto=True):
366
"""Trigger calltip and docstring rendering in the Object Inspector"""
367
# auto is True means that trigger_calltip was called automatically,
368
# i.e. the user has just entered an opening parenthesis -- in that
369
# case, we don't want to force the object inspector to be visible,
370
# to avoid polluting the window layout
371
source_code = unicode(self.editor.toPlainText())
374
helplist = self.rope_project.get_calltip_and_docs(source_code, offset,
380
cts, doc_text = helplist
382
cts = cts.replace('.__init__', '')
383
parpos = cts.find('(')
385
obj_fullname = cts[:parpos]
386
obj_name = obj_fullname.split('.')[-1]
387
cts = cts.replace(obj_fullname, obj_name)
390
# Either inspected object has no argument, or it's
391
# a builtin or an extension -- in this last case
392
# the following attempt may succeed:
393
signatures = getsignaturesfromtext(doc_text, obj_name)
395
obj_fullname = codeeditor.get_primary_at(source_code, offset)
396
if obj_fullname and not obj_fullname.startswith('self.') and doc_text:
397
# doc_text was generated by utils.dochelpers.getdoc
398
if type(doc_text) is dict:
399
obj_fullname = doc_text['name']
400
argspec = doc_text['argspec']
401
note = doc_text['note']
402
doc_text = doc_text['docstring']
404
argspec_st = signatures[0].find('(')
405
argspec = signatures[0][argspec_st:]
406
module_end = obj_fullname.rfind('.')
407
module = obj_fullname[:module_end]
408
note = 'Present in %s module' % module
413
"send_to_inspector(QString,QString,QString,QString,bool)"),
414
obj_fullname, argspec, note, doc_text, not auto)
416
signatures = ['<b>'+s.replace('(', '(</b>'
417
).replace(')', '<b>)</b>')
419
self.editor.show_calltip(obj_fullname, '<br>'.join(signatures),
420
at_position=position)
422
def go_to_definition(self, position):
423
"""Go to definition"""
424
source_code = unicode(self.editor.toPlainText())
426
fname, lineno = self.rope_project.get_definition_location(source_code,
427
offset, self.filename)
428
if fname is not None and lineno is not None:
429
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
432
def get_source_code(self):
433
"""Return associated editor source code"""
434
return unicode(self.editor.toPlainText()).encode('utf-8')
436
def run_code_analysis(self, run_pyflakes, run_pep8):
437
"""Run code analysis"""
438
run_pyflakes = run_pyflakes and codeanalysis.is_pyflakes_installed()
439
run_pep8 = run_pep8 and\
440
codeanalysis.get_checker_executable('pep8') is not None
441
self.pyflakes_results = []
442
self.pep8_results = []
443
if self.editor.is_python():
444
source_code = self.get_source_code()
446
self.pyflakes_results = None
448
self.pep8_results = None
450
self.threadmanager.add_thread(codeanalysis.check_with_pyflakes,
451
self.pyflakes_analysis_finished,
454
self.threadmanager.add_thread(codeanalysis.check_with_pep8,
455
self.pep8_analysis_finished,
458
def pyflakes_analysis_finished(self, results):
459
"""Pyflakes code analysis thread has finished"""
460
self.pyflakes_results = results
461
if self.pep8_results is not None:
462
self.code_analysis_finished()
464
def pep8_analysis_finished(self, results):
465
"""Pep8 code analysis thread has finished"""
466
self.pep8_results = results
467
if self.pyflakes_results is not None:
468
self.code_analysis_finished()
470
def code_analysis_finished(self):
471
"""Code analysis thread has finished"""
472
self.set_analysis_results(self.pyflakes_results+self.pep8_results)
473
self.emit(SIGNAL('analysis_results_changed()'))
475
def set_analysis_results(self, results):
476
"""Set analysis results and update warning markers in editor"""
477
self.analysis_results = results
478
self.editor.process_code_analysis(results)
480
def cleanup_analysis_results(self):
481
"""Clean-up analysis results"""
482
self.analysis_results = []
483
self.editor.cleanup_code_analysis()
485
def run_todo_finder(self):
486
"""Run TODO finder"""
487
if self.editor.is_python():
488
self.threadmanager.add_thread(codeanalysis.find_tasks,
490
self.get_source_code(), self)
492
def todo_finished(self, results):
493
"""Code analysis thread has finished"""
494
self.set_todo_results(results)
495
self.emit(SIGNAL('todo_results_changed()'))
497
def set_todo_results(self, results):
498
"""Set TODO results and update markers in editor"""
499
self.todo_results = results
500
self.editor.process_todo(results)
502
def cleanup_todo_results(self):
503
"""Clean-up TODO finder results"""
504
self.todo_results = []
506
def breakpoints_changed(self):
507
"""Breakpoint list has changed"""
508
breakpoints = self.editor.get_breakpoints()
509
if self.editor.breakpoints != breakpoints:
510
self.editor.breakpoints = breakpoints
511
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
512
self.filename, repr(breakpoints))
515
class EditorStack(QWidget):
516
def __init__(self, parent, actions):
517
QWidget.__init__(self, parent)
519
self.setAttribute(Qt.WA_DeleteOnClose)
521
self.threadmanager = ThreadManager(self)
523
self.newwindow_action = None
524
self.horsplit_action = None
525
self.versplit_action = None
526
self.close_action = None
527
self.__get_split_actions()
529
layout = QVBoxLayout()
530
layout.setContentsMargins(0, 0, 0, 0)
531
self.setLayout(layout)
534
self.filelist_dlg = None
535
# self.filelist_btn = None
536
# self.previous_btn = None
537
# self.next_btn = None
540
self.stack_history = []
542
self.setup_editorstack(parent, layout)
544
self.find_widget = None
548
filelist_action = create_action(self, _("File list management"),
549
icon=get_icon('filelist.png'),
550
triggered=self.open_filelistdialog)
551
copy_to_cb_action = create_action(self, _("Copy path to clipboard"),
554
QApplication.clipboard().setText(self.get_current_filename()))
555
self.menu_actions = actions+[None, filelist_action, copy_to_cb_action]
556
self.outlineexplorer = None
557
self.inspector = None
558
self.unregister_callback = None
559
self.is_closable = False
560
self.new_action = None
561
self.open_action = None
562
self.save_action = None
563
self.revert_action = None
564
self.tempfile_path = None
565
self.title = _("Editor")
566
self.pyflakes_enabled = True
567
self.pep8_enabled = False
568
self.todolist_enabled = True
569
self.realtime_analysis_enabled = False
570
self.is_analysis_done = False
571
self.linenumbers_enabled = True
572
self.edgeline_enabled = True
573
self.edgeline_column = 79
574
self.outlineexplorer_enabled = True
575
self.codecompletion_auto_enabled = True
576
self.codecompletion_case_enabled = False
577
self.codecompletion_single_enabled = False
578
self.codecompletion_enter_enabled = False
579
self.calltips_enabled = True
580
self.go_to_definition_enabled = True
581
self.close_parentheses_enabled = True
582
self.close_quotes_enabled = True
583
self.add_colons_enabled = True
584
self.auto_unindent_enabled = True
585
self.indent_chars = " "*4
586
self.tab_stop_width = 40
587
self.inspector_enabled = False
588
self.default_font = None
589
self.wrap_enabled = False
590
self.tabmode_enabled = False
591
self.intelligent_backspace_enabled = True
592
self.highlight_current_line_enabled = False
593
self.occurence_highlighting_enabled = True
594
self.checkeolchars_enabled = True
595
self.always_remove_trailing_spaces = False
596
self.fullpath_sorting_enabled = None
597
self.set_fullpath_sorting_enabled(False)
599
if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES:
600
ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0]
601
self.color_scheme = ccs
603
self.__file_status_flag = False
605
# Real-time code analysis
606
self.analysis_timer = QTimer(self)
607
self.analysis_timer.setSingleShot(True)
608
self.analysis_timer.setInterval(2000)
609
self.connect(self.analysis_timer, SIGNAL("timeout()"),
613
self.setAcceptDrops(True)
616
self.inspectsc = QShortcut(QKeySequence("Ctrl+I"), self,
617
self.inspect_current_object)
618
self.inspectsc.setContext(Qt.WidgetWithChildrenShortcut)
619
self.breakpointsc = QShortcut(QKeySequence("F12"), self,
620
self.set_or_clear_breakpoint)
621
self.breakpointsc.setContext(Qt.WidgetWithChildrenShortcut)
622
self.cbreakpointsc = QShortcut(QKeySequence("Shift+F12"), self,
623
self.set_or_edit_conditional_breakpoint)
624
self.cbreakpointsc.setContext(Qt.WidgetWithChildrenShortcut)
625
self.gotolinesc = QShortcut(QKeySequence("Ctrl+L"), self,
627
self.gotolinesc.setContext(Qt.WidgetWithChildrenShortcut)
628
self.filelistsc = QShortcut(QKeySequence("Ctrl+E"), self,
629
self.open_filelistdialog)
630
self.filelistsc.setContext(Qt.WidgetWithChildrenShortcut)
631
self.tabsc = QShortcut(QKeySequence("Ctrl+Tab"), self,
632
self.go_to_previous_file)
633
self.tabsc.setContext(Qt.WidgetWithChildrenShortcut)
634
self.closesc = QShortcut(QKeySequence("Ctrl+F4"), self,
636
self.closesc.setContext(Qt.WidgetWithChildrenShortcut)
637
self.tabshiftsc = QShortcut(QKeySequence("Ctrl+Shift+Tab"), self,
638
self.go_to_next_file)
639
self.tabshiftsc.setContext(Qt.WidgetWithChildrenShortcut)
641
def get_shortcut_data(self):
643
Returns shortcut data, a list of tuples (shortcut, text, default)
644
shortcut (QShortcut or QAction instance)
645
text (string): action/shortcut description
646
default (string): default key sequence
649
(self.inspectsc, "Inspect current object", "Ctrl+I"),
650
(self.breakpointsc, "Breakpoint", "F12"),
651
(self.cbreakpointsc, "Conditional breakpoint", "Shift+F12"),
652
(self.gotolinesc, "Go to line", "Ctrl+L"),
653
(self.filelistsc, "File list management", "Ctrl+E"),
656
def setup_editorstack(self, parent, layout):
657
"""Setup editorstack's layout"""
658
menu_btn = create_toolbutton(self, icon=get_icon("tooloptions.png"),
660
self.menu = QMenu(self)
661
menu_btn.setMenu(self.menu)
662
menu_btn.setPopupMode(menu_btn.InstantPopup)
663
self.connect(self.menu, SIGNAL("aboutToShow()"), self.__setup_menu)
665
# self.filelist_btn = create_toolbutton(self,
666
# icon=get_icon('filelist.png'),
667
# tip=_("File list management"),
668
# triggered=self.open_filelistdialog)
670
# self.previous_btn = create_toolbutton(self,
671
# icon=get_icon('previous.png'),
672
# tip=_("Previous file"),
673
# triggered=self.go_to_previous_file)
675
# self.next_btn = create_toolbutton(self,
676
# icon=get_icon('next.png'),
677
# tip=_("Next file"),
678
# triggered=self.go_to_next_file)
681
# corner_widgets = {Qt.TopRightCorner: [self.previous_btn,
682
# self.filelist_btn, self.next_btn,
684
corner_widgets = {Qt.TopRightCorner: [menu_btn]}
685
self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True,
686
corner_widgets=corner_widgets)
687
self.tabs.set_close_function(self.close_file)
688
if hasattr(self.tabs, 'setDocumentMode') \
689
and not sys.platform == 'darwin':
690
# Don't set document mode to true on OSX because it generates
691
# a crash when the editor is detached from the main window
693
self.tabs.setDocumentMode(True)
694
self.connect(self.tabs, SIGNAL('currentChanged(int)'),
695
self.current_changed)
696
layout.addWidget(self.tabs)
698
def add_corner_widgets_to_tabbar(self, widgets):
699
self.tabs.add_corner_widgets(widgets)
701
def closeEvent(self, event):
702
self.threadmanager.close_all_threads()
703
self.disconnect(self.analysis_timer, SIGNAL("timeout()"),
705
QWidget.closeEvent(self, event)
707
self.emit(SIGNAL('destroyed()'))
709
def clone_editor_from(self, other_finfo, set_current):
710
fname = other_finfo.filename
711
enc = other_finfo.encoding
712
new = other_finfo.newly_created
713
finfo = self.create_new_editor(fname, enc, "",
714
set_current=set_current, new=new,
715
cloned_from=other_finfo.editor)
716
finfo.set_analysis_results(other_finfo.analysis_results)
717
finfo.set_todo_results(other_finfo.todo_results)
720
def clone_from(self, other):
721
"""Clone EditorStack from other instance"""
722
for other_finfo in other.data:
723
self.clone_editor_from(other_finfo, set_current=True)
724
self.set_stack_index(other.get_stack_index())
726
def open_filelistdialog(self):
727
"""Open file list management dialog box"""
728
self.filelist_dlg = dlg = FileListDialog(self, self.tabs,
729
self.fullpath_sorting_enabled)
730
self.connect(dlg, SIGNAL("edit_file(int)"), self.set_stack_index)
731
self.connect(dlg, SIGNAL("close_file(int)"), self.close_file)
732
dlg.synchronize(self.get_stack_index())
734
self.filelist_dlg = None
736
def update_filelistdialog(self):
737
"""Synchronize file list dialog box with editor widget tabs"""
738
if self.filelist_dlg is not None:
739
self.filelist_dlg.synchronize(self.get_stack_index())
741
def go_to_line(self):
742
"""Go to line dialog"""
744
self.get_current_editor().exec_gotolinedialog()
746
def set_or_clear_breakpoint(self):
747
"""Set/clear breakpoint"""
749
editor = self.get_current_editor()
750
editor.add_remove_breakpoint()
752
def set_or_edit_conditional_breakpoint(self):
753
"""Set conditional breakpoint"""
755
editor = self.get_current_editor()
756
editor.add_remove_breakpoint(edit_condition=True)
758
def inspect_current_object(self):
759
"""Inspect current object in Object Inspector"""
760
if programs.is_module_installed('rope'):
761
editor = self.get_current_editor()
762
position = editor.get_position('cursor')
763
finfo = self.get_current_finfo()
764
finfo.trigger_calltip_and_doc_rendering(position, auto=False)
766
text = self.get_current_editor().get_current_object()
768
self.send_to_inspector(text, force=True)
771
#------ Editor Widget Settings
772
def set_closable(self, state):
773
"""Parent widget must handle the closable state"""
774
self.is_closable = state
776
def set_io_actions(self, new_action, open_action,
777
save_action, revert_action):
778
self.new_action = new_action
779
self.open_action = open_action
780
self.save_action = save_action
781
self.revert_action = revert_action
783
def set_find_widget(self, find_widget):
784
self.find_widget = find_widget
786
def set_outlineexplorer(self, outlineexplorer):
787
self.outlineexplorer = outlineexplorer
788
self.outlineexplorer_enabled = True
789
self.connect(self.outlineexplorer,
790
SIGNAL("outlineexplorer_is_visible()"),
791
self._refresh_outlineexplorer)
793
def add_outlineexplorer_button(self, editor_plugin):
794
oe_btn = create_toolbutton(editor_plugin)
795
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
796
self.add_corner_widgets_to_tabbar([5, oe_btn])
798
def set_inspector(self, inspector):
799
self.inspector = inspector
801
def set_tempfile_path(self, path):
802
self.tempfile_path = path
804
def set_title(self, text):
807
def __update_editor_margins(self, editor):
808
editor.setup_margins(linenumbers=self.linenumbers_enabled,
809
markers=self.has_markers())
811
def __codeanalysis_settings_changed(self, current_finfo):
813
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
814
for finfo in self.data:
815
self.__update_editor_margins(finfo.editor)
816
finfo.cleanup_analysis_results()
817
if (run_pyflakes or run_pep8) and current_finfo is not None:
818
if current_finfo is not finfo:
819
finfo.run_code_analysis(run_pyflakes, run_pep8)
821
def set_pyflakes_enabled(self, state, current_finfo=None):
822
# CONF.get(self.CONF_SECTION, 'code_analysis/pyflakes')
823
self.pyflakes_enabled = state
824
self.__codeanalysis_settings_changed(current_finfo)
826
def set_pep8_enabled(self, state, current_finfo=None):
827
# CONF.get(self.CONF_SECTION, 'code_analysis/pep8')
828
self.pep8_enabled = state
829
self.__codeanalysis_settings_changed(current_finfo)
831
def has_markers(self):
832
"""Return True if this editorstack has a marker margin for TODOs or
834
return self.todolist_enabled or self.pyflakes_enabled\
837
def set_todolist_enabled(self, state, current_finfo=None):
838
# CONF.get(self.CONF_SECTION, 'todo_list')
839
self.todolist_enabled = state
841
for finfo in self.data:
842
self.__update_editor_margins(finfo.editor)
843
finfo.cleanup_todo_results()
844
if state and current_finfo is not None:
845
if current_finfo is not finfo:
846
finfo.run_todo_finder()
848
def set_realtime_analysis_enabled(self, state):
849
self.realtime_analysis_enabled = state
851
def set_realtime_analysis_timeout(self, timeout):
852
self.analysis_timer.setInterval(timeout)
854
def set_linenumbers_enabled(self, state, current_finfo=None):
855
# CONF.get(self.CONF_SECTION, 'line_numbers')
856
self.linenumbers_enabled = state
858
for finfo in self.data:
859
self.__update_editor_margins(finfo.editor)
861
def set_edgeline_enabled(self, state):
862
# CONF.get(self.CONF_SECTION, 'edge_line')
863
self.edgeline_enabled = state
865
for finfo in self.data:
866
finfo.editor.set_edge_line_enabled(state)
868
def set_edgeline_column(self, column):
869
# CONF.get(self.CONF_SECTION, 'edge_line_column')
870
self.edgeline_column = column
872
for finfo in self.data:
873
finfo.editor.set_edge_line_column(column)
875
def set_codecompletion_auto_enabled(self, state):
876
# CONF.get(self.CONF_SECTION, 'codecompletion_auto')
877
self.codecompletion_auto_enabled = state
879
for finfo in self.data:
880
finfo.editor.set_codecompletion_auto(state)
882
def set_codecompletion_case_enabled(self, state):
883
self.codecompletion_case_enabled = state
885
for finfo in self.data:
886
finfo.editor.set_codecompletion_case(state)
888
def set_codecompletion_single_enabled(self, state):
889
self.codecompletion_single_enabled = state
891
for finfo in self.data:
892
finfo.editor.set_codecompletion_single(state)
894
def set_codecompletion_enter_enabled(self, state):
895
self.codecompletion_enter_enabled = state
897
for finfo in self.data:
898
finfo.editor.set_codecompletion_enter(state)
900
def set_calltips_enabled(self, state):
901
# CONF.get(self.CONF_SECTION, 'calltips')
902
self.calltips_enabled = state
904
for finfo in self.data:
905
finfo.editor.set_calltips(state)
907
def set_go_to_definition_enabled(self, state):
908
# CONF.get(self.CONF_SECTION, 'go_to_definition')
909
self.go_to_definition_enabled = state
911
for finfo in self.data:
912
finfo.editor.set_go_to_definition_enabled(state)
914
def set_close_parentheses_enabled(self, state):
915
# CONF.get(self.CONF_SECTION, 'close_parentheses')
916
self.close_parentheses_enabled = state
918
for finfo in self.data:
919
finfo.editor.set_close_parentheses_enabled(state)
921
def set_close_quotes_enabled(self, state):
922
# CONF.get(self.CONF_SECTION, 'close_quotes')
923
self.close_quotes_enabled = state
925
for finfo in self.data:
926
finfo.editor.set_close_quotes_enabled(state)
928
def set_add_colons_enabled(self, state):
929
# CONF.get(self.CONF_SECTION, 'add_colons')
930
self.add_colons_enabled = state
932
for finfo in self.data:
933
finfo.editor.set_add_colons_enabled(state)
935
def set_auto_unindent_enabled(self, state):
936
# CONF.get(self.CONF_SECTION, 'auto_unindent')
937
self.auto_unindent_enabled = state
939
for finfo in self.data:
940
finfo.editor.set_auto_unindent_enabled(state)
942
def set_indent_chars(self, indent_chars):
943
# CONF.get(self.CONF_SECTION, 'indent_chars')
944
indent_chars = indent_chars[1:-1] # removing the leading/ending '*'
945
self.indent_chars = indent_chars
947
for finfo in self.data:
948
finfo.editor.set_indent_chars(indent_chars)
950
def set_tab_stop_width(self, tab_stop_width):
951
# CONF.get(self.CONF_SECTION, 'tab_stop_width')
952
self.tab_stop_width = tab_stop_width
954
for finfo in self.data:
955
finfo.editor.setTabStopWidth(tab_stop_width)
957
def set_inspector_enabled(self, state):
958
self.inspector_enabled = state
960
def set_outlineexplorer_enabled(self, state):
961
# CONF.get(self.CONF_SECTION, 'outline_explorer')
962
self.outlineexplorer_enabled = state
964
def set_default_font(self, font, color_scheme=None):
965
# get_font(self.CONF_SECTION)
966
self.default_font = font
967
if color_scheme is not None:
968
self.color_scheme = color_scheme
970
for finfo in self.data:
971
finfo.editor.set_font(font, color_scheme)
973
def set_color_scheme(self, color_scheme):
974
self.color_scheme = color_scheme
976
for finfo in self.data:
977
finfo.editor.set_color_scheme(color_scheme)
979
def set_wrap_enabled(self, state):
980
# CONF.get(self.CONF_SECTION, 'wrap')
981
self.wrap_enabled = state
983
for finfo in self.data:
984
finfo.editor.toggle_wrap_mode(state)
986
def set_tabmode_enabled(self, state):
987
# CONF.get(self.CONF_SECTION, 'tab_always_indent')
988
self.tabmode_enabled = state
990
for finfo in self.data:
991
finfo.editor.set_tab_mode(state)
993
def set_intelligent_backspace_enabled(self, state):
994
# CONF.get(self.CONF_SECTION, 'intelligent_backspace')
995
self.intelligent_backspace_enabled = state
997
for finfo in self.data:
998
finfo.editor.toggle_intelligent_backspace(state)
1000
def set_occurence_highlighting_enabled(self, state):
1001
# CONF.get(self.CONF_SECTION, 'occurence_highlighting')
1002
self.occurence_highlighting_enabled = state
1004
for finfo in self.data:
1005
finfo.editor.set_occurence_highlighting(state)
1007
def set_occurence_highlighting_timeout(self, timeout):
1008
# CONF.get(self.CONF_SECTION, 'occurence_highlighting/timeout')
1009
self.occurence_highlighting_timeout = timeout
1011
for finfo in self.data:
1012
finfo.editor.set_occurence_timeout(timeout)
1014
def set_highlight_current_line_enabled(self, state):
1015
self.highlight_current_line_enabled = state
1017
for finfo in self.data:
1018
finfo.editor.set_highlight_current_line(state)
1020
def set_checkeolchars_enabled(self, state):
1021
# CONF.get(self.CONF_SECTION, 'check_eol_chars')
1022
self.checkeolchars_enabled = state
1024
def set_fullpath_sorting_enabled(self, state):
1025
# CONF.get(self.CONF_SECTION, 'fullpath_sorting')
1026
self.fullpath_sorting_enabled = state
1028
finfo = self.data[self.get_stack_index()]
1029
self.data.sort(key=self.__get_sorting_func())
1030
new_index = self.data.index(finfo)
1031
self.__repopulate_stack()
1032
self.set_stack_index(new_index)
1034
def set_always_remove_trailing_spaces(self, state):
1035
# CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces')
1036
self.always_remove_trailing_spaces = state
1039
#------ Stacked widget management
1040
def get_stack_index(self):
1041
return self.tabs.currentIndex()
1043
def get_current_finfo(self):
1045
return self.data[self.get_stack_index()]
1047
def get_current_editor(self):
1048
return self.tabs.currentWidget()
1050
def get_stack_count(self):
1051
return self.tabs.count()
1053
def set_stack_index(self, index):
1054
self.tabs.setCurrentIndex(index)
1056
def set_tabbar_visible(self, state):
1057
self.tabs.tabBar().setVisible(state)
1059
def remove_from_data(self, index):
1060
self.tabs.blockSignals(True)
1061
self.tabs.removeTab(index)
1062
self.data.pop(index)
1063
self.tabs.blockSignals(False)
1064
self.update_actions()
1065
self.update_filelistdialog()
1067
def __modified_readonly_title(self, title, is_modified, is_readonly):
1068
if is_modified is not None and is_modified:
1070
if is_readonly is not None and is_readonly:
1071
title = "(%s)" % title
1074
def get_tab_text(self, filename, is_modified=None, is_readonly=None):
1075
"""Return tab title"""
1076
return self.__modified_readonly_title(osp.basename(filename),
1077
is_modified, is_readonly)
1079
def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
1080
"""Return tab menu title"""
1081
if self.fullpath_sorting_enabled:
1085
text = self.__modified_readonly_title(text,
1086
is_modified, is_readonly)
1087
if self.tempfile_path is not None\
1088
and filename == encoding.to_unicode_from_fs(self.tempfile_path):
1089
temp_file_str = unicode(_("Temporary file"))
1090
if self.fullpath_sorting_enabled:
1091
return "%s (%s)" % (text, temp_file_str)
1093
return text % (temp_file_str, self.tempfile_path)
1095
if self.fullpath_sorting_enabled:
1098
return text % (osp.basename(filename), osp.dirname(filename))
1100
def __get_sorting_func(self):
1101
if self.fullpath_sorting_enabled:
1102
return lambda item: osp.join(osp.dirname(item.filename),
1103
'_'+osp.basename(item.filename))
1105
return lambda item: osp.basename(item.filename)
1107
def add_to_data(self, finfo, set_current):
1108
self.data.append(finfo)
1109
self.data.sort(key=self.__get_sorting_func())
1110
index = self.data.index(finfo)
1111
fname, editor = finfo.filename, finfo.editor
1112
self.tabs.insertTab(index, editor, get_filetype_icon(fname),
1113
self.get_tab_text(fname))
1114
self.set_stack_title(index, False)
1116
self.set_stack_index(index)
1117
self.current_changed(index)
1118
self.update_actions()
1119
self.update_filelistdialog()
1121
def __repopulate_stack(self):
1122
self.tabs.blockSignals(True)
1124
for finfo in self.data:
1125
icon = get_filetype_icon(finfo.filename)
1126
if finfo.newly_created:
1130
tab_text = self.get_tab_text(finfo.filename, is_modified)
1131
tab_tip = self.get_tab_tip(finfo.filename)
1132
index = self.tabs.addTab(finfo.editor, icon, tab_text)
1133
self.tabs.setTabToolTip(index, tab_tip)
1134
self.tabs.blockSignals(False)
1135
self.update_filelistdialog()
1137
def rename_in_data(self, index, new_filename):
1138
finfo = self.data[index]
1139
if osp.splitext(finfo.filename)[1] != osp.splitext(new_filename)[1]:
1140
# File type has changed!
1141
txt = unicode(finfo.editor.get_text_with_eol())
1142
language = get_file_language(new_filename, txt)
1143
finfo.editor.set_language(language)
1144
set_new_index = index == self.get_stack_index()
1145
finfo.filename = new_filename
1146
self.data.sort(key=self.__get_sorting_func())
1147
new_index = self.data.index(finfo)
1148
self.__repopulate_stack()
1150
self.set_stack_index(new_index)
1151
if self.outlineexplorer is not None:
1152
self.outlineexplorer.file_renamed(finfo.editor, finfo.filename)
1155
def set_stack_title(self, index, is_modified):
1156
finfo = self.data[index]
1157
fname = finfo.filename
1158
is_modified = is_modified or finfo.newly_created
1159
is_readonly = finfo.editor.isReadOnly()
1160
tab_text = self.get_tab_text(fname, is_modified, is_readonly)
1161
tab_tip = self.get_tab_tip(fname, is_modified, is_readonly)
1162
self.tabs.setTabText(index, tab_text)
1163
self.tabs.setTabToolTip(index, tab_tip)
1166
#------ Context menu
1167
def __setup_menu(self):
1168
"""Setup tab context menu before showing it"""
1171
actions = self.menu_actions
1173
actions = (self.new_action, self.open_action)
1174
self.setFocus() # --> Editor.__get_focus_editortabwidget
1175
add_actions(self.menu, list(actions)+self.__get_split_actions())
1176
self.close_action.setEnabled(self.is_closable)
1179
#------ Hor/Ver splitting
1180
def __get_split_actions(self):
1182
self.newwindow_action = create_action(self, _("New window"),
1183
icon="newwindow.png", tip=_("Create a new editor window"),
1184
triggered=lambda: self.emit(SIGNAL("create_new_window()")))
1186
self.versplit_action = create_action(self, _("Split vertically"),
1187
icon="versplit.png",
1188
tip=_("Split vertically this editor window"),
1189
triggered=lambda: self.emit(SIGNAL("split_vertically()")))
1190
self.horsplit_action = create_action(self, _("Split horizontally"),
1191
icon="horsplit.png",
1192
tip=_("Split horizontally this editor window"),
1193
triggered=lambda: self.emit(SIGNAL("split_horizontally()")))
1194
self.close_action = create_action(self, _("Close this panel"),
1195
icon="close_panel.png", triggered=self.close)
1196
return [None, self.newwindow_action, None,
1197
self.versplit_action, self.horsplit_action, self.close_action]
1199
def reset_orientation(self):
1200
self.horsplit_action.setEnabled(True)
1201
self.versplit_action.setEnabled(True)
1203
def set_orientation(self, orientation):
1204
self.horsplit_action.setEnabled(orientation == Qt.Horizontal)
1205
self.versplit_action.setEnabled(orientation == Qt.Vertical)
1207
def update_actions(self):
1208
state = self.get_stack_count() > 0
1209
self.horsplit_action.setEnabled(state)
1210
self.versplit_action.setEnabled(state)
1214
def get_current_filename(self):
1216
return self.data[self.get_stack_index()].filename
1218
def has_filename(self, filename):
1219
fixpath = lambda path: osp.normcase(osp.realpath(path))
1220
for index, finfo in enumerate(self.data):
1221
if fixpath(filename) == fixpath(finfo.filename):
1224
def set_current_filename(self, filename):
1225
"""Set current filename and return the associated editor instance"""
1226
index = self.has_filename(filename)
1227
if index is not None:
1228
self.set_stack_index(index)
1229
editor = self.data[index].editor
1233
def is_file_opened(self, filename=None):
1234
if filename is None:
1235
# Is there any file opened?
1236
return len(self.data) > 0
1238
return self.has_filename(filename)
1241
#------ Close file, tabwidget...
1242
def close_file(self, index=None, force=False):
1243
"""Close file (index=None -> close current file)
1244
Keep current file index unchanged (if current file
1245
that is being closed)"""
1246
current_index = self.get_stack_index()
1247
count = self.get_stack_count()
1250
index = current_index
1252
self.find_widget.set_editor(None)
1256
if current_index == index:
1257
new_index = self._get_previous_file_index()
1259
new_index = current_index
1260
is_ok = force or self.save_if_changed(cancelable=True, index=index)
1262
finfo = self.data[index]
1263
self.threadmanager.close_threads(finfo)
1264
# Removing editor reference from outline explorer settings:
1265
if self.outlineexplorer is not None:
1266
self.outlineexplorer.remove_editor(finfo.editor)
1268
self.remove_from_data(index)
1270
# We pass self object ID as a QString, because otherwise it would
1271
# depend on the platform: long for 64bit, int for 32bit. Replacing
1272
# by long all the time is not working on some 32bit platforms
1273
# (see Issue 1094, Issue 1098)
1274
self.emit(SIGNAL('close_file(QString,int)'), str(id(self)), index)
1276
if not self.data and self.is_closable:
1277
# editortabwidget is empty: removing it
1278
# (if it's not the first editortabwidget)
1280
self.emit(SIGNAL('opened_files_list_changed()'))
1281
self.emit(SIGNAL('update_code_analysis_actions()'))
1282
self._refresh_outlineexplorer()
1283
self.emit(SIGNAL('refresh_file_dependent_actions()'))
1284
self.emit(SIGNAL('update_plugin_title()'))
1286
if new_index is not None:
1287
if index < new_index:
1289
self.set_stack_index(new_index)
1292
def close_all_files(self):
1293
"""Close all opened scripts"""
1294
while self.close_file():
1299
def save_if_changed(self, cancelable=False, index=None):
1300
"""Ask user to save file if modified"""
1302
indexes = range(self.get_stack_count())
1305
buttons = QMessageBox.Yes | QMessageBox.No
1307
buttons |= QMessageBox.Cancel
1309
for index in indexes:
1310
if self.data[index].editor.document().isModified():
1316
buttons |= QMessageBox.YesAll | QMessageBox.NoAll
1318
for index in indexes:
1319
self.set_stack_index(index)
1320
finfo = self.data[index]
1321
if finfo.filename == self.tempfile_path or yes_all:
1324
elif finfo.editor.document().isModified():
1325
answer = QMessageBox.question(self, self.title,
1326
_("<b>%s</b> has been modified."
1327
"<br>Do you want to save changes?"
1328
) % osp.basename(finfo.filename),
1330
if answer == QMessageBox.Yes:
1333
elif answer == QMessageBox.YesAll:
1337
elif answer == QMessageBox.NoAll:
1339
elif answer == QMessageBox.Cancel:
1343
def save(self, index=None, force=False):
1346
# Save the currently edited file
1347
if not self.get_stack_count():
1349
index = self.get_stack_index()
1351
finfo = self.data[index]
1352
if not finfo.editor.document().isModified() and not force:
1354
if not osp.isfile(finfo.filename) and not force:
1355
# File has not been saved yet
1356
return self.save_as(index=index)
1357
if self.always_remove_trailing_spaces:
1358
self.remove_trailing_spaces(index)
1359
txt = unicode(finfo.editor.get_text_with_eol())
1361
finfo.encoding = encoding.write(txt, finfo.filename,
1363
finfo.newly_created = False
1364
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
1365
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1367
# We pass self object ID as a QString, because otherwise it would
1368
# depend on the platform: long for 64bit, int for 32bit. Replacing
1369
# by long all the time is not working on some 32bit platforms
1370
# (see Issue 1094, Issue 1098)
1371
self.emit(SIGNAL('file_saved(QString,int,QString)'),
1372
str(id(self)), index, finfo.filename)
1374
finfo.editor.document().setModified(False)
1375
self.modification_changed(index=index)
1376
self.analyze_script(index)
1377
codeeditor.validate_rope_project()
1379
#XXX CodeEditor-only: re-scan the whole text to rebuild outline
1380
# explorer data from scratch (could be optimized because
1381
# rehighlighting text means searching for all syntax coloring
1382
# patterns instead of only searching for class/def patterns which
1383
# would be sufficient for outline explorer data.
1384
finfo.editor.rehighlight()
1386
self._refresh_outlineexplorer(index)
1388
except EnvironmentError, error:
1389
QMessageBox.critical(self, _("Save"),
1390
_("<b>Unable to save script '%s'</b>"
1391
"<br><br>Error message:<br>%s"
1392
) % (osp.basename(finfo.filename),
1396
def file_saved_in_other_editorstack(self, index, filename):
1398
File was just saved in another editorstack, let's synchronize!
1399
This avoid file to be automatically reloaded
1401
Filename is passed in case file was just saved as another name
1403
finfo = self.data[index]
1404
finfo.newly_created = False
1405
finfo.filename = unicode(filename)
1406
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1408
def select_savename(self, original_filename):
1409
self.emit(SIGNAL('redirect_stdio(bool)'), False)
1410
filename, _selfilter = getsavefilename(self, _("Save Python script"),
1411
original_filename, EDIT_FILTERS)
1412
self.emit(SIGNAL('redirect_stdio(bool)'), True)
1414
return osp.normpath(filename)
1416
def save_as(self, index=None):
1417
"""Save file as..."""
1419
# Save the currently edited file
1420
index = self.get_stack_index()
1421
finfo = self.data[index]
1422
filename = self.select_savename(finfo.filename)
1424
ao_index = self.has_filename(filename)
1425
# Note: ao_index == index --> saving an untitled file
1426
if ao_index and ao_index != index:
1427
if not self.close_file(ao_index):
1429
if ao_index < index:
1432
new_index = self.rename_in_data(index, new_filename=filename)
1434
# We pass self object ID as a QString, because otherwise it would
1435
# depend on the platform: long for 64bit, int for 32bit. Replacing
1436
# by long all the time is not working on some 32bit platforms
1437
# (see Issue 1094, Issue 1098)
1438
self.emit(SIGNAL('file_renamed_in_data(QString,int,QString)'),
1439
str(id(self)), index, filename)
1441
ok = self.save(index=new_index, force=True)
1442
self.refresh(new_index)
1443
self.set_stack_index(new_index)
1449
"""Save all opened files"""
1451
for index in range(self.get_stack_count()):
1452
if self.data[index].editor.document().isModified():
1453
folders.add(osp.dirname(self.data[index].filename))
1457
def start_stop_analysis_timer(self):
1458
self.is_analysis_done = False
1459
if self.realtime_analysis_enabled:
1460
self.analysis_timer.stop()
1461
self.analysis_timer.start()
1463
def analyze_script(self, index=None):
1464
"""Analyze current script with pyflakes + find todos"""
1465
if self.is_analysis_done:
1468
index = self.get_stack_index()
1470
finfo = self.data[index]
1471
run_pyflakes, run_pep8 = self.pyflakes_enabled, self.pep8_enabled
1472
if run_pyflakes or run_pep8:
1473
finfo.run_code_analysis(run_pyflakes, run_pep8)
1474
if self.todolist_enabled:
1475
finfo.run_todo_finder()
1476
self.is_analysis_done = True
1478
def set_analysis_results(self, index, analysis_results):
1479
"""Synchronize analysis results between editorstacks"""
1480
self.data[index].set_analysis_results(analysis_results)
1482
def get_analysis_results(self):
1484
return self.data[self.get_stack_index()].analysis_results
1486
def set_todo_results(self, index, todo_results):
1487
"""Synchronize todo results between editorstacks"""
1488
self.data[index].set_todo_results(todo_results)
1490
def get_todo_results(self):
1492
return self.data[self.get_stack_index()].todo_results
1494
def current_changed(self, index):
1495
"""Stack index has changed"""
1496
# count = self.get_stack_count()
1497
# for btn in (self.filelist_btn, self.previous_btn, self.next_btn):
1498
# btn.setEnabled(count > 1)
1500
editor = self.get_current_editor()
1504
print >>STDOUT, "setfocusto:", editor
1506
self.emit(SIGNAL('reset_statusbar()'))
1507
self.emit(SIGNAL('opened_files_list_changed()'))
1509
# Index history management
1510
id_list = [id(self.tabs.widget(_i))
1511
for _i in range(self.tabs.count())]
1512
for _id in self.stack_history[:]:
1513
if _id not in id_list:
1514
self.stack_history.pop(self.stack_history.index(_id))
1515
current_id = id(self.tabs.widget(index))
1516
while current_id in self.stack_history:
1517
self.stack_history.pop(self.stack_history.index(current_id))
1518
self.stack_history.append(current_id)
1520
print >>STDOUT, "current_changed:", index, self.data[index].editor,
1521
print >>STDOUT, self.data[index].editor.get_document_id()
1523
self.emit(SIGNAL('update_plugin_title()'))
1524
if editor is not None:
1525
self.emit(SIGNAL('current_file_changed(QString,int)'),
1526
self.data[index].filename, editor.get_position('cursor'))
1528
def _get_previous_file_index(self):
1529
if len(self.stack_history) > 1:
1530
last = len(self.stack_history)-1
1531
w_id = self.stack_history.pop(last)
1532
self.stack_history.insert(0, w_id)
1533
last_id = self.stack_history[last]
1534
for _i in range(self.tabs.count()):
1535
if id(self.tabs.widget(_i)) == last_id:
1538
def go_to_previous_file(self):
1540
prev_index = self._get_previous_file_index()
1541
if prev_index is not None:
1542
self.set_stack_index(prev_index)
1543
elif len(self.stack_history) == 0 and self.get_stack_count():
1544
self.stack_history = [id(self.tabs.currentWidget())]
1546
def go_to_next_file(self):
1547
"""Ctrl+Shift+Tab"""
1548
if len(self.stack_history) > 1:
1549
last = len(self.stack_history)-1
1550
w_id = self.stack_history.pop(0)
1551
self.stack_history.append(w_id)
1552
last_id = self.stack_history[last]
1553
for _i in range(self.tabs.count()):
1554
if id(self.tabs.widget(_i)) == last_id:
1555
self.set_stack_index(_i)
1557
elif len(self.stack_history) == 0 and self.get_stack_count():
1558
self.stack_history = [id(self.tabs.currentWidget())]
1560
def focus_changed(self):
1561
"""Editor focus has changed"""
1562
fwidget = QApplication.focusWidget()
1563
for finfo in self.data:
1564
if fwidget is finfo.editor:
1566
self.emit(SIGNAL("editor_focus_changed()"))
1568
def _refresh_outlineexplorer(self, index=None, update=True, clear=False):
1569
"""Refresh outline explorer panel"""
1570
oe = self.outlineexplorer
1574
index = self.get_stack_index()
1577
finfo = self.data[index]
1578
# oe_visible: if outline explorer is not visible, maybe the whole
1579
# GUI is not visible (Spyder is starting up) -> in this case,
1580
# it is necessary to update the outline explorer
1581
oe_visible = oe.isVisible() or not self.isVisible()
1582
if self.outlineexplorer_enabled and finfo.editor.is_python() \
1586
oe.set_current_editor(finfo.editor, finfo.filename,
1587
update=update, clear=clear)
1589
oe.setEnabled(False)
1591
def __refresh_statusbar(self, index):
1592
"""Refreshing statusbar widgets"""
1593
finfo = self.data[index]
1594
self.emit(SIGNAL('encoding_changed(QString)'), finfo.encoding)
1595
# Refresh cursor position status:
1596
line, index = finfo.editor.get_cursor_line_column()
1597
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
1600
def __refresh_readonly(self, index):
1601
finfo = self.data[index]
1602
read_only = not QFileInfo(finfo.filename).isWritable()
1603
if not osp.isfile(finfo.filename):
1604
# This is an 'untitledX.py' file (newly created)
1606
finfo.editor.setReadOnly(read_only)
1607
self.emit(SIGNAL('readonly_changed(bool)'), read_only)
1609
def __check_file_status(self, index):
1610
"""Check if file has been changed in any way outside Spyder:
1611
1. removed, moved or renamed outside Spyder
1612
2. modified outside Spyder"""
1613
if self.__file_status_flag:
1614
# Avoid infinite loop: when the QMessageBox.question pops, it
1615
# gets focus and then give it back to the CodeEditor instance,
1616
# triggering a refresh cycle which calls this method
1618
self.__file_status_flag = True
1620
finfo = self.data[index]
1621
if finfo.newly_created:
1623
name = osp.basename(finfo.filename)
1624
# First, testing if file still exists (removed, moved or offline):
1625
if not osp.isfile(finfo.filename):
1626
answer = QMessageBox.warning(self, self.title,
1627
_("<b>%s</b> is unavailable "
1628
"(this file may have been removed, moved "
1629
"or renamed outside Spyder)."
1630
"<br>Do you want to close it?") % name,
1631
QMessageBox.Yes | QMessageBox.No)
1632
if answer == QMessageBox.Yes:
1633
self.close_file(index)
1635
finfo.newly_created = True
1636
finfo.editor.document().setModified(True)
1637
self.modification_changed(index=index)
1639
# Else, testing if it has been modified elsewhere:
1640
lastm = QFileInfo(finfo.filename).lastModified()
1641
if unicode(lastm.toString()) \
1642
!= unicode(finfo.lastmodified.toString()):
1643
if finfo.editor.document().isModified():
1644
answer = QMessageBox.question(self,
1646
_("<b>%s</b> has been modified outside Spyder."
1647
"<br>Do you want to reload it and loose all "
1648
"your changes?") % name,
1649
QMessageBox.Yes | QMessageBox.No)
1650
if answer == QMessageBox.Yes:
1653
finfo.lastmodified = lastm
1657
# Finally, resetting temporary flag:
1658
self.__file_status_flag = False
1660
def refresh(self, index=None):
1661
"""Refresh tabwidget"""
1663
index = self.get_stack_index()
1664
# Set current editor
1665
if self.get_stack_count():
1666
index = self.get_stack_index()
1667
finfo = self.data[index]
1668
editor = finfo.editor
1670
self._refresh_outlineexplorer(index, update=False)
1671
self.emit(SIGNAL('update_code_analysis_actions()'))
1672
self.__refresh_statusbar(index)
1673
self.__refresh_readonly(index)
1674
self.__check_file_status(index)
1675
self.emit(SIGNAL('update_plugin_title()'))
1678
# Update the modification-state-dependent parameters
1679
self.modification_changed()
1680
# Update FindReplace binding
1681
self.find_widget.set_editor(editor, refresh=False)
1683
def modification_changed(self, state=None, index=None, editor_id=None):
1685
Current editor's modification state has changed
1686
--> change tab title depending on new modification state
1687
--> enable/disable save/save all actions
1689
if editor_id is not None:
1690
for index, _finfo in enumerate(self.data):
1691
if id(_finfo.editor) == editor_id:
1693
# This must be done before refreshing save/save all actions:
1694
# (otherwise Save/Save all actions will always be enabled)
1695
self.emit(SIGNAL('opened_files_list_changed()'))
1698
index = self.get_stack_index()
1701
finfo = self.data[index]
1703
state = finfo.editor.document().isModified()
1704
self.set_stack_title(index, state)
1705
# Toggle save/save all actions state
1706
self.save_action.setEnabled(state)
1707
self.emit(SIGNAL('refresh_save_all_action()'))
1708
# Refreshing eol mode
1709
eol_chars = finfo.editor.get_line_separator()
1710
os_name = sourcecode.get_os_name_from_eol_chars(eol_chars)
1711
self.emit(SIGNAL('refresh_eol_chars(QString)'), os_name)
1714
#------ Load, reload
1715
def reload(self, index):
1716
"""Reload file from disk"""
1717
finfo = self.data[index]
1718
txt, finfo.encoding = encoding.read(finfo.filename)
1719
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
1720
position = finfo.editor.get_position('cursor')
1721
finfo.editor.set_text(txt)
1722
finfo.editor.document().setModified(False)
1723
finfo.editor.set_cursor_position(position)
1724
codeeditor.validate_rope_project()
1725
self._refresh_outlineexplorer(index, update=True, clear=True)
1728
"""Revert file from disk"""
1729
index = self.get_stack_index()
1730
finfo = self.data[index]
1731
filename = finfo.filename
1732
if finfo.editor.document().isModified():
1733
answer = QMessageBox.warning(self, self.title,
1734
_("All changes to <b>%s</b> will be lost."
1735
"<br>Do you want to revert file from disk?"
1736
) % osp.basename(filename),
1737
QMessageBox.Yes|QMessageBox.No)
1738
if answer != QMessageBox.Yes:
1742
def create_new_editor(self, fname, enc, txt,
1743
set_current, new=False, cloned_from=None):
1745
Create a new editor instance
1746
Returns finfo object (instead of editor as in previous releases)
1748
editor = codeeditor.CodeEditor(self)
1749
finfo = FileInfo(fname, enc, editor, new, self.threadmanager)
1750
self.add_to_data(finfo, set_current)
1751
self.connect(finfo, SIGNAL(
1752
"send_to_inspector(QString,QString,QString,QString,bool)"),
1753
self.send_to_inspector)
1754
self.connect(finfo, SIGNAL('analysis_results_changed()'),
1755
lambda: self.emit(SIGNAL('analysis_results_changed()')))
1756
self.connect(finfo, SIGNAL('todo_results_changed()'),
1757
lambda: self.emit(SIGNAL('todo_results_changed()')))
1758
self.connect(finfo, SIGNAL("edit_goto(QString,int,QString)"),
1759
lambda fname, lineno, name:
1760
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
1761
fname, lineno, name))
1762
self.connect(finfo, SIGNAL("save_breakpoints(QString,QString)"),
1764
self.emit(SIGNAL("save_breakpoints(QString,QString)"),
1766
language = get_file_language(fname, txt)
1767
editor.setup_editor(
1768
linenumbers=self.linenumbers_enabled,
1769
edge_line=self.edgeline_enabled,
1770
edge_line_column=self.edgeline_column, language=language,
1771
markers=self.has_markers(), font=self.default_font,
1772
color_scheme=self.color_scheme,
1773
wrap=self.wrap_enabled, tab_mode=self.tabmode_enabled,
1774
intelligent_backspace=self.intelligent_backspace_enabled,
1775
highlight_current_line=self.highlight_current_line_enabled,
1776
occurence_highlighting=self.occurence_highlighting_enabled,
1777
codecompletion_auto=self.codecompletion_auto_enabled,
1778
codecompletion_case=self.codecompletion_case_enabled,
1779
codecompletion_single=self.codecompletion_single_enabled,
1780
codecompletion_enter=self.codecompletion_enter_enabled,
1781
calltips=self.calltips_enabled,
1782
go_to_definition=self.go_to_definition_enabled,
1783
close_parentheses=self.close_parentheses_enabled,
1784
close_quotes=self.close_quotes_enabled,
1785
add_colons=self.add_colons_enabled,
1786
auto_unindent=self.auto_unindent_enabled,
1787
indent_chars=self.indent_chars,
1788
tab_stop_width=self.tab_stop_width,
1789
cloned_from=cloned_from)
1790
if cloned_from is None:
1791
editor.set_text(txt)
1792
editor.document().setModified(False)
1793
self.connect(finfo, SIGNAL('text_changed_at(QString,int)'),
1794
lambda fname, position:
1795
self.emit(SIGNAL('text_changed_at(QString,int)'),
1797
self.connect(editor, SIGNAL('cursorPositionChanged(int,int)'),
1798
self.editor_cursor_position_changed)
1799
self.connect(editor, SIGNAL('textChanged()'),
1800
self.start_stop_analysis_timer)
1801
self.connect(editor, SIGNAL('modificationChanged(bool)'),
1802
lambda state: self.modification_changed(state,
1803
editor_id=id(editor)))
1804
self.connect(editor, SIGNAL("focus_in()"), self.focus_changed)
1805
self.connect(editor, SIGNAL('zoom_in()'),
1806
lambda: self.emit(SIGNAL('zoom_in()')))
1807
self.connect(editor, SIGNAL('zoom_out()'),
1808
lambda: self.emit(SIGNAL('zoom_out()')))
1809
if self.outlineexplorer is not None:
1810
# Removing editor reference from outline explorer settings:
1811
self.connect(editor, SIGNAL("destroyed()"),
1813
self.outlineexplorer.remove_editor(obj))
1815
self.find_widget.set_editor(editor)
1817
self.emit(SIGNAL('refresh_file_dependent_actions()'))
1818
self.modification_changed(index=self.data.index(finfo))
1822
def editor_cursor_position_changed(self, line, index):
1823
"""Cursor position of one of the editor in the stack has changed"""
1824
self.emit(SIGNAL('editor_cursor_position_changed(int,int)'),
1827
def send_to_inspector(self, qstr1, qstr2=None, qstr3=None,
1828
qstr4=None, force=False):
1829
"""qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text"""
1830
if not force and not self.inspector_enabled:
1832
if self.inspector is not None \
1833
and (force or self.inspector.dockwidget.isVisible()):
1834
# ObjectInspector widget exists and is visible
1836
self.inspector.set_object_text(qstr1, ignore_unknown=True,
1837
force_refresh=force)
1839
objtxt = unicode(qstr1)
1840
name = objtxt.split('.')[-1]
1841
argspec = unicode(qstr2)
1842
note = unicode(qstr3)
1843
docstring = unicode(qstr4)
1844
text = {'obj_text': objtxt, 'name': name, 'argspec': argspec,
1845
'note': note, 'docstring': docstring}
1846
self.inspector.set_rope_doc(text, force_refresh=force)
1847
editor = self.get_current_editor()
1850
def new(self, filename, encoding, text):
1852
Create new filename with *encoding* and *text*
1854
finfo = self.create_new_editor(filename, encoding, text,
1855
set_current=False, new=True)
1856
finfo.editor.set_cursor_position('eof')
1857
finfo.editor.insert_text(os.linesep)
1860
def load(self, filename, set_current=True):
1862
Load filename, create an editor instance and return it
1863
*Warning* This is loading file, creating editor but not executing
1864
the source code analysis -- the analysis must be done by the editor
1865
plugin (in case multiple editorstack instances are handled)
1867
filename = osp.abspath(unicode(filename))
1868
self.emit(SIGNAL('starting_long_process(QString)'),
1869
_("Loading %s...") % filename)
1870
text, enc = encoding.read(filename)
1871
finfo = self.create_new_editor(filename, enc, text, set_current)
1872
index = self.data.index(finfo)
1873
self._refresh_outlineexplorer(index, update=True)
1874
self.emit(SIGNAL('ending_long_process(QString)'), "")
1875
if self.isVisible() and self.checkeolchars_enabled \
1876
and sourcecode.has_mixed_eol_chars(text):
1877
name = osp.basename(filename)
1878
QMessageBox.warning(self, self.title,
1879
_("<b>%s</b> contains mixed end-of-line "
1880
"characters.<br>Spyder will fix this "
1881
"automatically.") % name,
1883
self.set_os_eol_chars(index)
1884
self.is_analysis_done = False
1887
def set_os_eol_chars(self, index=None):
1889
index = self.get_stack_index()
1890
finfo = self.data[index]
1891
eol_chars = sourcecode.get_eol_chars_from_os_name(os.name)
1892
finfo.editor.set_eol_chars(eol_chars)
1893
finfo.editor.document().setModified(True)
1895
def remove_trailing_spaces(self, index=None):
1896
"""Remove trailing spaces"""
1898
index = self.get_stack_index()
1899
finfo = self.data[index]
1900
finfo.editor.remove_trailing_spaces()
1902
def fix_indentation(self, index=None):
1903
"""Replace tab characters by spaces"""
1905
index = self.get_stack_index()
1906
finfo = self.data[index]
1907
finfo.editor.fix_indentation()
1910
def run_selection_or_block(self):
1912
Run selected text in console and set focus to console
1913
*or*, if there is no selection,
1914
Run current block of lines in console and go to next block
1916
self.emit(SIGNAL('external_console_execute_lines(QString)'),
1917
self.get_current_editor().get_executable_text())
1919
#------ Drag and drop
1920
def dragEnterEvent(self, event):
1921
"""Reimplement Qt method
1922
Inform Qt about the types of data that the widget accepts"""
1923
source = event.mimeData()
1924
if source.hasUrls():
1925
if mimedata2url(source, extlist=EDIT_EXT):
1926
event.acceptProposedAction()
1929
elif source.hasText():
1930
event.acceptProposedAction()
1934
def dropEvent(self, event):
1935
"""Reimplement Qt method
1936
Unpack dropped data and handle it"""
1937
source = event.mimeData()
1938
if source.hasUrls():
1939
files = mimedata2url(source, extlist=EDIT_EXT)
1942
self.emit(SIGNAL('plugin_load(QString)'), fname)
1943
elif source.hasText():
1944
editor = self.get_current_editor()
1945
if editor is not None:
1946
editor.insert_text( source.text() )
1947
event.acceptProposedAction()
1950
class EditorSplitter(QSplitter):
1951
def __init__(self, parent, plugin, menu_actions, first=False,
1952
register_editorstack_cb=None, unregister_editorstack_cb=None):
1953
QSplitter.__init__(self, parent)
1954
self.setAttribute(Qt.WA_DeleteOnClose)
1955
self.setChildrenCollapsible(False)
1957
self.toolbar_list = None
1958
self.menu_list = None
1960
self.plugin = plugin
1962
if register_editorstack_cb is None:
1963
register_editorstack_cb = self.plugin.register_editorstack
1964
self.register_editorstack_cb = register_editorstack_cb
1965
if unregister_editorstack_cb is None:
1966
unregister_editorstack_cb = self.plugin.unregister_editorstack
1967
self.unregister_editorstack_cb = unregister_editorstack_cb
1969
self.menu_actions = menu_actions
1970
self.editorstack = EditorStack(self, menu_actions)
1971
self.register_editorstack_cb(self.editorstack)
1973
self.plugin.clone_editorstack(editorstack=self.editorstack)
1974
self.connect(self.editorstack, SIGNAL("destroyed()"),
1975
lambda: self.editorstack_closed())
1976
self.connect(self.editorstack, SIGNAL("split_vertically()"),
1977
lambda: self.split(orientation=Qt.Vertical))
1978
self.connect(self.editorstack, SIGNAL("split_horizontally()"),
1979
lambda: self.split(orientation=Qt.Horizontal))
1980
self.addWidget(self.editorstack)
1982
def closeEvent(self, event):
1983
QSplitter.closeEvent(self, event)
1985
self.emit(SIGNAL('destroyed()'))
1987
def __give_focus_to_remaining_editor(self):
1988
focus_widget = self.plugin.get_focus_widget()
1989
if focus_widget is not None:
1990
focus_widget.setFocus()
1992
def editorstack_closed(self):
1994
print >>STDOUT, "method 'editorstack_closed':"
1995
print >>STDOUT, " self :", self
1996
# print >>STDOUT, " sender:", self.sender()
1997
self.unregister_editorstack_cb(self.editorstack)
1998
self.editorstack = None
2000
close_splitter = self.count() == 1
2001
except RuntimeError:
2002
# editorsplitter has been destroyed (happens when closing a
2003
# EditorMainWindow instance)
2006
# editorstack just closed was the last widget in this QSplitter
2009
self.__give_focus_to_remaining_editor()
2011
def editorsplitter_closed(self):
2013
print >>STDOUT, "method 'editorsplitter_closed':"
2014
print >>STDOUT, " self :", self
2015
# print >>STDOUT, " sender:", self.sender()
2017
close_splitter = self.count() == 1 and self.editorstack is None
2018
except RuntimeError:
2019
# editorsplitter has been destroyed (happens when closing a
2020
# EditorMainWindow instance)
2023
# editorsplitter just closed was the last widget in this QSplitter
2026
elif self.count() == 2 and self.editorstack:
2027
# back to the initial state: a single editorstack instance,
2028
# as a single widget in this QSplitter: orientation may be changed
2029
self.editorstack.reset_orientation()
2030
self.__give_focus_to_remaining_editor()
2032
def split(self, orientation=Qt.Vertical):
2033
self.setOrientation(orientation)
2034
self.editorstack.set_orientation(orientation)
2035
editorsplitter = EditorSplitter(self.parent(), self.plugin,
2037
register_editorstack_cb=self.register_editorstack_cb,
2038
unregister_editorstack_cb=self.unregister_editorstack_cb)
2039
self.addWidget(editorsplitter)
2040
self.connect(editorsplitter, SIGNAL("destroyed()"),
2041
lambda: self.editorsplitter_closed())
2042
current_editor = editorsplitter.editorstack.get_current_editor()
2043
if current_editor is not None:
2044
current_editor.setFocus()
2046
def iter_editorstacks(self):
2047
editorstacks = [(self.widget(0), self.orientation())]
2048
if self.count() > 1:
2049
editorsplitter = self.widget(1)
2050
editorstacks += editorsplitter.iter_editorstacks()
2053
def get_layout_settings(self):
2054
"""Return layout state"""
2056
for editorstack, orientation in self.iter_editorstacks():
2057
clines = [finfo.editor.get_cursor_line_number()
2058
for finfo in editorstack.data]
2059
cfname = editorstack.get_current_filename()
2060
splitsettings.append((orientation == Qt.Vertical, cfname, clines))
2061
return dict(hexstate=str(self.saveState().toHex()),
2062
sizes=self.sizes(), splitsettings=splitsettings)
2064
def set_layout_settings(self, settings):
2065
"""Restore layout state"""
2066
splitsettings = settings.get('splitsettings')
2067
if splitsettings is None:
2071
for index, (is_vertical, cfname, clines) in enumerate(splitsettings):
2073
splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal)
2074
splitter = splitter.widget(1)
2075
editorstack = splitter.widget(0)
2076
for index, finfo in enumerate(editorstack.data):
2077
editor = finfo.editor
2078
editor.go_to_line(clines[index])
2079
editorstack.set_current_filename(cfname)
2080
hexstate = settings.get('hexstate')
2081
if hexstate is not None:
2082
self.restoreState( QByteArray().fromHex(str(hexstate)) )
2083
sizes = settings.get('sizes')
2084
if sizes is not None:
2085
self.setSizes(sizes)
2086
if editor is not None:
2091
class EditorWidget(QSplitter):
2092
def __init__(self, parent, plugin, menu_actions, show_fullpath,
2093
fullpath_sorting, show_all_files, show_comments):
2094
QSplitter.__init__(self, parent)
2095
self.setAttribute(Qt.WA_DeleteOnClose)
2097
statusbar = parent.statusBar() # Create a status bar
2098
self.readwrite_status = ReadWriteStatus(self, statusbar)
2099
self.eol_status = EOLStatus(self, statusbar)
2100
self.encoding_status = EncodingStatus(self, statusbar)
2101
self.cursorpos_status = CursorPositionStatus(self, statusbar)
2103
self.editorstacks = []
2105
self.plugin = plugin
2107
self.find_widget = FindReplace(self, enable_replace=True)
2108
self.plugin.register_widget_shortcuts("Editor", self.find_widget)
2109
self.find_widget.hide()
2110
self.outlineexplorer = OutlineExplorerWidget(self,
2111
show_fullpath=show_fullpath,
2112
fullpath_sorting=fullpath_sorting,
2113
show_all_files=show_all_files,
2114
show_comments=show_comments)
2115
self.connect(self.outlineexplorer,
2116
SIGNAL("edit_goto(QString,int,QString)"),
2117
lambda filenames, goto, word:
2118
plugin.load(filenames=filenames, goto=goto, word=word,
2119
editorwindow=self.parent()))
2121
editor_widgets = QWidget(self)
2122
editor_layout = QVBoxLayout()
2123
editor_layout.setContentsMargins(0, 0, 0, 0)
2124
editor_widgets.setLayout(editor_layout)
2125
editorsplitter = EditorSplitter(self, plugin, menu_actions,
2126
register_editorstack_cb=self.register_editorstack,
2127
unregister_editorstack_cb=self.unregister_editorstack)
2128
self.editorsplitter = editorsplitter
2129
editor_layout.addWidget(editorsplitter)
2130
editor_layout.addWidget(self.find_widget)
2132
splitter = QSplitter(self)
2133
splitter.setContentsMargins(0, 0, 0, 0)
2134
splitter.addWidget(editor_widgets)
2135
splitter.addWidget(self.outlineexplorer)
2136
splitter.setStretchFactor(0, 5)
2137
splitter.setStretchFactor(1, 1)
2139
# Refreshing outline explorer
2140
for index in range(editorsplitter.editorstack.get_stack_count()):
2141
editorsplitter.editorstack._refresh_outlineexplorer(index,
2144
def register_editorstack(self, editorstack):
2145
self.editorstacks.append(editorstack)
2147
print >>STDOUT, "EditorWidget.register_editorstack:", editorstack
2148
self.__print_editorstacks()
2149
self.plugin.last_focus_editorstack[self.parent()] = editorstack
2150
editorstack.set_closable( len(self.editorstacks) > 1 )
2151
editorstack.set_outlineexplorer(self.outlineexplorer)
2152
editorstack.set_find_widget(self.find_widget)
2153
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2154
self.readwrite_status.hide)
2155
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2156
self.encoding_status.hide)
2157
self.connect(editorstack, SIGNAL('reset_statusbar()'),
2158
self.cursorpos_status.hide)
2159
self.connect(editorstack, SIGNAL('readonly_changed(bool)'),
2160
self.readwrite_status.readonly_changed)
2161
self.connect(editorstack, SIGNAL('encoding_changed(QString)'),
2162
self.encoding_status.encoding_changed)
2163
self.connect(editorstack,
2164
SIGNAL('editor_cursor_position_changed(int,int)'),
2165
self.cursorpos_status.cursor_position_changed)
2166
self.connect(editorstack, SIGNAL('refresh_eol_chars(QString)'),
2167
self.eol_status.eol_changed)
2168
self.plugin.register_editorstack(editorstack)
2169
oe_btn = create_toolbutton(self)
2170
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
2171
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
2173
def __print_editorstacks(self):
2174
print >>STDOUT, "%d editorstack(s) in editorwidget:" \
2175
% len(self.editorstacks)
2176
for edst in self.editorstacks:
2177
print >>STDOUT, " ", edst
2179
def unregister_editorstack(self, editorstack):
2181
print >>STDOUT, "EditorWidget.unregister_editorstack:", editorstack
2182
self.plugin.unregister_editorstack(editorstack)
2183
self.editorstacks.pop(self.editorstacks.index(editorstack))
2185
self.__print_editorstacks()
2188
class EditorMainWindow(QMainWindow):
2189
def __init__(self, plugin, menu_actions, toolbar_list, menu_list,
2190
show_fullpath, fullpath_sorting, show_all_files,
2192
QMainWindow.__init__(self)
2193
self.setAttribute(Qt.WA_DeleteOnClose)
2195
self.window_size = None
2197
self.editorwidget = EditorWidget(self, plugin, menu_actions,
2198
show_fullpath, fullpath_sorting,
2199
show_all_files, show_comments)
2200
self.setCentralWidget(self.editorwidget)
2202
# Give focus to current editor to update/show all status bar widgets
2203
editorstack = self.editorwidget.editorsplitter.editorstack
2204
editor = editorstack.get_current_editor()
2205
if editor is not None:
2208
self.setWindowTitle("Spyder - %s" % plugin.windowTitle())
2209
self.setWindowIcon(plugin.windowIcon())
2213
for title, actions in toolbar_list:
2214
toolbar = self.addToolBar(title)
2215
toolbar.setObjectName(str(id(toolbar)))
2216
add_actions(toolbar, actions)
2217
toolbars.append(toolbar)
2219
quit_action = create_action(self, _("Close window"),
2220
icon="close_panel.png",
2221
tip=_("Close this window"),
2222
triggered=self.close)
2224
for index, (title, actions) in enumerate(menu_list):
2225
menu = self.menuBar().addMenu(title)
2228
add_actions(menu, actions+[None, quit_action])
2230
add_actions(menu, actions)
2233
def resizeEvent(self, event):
2234
"""Reimplement Qt method"""
2235
if not self.isMaximized() and not self.isFullScreen():
2236
self.window_size = self.size()
2237
QMainWindow.resizeEvent(self, event)
2239
def closeEvent(self, event):
2240
"""Reimplement Qt method"""
2241
QMainWindow.closeEvent(self, event)
2243
self.emit(SIGNAL('destroyed()'))
2244
for editorstack in self.editorwidget.editorstacks[:]:
2246
print >>STDOUT, "--> destroy_editorstack:", editorstack
2247
editorstack.emit(SIGNAL('destroyed()'))
2249
def get_layout_settings(self):
2250
"""Return layout state"""
2251
splitsettings = self.editorwidget.editorsplitter.get_layout_settings()
2252
return dict(size=(self.window_size.width(), self.window_size.height()),
2253
pos=(self.pos().x(), self.pos().y()),
2254
is_maximized=self.isMaximized(),
2255
is_fullscreen=self.isFullScreen(),
2256
hexstate=str(self.saveState().toHex()),
2257
splitsettings=splitsettings)
2259
def set_layout_settings(self, settings):
2260
"""Restore layout state"""
2261
size = settings.get('size')
2262
if size is not None:
2263
self.resize( QSize(*size) )
2264
self.window_size = self.size()
2265
pos = settings.get('pos')
2267
self.move( QPoint(*pos) )
2268
hexstate = settings.get('hexstate')
2269
if hexstate is not None:
2270
self.restoreState( QByteArray().fromHex(str(hexstate)) )
2271
if settings.get('is_maximized'):
2272
self.setWindowState(Qt.WindowMaximized)
2273
if settings.get('is_fullscreen'):
2274
self.setWindowState(Qt.WindowFullScreen)
2275
splitsettings = settings.get('splitsettings')
2276
if splitsettings is not None:
2277
self.editorwidget.editorsplitter.set_layout_settings(splitsettings)
2280
class EditorPluginExample(QSplitter):
2282
QSplitter.__init__(self)
2286
self.editorstacks = []
2287
self.editorwindows = []
2289
self.last_focus_editorstack = {} # fake
2291
self.find_widget = FindReplace(self, enable_replace=True)
2292
self.outlineexplorer = OutlineExplorerWidget(self, show_fullpath=False,
2293
show_all_files=False)
2294
self.connect(self.outlineexplorer,
2295
SIGNAL("edit_goto(QString,int,QString)"),
2298
editor_widgets = QWidget(self)
2299
editor_layout = QVBoxLayout()
2300
editor_layout.setContentsMargins(0, 0, 0, 0)
2301
editor_widgets.setLayout(editor_layout)
2302
editor_layout.addWidget(EditorSplitter(self, self, menu_actions,
2304
editor_layout.addWidget(self.find_widget)
2306
self.setContentsMargins(0, 0, 0, 0)
2307
self.addWidget(editor_widgets)
2308
self.addWidget(self.outlineexplorer)
2310
self.setStretchFactor(0, 5)
2311
self.setStretchFactor(1, 1)
2313
self.menu_actions = menu_actions
2314
self.toolbar_list = None
2315
self.menu_list = None
2316
self.setup_window([], [])
2318
def go_to_file(self, fname, lineno, text):
2319
editorstack = self.editorstacks[0]
2320
editorstack.set_current_filename(unicode(fname))
2321
editor = editorstack.get_current_editor()
2322
editor.go_to_line(lineno, word=text)
2324
def closeEvent(self, event):
2325
for win in self.editorwindows[:]:
2328
print >>STDOUT, len(self.editorwindows), ":", self.editorwindows
2329
print >>STDOUT, len(self.editorstacks), ":", self.editorstacks
2333
def load(self, fname):
2334
QApplication.processEvents()
2335
editorstack = self.editorstacks[0]
2336
editorstack.load(fname)
2337
editorstack.analyze_script()
2339
def register_editorstack(self, editorstack):
2341
print >>STDOUT, "FakePlugin.register_editorstack:", editorstack
2342
self.editorstacks.append(editorstack)
2343
if self.isAncestorOf(editorstack):
2344
# editorstack is a child of the Editor plugin
2345
editorstack.set_fullpath_sorting_enabled(True)
2346
editorstack.set_closable( len(self.editorstacks) > 1 )
2347
editorstack.set_outlineexplorer(self.outlineexplorer)
2348
editorstack.set_find_widget(self.find_widget)
2349
oe_btn = create_toolbutton(self)
2350
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
2351
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
2353
action = QAction(self)
2354
editorstack.set_io_actions(action, action, action, action)
2355
font = QFont("Courier New")
2356
font.setPointSize(10)
2357
editorstack.set_default_font(font, color_scheme='Spyder')
2359
self.connect(editorstack, SIGNAL('close_file(QString,int)'),
2360
self.close_file_in_all_editorstacks)
2361
self.connect(editorstack, SIGNAL('file_saved(QString,int,QString)'),
2362
self.file_saved_in_editorstack)
2363
self.connect(editorstack,
2364
SIGNAL('file_renamed_in_data(QString,int,QString)'),
2365
self.file_renamed_in_data_in_editorstack)
2367
self.connect(editorstack, SIGNAL("create_new_window()"),
2368
self.create_new_window)
2369
self.connect(editorstack, SIGNAL('plugin_load(QString)'),
2372
def unregister_editorstack(self, editorstack):
2374
print >>STDOUT, "FakePlugin.unregister_editorstack:", editorstack
2375
self.editorstacks.pop(self.editorstacks.index(editorstack))
2377
def clone_editorstack(self, editorstack):
2378
editorstack.clone_from(self.editorstacks[0])
2380
def setup_window(self, toolbar_list, menu_list):
2381
self.toolbar_list = toolbar_list
2382
self.menu_list = menu_list
2384
def create_new_window(self):
2385
window = EditorMainWindow(self, self.menu_actions,
2386
self.toolbar_list, self.menu_list,
2387
show_fullpath=False, fullpath_sorting=True,
2388
show_all_files=False, show_comments=True)
2389
window.resize(self.size())
2391
self.register_editorwindow(window)
2392
self.connect(window, SIGNAL("destroyed()"),
2393
lambda win=window: self.unregister_editorwindow(win))
2395
def register_editorwindow(self, window):
2397
print >>STDOUT, "register_editorwindowQObject*:", window
2398
self.editorwindows.append(window)
2400
def unregister_editorwindow(self, window):
2402
print >>STDOUT, "unregister_editorwindow:", window
2403
self.editorwindows.pop(self.editorwindows.index(window))
2405
def get_focus_widget(self):
2409
def close_file_in_all_editorstacks(self, editorstack_id_str, index):
2410
for editorstack in self.editorstacks:
2411
if str(id(editorstack)) != editorstack_id_str:
2412
editorstack.blockSignals(True)
2413
editorstack.close_file(index, force=True)
2414
editorstack.blockSignals(False)
2416
# This method is never called in this plugin example. It's here only
2417
# to show how to use the file_saved signal (see above).
2419
def file_saved_in_editorstack(self, editorstack_id_str, index, filename):
2420
"""A file was saved in editorstack, this notifies others"""
2421
for editorstack in self.editorstacks:
2422
if str(id(editorstack)) != editorstack_id_str:
2423
editorstack.file_saved_in_other_editorstack(index, filename)
2425
# This method is never called in this plugin example. It's here only
2426
# to show how to use the file_saved signal (see above).
2428
def file_renamed_in_data_in_editorstack(self, editorstack_id_str,
2430
"""A file was renamed in data in editorstack, this notifies others"""
2431
for editorstack in self.editorstacks:
2432
if str(id(editorstack)) != editorstack_id_str:
2433
editorstack.rename_in_data(index, filename)
2435
def register_widget_shortcuts(self, context, widget):
2440
from spyderlib.utils.qthelpers import qapplication
2441
app = qapplication()
2442
test = EditorPluginExample()
2443
test.resize(900, 700)
2448
test.load("explorer.py")
2449
test.load("dicteditor.py")
2450
test.load("sourcecode/codeeditor.py")
2451
test.load("../spyder.py")
2452
print "Elapsed time: %.3f s" % (time.time()-t0)
2453
sys.exit(app.exec_())
2455
if __name__ == "__main__":