1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2010 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
7
"""QPlainTextEdit base class"""
9
# pylint: disable=C0103
10
# pylint: disable=R0903
11
# pylint: disable=R0911
12
# pylint: disable=R0201
17
from spyderlib.qt.QtGui import (QTextCursor, QColor, QFont, QApplication,
18
QTextEdit, QTextCharFormat, QToolTip,
19
QListWidget, QPlainTextEdit, QPalette,
20
QMainWindow, QTextOption)
21
from spyderlib.qt.QtCore import QPoint, SIGNAL, Qt, QEventLoop
25
from spyderlib.widgets.sourcecode.terminal import ANSIEscapeCodeHandler
26
from spyderlib.widgets.sourcecode import mixins
29
class CompletionWidget(QListWidget):
30
"""Completion list widget"""
31
def __init__(self, parent, ancestor):
32
QListWidget.__init__(self, ancestor)
33
self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint)
34
self.textedit = parent
35
self.completion_list = None
36
self.case_sensitive = False
37
self.show_single = None
38
self.enter_select = None
40
self.connect(self, SIGNAL("itemActivated(QListWidgetItem*)"),
43
def setup_appearance(self, size, font):
47
def show_list(self, completion_list, automatic=True):
48
if not self.show_single and len(completion_list) == 1 and not automatic:
49
self.textedit.insert_completion(completion_list[0])
52
self.completion_list = completion_list
54
self.addItems(completion_list)
57
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
62
# Retrieving current screen height
63
desktop = QApplication.desktop()
64
srect = desktop.availableGeometry(desktop.screenNumber(self))
65
screen_right = srect.right()
66
screen_bottom = srect.bottom()
68
point = self.textedit.cursorRect().bottomRight()
69
point.setX(point.x()+self.textedit.get_linenumberarea_width())
70
point = self.textedit.mapToGlobal(point)
72
# Computing completion widget and its parent right positions
73
comp_right = point.x()+self.width()
74
ancestor = self.parent()
76
anc_right = screen_right
78
anc_right = min([ancestor.x()+ancestor.width(), screen_right])
80
# Moving completion widget to the left
81
# if there is not enough space to the right
82
if comp_right > anc_right:
83
point.setX(point.x()-self.width())
85
# Computing completion widget and its parent bottom positions
86
comp_bottom = point.y()+self.height()
87
ancestor = self.parent()
89
anc_bottom = screen_bottom
91
anc_bottom = min([ancestor.y()+ancestor.height(), screen_bottom])
93
# Moving completion widget above if there is not enough space below
94
x_position = point.x()
95
if comp_bottom > anc_bottom:
96
point = self.textedit.cursorRect().topRight()
97
point = self.textedit.mapToGlobal(point)
98
point.setX(x_position)
99
point.setY(point.y()-self.height())
101
if ancestor is not None:
102
# Useful only if we set parent to 'ancestor' in __init__
103
point = ancestor.mapFromGlobal(point)
106
if unicode(self.textedit.completion_text):
107
# When initialized, if completion text is not empty, we need
108
# to update the displayed list:
109
self.update_current()
112
QListWidget.hide(self)
113
self.textedit.setFocus()
115
def keyPressEvent(self, event):
116
text, key = event.text(), event.key()
117
alt = event.modifiers() & Qt.AltModifier
118
shift = event.modifiers() & Qt.ShiftModifier
119
ctrl = event.modifiers() & Qt.ControlModifier
120
modifier = shift or ctrl or alt
121
if (key in (Qt.Key_Return, Qt.Key_Enter) and self.enter_select) \
122
or key == Qt.Key_Tab:
124
elif key in (Qt.Key_Return, Qt.Key_Enter,
125
Qt.Key_Left, Qt.Key_Right) or text in ('.', ':'):
127
self.textedit.keyPressEvent(event)
128
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown,
129
Qt.Key_Home, Qt.Key_End,
130
Qt.Key_CapsLock) and not modifier:
131
QListWidget.keyPressEvent(self, event)
132
elif len(text) or key == Qt.Key_Backspace:
133
self.textedit.keyPressEvent(event)
134
self.update_current()
136
self.textedit.keyPressEvent(event)
139
QListWidget.keyPressEvent(self, event)
141
def update_current(self):
142
completion_text = unicode(self.textedit.completion_text)
144
for row, completion in enumerate(self.completion_list):
145
if not self.case_sensitive:
146
completion = completion.lower()
147
completion_text = completion_text.lower()
148
if completion.startswith(completion_text):
149
self.setCurrentRow(row)
156
def focusOutEvent(self, event):
160
def item_selected(self, item=None):
162
item = self.currentItem()
163
self.textedit.insert_completion( unicode(item.text()) )
167
class TextEditBaseWidget(QPlainTextEdit, mixins.BaseEditMixin):
169
Text edit base widget
171
BRACE_MATCHING_SCOPE = ('sof', 'eof')
173
def __init__(self, parent=None):
174
QPlainTextEdit.__init__(self, parent)
175
mixins.BaseEditMixin.__init__(self)
176
self.setAttribute(Qt.WA_DeleteOnClose)
178
self.extra_selections_dict = {}
180
self.connect(self, SIGNAL('textChanged()'), self.changed)
181
self.connect(self, SIGNAL('cursorPositionChanged()'),
182
self.cursor_position_changed)
184
self.indent_chars = " "*4
186
# Code completion / calltips
187
if parent is not None:
189
while not isinstance(mainwin, QMainWindow):
190
mainwin = mainwin.parent()
193
if mainwin is not None:
195
self.completion_widget = CompletionWidget(self, parent)
196
self.codecompletion_auto = False
197
self.codecompletion_case = True
198
self.codecompletion_single = False
199
self.codecompletion_enter = False
201
self.calltip_position = None
202
self.calltip_size = 600
203
self.calltip_font = QFont()
204
self.completion_text = ""
208
self.matched_p_color = QColor(Qt.green)
209
self.unmatched_p_color = QColor(Qt.red)
211
def setup_calltips(self, size=None, font=None):
212
self.calltip_size = size
213
self.calltip_font = font
215
def setup_completion(self, size=None, font=None):
216
self.completion_widget.setup_appearance(size, font)
218
def set_indent_chars(self, indent_chars):
219
self.indent_chars = indent_chars
221
def set_palette(self, background, foreground):
223
Set text editor palette colors:
224
background color and caret (text cursor) color
227
palette.setColor(QPalette.Base, background)
228
palette.setColor(QPalette.Text, foreground)
229
self.setPalette(palette)
231
#------Line number area
232
def get_linenumberarea_width(self):
233
"""Return line number area width"""
234
# Implemented in CodeEditor, but needed here for completion widget
237
#------Extra selections
238
def get_extra_selections(self, key):
239
return self.extra_selections_dict.get(key, [])
241
def set_extra_selections(self, key, extra_selections):
242
self.extra_selections_dict[key] = extra_selections
244
def update_extra_selections(self):
245
extra_selections = []
246
for _key, extra in self.extra_selections_dict.iteritems():
247
extra_selections.extend(extra)
248
self.setExtraSelections(extra_selections)
250
def clear_extra_selections(self, key):
251
self.extra_selections_dict[key] = []
252
self.update_extra_selections()
256
"""Emit changed signal"""
257
self.emit(SIGNAL('modificationChanged(bool)'),
258
self.document().isModified())
261
#------Brace matching
262
def find_brace_match(self, position, brace, forward):
263
start_pos, end_pos = self.BRACE_MATCHING_SCOPE
265
bracemap = {'(': ')', '[': ']', '{': '}'}
266
text = self.get_text(position, end_pos)
270
bracemap = {')': '(', ']': '[', '}': '{'}
271
text = self.get_text(start_pos, position)
272
i_start_open = len(text)-1
273
i_start_close = len(text)-1
277
i_close = text.find(bracemap[brace], i_start_close)
279
i_close = text.rfind(bracemap[brace], 0, i_start_close+1)
282
i_start_close = i_close+1
283
i_open = text.find(brace, i_start_open, i_close)
285
i_start_close = i_close-1
286
i_open = text.rfind(brace, i_close, i_start_open+1)
289
i_start_open = i_open+1
291
i_start_open = i_open-1
293
# found matching brace
295
return position+i_close
297
return position-(len(text)-i_close)
302
def __highlight(self, positions, color=None, cancel=False):
304
self.clear_extra_selections('brace_matching')
306
extra_selections = []
307
for position in positions:
308
if position > self.get_position('eof'):
310
selection = QTextEdit.ExtraSelection()
311
selection.format.setBackground(color)
312
selection.cursor = self.textCursor()
313
selection.cursor.clearSelection()
314
selection.cursor.setPosition(position)
315
selection.cursor.movePosition(QTextCursor.NextCharacter,
316
QTextCursor.KeepAnchor)
317
extra_selections.append(selection)
318
self.set_extra_selections('brace_matching', extra_selections)
319
self.update_extra_selections()
321
def cursor_position_changed(self):
323
if self.bracepos is not None:
324
self.__highlight(self.bracepos, cancel=True)
326
cursor = self.textCursor()
327
if cursor.position() == 0:
329
cursor.movePosition(QTextCursor.PreviousCharacter,
330
QTextCursor.KeepAnchor)
331
text = unicode(cursor.selectedText())
332
pos1 = cursor.position()
333
if text in (')', ']', '}'):
334
pos2 = self.find_brace_match(pos1, text, forward=False)
335
elif text in ('(', '[', '{'):
336
pos2 = self.find_brace_match(pos1, text, forward=True)
340
self.bracepos = (pos1, pos2)
341
self.__highlight(self.bracepos, color=self.matched_p_color)
343
self.bracepos = (pos1,)
344
self.__highlight(self.bracepos, color=self.unmatched_p_color)
347
#-----Widget setup and options
348
def set_codecompletion_auto(self, state):
349
"""Set code completion state"""
350
self.codecompletion_auto = state
352
def set_codecompletion_case(self, state):
353
"""Case sensitive completion"""
354
self.codecompletion_case = state
355
self.completion_widget.case_sensitive = state
357
def set_codecompletion_single(self, state):
358
"""Show single completion"""
359
self.codecompletion_single = state
360
self.completion_widget.show_single = state
362
def set_codecompletion_enter(self, state):
363
"""Enable Enter key to select completion"""
364
self.codecompletion_enter = state
365
self.completion_widget.enter_select = state
367
def set_calltips(self, state):
368
"""Set calltips state"""
369
self.calltips = state
371
def set_wrap_mode(self, mode=None):
374
Valid *mode* values: None, 'word', 'character'
377
wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere
378
elif mode == 'character':
379
wrap_mode = QTextOption.WrapAnywhere
381
wrap_mode = QTextOption.NoWrap
382
self.setWordWrapMode(wrap_mode)
385
#------Reimplementing Qt methods
388
Reimplement Qt method
389
Copy text to clipboard with correct EOL chars
391
QApplication.clipboard().setText(self.get_selected_text())
394
#------Text: get, set, ...
395
def get_executable_text(self):
396
"""Return selected text or current line as an processed text,
397
to be executable in a Python/IPython interpreter"""
398
no_selection = not self.has_selected_text()
400
self.select_current_block()
402
ls = self.get_line_separator()
404
_indent = lambda line: len(line)-len(line.lstrip())
406
line_from, line_to = self.get_selection_bounds()
407
text = self.get_selected_text()
409
lines = text.split(ls)
411
# Multiline selection -> eventually fixing indentation
412
original_indent = _indent(self.get_text_line(line_from))
413
text = (" "*(original_indent-_indent(lines[0])))+text
415
# If there is a common indent to all lines, remove it
417
for line in text.split(ls):
419
min_indent = min(_indent(line), min_indent)
421
text = ls.join([line[min_indent:] for line in text.split(ls)])
423
# Add an EOL character if a block stars with various Python
424
# reserved words, so that it gets evaluated automatically
426
first_line = lines[0].lstrip()
427
last_line = self.get_text_line(line_to).strip()
428
words = ['def', 'for', 'if', 'while', 'try', 'with', 'class']
429
if any([first_line.startswith(w) for w in words]):
436
self.clear_selection()
440
def get_line_count(self):
441
"""Return document total line number"""
442
return self.blockCount()
444
def __duplicate_line_or_selection(self, after_current_line=True):
445
"""Duplicate current line or selected text"""
446
cursor = self.textCursor()
447
cursor.beginEditBlock()
448
orig_sel = start_pos, end_pos = (cursor.selectionStart(),
449
cursor.selectionEnd())
450
if unicode(cursor.selectedText()):
451
cursor.setPosition(end_pos)
452
# Check if end_pos is at the start of a block: if so, starting
453
# changes from the previous block
454
cursor.movePosition(QTextCursor.StartOfBlock,
455
QTextCursor.KeepAnchor)
456
if not unicode(cursor.selectedText()):
457
cursor.movePosition(QTextCursor.PreviousBlock)
458
end_pos = cursor.position()
460
cursor.setPosition(start_pos)
461
cursor.movePosition(QTextCursor.StartOfBlock)
462
while cursor.position() <= end_pos:
463
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
465
cursor_temp = QTextCursor(cursor)
466
cursor_temp.clearSelection()
467
cursor_temp.insertText(self.get_line_separator())
469
cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
470
text = cursor.selectedText()
471
cursor.clearSelection()
473
if not after_current_line:
474
# Moving cursor before current line/selected text
475
cursor.setPosition(start_pos)
476
cursor.movePosition(QTextCursor.StartOfBlock)
477
orig_sel = (orig_sel[0]+len(text), orig_sel[1]+len(text))
479
cursor.insertText(text)
480
cursor.endEditBlock()
481
cursor.setPosition(orig_sel[0])
482
cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
483
self.setTextCursor(cursor)
485
def duplicate_line(self):
487
Duplicate current line or selected text
488
Paste the duplicated text *after* the current line/selected text
490
self.__duplicate_line_or_selection(after_current_line=True)
494
Copy current line or selected text
495
Paste the duplicated text *before* the current line/selected text
497
self.__duplicate_line_or_selection(after_current_line=False)
499
def __move_line_or_selection(self, after_current_line=True):
500
"""Move current line or selected text"""
501
cursor = self.textCursor()
502
cursor.beginEditBlock()
503
orig_sel = start_pos, end_pos = (cursor.selectionStart(),
504
cursor.selectionEnd())
505
if unicode(cursor.selectedText()):
506
# Check if start_pos is at the start of a block
507
cursor.setPosition(start_pos)
508
cursor.movePosition(QTextCursor.StartOfBlock)
509
start_pos = cursor.position()
511
cursor.setPosition(end_pos)
512
# Check if end_pos is at the start of a block: if so, starting
513
# changes from the previous block
514
cursor.movePosition(QTextCursor.StartOfBlock,
515
QTextCursor.KeepAnchor)
516
if unicode(cursor.selectedText()):
517
cursor.movePosition(QTextCursor.NextBlock)
518
end_pos = cursor.position()
520
cursor.movePosition(QTextCursor.StartOfBlock)
521
start_pos = cursor.position()
522
cursor.movePosition(QTextCursor.NextBlock)
523
end_pos = cursor.position()
524
cursor.setPosition(start_pos)
525
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
527
sel_text = unicode(cursor.selectedText())
528
cursor.removeSelectedText()
530
if after_current_line:
531
text = unicode(cursor.block().text())
532
orig_sel = (orig_sel[0]+len(text)+1, orig_sel[1]+len(text)+1)
533
cursor.movePosition(QTextCursor.NextBlock)
535
cursor.movePosition(QTextCursor.PreviousBlock)
536
text = unicode(cursor.block().text())
537
orig_sel = (orig_sel[0]-len(text)-1, orig_sel[1]-len(text)-1)
538
cursor.insertText(sel_text)
540
cursor.endEditBlock()
542
cursor.setPosition(orig_sel[0])
543
cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
544
self.setTextCursor(cursor)
546
def move_line_up(self):
547
"""Move up current line or selected text"""
548
self.__move_line_or_selection(after_current_line=False)
550
def move_line_down(self):
551
"""Move down current line or selected text"""
552
self.__move_line_or_selection(after_current_line=True)
554
def extend_selection_to_complete_lines(self):
555
"""Extend current selection to complete lines"""
556
cursor = self.textCursor()
557
start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
558
cursor.setPosition(start_pos)
559
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
560
if cursor.atBlockStart():
561
cursor.movePosition(QTextCursor.PreviousBlock,
562
QTextCursor.KeepAnchor)
563
cursor.movePosition(QTextCursor.EndOfBlock,
564
QTextCursor.KeepAnchor)
565
self.setTextCursor(cursor)
567
def delete_line(self):
568
"""Delete current line"""
569
cursor = self.textCursor()
570
if self.has_selected_text():
571
self.extend_selection_to_complete_lines()
572
start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
573
cursor.setPosition(start_pos)
575
start_pos = end_pos = cursor.position()
576
cursor.beginEditBlock()
577
cursor.setPosition(start_pos)
578
cursor.movePosition(QTextCursor.StartOfBlock)
579
while cursor.position() <= end_pos:
580
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
583
cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
584
cursor.removeSelectedText()
585
cursor.endEditBlock()
586
self.ensureCursorVisible()
589
#------Code completion / Calltips
590
def show_calltip(self, title, text, color='#2D62FF',
591
at_line=None, at_position=None):
593
if text is None or len(text) == 0:
595
# Saving cursor position:
596
if at_position is None:
597
at_position = self.get_position('cursor')
598
self.calltip_position = at_position
600
weight = 'bold' if self.calltip_font.bold() else 'normal'
601
size = self.calltip_font.pointSize()
602
family = self.calltip_font.family()
603
format1 = '<div style=\'font-size: %spt; color: %s\'>' % (size, color)
604
format2 = '<hr><div style=\'font-family: "%s"; font-size: %spt; font-weight: %s\'>' % (family, size, weight)
605
if isinstance(text, list):
606
text = "\n ".join(text)
607
text = text.replace('\n', '<br>')
608
if len(text) > self.calltip_size:
609
text = text[:self.calltip_size] + " ..."
610
tiptext = format1 + ('<b>%s</b></div>' % title) \
611
+ format2 + text + "</div>"
612
# Showing tooltip at cursor position:
613
cx, cy = self.get_coordinates('cursor')
614
if at_line is not None:
616
cursor = QTextCursor(self.document().findBlockByNumber(at_line-1))
617
cy = self.cursorRect(cursor).top()
618
point = self.mapToGlobal(QPoint(cx, cy))
619
point.setX(point.x()+self.get_linenumberarea_width())
620
QToolTip.showText(point, tiptext)
622
def hide_tooltip_if_necessary(self, key):
623
"""Hide calltip when necessary"""
625
calltip_char = self.get_character(self.calltip_position)
626
before = self.is_cursor_before(self.calltip_position,
628
other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab)
629
if calltip_char not in ('?','(') or before or other:
631
except (IndexError, TypeError):
634
def show_completion_widget(self, textlist, automatic=True):
635
"""Show completion widget"""
636
self.completion_widget.show_list(textlist, automatic=automatic)
638
def hide_completion_widget(self):
639
"""Hide completion widget"""
640
self.completion_widget.hide()
642
def show_completion_list(self, completions, completion_text="",
644
"""Display the possible completions"""
645
if len(completions) == 0 or completions == [completion_text]:
647
self.completion_text = completion_text
648
# Sorting completion list (entries starting with underscore are
649
# put at the end of the list):
650
underscore = set([comp for comp in completions if comp.startswith('_')])
651
completions = sorted(set(completions)-underscore, key=string.lower)+\
652
sorted(underscore, key=string.lower)
653
self.show_completion_widget(completions, automatic=automatic)
655
def select_completion_list(self):
656
"""Completion list is active, Enter was just pressed"""
657
self.completion_widget.item_selected()
659
def insert_completion(self, text):
661
cursor = self.textCursor()
662
cursor.movePosition(QTextCursor.PreviousCharacter,
663
QTextCursor.KeepAnchor,
664
len(self.completion_text))
665
cursor.removeSelectedText()
666
self.insert_text(text)
668
def is_completion_widget_visible(self):
669
"""Return True is completion list widget is visible"""
670
return self.completion_widget.isVisible()
674
def stdkey_clear(self):
675
if not self.has_selected_text():
676
self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
677
self.remove_selected_text()
679
def stdkey_backspace(self):
680
if not self.has_selected_text():
681
self.moveCursor(QTextCursor.PreviousCharacter,
682
QTextCursor.KeepAnchor)
683
self.remove_selected_text()
685
def __get_move_mode(self, shift):
686
return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor
688
def stdkey_up(self, shift):
689
self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift))
691
def stdkey_down(self, shift):
692
self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift))
694
def stdkey_tab(self):
695
self.insert_text(self.indent_chars)
697
def stdkey_home(self, shift, ctrl, prompt_pos=None):
698
"""Smart HOME feature: cursor is first moved at
699
indentation position, then at the start of the line"""
700
move_mode = self.__get_move_mode(shift)
702
self.moveCursor(QTextCursor.Start, move_mode)
704
cursor = self.textCursor()
705
if prompt_pos is None:
706
start_position = self.get_position('sol')
708
start_position = self.get_position(prompt_pos)
709
text = self.get_text(start_position, 'eol')
710
indent_pos = start_position+len(text)-len(text.lstrip())
711
if cursor.position() != indent_pos:
712
cursor.setPosition(indent_pos, move_mode)
714
cursor.setPosition(start_position, move_mode)
715
self.setTextCursor(cursor)
717
def stdkey_end(self, shift, ctrl):
718
move_mode = self.__get_move_mode(shift)
720
self.moveCursor(QTextCursor.End, move_mode)
722
self.moveCursor(QTextCursor.EndOfBlock, move_mode)
724
def stdkey_pageup(self):
727
def stdkey_pagedown(self):
730
def stdkey_escape(self):
735
def focusInEvent(self, event):
736
"""Reimplemented to handle focus"""
737
self.emit(SIGNAL("focus_changed()"))
738
self.emit(SIGNAL("focus_in()"))
739
QPlainTextEdit.focusInEvent(self, event)
741
def focusOutEvent(self, event):
742
"""Reimplemented to handle focus"""
743
self.emit(SIGNAL("focus_changed()"))
744
QPlainTextEdit.focusOutEvent(self, event)
747
class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler):
749
ANSIEscapeCodeHandler.__init__(self)
750
self.base_format = None
751
self.current_format = None
753
def set_light_background(self, state):
755
self.default_foreground_color = 30
756
self.default_background_color = 47
758
self.default_foreground_color = 37
759
self.default_background_color = 40
761
def set_base_format(self, base_format):
762
self.base_format = base_format
764
def get_format(self):
765
return self.current_format
769
Set font style with the following attributes:
770
'foreground_color', 'background_color', 'italic',
771
'bold' and 'underline'
773
if self.current_format is None:
774
assert self.base_format is not None
775
self.current_format = QTextCharFormat(self.base_format)
777
if self.foreground_color is None:
778
qcolor = self.base_format.foreground()
780
cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity]
781
qcolor = QColor(cstr)
782
self.current_format.setForeground(qcolor)
784
if self.background_color is None:
785
qcolor = self.base_format.background()
787
cstr = self.ANSI_COLORS[self.background_color-40][self.intensity]
788
qcolor = QColor(cstr)
789
self.current_format.setBackground(qcolor)
791
font = self.current_format.font()
793
if self.italic is None:
794
italic = self.base_format.fontItalic()
797
font.setItalic(italic)
799
if self.bold is None:
800
bold = self.base_format.font().bold()
805
if self.underline is None:
806
underline = self.base_format.font().underline()
808
underline = self.underline
809
font.setUnderline(underline)
810
self.current_format.setFont(font)
813
def inverse_color(color):
814
color.setHsv(color.hue(), color.saturation(), 255-color.value())
816
class ConsoleFontStyle(object):
817
def __init__(self, foregroundcolor, backgroundcolor,
818
bold, italic, underline):
819
self.foregroundcolor = foregroundcolor
820
self.backgroundcolor = backgroundcolor
823
self.underline = underline
826
def apply_style(self, font, light_background, is_default):
827
self.format = QTextCharFormat()
828
self.format.setFont(font)
829
foreground = QColor(self.foregroundcolor)
830
if not light_background and is_default:
831
inverse_color(foreground)
832
self.format.setForeground(foreground)
833
background = QColor(self.backgroundcolor)
834
if not light_background:
835
inverse_color(background)
836
self.format.setBackground(background)
837
font = self.format.font()
838
font.setBold(self.bold)
839
font.setItalic(self.italic)
840
font.setUnderline(self.underline)
841
self.format.setFont(font)
843
class ConsoleBaseWidget(TextEditBaseWidget):
844
"""Console base widget"""
845
BRACE_MATCHING_SCOPE = ('sol', 'eol')
846
COLOR_PATTERN = re.compile('\x01?\x1b\[(.*?)m\x02?')
848
def __init__(self, parent=None):
849
TextEditBaseWidget.__init__(self, parent)
851
self.light_background = True
853
self.setMaximumBlockCount(300)
855
# ANSI escape code handler
856
self.ansi_handler = QtANSIEscapeCodeHandler()
858
# Disable undo/redo (nonsense for a console widget...):
859
self.setUndoRedoEnabled(False)
861
self.connect(self, SIGNAL('userListActivated(int, const QString)'),
862
lambda user_id, text:
863
self.emit(SIGNAL('completion_widget_activated(QString)'),
866
self.default_style = ConsoleFontStyle(
867
foregroundcolor=0x000000, backgroundcolor=0xFFFFFF,
868
bold=False, italic=False, underline=False)
869
self.error_style = ConsoleFontStyle(
870
foregroundcolor=0xFF0000, backgroundcolor=0xFFFFFF,
871
bold=False, italic=False, underline=False)
872
self.traceback_link_style = ConsoleFontStyle(
873
foregroundcolor=0x0000FF, backgroundcolor=0xFFFFFF,
874
bold=True, italic=False, underline=True)
875
self.prompt_style = ConsoleFontStyle(
876
foregroundcolor=0x00AA00, backgroundcolor=0xFFFFFF,
877
bold=True, italic=False, underline=False)
878
self.font_styles = (self.default_style, self.error_style,
879
self.traceback_link_style, self.prompt_style)
880
self.set_pythonshell_font()
881
self.setMouseTracking(True)
883
def set_light_background(self, state):
884
self.light_background = state
886
self.set_palette(background=QColor(Qt.white),
887
foreground=QColor(Qt.darkGray))
889
self.set_palette(background=QColor(Qt.black),
890
foreground=QColor(Qt.lightGray))
891
self.ansi_handler.set_light_background(state)
892
self.set_pythonshell_font()
894
def set_selection(self, start, end):
895
cursor = self.textCursor()
896
cursor.setPosition(start)
897
cursor.setPosition(end, QTextCursor.KeepAnchor)
898
self.setTextCursor(cursor)
900
def truncate_selection(self, position_from):
901
"""Unselect read-only parts in shell, like prompt"""
902
position_from = self.get_position(position_from)
903
cursor = self.textCursor()
904
start, end = cursor.selectionStart(), cursor.selectionEnd()
906
start = max([position_from, start])
908
end = max([position_from, end])
909
self.set_selection(start, end)
911
def restrict_cursor_position(self, position_from, position_to):
912
"""In shell, avoid editing text except between prompt and EOF"""
913
position_from = self.get_position(position_from)
914
position_to = self.get_position(position_to)
915
cursor = self.textCursor()
916
cursor_position = cursor.position()
917
if cursor_position < position_from or cursor_position > position_to:
918
self.set_cursor_position(position_to)
921
def insert_text(self, text):
922
"""Reimplement TextEditBaseWidget method"""
923
self.textCursor().insertText(text, self.default_style.format)
926
"""Reimplement Qt method"""
927
if self.has_selected_text():
928
self.remove_selected_text()
929
self.insert_text(QApplication.clipboard().text())
931
def append_text_to_shell(self, text, error, prompt):
933
Append text to Python shell
934
In a way, this method overrides the method 'insert_text' when text is
935
inserted at the end of the text widget for a Python shell
937
Handles error messages and show blue underlined links
938
Handles ANSI color sequences
939
Handles ANSI FF sequence
941
cursor = self.textCursor()
942
cursor.movePosition(QTextCursor.End)
944
index = text.find(chr(12))
947
text = text[index+1:]
951
for text in text.splitlines(True):
952
if text.startswith(' File') \
953
and not text.startswith(' File "<'):
955
# Show error links in blue underlined text
956
cursor.insertText(' ', self.default_style.format)
957
cursor.insertText(text[2:],
958
self.traceback_link_style.format)
960
# Show error/warning messages in red
961
cursor.insertText(text, self.error_style.format)
963
self.emit(SIGNAL('traceback_available()'))
965
# Show prompt in green
966
cursor.insertText(text, self.prompt_style.format)
968
# Show other outputs in black
970
for match in self.COLOR_PATTERN.finditer(text):
971
cursor.insertText(text[last_end:match.start()],
972
self.default_style.format)
973
last_end = match.end()
974
for code in [int(_c) for _c in match.group(1).split(';')]:
975
self.ansi_handler.set_code(code)
976
self.default_style.format = self.ansi_handler.get_format()
977
cursor.insertText(text[last_end:], self.default_style.format)
978
# # Slower alternative:
979
# segments = self.COLOR_PATTERN.split(text)
980
# cursor.insertText(segments.pop(0), self.default_style.format)
982
# for ansi_tags, text in zip(segments[::2], segments[1::2]):
983
# for ansi_tag in ansi_tags.split(';'):
984
# self.ansi_handler.set_code(int(ansi_tag))
985
# self.default_style.format = self.ansi_handler.get_format()
986
# cursor.insertText(text, self.default_style.format)
987
self.set_cursor_position('eof')
988
self.setCurrentCharFormat(self.default_style.format)
990
def set_pythonshell_font(self, font=None):
991
"""Python Shell only"""
994
for style in self.font_styles:
995
style.apply_style(font=font,
996
light_background=self.light_background,
997
is_default=style is self.default_style)
998
self.ansi_handler.set_base_format(self.default_style.format)
1
# -*- coding: utf-8 -*-
3
# Copyright © 2009-2010 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
7
"""QPlainTextEdit base class"""
9
# pylint: disable=C0103
10
# pylint: disable=R0903
11
# pylint: disable=R0911
12
# pylint: disable=R0201
18
from spyderlib.qt.QtGui import (QTextCursor, QColor, QFont, QApplication,
19
QTextEdit, QTextCharFormat, QToolTip,
20
QListWidget, QPlainTextEdit, QPalette,
21
QMainWindow, QTextOption, QMouseEvent)
22
from spyderlib.qt.QtCore import QPoint, SIGNAL, Qt, QEventLoop, QEvent
26
from spyderlib.widgets.sourcecode.terminal import ANSIEscapeCodeHandler
27
from spyderlib.widgets.sourcecode import mixins
30
class CompletionWidget(QListWidget):
31
"""Completion list widget"""
32
def __init__(self, parent, ancestor):
33
QListWidget.__init__(self, ancestor)
34
self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint)
35
self.textedit = parent
36
self.completion_list = None
37
self.case_sensitive = False
38
self.show_single = None
39
self.enter_select = None
41
self.connect(self, SIGNAL("itemActivated(QListWidgetItem*)"),
44
def setup_appearance(self, size, font):
48
def show_list(self, completion_list, automatic=True):
49
if not self.show_single and len(completion_list) == 1 and not automatic:
50
self.textedit.insert_completion(completion_list[0])
53
self.completion_list = completion_list
55
self.addItems(completion_list)
58
QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
63
# Retrieving current screen height
64
desktop = QApplication.desktop()
65
srect = desktop.availableGeometry(desktop.screenNumber(self))
66
screen_right = srect.right()
67
screen_bottom = srect.bottom()
69
point = self.textedit.cursorRect().bottomRight()
70
point.setX(point.x()+self.textedit.get_linenumberarea_width())
71
point = self.textedit.mapToGlobal(point)
73
# Computing completion widget and its parent right positions
74
comp_right = point.x()+self.width()
75
ancestor = self.parent()
77
anc_right = screen_right
79
anc_right = min([ancestor.x()+ancestor.width(), screen_right])
81
# Moving completion widget to the left
82
# if there is not enough space to the right
83
if comp_right > anc_right:
84
point.setX(point.x()-self.width())
86
# Computing completion widget and its parent bottom positions
87
comp_bottom = point.y()+self.height()
88
ancestor = self.parent()
90
anc_bottom = screen_bottom
92
anc_bottom = min([ancestor.y()+ancestor.height(), screen_bottom])
94
# Moving completion widget above if there is not enough space below
95
x_position = point.x()
96
if comp_bottom > anc_bottom:
97
point = self.textedit.cursorRect().topRight()
98
point = self.textedit.mapToGlobal(point)
99
point.setX(x_position)
100
point.setY(point.y()-self.height())
102
if ancestor is not None:
103
# Useful only if we set parent to 'ancestor' in __init__
104
point = ancestor.mapFromGlobal(point)
107
if unicode(self.textedit.completion_text):
108
# When initialized, if completion text is not empty, we need
109
# to update the displayed list:
110
self.update_current()
113
QListWidget.hide(self)
114
self.textedit.setFocus()
116
def keyPressEvent(self, event):
117
text, key = event.text(), event.key()
118
alt = event.modifiers() & Qt.AltModifier
119
shift = event.modifiers() & Qt.ShiftModifier
120
ctrl = event.modifiers() & Qt.ControlModifier
121
modifier = shift or ctrl or alt
122
if (key in (Qt.Key_Return, Qt.Key_Enter) and self.enter_select) \
123
or key == Qt.Key_Tab:
125
elif key in (Qt.Key_Return, Qt.Key_Enter,
126
Qt.Key_Left, Qt.Key_Right) or text in ('.', ':'):
128
self.textedit.keyPressEvent(event)
129
elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown,
130
Qt.Key_Home, Qt.Key_End,
131
Qt.Key_CapsLock) and not modifier:
132
QListWidget.keyPressEvent(self, event)
133
elif len(text) or key == Qt.Key_Backspace:
134
self.textedit.keyPressEvent(event)
135
self.update_current()
137
self.textedit.keyPressEvent(event)
140
QListWidget.keyPressEvent(self, event)
142
def update_current(self):
143
completion_text = unicode(self.textedit.completion_text)
145
for row, completion in enumerate(self.completion_list):
146
if not self.case_sensitive:
147
completion = completion.lower()
148
completion_text = completion_text.lower()
149
if completion.startswith(completion_text):
150
self.setCurrentRow(row)
157
def focusOutEvent(self, event):
161
def item_selected(self, item=None):
163
item = self.currentItem()
164
self.textedit.insert_completion( unicode(item.text()) )
168
class TextEditBaseWidget(QPlainTextEdit, mixins.BaseEditMixin):
170
Text edit base widget
172
BRACE_MATCHING_SCOPE = ('sof', 'eof')
174
def __init__(self, parent=None):
175
QPlainTextEdit.__init__(self, parent)
176
mixins.BaseEditMixin.__init__(self)
177
self.setAttribute(Qt.WA_DeleteOnClose)
179
self.extra_selections_dict = {}
181
self.connect(self, SIGNAL('textChanged()'), self.changed)
182
self.connect(self, SIGNAL('cursorPositionChanged()'),
183
self.cursor_position_changed)
185
self.indent_chars = " "*4
187
# Code completion / calltips
188
if parent is not None:
190
while not isinstance(mainwin, QMainWindow):
191
mainwin = mainwin.parent()
194
if mainwin is not None:
196
self.completion_widget = CompletionWidget(self, parent)
197
self.codecompletion_auto = False
198
self.codecompletion_case = True
199
self.codecompletion_single = False
200
self.codecompletion_enter = False
202
self.calltip_position = None
203
self.calltip_size = 600
204
self.calltip_font = QFont()
205
self.completion_text = ""
209
self.matched_p_color = QColor(Qt.green)
210
self.unmatched_p_color = QColor(Qt.red)
212
def setup_calltips(self, size=None, font=None):
213
self.calltip_size = size
214
self.calltip_font = font
216
def setup_completion(self, size=None, font=None):
217
self.completion_widget.setup_appearance(size, font)
219
def set_indent_chars(self, indent_chars):
220
self.indent_chars = indent_chars
222
def set_palette(self, background, foreground):
224
Set text editor palette colors:
225
background color and caret (text cursor) color
228
palette.setColor(QPalette.Base, background)
229
palette.setColor(QPalette.Text, foreground)
230
self.setPalette(palette)
232
#------Line number area
233
def get_linenumberarea_width(self):
234
"""Return line number area width"""
235
# Implemented in CodeEditor, but needed here for completion widget
238
#------Extra selections
239
def get_extra_selections(self, key):
240
return self.extra_selections_dict.get(key, [])
242
def set_extra_selections(self, key, extra_selections):
243
self.extra_selections_dict[key] = extra_selections
245
def update_extra_selections(self):
246
extra_selections = []
247
for _key, extra in self.extra_selections_dict.iteritems():
248
extra_selections.extend(extra)
249
self.setExtraSelections(extra_selections)
251
def clear_extra_selections(self, key):
252
self.extra_selections_dict[key] = []
253
self.update_extra_selections()
257
"""Emit changed signal"""
258
self.emit(SIGNAL('modificationChanged(bool)'),
259
self.document().isModified())
262
#------Brace matching
263
def find_brace_match(self, position, brace, forward):
264
start_pos, end_pos = self.BRACE_MATCHING_SCOPE
266
bracemap = {'(': ')', '[': ']', '{': '}'}
267
text = self.get_text(position, end_pos)
271
bracemap = {')': '(', ']': '[', '}': '{'}
272
text = self.get_text(start_pos, position)
273
i_start_open = len(text)-1
274
i_start_close = len(text)-1
278
i_close = text.find(bracemap[brace], i_start_close)
280
i_close = text.rfind(bracemap[brace], 0, i_start_close+1)
283
i_start_close = i_close+1
284
i_open = text.find(brace, i_start_open, i_close)
286
i_start_close = i_close-1
287
i_open = text.rfind(brace, i_close, i_start_open+1)
290
i_start_open = i_open+1
292
i_start_open = i_open-1
294
# found matching brace
296
return position+i_close
298
return position-(len(text)-i_close)
303
def __highlight(self, positions, color=None, cancel=False):
305
self.clear_extra_selections('brace_matching')
307
extra_selections = []
308
for position in positions:
309
if position > self.get_position('eof'):
311
selection = QTextEdit.ExtraSelection()
312
selection.format.setBackground(color)
313
selection.cursor = self.textCursor()
314
selection.cursor.clearSelection()
315
selection.cursor.setPosition(position)
316
selection.cursor.movePosition(QTextCursor.NextCharacter,
317
QTextCursor.KeepAnchor)
318
extra_selections.append(selection)
319
self.set_extra_selections('brace_matching', extra_selections)
320
self.update_extra_selections()
322
def cursor_position_changed(self):
324
if self.bracepos is not None:
325
self.__highlight(self.bracepos, cancel=True)
327
cursor = self.textCursor()
328
if cursor.position() == 0:
330
cursor.movePosition(QTextCursor.PreviousCharacter,
331
QTextCursor.KeepAnchor)
332
text = unicode(cursor.selectedText())
333
pos1 = cursor.position()
334
if text in (')', ']', '}'):
335
pos2 = self.find_brace_match(pos1, text, forward=False)
336
elif text in ('(', '[', '{'):
337
pos2 = self.find_brace_match(pos1, text, forward=True)
341
self.bracepos = (pos1, pos2)
342
self.__highlight(self.bracepos, color=self.matched_p_color)
344
self.bracepos = (pos1,)
345
self.__highlight(self.bracepos, color=self.unmatched_p_color)
348
#-----Widget setup and options
349
def set_codecompletion_auto(self, state):
350
"""Set code completion state"""
351
self.codecompletion_auto = state
353
def set_codecompletion_case(self, state):
354
"""Case sensitive completion"""
355
self.codecompletion_case = state
356
self.completion_widget.case_sensitive = state
358
def set_codecompletion_single(self, state):
359
"""Show single completion"""
360
self.codecompletion_single = state
361
self.completion_widget.show_single = state
363
def set_codecompletion_enter(self, state):
364
"""Enable Enter key to select completion"""
365
self.codecompletion_enter = state
366
self.completion_widget.enter_select = state
368
def set_calltips(self, state):
369
"""Set calltips state"""
370
self.calltips = state
372
def set_wrap_mode(self, mode=None):
375
Valid *mode* values: None, 'word', 'character'
378
wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere
379
elif mode == 'character':
380
wrap_mode = QTextOption.WrapAnywhere
382
wrap_mode = QTextOption.NoWrap
383
self.setWordWrapMode(wrap_mode)
386
#------Reimplementing Qt methods
389
Reimplement Qt method
390
Copy text to clipboard with correct EOL chars
392
QApplication.clipboard().setText(self.get_selected_text())
395
#------Text: get, set, ...
396
def get_executable_text(self):
397
"""Return selected text or current line as an processed text,
398
to be executable in a Python/IPython interpreter"""
399
no_selection = not self.has_selected_text()
401
self.select_current_block()
403
ls = self.get_line_separator()
405
_indent = lambda line: len(line)-len(line.lstrip())
407
line_from, line_to = self.get_selection_bounds()
408
text = self.get_selected_text()
410
lines = text.split(ls)
412
# Multiline selection -> eventually fixing indentation
413
original_indent = _indent(self.get_text_line(line_from))
414
text = (" "*(original_indent-_indent(lines[0])))+text
416
# If there is a common indent to all lines, remove it
418
for line in text.split(ls):
420
min_indent = min(_indent(line), min_indent)
422
text = ls.join([line[min_indent:] for line in text.split(ls)])
424
# Add an EOL character if a block stars with various Python
425
# reserved words, so that it gets evaluated automatically
427
first_line = lines[0].lstrip()
428
last_line = self.get_text_line(line_to).strip()
429
words = ['def', 'for', 'if', 'while', 'try', 'with', 'class']
430
if any([first_line.startswith(w) for w in words]):
437
self.clear_selection()
441
def get_line_count(self):
442
"""Return document total line number"""
443
return self.blockCount()
445
def __duplicate_line_or_selection(self, after_current_line=True):
446
"""Duplicate current line or selected text"""
447
cursor = self.textCursor()
448
cursor.beginEditBlock()
449
orig_sel = start_pos, end_pos = (cursor.selectionStart(),
450
cursor.selectionEnd())
451
if unicode(cursor.selectedText()):
452
cursor.setPosition(end_pos)
453
# Check if end_pos is at the start of a block: if so, starting
454
# changes from the previous block
455
cursor.movePosition(QTextCursor.StartOfBlock,
456
QTextCursor.KeepAnchor)
457
if not unicode(cursor.selectedText()):
458
cursor.movePosition(QTextCursor.PreviousBlock)
459
end_pos = cursor.position()
461
cursor.setPosition(start_pos)
462
cursor.movePosition(QTextCursor.StartOfBlock)
463
while cursor.position() <= end_pos:
464
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
466
cursor_temp = QTextCursor(cursor)
467
cursor_temp.clearSelection()
468
cursor_temp.insertText(self.get_line_separator())
470
cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
471
text = cursor.selectedText()
472
cursor.clearSelection()
474
if not after_current_line:
475
# Moving cursor before current line/selected text
476
cursor.setPosition(start_pos)
477
cursor.movePosition(QTextCursor.StartOfBlock)
478
orig_sel = (orig_sel[0]+len(text), orig_sel[1]+len(text))
480
cursor.insertText(text)
481
cursor.endEditBlock()
482
cursor.setPosition(orig_sel[0])
483
cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
484
self.setTextCursor(cursor)
486
def duplicate_line(self):
488
Duplicate current line or selected text
489
Paste the duplicated text *after* the current line/selected text
491
self.__duplicate_line_or_selection(after_current_line=True)
495
Copy current line or selected text
496
Paste the duplicated text *before* the current line/selected text
498
self.__duplicate_line_or_selection(after_current_line=False)
500
def __move_line_or_selection(self, after_current_line=True):
501
"""Move current line or selected text"""
502
cursor = self.textCursor()
503
cursor.beginEditBlock()
504
orig_sel = start_pos, end_pos = (cursor.selectionStart(),
505
cursor.selectionEnd())
506
if unicode(cursor.selectedText()):
507
# Check if start_pos is at the start of a block
508
cursor.setPosition(start_pos)
509
cursor.movePosition(QTextCursor.StartOfBlock)
510
start_pos = cursor.position()
512
cursor.setPosition(end_pos)
513
# Check if end_pos is at the start of a block: if so, starting
514
# changes from the previous block
515
cursor.movePosition(QTextCursor.StartOfBlock,
516
QTextCursor.KeepAnchor)
517
if unicode(cursor.selectedText()):
518
cursor.movePosition(QTextCursor.NextBlock)
519
end_pos = cursor.position()
521
cursor.movePosition(QTextCursor.StartOfBlock)
522
start_pos = cursor.position()
523
cursor.movePosition(QTextCursor.NextBlock)
524
end_pos = cursor.position()
525
cursor.setPosition(start_pos)
526
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
528
sel_text = unicode(cursor.selectedText())
529
cursor.removeSelectedText()
531
if after_current_line:
532
text = unicode(cursor.block().text())
533
orig_sel = (orig_sel[0]+len(text)+1, orig_sel[1]+len(text)+1)
534
cursor.movePosition(QTextCursor.NextBlock)
536
cursor.movePosition(QTextCursor.PreviousBlock)
537
text = unicode(cursor.block().text())
538
orig_sel = (orig_sel[0]-len(text)-1, orig_sel[1]-len(text)-1)
539
cursor.insertText(sel_text)
541
cursor.endEditBlock()
543
cursor.setPosition(orig_sel[0])
544
cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
545
self.setTextCursor(cursor)
547
def move_line_up(self):
548
"""Move up current line or selected text"""
549
self.__move_line_or_selection(after_current_line=False)
551
def move_line_down(self):
552
"""Move down current line or selected text"""
553
self.__move_line_or_selection(after_current_line=True)
555
def extend_selection_to_complete_lines(self):
556
"""Extend current selection to complete lines"""
557
cursor = self.textCursor()
558
start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
559
cursor.setPosition(start_pos)
560
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
561
if cursor.atBlockStart():
562
cursor.movePosition(QTextCursor.PreviousBlock,
563
QTextCursor.KeepAnchor)
564
cursor.movePosition(QTextCursor.EndOfBlock,
565
QTextCursor.KeepAnchor)
566
self.setTextCursor(cursor)
568
def delete_line(self):
569
"""Delete current line"""
570
cursor = self.textCursor()
571
if self.has_selected_text():
572
self.extend_selection_to_complete_lines()
573
start_pos, end_pos = cursor.selectionStart(), cursor.selectionEnd()
574
cursor.setPosition(start_pos)
576
start_pos = end_pos = cursor.position()
577
cursor.beginEditBlock()
578
cursor.setPosition(start_pos)
579
cursor.movePosition(QTextCursor.StartOfBlock)
580
while cursor.position() <= end_pos:
581
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
584
cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
585
cursor.removeSelectedText()
586
cursor.endEditBlock()
587
self.ensureCursorVisible()
590
#------Code completion / Calltips
591
def show_calltip(self, title, text, color='#2D62FF',
592
at_line=None, at_position=None):
594
if text is None or len(text) == 0:
596
# Saving cursor position:
597
if at_position is None:
598
at_position = self.get_position('cursor')
599
self.calltip_position = at_position
601
weight = 'bold' if self.calltip_font.bold() else 'normal'
602
size = self.calltip_font.pointSize()
603
family = self.calltip_font.family()
604
format1 = '<div style=\'font-size: %spt; color: %s\'>' % (size, color)
605
format2 = '<hr><div style=\'font-family: "%s"; font-size: %spt; font-weight: %s\'>' % (family, size, weight)
606
if isinstance(text, list):
607
text = "\n ".join(text)
608
text = text.replace('\n', '<br>')
609
if len(text) > self.calltip_size:
610
text = text[:self.calltip_size] + " ..."
611
tiptext = format1 + ('<b>%s</b></div>' % title) \
612
+ format2 + text + "</div>"
613
# Showing tooltip at cursor position:
614
cx, cy = self.get_coordinates('cursor')
615
if at_line is not None:
617
cursor = QTextCursor(self.document().findBlockByNumber(at_line-1))
618
cy = self.cursorRect(cursor).top()
619
point = self.mapToGlobal(QPoint(cx, cy))
620
point.setX(point.x()+self.get_linenumberarea_width())
621
QToolTip.showText(point, tiptext)
623
def hide_tooltip_if_necessary(self, key):
624
"""Hide calltip when necessary"""
626
calltip_char = self.get_character(self.calltip_position)
627
before = self.is_cursor_before(self.calltip_position,
629
other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab)
630
if calltip_char not in ('?','(') or before or other:
632
except (IndexError, TypeError):
635
def show_completion_widget(self, textlist, automatic=True):
636
"""Show completion widget"""
637
self.completion_widget.show_list(textlist, automatic=automatic)
639
def hide_completion_widget(self):
640
"""Hide completion widget"""
641
self.completion_widget.hide()
643
def show_completion_list(self, completions, completion_text="",
645
"""Display the possible completions"""
646
if len(completions) == 0 or completions == [completion_text]:
648
self.completion_text = completion_text
649
# Sorting completion list (entries starting with underscore are
650
# put at the end of the list):
651
underscore = set([comp for comp in completions if comp.startswith('_')])
652
completions = sorted(set(completions)-underscore, key=string.lower)+\
653
sorted(underscore, key=string.lower)
654
self.show_completion_widget(completions, automatic=automatic)
656
def select_completion_list(self):
657
"""Completion list is active, Enter was just pressed"""
658
self.completion_widget.item_selected()
660
def insert_completion(self, text):
662
cursor = self.textCursor()
663
cursor.movePosition(QTextCursor.PreviousCharacter,
664
QTextCursor.KeepAnchor,
665
len(self.completion_text))
666
cursor.removeSelectedText()
667
self.insert_text(text)
669
def is_completion_widget_visible(self):
670
"""Return True is completion list widget is visible"""
671
return self.completion_widget.isVisible()
675
def stdkey_clear(self):
676
if not self.has_selected_text():
677
self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
678
self.remove_selected_text()
680
def stdkey_backspace(self):
681
if not self.has_selected_text():
682
self.moveCursor(QTextCursor.PreviousCharacter,
683
QTextCursor.KeepAnchor)
684
self.remove_selected_text()
686
def __get_move_mode(self, shift):
687
return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor
689
def stdkey_up(self, shift):
690
self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift))
692
def stdkey_down(self, shift):
693
self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift))
695
def stdkey_tab(self):
696
self.insert_text(self.indent_chars)
698
def stdkey_home(self, shift, ctrl, prompt_pos=None):
699
"""Smart HOME feature: cursor is first moved at
700
indentation position, then at the start of the line"""
701
move_mode = self.__get_move_mode(shift)
703
self.moveCursor(QTextCursor.Start, move_mode)
705
cursor = self.textCursor()
706
if prompt_pos is None:
707
start_position = self.get_position('sol')
709
start_position = self.get_position(prompt_pos)
710
text = self.get_text(start_position, 'eol')
711
indent_pos = start_position+len(text)-len(text.lstrip())
712
if cursor.position() != indent_pos:
713
cursor.setPosition(indent_pos, move_mode)
715
cursor.setPosition(start_position, move_mode)
716
self.setTextCursor(cursor)
718
def stdkey_end(self, shift, ctrl):
719
move_mode = self.__get_move_mode(shift)
721
self.moveCursor(QTextCursor.End, move_mode)
723
self.moveCursor(QTextCursor.EndOfBlock, move_mode)
725
def stdkey_pageup(self):
728
def stdkey_pagedown(self):
731
def stdkey_escape(self):
736
def mousePressEvent(self, event):
737
"""Reimplement Qt method"""
738
if os.name != 'posix' and event.button() == Qt.MidButton:
740
event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
741
Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
742
QPlainTextEdit.mousePressEvent(self, event)
743
QPlainTextEdit.mouseReleaseEvent(self, event)
746
QPlainTextEdit.mousePressEvent(self, event)
748
def focusInEvent(self, event):
749
"""Reimplemented to handle focus"""
750
self.emit(SIGNAL("focus_changed()"))
751
self.emit(SIGNAL("focus_in()"))
752
QPlainTextEdit.focusInEvent(self, event)
754
def focusOutEvent(self, event):
755
"""Reimplemented to handle focus"""
756
self.emit(SIGNAL("focus_changed()"))
757
QPlainTextEdit.focusOutEvent(self, event)
759
def wheelEvent(self, event):
760
"""Reimplemented to emit zoom in/out signals when Ctrl is pressed"""
761
if event.modifiers() & Qt.ControlModifier:
762
if event.delta() < 0:
763
self.emit(SIGNAL("zoom_out()"))
764
elif event.delta() > 0:
765
self.emit(SIGNAL("zoom_in()"))
767
QPlainTextEdit.wheelEvent(self, event)
770
class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler):
772
ANSIEscapeCodeHandler.__init__(self)
773
self.base_format = None
774
self.current_format = None
776
def set_light_background(self, state):
778
self.default_foreground_color = 30
779
self.default_background_color = 47
781
self.default_foreground_color = 37
782
self.default_background_color = 40
784
def set_base_format(self, base_format):
785
self.base_format = base_format
787
def get_format(self):
788
return self.current_format
792
Set font style with the following attributes:
793
'foreground_color', 'background_color', 'italic',
794
'bold' and 'underline'
796
if self.current_format is None:
797
assert self.base_format is not None
798
self.current_format = QTextCharFormat(self.base_format)
800
if self.foreground_color is None:
801
qcolor = self.base_format.foreground()
803
cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity]
804
qcolor = QColor(cstr)
805
self.current_format.setForeground(qcolor)
807
if self.background_color is None:
808
qcolor = self.base_format.background()
810
cstr = self.ANSI_COLORS[self.background_color-40][self.intensity]
811
qcolor = QColor(cstr)
812
self.current_format.setBackground(qcolor)
814
font = self.current_format.font()
816
if self.italic is None:
817
italic = self.base_format.fontItalic()
820
font.setItalic(italic)
822
if self.bold is None:
823
bold = self.base_format.font().bold()
828
if self.underline is None:
829
underline = self.base_format.font().underline()
831
underline = self.underline
832
font.setUnderline(underline)
833
self.current_format.setFont(font)
836
def inverse_color(color):
837
color.setHsv(color.hue(), color.saturation(), 255-color.value())
839
class ConsoleFontStyle(object):
840
def __init__(self, foregroundcolor, backgroundcolor,
841
bold, italic, underline):
842
self.foregroundcolor = foregroundcolor
843
self.backgroundcolor = backgroundcolor
846
self.underline = underline
849
def apply_style(self, font, light_background, is_default):
850
self.format = QTextCharFormat()
851
self.format.setFont(font)
852
foreground = QColor(self.foregroundcolor)
853
if not light_background and is_default:
854
inverse_color(foreground)
855
self.format.setForeground(foreground)
856
background = QColor(self.backgroundcolor)
857
if not light_background:
858
inverse_color(background)
859
self.format.setBackground(background)
860
font = self.format.font()
861
font.setBold(self.bold)
862
font.setItalic(self.italic)
863
font.setUnderline(self.underline)
864
self.format.setFont(font)
866
class ConsoleBaseWidget(TextEditBaseWidget):
867
"""Console base widget"""
868
BRACE_MATCHING_SCOPE = ('sol', 'eol')
869
COLOR_PATTERN = re.compile('\x01?\x1b\[(.*?)m\x02?')
871
def __init__(self, parent=None):
872
TextEditBaseWidget.__init__(self, parent)
874
self.light_background = True
876
self.setMaximumBlockCount(300)
878
# ANSI escape code handler
879
self.ansi_handler = QtANSIEscapeCodeHandler()
881
# Disable undo/redo (nonsense for a console widget...):
882
self.setUndoRedoEnabled(False)
884
self.connect(self, SIGNAL('userListActivated(int, const QString)'),
885
lambda user_id, text:
886
self.emit(SIGNAL('completion_widget_activated(QString)'),
889
self.default_style = ConsoleFontStyle(
890
foregroundcolor=0x000000, backgroundcolor=0xFFFFFF,
891
bold=False, italic=False, underline=False)
892
self.error_style = ConsoleFontStyle(
893
foregroundcolor=0xFF0000, backgroundcolor=0xFFFFFF,
894
bold=False, italic=False, underline=False)
895
self.traceback_link_style = ConsoleFontStyle(
896
foregroundcolor=0x0000FF, backgroundcolor=0xFFFFFF,
897
bold=True, italic=False, underline=True)
898
self.prompt_style = ConsoleFontStyle(
899
foregroundcolor=0x00AA00, backgroundcolor=0xFFFFFF,
900
bold=True, italic=False, underline=False)
901
self.font_styles = (self.default_style, self.error_style,
902
self.traceback_link_style, self.prompt_style)
903
self.set_pythonshell_font()
904
self.setMouseTracking(True)
906
def set_light_background(self, state):
907
self.light_background = state
909
self.set_palette(background=QColor(Qt.white),
910
foreground=QColor(Qt.darkGray))
912
self.set_palette(background=QColor(Qt.black),
913
foreground=QColor(Qt.lightGray))
914
self.ansi_handler.set_light_background(state)
915
self.set_pythonshell_font()
917
def set_selection(self, start, end):
918
cursor = self.textCursor()
919
cursor.setPosition(start)
920
cursor.setPosition(end, QTextCursor.KeepAnchor)
921
self.setTextCursor(cursor)
923
def truncate_selection(self, position_from):
924
"""Unselect read-only parts in shell, like prompt"""
925
position_from = self.get_position(position_from)
926
cursor = self.textCursor()
927
start, end = cursor.selectionStart(), cursor.selectionEnd()
929
start = max([position_from, start])
931
end = max([position_from, end])
932
self.set_selection(start, end)
934
def restrict_cursor_position(self, position_from, position_to):
935
"""In shell, avoid editing text except between prompt and EOF"""
936
position_from = self.get_position(position_from)
937
position_to = self.get_position(position_to)
938
cursor = self.textCursor()
939
cursor_position = cursor.position()
940
if cursor_position < position_from or cursor_position > position_to:
941
self.set_cursor_position(position_to)
944
def insert_text(self, text):
945
"""Reimplement TextEditBaseWidget method"""
946
self.textCursor().insertText(text, self.default_style.format)
949
"""Reimplement Qt method"""
950
if self.has_selected_text():
951
self.remove_selected_text()
952
self.insert_text(QApplication.clipboard().text())
954
def append_text_to_shell(self, text, error, prompt):
956
Append text to Python shell
957
In a way, this method overrides the method 'insert_text' when text is
958
inserted at the end of the text widget for a Python shell
960
Handles error messages and show blue underlined links
961
Handles ANSI color sequences
962
Handles ANSI FF sequence
964
cursor = self.textCursor()
965
cursor.movePosition(QTextCursor.End)
967
index = text.find(chr(12))
970
text = text[index+1:]
974
for text in text.splitlines(True):
975
if text.startswith(' File') \
976
and not text.startswith(' File "<'):
978
# Show error links in blue underlined text
979
cursor.insertText(' ', self.default_style.format)
980
cursor.insertText(text[2:],
981
self.traceback_link_style.format)
983
# Show error/warning messages in red
984
cursor.insertText(text, self.error_style.format)
986
self.emit(SIGNAL('traceback_available()'))
988
# Show prompt in green
989
cursor.insertText(text, self.prompt_style.format)
991
# Show other outputs in black
993
for match in self.COLOR_PATTERN.finditer(text):
994
cursor.insertText(text[last_end:match.start()],
995
self.default_style.format)
996
last_end = match.end()
997
for code in [int(_c) for _c in match.group(1).split(';')]:
998
self.ansi_handler.set_code(code)
999
self.default_style.format = self.ansi_handler.get_format()
1000
cursor.insertText(text[last_end:], self.default_style.format)
1001
# # Slower alternative:
1002
# segments = self.COLOR_PATTERN.split(text)
1003
# cursor.insertText(segments.pop(0), self.default_style.format)
1005
# for ansi_tags, text in zip(segments[::2], segments[1::2]):
1006
# for ansi_tag in ansi_tags.split(';'):
1007
# self.ansi_handler.set_code(int(ansi_tag))
1008
# self.default_style.format = self.ansi_handler.get_format()
1009
# cursor.insertText(text, self.default_style.format)
1010
self.set_cursor_position('eof')
1011
self.setCurrentCharFormat(self.default_style.format)
1013
def set_pythonshell_font(self, font=None):
1014
"""Python Shell only"""
1017
for style in self.font_styles:
1018
style.apply_style(font=font,
1019
light_background=self.light_background,
1020
is_default=style is self.default_style)
1021
self.ansi_handler.set_base_format(self.default_style.format)
b'\\ No newline at end of file'