~ubuntu-branches/debian/experimental/spyder/experimental

« back to all changes in this revision

Viewing changes to spyderlib/widgets/sourcecode/base.py

  • Committer: Package Import Robot
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2013-02-27 09:51:28 UTC
  • mfrom: (1.1.18)
  • Revision ID: package-import@ubuntu.com-20130227095128-wtx1irpvf4vl79lj
Tags: 2.2.0~beta3+dfsg-1
* Imported Upstream version 2.2.0~beta3+dfsg
* debian /patches
  - 0002-feature-forwarded-add-icon-to-desktop-file.patch (deleted)
    this patch was integrated by the upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright © 2009-2010 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""QPlainTextEdit base class"""
8
 
 
9
 
# pylint: disable=C0103
10
 
# pylint: disable=R0903
11
 
# pylint: disable=R0911
12
 
# pylint: disable=R0201
13
 
 
14
 
import re
15
 
import string
16
 
 
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
22
 
 
23
 
 
24
 
# Local imports
25
 
from spyderlib.widgets.sourcecode.terminal import ANSIEscapeCodeHandler
26
 
from spyderlib.widgets.sourcecode import mixins
27
 
 
28
 
 
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
39
 
        self.hide()
40
 
        self.connect(self, SIGNAL("itemActivated(QListWidgetItem*)"),
41
 
                     self.item_selected)
42
 
        
43
 
    def setup_appearance(self, size, font):
44
 
        self.resize(*size)
45
 
        self.setFont(font)
46
 
        
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])
50
 
            return
51
 
        
52
 
        self.completion_list = completion_list
53
 
        self.clear()
54
 
        self.addItems(completion_list)
55
 
        self.setCurrentRow(0)
56
 
        
57
 
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
58
 
        self.show()
59
 
        self.setFocus()
60
 
        self.raise_()
61
 
        
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()
67
 
        
68
 
        point = self.textedit.cursorRect().bottomRight()
69
 
        point.setX(point.x()+self.textedit.get_linenumberarea_width())
70
 
        point = self.textedit.mapToGlobal(point)
71
 
 
72
 
        # Computing completion widget and its parent right positions
73
 
        comp_right = point.x()+self.width()
74
 
        ancestor = self.parent()
75
 
        if ancestor is None:
76
 
            anc_right = screen_right
77
 
        else:
78
 
            anc_right = min([ancestor.x()+ancestor.width(), screen_right])
79
 
        
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())
84
 
        
85
 
        # Computing completion widget and its parent bottom positions
86
 
        comp_bottom = point.y()+self.height()
87
 
        ancestor = self.parent()
88
 
        if ancestor is None:
89
 
            anc_bottom = screen_bottom
90
 
        else:
91
 
            anc_bottom = min([ancestor.y()+ancestor.height(), screen_bottom])
92
 
        
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())
100
 
            
101
 
        if ancestor is not None:
102
 
            # Useful only if we set parent to 'ancestor' in __init__
103
 
            point = ancestor.mapFromGlobal(point)
104
 
        self.move(point)
105
 
        
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()
110
 
        
111
 
    def hide(self):
112
 
        QListWidget.hide(self)
113
 
        self.textedit.setFocus()
114
 
        
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:
123
 
            self.item_selected()
124
 
        elif key in (Qt.Key_Return, Qt.Key_Enter,
125
 
                     Qt.Key_Left, Qt.Key_Right) or text in ('.', ':'):
126
 
            self.hide()
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()
135
 
        elif modifier:
136
 
            self.textedit.keyPressEvent(event)
137
 
        else:
138
 
            self.hide()
139
 
            QListWidget.keyPressEvent(self, event)
140
 
            
141
 
    def update_current(self):
142
 
        completion_text = unicode(self.textedit.completion_text)
143
 
        if 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)
150
 
                    break
151
 
            else:
152
 
                self.hide()
153
 
        else:
154
 
            self.hide()
155
 
    
156
 
    def focusOutEvent(self, event):
157
 
        event.ignore()
158
 
        self.hide()
159
 
        
160
 
    def item_selected(self, item=None):
161
 
        if item is None:
162
 
            item = self.currentItem()
163
 
        self.textedit.insert_completion( unicode(item.text()) )
164
 
        self.hide()
165
 
 
166
 
 
167
 
class TextEditBaseWidget(QPlainTextEdit, mixins.BaseEditMixin):
168
 
    """
169
 
    Text edit base widget
170
 
    """
171
 
    BRACE_MATCHING_SCOPE = ('sof', 'eof')
172
 
    
173
 
    def __init__(self, parent=None):
174
 
        QPlainTextEdit.__init__(self, parent)
175
 
        mixins.BaseEditMixin.__init__(self)
176
 
        self.setAttribute(Qt.WA_DeleteOnClose)
177
 
        
178
 
        self.extra_selections_dict = {}
179
 
        
180
 
        self.connect(self, SIGNAL('textChanged()'), self.changed)
181
 
        self.connect(self, SIGNAL('cursorPositionChanged()'),
182
 
                     self.cursor_position_changed)
183
 
        
184
 
        self.indent_chars = " "*4
185
 
        
186
 
        # Code completion / calltips
187
 
        if parent is not None:
188
 
            mainwin = parent
189
 
            while not isinstance(mainwin, QMainWindow):
190
 
                mainwin = mainwin.parent()
191
 
                if mainwin is None:
192
 
                    break
193
 
            if mainwin is not None:
194
 
                parent = mainwin
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
200
 
        self.calltips = True
201
 
        self.calltip_position = None
202
 
        self.calltip_size = 600
203
 
        self.calltip_font = QFont()
204
 
        self.completion_text = ""
205
 
 
206
 
        # Brace matching
207
 
        self.bracepos = None
208
 
        self.matched_p_color = QColor(Qt.green)
209
 
        self.unmatched_p_color = QColor(Qt.red)
210
 
        
211
 
    def setup_calltips(self, size=None, font=None):
212
 
        self.calltip_size = size
213
 
        self.calltip_font = font
214
 
    
215
 
    def setup_completion(self, size=None, font=None):
216
 
        self.completion_widget.setup_appearance(size, font)
217
 
        
218
 
    def set_indent_chars(self, indent_chars):
219
 
        self.indent_chars = indent_chars
220
 
        
221
 
    def set_palette(self, background, foreground):
222
 
        """
223
 
        Set text editor palette colors:
224
 
        background color and caret (text cursor) color
225
 
        """
226
 
        palette = QPalette()
227
 
        palette.setColor(QPalette.Base, background)
228
 
        palette.setColor(QPalette.Text, foreground)
229
 
        self.setPalette(palette)
230
 
        
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
235
 
        return 0
236
 
        
237
 
    #------Extra selections
238
 
    def get_extra_selections(self, key):
239
 
        return self.extra_selections_dict.get(key, [])
240
 
 
241
 
    def set_extra_selections(self, key, extra_selections):
242
 
        self.extra_selections_dict[key] = extra_selections
243
 
        
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)
249
 
        
250
 
    def clear_extra_selections(self, key):
251
 
        self.extra_selections_dict[key] = []
252
 
        self.update_extra_selections()
253
 
        
254
 
        
255
 
    def changed(self):
256
 
        """Emit changed signal"""
257
 
        self.emit(SIGNAL('modificationChanged(bool)'),
258
 
                  self.document().isModified())
259
 
 
260
 
 
261
 
    #------Brace matching
262
 
    def find_brace_match(self, position, brace, forward):
263
 
        start_pos, end_pos = self.BRACE_MATCHING_SCOPE
264
 
        if forward:
265
 
            bracemap = {'(': ')', '[': ']', '{': '}'}
266
 
            text = self.get_text(position, end_pos)
267
 
            i_start_open = 1
268
 
            i_start_close = 1
269
 
        else:
270
 
            bracemap = {')': '(', ']': '[', '}': '{'}
271
 
            text = self.get_text(start_pos, position)
272
 
            i_start_open = len(text)-1
273
 
            i_start_close = len(text)-1
274
 
 
275
 
        while True:
276
 
            if forward:
277
 
                i_close = text.find(bracemap[brace], i_start_close)
278
 
            else:
279
 
                i_close = text.rfind(bracemap[brace], 0, i_start_close+1)
280
 
            if i_close > -1:
281
 
                if forward:
282
 
                    i_start_close = i_close+1
283
 
                    i_open = text.find(brace, i_start_open, i_close)
284
 
                else:
285
 
                    i_start_close = i_close-1
286
 
                    i_open = text.rfind(brace, i_close, i_start_open+1)
287
 
                if i_open > -1:
288
 
                    if forward:
289
 
                        i_start_open = i_open+1
290
 
                    else:
291
 
                        i_start_open = i_open-1
292
 
                else:
293
 
                    # found matching brace
294
 
                    if forward:
295
 
                        return position+i_close
296
 
                    else:
297
 
                        return position-(len(text)-i_close)
298
 
            else:
299
 
                # no matching brace
300
 
                return
301
 
    
302
 
    def __highlight(self, positions, color=None, cancel=False):
303
 
        if cancel:
304
 
            self.clear_extra_selections('brace_matching')
305
 
            return
306
 
        extra_selections = []
307
 
        for position in positions:
308
 
            if position > self.get_position('eof'):
309
 
                return
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()
320
 
 
321
 
    def cursor_position_changed(self):
322
 
        """Brace matching"""
323
 
        if self.bracepos is not None:
324
 
            self.__highlight(self.bracepos, cancel=True)
325
 
            self.bracepos = None
326
 
        cursor = self.textCursor()
327
 
        if cursor.position() == 0:
328
 
            return
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)
337
 
        else:
338
 
            return
339
 
        if pos2 is not None:
340
 
            self.bracepos = (pos1, pos2)
341
 
            self.__highlight(self.bracepos, color=self.matched_p_color)
342
 
        else:
343
 
            self.bracepos = (pos1,)
344
 
            self.__highlight(self.bracepos, color=self.unmatched_p_color)
345
 
 
346
 
 
347
 
    #-----Widget setup and options
348
 
    def set_codecompletion_auto(self, state):
349
 
        """Set code completion state"""
350
 
        self.codecompletion_auto = state
351
 
        
352
 
    def set_codecompletion_case(self, state):
353
 
        """Case sensitive completion"""
354
 
        self.codecompletion_case = state
355
 
        self.completion_widget.case_sensitive = state
356
 
        
357
 
    def set_codecompletion_single(self, state):
358
 
        """Show single completion"""
359
 
        self.codecompletion_single = state
360
 
        self.completion_widget.show_single = state
361
 
        
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
366
 
        
367
 
    def set_calltips(self, state):
368
 
        """Set calltips state"""
369
 
        self.calltips = state
370
 
 
371
 
    def set_wrap_mode(self, mode=None):
372
 
        """
373
 
        Set wrap mode
374
 
        Valid *mode* values: None, 'word', 'character'
375
 
        """
376
 
        if mode == 'word':
377
 
            wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere
378
 
        elif mode == 'character':
379
 
            wrap_mode = QTextOption.WrapAnywhere
380
 
        else:
381
 
            wrap_mode = QTextOption.NoWrap
382
 
        self.setWordWrapMode(wrap_mode)
383
 
        
384
 
        
385
 
    #------Reimplementing Qt methods
386
 
    def copy(self):
387
 
        """
388
 
        Reimplement Qt method
389
 
        Copy text to clipboard with correct EOL chars
390
 
        """
391
 
        QApplication.clipboard().setText(self.get_selected_text())
392
 
        
393
 
 
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()
399
 
        if no_selection:
400
 
            self.select_current_block()
401
 
        
402
 
        ls = self.get_line_separator()
403
 
        
404
 
        _indent = lambda line: len(line)-len(line.lstrip())
405
 
        
406
 
        line_from, line_to = self.get_selection_bounds()
407
 
        text = self.get_selected_text()
408
 
 
409
 
        lines = text.split(ls)
410
 
        if len(lines) > 1:
411
 
            # Multiline selection -> eventually fixing indentation
412
 
            original_indent = _indent(self.get_text_line(line_from))
413
 
            text = (" "*(original_indent-_indent(lines[0])))+text
414
 
        
415
 
        # If there is a common indent to all lines, remove it
416
 
        min_indent = 999
417
 
        for line in text.split(ls):
418
 
            if line.strip():
419
 
                min_indent = min(_indent(line), min_indent)
420
 
        if min_indent:
421
 
            text = ls.join([line[min_indent:] for line in text.split(ls)])
422
 
        
423
 
        # Add an EOL character if a block stars with various Python
424
 
        # reserved words, so that it gets evaluated automatically
425
 
        # by the console
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]):
430
 
            text += ls
431
 
            if last_line != '':
432
 
                text += ls
433
 
 
434
 
        if no_selection:
435
 
            self.setFocus()
436
 
            self.clear_selection()
437
 
        
438
 
        return text
439
 
    
440
 
    def get_line_count(self):
441
 
        """Return document total line number"""
442
 
        return self.blockCount()
443
 
    
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()
459
 
            
460
 
        cursor.setPosition(start_pos)
461
 
        cursor.movePosition(QTextCursor.StartOfBlock)
462
 
        while cursor.position() <= end_pos:
463
 
            cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
464
 
            if cursor.atEnd():
465
 
                cursor_temp = QTextCursor(cursor)
466
 
                cursor_temp.clearSelection()
467
 
                cursor_temp.insertText(self.get_line_separator())
468
 
                break
469
 
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)            
470
 
        text = cursor.selectedText()
471
 
        cursor.clearSelection()
472
 
        
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))
478
 
        
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)
484
 
    
485
 
    def duplicate_line(self):
486
 
        """
487
 
        Duplicate current line or selected text
488
 
        Paste the duplicated text *after* the current line/selected text
489
 
        """
490
 
        self.__duplicate_line_or_selection(after_current_line=True)
491
 
    
492
 
    def copy_line(self):
493
 
        """
494
 
        Copy current line or selected text
495
 
        Paste the duplicated text *before* the current line/selected text
496
 
        """
497
 
        self.__duplicate_line_or_selection(after_current_line=False)
498
 
        
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()
510
 
 
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()
519
 
        else:
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)
526
 
        
527
 
        sel_text = unicode(cursor.selectedText())
528
 
        cursor.removeSelectedText()
529
 
        
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)
534
 
        else:
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)
539
 
 
540
 
        cursor.endEditBlock()
541
 
 
542
 
        cursor.setPosition(orig_sel[0])
543
 
        cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
544
 
        self.setTextCursor(cursor)
545
 
    
546
 
    def move_line_up(self):
547
 
        """Move up current line or selected text"""
548
 
        self.__move_line_or_selection(after_current_line=False)
549
 
        
550
 
    def move_line_down(self):
551
 
        """Move down current line or selected text"""
552
 
        self.__move_line_or_selection(after_current_line=True)
553
 
        
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)
566
 
        
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)
574
 
        else:
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)
581
 
            if cursor.atEnd():
582
 
                break
583
 
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
584
 
        cursor.removeSelectedText()
585
 
        cursor.endEditBlock()
586
 
        self.ensureCursorVisible()
587
 
 
588
 
 
589
 
    #------Code completion / Calltips
590
 
    def show_calltip(self, title, text, color='#2D62FF',
591
 
                     at_line=None, at_position=None):
592
 
        """Show calltip"""
593
 
        if text is None or len(text) == 0:
594
 
            return
595
 
        # Saving cursor position:
596
 
        if at_position is None:
597
 
            at_position = self.get_position('cursor')
598
 
        self.calltip_position = at_position
599
 
        # Preparing text:
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:
615
 
            cx = 5
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)
621
 
 
622
 
    def hide_tooltip_if_necessary(self, key):
623
 
        """Hide calltip when necessary"""
624
 
        try:
625
 
            calltip_char = self.get_character(self.calltip_position)
626
 
            before = self.is_cursor_before(self.calltip_position,
627
 
                                           char_offset=1)
628
 
            other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab)
629
 
            if calltip_char not in ('?','(') or before or other:
630
 
                QToolTip.hideText()
631
 
        except (IndexError, TypeError):
632
 
            QToolTip.hideText()
633
 
 
634
 
    def show_completion_widget(self, textlist, automatic=True):
635
 
        """Show completion widget"""
636
 
        self.completion_widget.show_list(textlist, automatic=automatic)
637
 
        
638
 
    def hide_completion_widget(self):
639
 
        """Hide completion widget"""
640
 
        self.completion_widget.hide()
641
 
 
642
 
    def show_completion_list(self, completions, completion_text="",
643
 
                             automatic=True):
644
 
        """Display the possible completions"""
645
 
        if len(completions) == 0 or completions == [completion_text]:
646
 
            return
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)
654
 
        
655
 
    def select_completion_list(self):
656
 
        """Completion list is active, Enter was just pressed"""
657
 
        self.completion_widget.item_selected()
658
 
        
659
 
    def insert_completion(self, text):
660
 
        if 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)
667
 
 
668
 
    def is_completion_widget_visible(self):
669
 
        """Return True is completion list widget is visible"""
670
 
        return self.completion_widget.isVisible()
671
 
    
672
 
        
673
 
    #------Standard keys
674
 
    def stdkey_clear(self):
675
 
        if not self.has_selected_text():
676
 
            self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
677
 
        self.remove_selected_text()
678
 
    
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()
684
 
 
685
 
    def __get_move_mode(self, shift):
686
 
        return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor
687
 
 
688
 
    def stdkey_up(self, shift):
689
 
        self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift))
690
 
 
691
 
    def stdkey_down(self, shift):
692
 
        self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift))
693
 
 
694
 
    def stdkey_tab(self):
695
 
        self.insert_text(self.indent_chars)
696
 
 
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)
701
 
        if ctrl:
702
 
            self.moveCursor(QTextCursor.Start, move_mode)
703
 
        else:
704
 
            cursor = self.textCursor()
705
 
            if prompt_pos is None:
706
 
                start_position = self.get_position('sol')
707
 
            else:
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)
713
 
            else:
714
 
                cursor.setPosition(start_position, move_mode)
715
 
            self.setTextCursor(cursor)
716
 
 
717
 
    def stdkey_end(self, shift, ctrl):
718
 
        move_mode = self.__get_move_mode(shift)
719
 
        if ctrl:
720
 
            self.moveCursor(QTextCursor.End, move_mode)
721
 
        else:
722
 
            self.moveCursor(QTextCursor.EndOfBlock, move_mode)
723
 
 
724
 
    def stdkey_pageup(self):
725
 
        pass
726
 
 
727
 
    def stdkey_pagedown(self):
728
 
        pass
729
 
 
730
 
    def stdkey_escape(self):
731
 
        pass
732
 
 
733
 
                
734
 
    #----Focus
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)
740
 
        
741
 
    def focusOutEvent(self, event):
742
 
        """Reimplemented to handle focus"""
743
 
        self.emit(SIGNAL("focus_changed()"))
744
 
        QPlainTextEdit.focusOutEvent(self, event)
745
 
 
746
 
 
747
 
class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler):
748
 
    def __init__(self):
749
 
        ANSIEscapeCodeHandler.__init__(self)
750
 
        self.base_format = None
751
 
        self.current_format = None
752
 
        
753
 
    def set_light_background(self, state):
754
 
        if state:
755
 
            self.default_foreground_color = 30
756
 
            self.default_background_color = 47
757
 
        else:
758
 
            self.default_foreground_color = 37
759
 
            self.default_background_color = 40
760
 
        
761
 
    def set_base_format(self, base_format):
762
 
        self.base_format = base_format
763
 
        
764
 
    def get_format(self):
765
 
        return self.current_format
766
 
        
767
 
    def set_style(self):
768
 
        """
769
 
        Set font style with the following attributes:
770
 
        'foreground_color', 'background_color', 'italic',
771
 
        'bold' and 'underline'
772
 
        """
773
 
        if self.current_format is None:
774
 
            assert self.base_format is not None
775
 
            self.current_format = QTextCharFormat(self.base_format)
776
 
        # Foreground color
777
 
        if self.foreground_color is None:
778
 
            qcolor = self.base_format.foreground()
779
 
        else:
780
 
            cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity]
781
 
            qcolor = QColor(cstr)
782
 
        self.current_format.setForeground(qcolor)        
783
 
        # Background color
784
 
        if self.background_color is None:
785
 
            qcolor = self.base_format.background()
786
 
        else:
787
 
            cstr = self.ANSI_COLORS[self.background_color-40][self.intensity]
788
 
            qcolor = QColor(cstr)
789
 
        self.current_format.setBackground(qcolor)
790
 
        
791
 
        font = self.current_format.font()
792
 
        # Italic
793
 
        if self.italic is None:
794
 
            italic = self.base_format.fontItalic()
795
 
        else:
796
 
            italic = self.italic
797
 
        font.setItalic(italic)
798
 
        # Bold
799
 
        if self.bold is None:
800
 
            bold = self.base_format.font().bold()
801
 
        else:
802
 
            bold = self.bold
803
 
        font.setBold(bold)
804
 
        # Underline
805
 
        if self.underline is None:
806
 
            underline = self.base_format.font().underline()
807
 
        else:
808
 
            underline = self.underline
809
 
        font.setUnderline(underline)
810
 
        self.current_format.setFont(font)
811
 
 
812
 
 
813
 
def inverse_color(color):
814
 
    color.setHsv(color.hue(), color.saturation(), 255-color.value())
815
 
    
816
 
class ConsoleFontStyle(object):
817
 
    def __init__(self, foregroundcolor, backgroundcolor, 
818
 
                 bold, italic, underline):
819
 
        self.foregroundcolor = foregroundcolor
820
 
        self.backgroundcolor = backgroundcolor
821
 
        self.bold = bold
822
 
        self.italic = italic
823
 
        self.underline = underline
824
 
        self.format = None
825
 
        
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)
842
 
    
843
 
class ConsoleBaseWidget(TextEditBaseWidget):
844
 
    """Console base widget"""
845
 
    BRACE_MATCHING_SCOPE = ('sol', 'eol')
846
 
    COLOR_PATTERN = re.compile('\x01?\x1b\[(.*?)m\x02?')
847
 
    
848
 
    def __init__(self, parent=None):
849
 
        TextEditBaseWidget.__init__(self, parent)
850
 
        
851
 
        self.light_background = True
852
 
 
853
 
        self.setMaximumBlockCount(300)
854
 
 
855
 
        # ANSI escape code handler
856
 
        self.ansi_handler = QtANSIEscapeCodeHandler()
857
 
                
858
 
        # Disable undo/redo (nonsense for a console widget...):
859
 
        self.setUndoRedoEnabled(False)
860
 
        
861
 
        self.connect(self, SIGNAL('userListActivated(int, const QString)'),
862
 
                     lambda user_id, text:
863
 
                     self.emit(SIGNAL('completion_widget_activated(QString)'),
864
 
                               text))
865
 
        
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)
882
 
        
883
 
    def set_light_background(self, state):
884
 
        self.light_background = state
885
 
        if state:
886
 
            self.set_palette(background=QColor(Qt.white),
887
 
                             foreground=QColor(Qt.darkGray))
888
 
        else:
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()
893
 
        
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)
899
 
 
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()
905
 
        if start < end:
906
 
            start = max([position_from, start])
907
 
        else:
908
 
            end = max([position_from, end])
909
 
        self.set_selection(start, end)
910
 
 
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)
919
 
 
920
 
    #------Python shell
921
 
    def insert_text(self, text):
922
 
        """Reimplement TextEditBaseWidget method"""
923
 
        self.textCursor().insertText(text, self.default_style.format)
924
 
        
925
 
    def paste(self):
926
 
        """Reimplement Qt method"""
927
 
        if self.has_selected_text():
928
 
            self.remove_selected_text()
929
 
        self.insert_text(QApplication.clipboard().text())
930
 
        
931
 
    def append_text_to_shell(self, text, error, prompt):
932
 
        """
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
936
 
        
937
 
        Handles error messages and show blue underlined links
938
 
        Handles ANSI color sequences
939
 
        Handles ANSI FF sequence
940
 
        """
941
 
        cursor = self.textCursor()
942
 
        cursor.movePosition(QTextCursor.End)
943
 
        while True:
944
 
            index = text.find(chr(12))
945
 
            if index == -1:
946
 
                break
947
 
            text = text[index+1:]
948
 
            self.clear()
949
 
        if error:
950
 
            is_traceback = False
951
 
            for text in text.splitlines(True):
952
 
                if text.startswith('  File') \
953
 
                and not text.startswith('  File "<'):
954
 
                    is_traceback = True
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)
959
 
                else:
960
 
                    # Show error/warning messages in red
961
 
                    cursor.insertText(text, self.error_style.format)
962
 
            if is_traceback:
963
 
                self.emit(SIGNAL('traceback_available()'))
964
 
        elif prompt:
965
 
            # Show prompt in green
966
 
            cursor.insertText(text, self.prompt_style.format)
967
 
        else:
968
 
            # Show other outputs in black
969
 
            last_end = 0
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)
981
 
#            if segments:
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)
989
 
    
990
 
    def set_pythonshell_font(self, font=None):
991
 
        """Python Shell only"""
992
 
        if font is None:
993
 
            font = QFont()
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 -*-
 
2
#
 
3
# Copyright © 2009-2010 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""QPlainTextEdit base class"""
 
8
 
 
9
# pylint: disable=C0103
 
10
# pylint: disable=R0903
 
11
# pylint: disable=R0911
 
12
# pylint: disable=R0201
 
13
 
 
14
import os
 
15
import re
 
16
import string
 
17
 
 
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
 
23
 
 
24
 
 
25
# Local imports
 
26
from spyderlib.widgets.sourcecode.terminal import ANSIEscapeCodeHandler
 
27
from spyderlib.widgets.sourcecode import mixins
 
28
 
 
29
 
 
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
 
40
        self.hide()
 
41
        self.connect(self, SIGNAL("itemActivated(QListWidgetItem*)"),
 
42
                     self.item_selected)
 
43
        
 
44
    def setup_appearance(self, size, font):
 
45
        self.resize(*size)
 
46
        self.setFont(font)
 
47
        
 
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])
 
51
            return
 
52
        
 
53
        self.completion_list = completion_list
 
54
        self.clear()
 
55
        self.addItems(completion_list)
 
56
        self.setCurrentRow(0)
 
57
        
 
58
        QApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
 
59
        self.show()
 
60
        self.setFocus()
 
61
        self.raise_()
 
62
        
 
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()
 
68
        
 
69
        point = self.textedit.cursorRect().bottomRight()
 
70
        point.setX(point.x()+self.textedit.get_linenumberarea_width())
 
71
        point = self.textedit.mapToGlobal(point)
 
72
 
 
73
        # Computing completion widget and its parent right positions
 
74
        comp_right = point.x()+self.width()
 
75
        ancestor = self.parent()
 
76
        if ancestor is None:
 
77
            anc_right = screen_right
 
78
        else:
 
79
            anc_right = min([ancestor.x()+ancestor.width(), screen_right])
 
80
        
 
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())
 
85
        
 
86
        # Computing completion widget and its parent bottom positions
 
87
        comp_bottom = point.y()+self.height()
 
88
        ancestor = self.parent()
 
89
        if ancestor is None:
 
90
            anc_bottom = screen_bottom
 
91
        else:
 
92
            anc_bottom = min([ancestor.y()+ancestor.height(), screen_bottom])
 
93
        
 
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())
 
101
            
 
102
        if ancestor is not None:
 
103
            # Useful only if we set parent to 'ancestor' in __init__
 
104
            point = ancestor.mapFromGlobal(point)
 
105
        self.move(point)
 
106
        
 
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()
 
111
        
 
112
    def hide(self):
 
113
        QListWidget.hide(self)
 
114
        self.textedit.setFocus()
 
115
        
 
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:
 
124
            self.item_selected()
 
125
        elif key in (Qt.Key_Return, Qt.Key_Enter,
 
126
                     Qt.Key_Left, Qt.Key_Right) or text in ('.', ':'):
 
127
            self.hide()
 
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()
 
136
        elif modifier:
 
137
            self.textedit.keyPressEvent(event)
 
138
        else:
 
139
            self.hide()
 
140
            QListWidget.keyPressEvent(self, event)
 
141
            
 
142
    def update_current(self):
 
143
        completion_text = unicode(self.textedit.completion_text)
 
144
        if 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)
 
151
                    break
 
152
            else:
 
153
                self.hide()
 
154
        else:
 
155
            self.hide()
 
156
    
 
157
    def focusOutEvent(self, event):
 
158
        event.ignore()
 
159
        self.hide()
 
160
        
 
161
    def item_selected(self, item=None):
 
162
        if item is None:
 
163
            item = self.currentItem()
 
164
        self.textedit.insert_completion( unicode(item.text()) )
 
165
        self.hide()
 
166
 
 
167
 
 
168
class TextEditBaseWidget(QPlainTextEdit, mixins.BaseEditMixin):
 
169
    """
 
170
    Text edit base widget
 
171
    """
 
172
    BRACE_MATCHING_SCOPE = ('sof', 'eof')
 
173
    
 
174
    def __init__(self, parent=None):
 
175
        QPlainTextEdit.__init__(self, parent)
 
176
        mixins.BaseEditMixin.__init__(self)
 
177
        self.setAttribute(Qt.WA_DeleteOnClose)
 
178
        
 
179
        self.extra_selections_dict = {}
 
180
        
 
181
        self.connect(self, SIGNAL('textChanged()'), self.changed)
 
182
        self.connect(self, SIGNAL('cursorPositionChanged()'),
 
183
                     self.cursor_position_changed)
 
184
        
 
185
        self.indent_chars = " "*4
 
186
        
 
187
        # Code completion / calltips
 
188
        if parent is not None:
 
189
            mainwin = parent
 
190
            while not isinstance(mainwin, QMainWindow):
 
191
                mainwin = mainwin.parent()
 
192
                if mainwin is None:
 
193
                    break
 
194
            if mainwin is not None:
 
195
                parent = mainwin
 
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
 
201
        self.calltips = True
 
202
        self.calltip_position = None
 
203
        self.calltip_size = 600
 
204
        self.calltip_font = QFont()
 
205
        self.completion_text = ""
 
206
 
 
207
        # Brace matching
 
208
        self.bracepos = None
 
209
        self.matched_p_color = QColor(Qt.green)
 
210
        self.unmatched_p_color = QColor(Qt.red)
 
211
        
 
212
    def setup_calltips(self, size=None, font=None):
 
213
        self.calltip_size = size
 
214
        self.calltip_font = font
 
215
    
 
216
    def setup_completion(self, size=None, font=None):
 
217
        self.completion_widget.setup_appearance(size, font)
 
218
        
 
219
    def set_indent_chars(self, indent_chars):
 
220
        self.indent_chars = indent_chars
 
221
        
 
222
    def set_palette(self, background, foreground):
 
223
        """
 
224
        Set text editor palette colors:
 
225
        background color and caret (text cursor) color
 
226
        """
 
227
        palette = QPalette()
 
228
        palette.setColor(QPalette.Base, background)
 
229
        palette.setColor(QPalette.Text, foreground)
 
230
        self.setPalette(palette)
 
231
        
 
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
 
236
        return 0
 
237
        
 
238
    #------Extra selections
 
239
    def get_extra_selections(self, key):
 
240
        return self.extra_selections_dict.get(key, [])
 
241
 
 
242
    def set_extra_selections(self, key, extra_selections):
 
243
        self.extra_selections_dict[key] = extra_selections
 
244
        
 
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)
 
250
        
 
251
    def clear_extra_selections(self, key):
 
252
        self.extra_selections_dict[key] = []
 
253
        self.update_extra_selections()
 
254
        
 
255
        
 
256
    def changed(self):
 
257
        """Emit changed signal"""
 
258
        self.emit(SIGNAL('modificationChanged(bool)'),
 
259
                  self.document().isModified())
 
260
 
 
261
 
 
262
    #------Brace matching
 
263
    def find_brace_match(self, position, brace, forward):
 
264
        start_pos, end_pos = self.BRACE_MATCHING_SCOPE
 
265
        if forward:
 
266
            bracemap = {'(': ')', '[': ']', '{': '}'}
 
267
            text = self.get_text(position, end_pos)
 
268
            i_start_open = 1
 
269
            i_start_close = 1
 
270
        else:
 
271
            bracemap = {')': '(', ']': '[', '}': '{'}
 
272
            text = self.get_text(start_pos, position)
 
273
            i_start_open = len(text)-1
 
274
            i_start_close = len(text)-1
 
275
 
 
276
        while True:
 
277
            if forward:
 
278
                i_close = text.find(bracemap[brace], i_start_close)
 
279
            else:
 
280
                i_close = text.rfind(bracemap[brace], 0, i_start_close+1)
 
281
            if i_close > -1:
 
282
                if forward:
 
283
                    i_start_close = i_close+1
 
284
                    i_open = text.find(brace, i_start_open, i_close)
 
285
                else:
 
286
                    i_start_close = i_close-1
 
287
                    i_open = text.rfind(brace, i_close, i_start_open+1)
 
288
                if i_open > -1:
 
289
                    if forward:
 
290
                        i_start_open = i_open+1
 
291
                    else:
 
292
                        i_start_open = i_open-1
 
293
                else:
 
294
                    # found matching brace
 
295
                    if forward:
 
296
                        return position+i_close
 
297
                    else:
 
298
                        return position-(len(text)-i_close)
 
299
            else:
 
300
                # no matching brace
 
301
                return
 
302
    
 
303
    def __highlight(self, positions, color=None, cancel=False):
 
304
        if cancel:
 
305
            self.clear_extra_selections('brace_matching')
 
306
            return
 
307
        extra_selections = []
 
308
        for position in positions:
 
309
            if position > self.get_position('eof'):
 
310
                return
 
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()
 
321
 
 
322
    def cursor_position_changed(self):
 
323
        """Brace matching"""
 
324
        if self.bracepos is not None:
 
325
            self.__highlight(self.bracepos, cancel=True)
 
326
            self.bracepos = None
 
327
        cursor = self.textCursor()
 
328
        if cursor.position() == 0:
 
329
            return
 
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)
 
338
        else:
 
339
            return
 
340
        if pos2 is not None:
 
341
            self.bracepos = (pos1, pos2)
 
342
            self.__highlight(self.bracepos, color=self.matched_p_color)
 
343
        else:
 
344
            self.bracepos = (pos1,)
 
345
            self.__highlight(self.bracepos, color=self.unmatched_p_color)
 
346
 
 
347
 
 
348
    #-----Widget setup and options
 
349
    def set_codecompletion_auto(self, state):
 
350
        """Set code completion state"""
 
351
        self.codecompletion_auto = state
 
352
        
 
353
    def set_codecompletion_case(self, state):
 
354
        """Case sensitive completion"""
 
355
        self.codecompletion_case = state
 
356
        self.completion_widget.case_sensitive = state
 
357
        
 
358
    def set_codecompletion_single(self, state):
 
359
        """Show single completion"""
 
360
        self.codecompletion_single = state
 
361
        self.completion_widget.show_single = state
 
362
        
 
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
 
367
        
 
368
    def set_calltips(self, state):
 
369
        """Set calltips state"""
 
370
        self.calltips = state
 
371
 
 
372
    def set_wrap_mode(self, mode=None):
 
373
        """
 
374
        Set wrap mode
 
375
        Valid *mode* values: None, 'word', 'character'
 
376
        """
 
377
        if mode == 'word':
 
378
            wrap_mode = QTextOption.WrapAtWordBoundaryOrAnywhere
 
379
        elif mode == 'character':
 
380
            wrap_mode = QTextOption.WrapAnywhere
 
381
        else:
 
382
            wrap_mode = QTextOption.NoWrap
 
383
        self.setWordWrapMode(wrap_mode)
 
384
        
 
385
        
 
386
    #------Reimplementing Qt methods
 
387
    def copy(self):
 
388
        """
 
389
        Reimplement Qt method
 
390
        Copy text to clipboard with correct EOL chars
 
391
        """
 
392
        QApplication.clipboard().setText(self.get_selected_text())
 
393
        
 
394
 
 
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()
 
400
        if no_selection:
 
401
            self.select_current_block()
 
402
        
 
403
        ls = self.get_line_separator()
 
404
        
 
405
        _indent = lambda line: len(line)-len(line.lstrip())
 
406
        
 
407
        line_from, line_to = self.get_selection_bounds()
 
408
        text = self.get_selected_text()
 
409
 
 
410
        lines = text.split(ls)
 
411
        if len(lines) > 1:
 
412
            # Multiline selection -> eventually fixing indentation
 
413
            original_indent = _indent(self.get_text_line(line_from))
 
414
            text = (" "*(original_indent-_indent(lines[0])))+text
 
415
        
 
416
        # If there is a common indent to all lines, remove it
 
417
        min_indent = 999
 
418
        for line in text.split(ls):
 
419
            if line.strip():
 
420
                min_indent = min(_indent(line), min_indent)
 
421
        if min_indent:
 
422
            text = ls.join([line[min_indent:] for line in text.split(ls)])
 
423
        
 
424
        # Add an EOL character if a block stars with various Python
 
425
        # reserved words, so that it gets evaluated automatically
 
426
        # by the console
 
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]):
 
431
            text += ls
 
432
            if last_line != '':
 
433
                text += ls
 
434
 
 
435
        if no_selection:
 
436
            self.setFocus()
 
437
            self.clear_selection()
 
438
        
 
439
        return text
 
440
    
 
441
    def get_line_count(self):
 
442
        """Return document total line number"""
 
443
        return self.blockCount()
 
444
    
 
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()
 
460
            
 
461
        cursor.setPosition(start_pos)
 
462
        cursor.movePosition(QTextCursor.StartOfBlock)
 
463
        while cursor.position() <= end_pos:
 
464
            cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
 
465
            if cursor.atEnd():
 
466
                cursor_temp = QTextCursor(cursor)
 
467
                cursor_temp.clearSelection()
 
468
                cursor_temp.insertText(self.get_line_separator())
 
469
                break
 
470
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)            
 
471
        text = cursor.selectedText()
 
472
        cursor.clearSelection()
 
473
        
 
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))
 
479
        
 
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)
 
485
    
 
486
    def duplicate_line(self):
 
487
        """
 
488
        Duplicate current line or selected text
 
489
        Paste the duplicated text *after* the current line/selected text
 
490
        """
 
491
        self.__duplicate_line_or_selection(after_current_line=True)
 
492
    
 
493
    def copy_line(self):
 
494
        """
 
495
        Copy current line or selected text
 
496
        Paste the duplicated text *before* the current line/selected text
 
497
        """
 
498
        self.__duplicate_line_or_selection(after_current_line=False)
 
499
        
 
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()
 
511
 
 
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()
 
520
        else:
 
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)
 
527
        
 
528
        sel_text = unicode(cursor.selectedText())
 
529
        cursor.removeSelectedText()
 
530
        
 
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)
 
535
        else:
 
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)
 
540
 
 
541
        cursor.endEditBlock()
 
542
 
 
543
        cursor.setPosition(orig_sel[0])
 
544
        cursor.setPosition(orig_sel[1], QTextCursor.KeepAnchor)
 
545
        self.setTextCursor(cursor)
 
546
    
 
547
    def move_line_up(self):
 
548
        """Move up current line or selected text"""
 
549
        self.__move_line_or_selection(after_current_line=False)
 
550
        
 
551
    def move_line_down(self):
 
552
        """Move down current line or selected text"""
 
553
        self.__move_line_or_selection(after_current_line=True)
 
554
        
 
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)
 
567
        
 
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)
 
575
        else:
 
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)
 
582
            if cursor.atEnd():
 
583
                break
 
584
            cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
 
585
        cursor.removeSelectedText()
 
586
        cursor.endEditBlock()
 
587
        self.ensureCursorVisible()
 
588
 
 
589
 
 
590
    #------Code completion / Calltips
 
591
    def show_calltip(self, title, text, color='#2D62FF',
 
592
                     at_line=None, at_position=None):
 
593
        """Show calltip"""
 
594
        if text is None or len(text) == 0:
 
595
            return
 
596
        # Saving cursor position:
 
597
        if at_position is None:
 
598
            at_position = self.get_position('cursor')
 
599
        self.calltip_position = at_position
 
600
        # Preparing text:
 
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:
 
616
            cx = 5
 
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)
 
622
 
 
623
    def hide_tooltip_if_necessary(self, key):
 
624
        """Hide calltip when necessary"""
 
625
        try:
 
626
            calltip_char = self.get_character(self.calltip_position)
 
627
            before = self.is_cursor_before(self.calltip_position,
 
628
                                           char_offset=1)
 
629
            other = key in (Qt.Key_ParenRight, Qt.Key_Period, Qt.Key_Tab)
 
630
            if calltip_char not in ('?','(') or before or other:
 
631
                QToolTip.hideText()
 
632
        except (IndexError, TypeError):
 
633
            QToolTip.hideText()
 
634
 
 
635
    def show_completion_widget(self, textlist, automatic=True):
 
636
        """Show completion widget"""
 
637
        self.completion_widget.show_list(textlist, automatic=automatic)
 
638
        
 
639
    def hide_completion_widget(self):
 
640
        """Hide completion widget"""
 
641
        self.completion_widget.hide()
 
642
 
 
643
    def show_completion_list(self, completions, completion_text="",
 
644
                             automatic=True):
 
645
        """Display the possible completions"""
 
646
        if len(completions) == 0 or completions == [completion_text]:
 
647
            return
 
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)
 
655
        
 
656
    def select_completion_list(self):
 
657
        """Completion list is active, Enter was just pressed"""
 
658
        self.completion_widget.item_selected()
 
659
        
 
660
    def insert_completion(self, text):
 
661
        if 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)
 
668
 
 
669
    def is_completion_widget_visible(self):
 
670
        """Return True is completion list widget is visible"""
 
671
        return self.completion_widget.isVisible()
 
672
    
 
673
        
 
674
    #------Standard keys
 
675
    def stdkey_clear(self):
 
676
        if not self.has_selected_text():
 
677
            self.moveCursor(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
 
678
        self.remove_selected_text()
 
679
    
 
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()
 
685
 
 
686
    def __get_move_mode(self, shift):
 
687
        return QTextCursor.KeepAnchor if shift else QTextCursor.MoveAnchor
 
688
 
 
689
    def stdkey_up(self, shift):
 
690
        self.moveCursor(QTextCursor.Up, self.__get_move_mode(shift))
 
691
 
 
692
    def stdkey_down(self, shift):
 
693
        self.moveCursor(QTextCursor.Down, self.__get_move_mode(shift))
 
694
 
 
695
    def stdkey_tab(self):
 
696
        self.insert_text(self.indent_chars)
 
697
 
 
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)
 
702
        if ctrl:
 
703
            self.moveCursor(QTextCursor.Start, move_mode)
 
704
        else:
 
705
            cursor = self.textCursor()
 
706
            if prompt_pos is None:
 
707
                start_position = self.get_position('sol')
 
708
            else:
 
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)
 
714
            else:
 
715
                cursor.setPosition(start_position, move_mode)
 
716
            self.setTextCursor(cursor)
 
717
 
 
718
    def stdkey_end(self, shift, ctrl):
 
719
        move_mode = self.__get_move_mode(shift)
 
720
        if ctrl:
 
721
            self.moveCursor(QTextCursor.End, move_mode)
 
722
        else:
 
723
            self.moveCursor(QTextCursor.EndOfBlock, move_mode)
 
724
 
 
725
    def stdkey_pageup(self):
 
726
        pass
 
727
 
 
728
    def stdkey_pagedown(self):
 
729
        pass
 
730
 
 
731
    def stdkey_escape(self):
 
732
        pass
 
733
 
 
734
                
 
735
    #----Qt Events
 
736
    def mousePressEvent(self, event):
 
737
        """Reimplement Qt method"""
 
738
        if os.name != 'posix' and event.button() == Qt.MidButton:
 
739
            self.setFocus()
 
740
            event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
 
741
                                Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
 
742
            QPlainTextEdit.mousePressEvent(self, event)
 
743
            QPlainTextEdit.mouseReleaseEvent(self, event)
 
744
            self.paste()
 
745
        else:
 
746
            QPlainTextEdit.mousePressEvent(self, event)
 
747
 
 
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)
 
753
        
 
754
    def focusOutEvent(self, event):
 
755
        """Reimplemented to handle focus"""
 
756
        self.emit(SIGNAL("focus_changed()"))
 
757
        QPlainTextEdit.focusOutEvent(self, event)
 
758
    
 
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()"))
 
766
            return
 
767
        QPlainTextEdit.wheelEvent(self, event)
 
768
 
 
769
 
 
770
class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler):
 
771
    def __init__(self):
 
772
        ANSIEscapeCodeHandler.__init__(self)
 
773
        self.base_format = None
 
774
        self.current_format = None
 
775
        
 
776
    def set_light_background(self, state):
 
777
        if state:
 
778
            self.default_foreground_color = 30
 
779
            self.default_background_color = 47
 
780
        else:
 
781
            self.default_foreground_color = 37
 
782
            self.default_background_color = 40
 
783
        
 
784
    def set_base_format(self, base_format):
 
785
        self.base_format = base_format
 
786
        
 
787
    def get_format(self):
 
788
        return self.current_format
 
789
        
 
790
    def set_style(self):
 
791
        """
 
792
        Set font style with the following attributes:
 
793
        'foreground_color', 'background_color', 'italic',
 
794
        'bold' and 'underline'
 
795
        """
 
796
        if self.current_format is None:
 
797
            assert self.base_format is not None
 
798
            self.current_format = QTextCharFormat(self.base_format)
 
799
        # Foreground color
 
800
        if self.foreground_color is None:
 
801
            qcolor = self.base_format.foreground()
 
802
        else:
 
803
            cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity]
 
804
            qcolor = QColor(cstr)
 
805
        self.current_format.setForeground(qcolor)        
 
806
        # Background color
 
807
        if self.background_color is None:
 
808
            qcolor = self.base_format.background()
 
809
        else:
 
810
            cstr = self.ANSI_COLORS[self.background_color-40][self.intensity]
 
811
            qcolor = QColor(cstr)
 
812
        self.current_format.setBackground(qcolor)
 
813
        
 
814
        font = self.current_format.font()
 
815
        # Italic
 
816
        if self.italic is None:
 
817
            italic = self.base_format.fontItalic()
 
818
        else:
 
819
            italic = self.italic
 
820
        font.setItalic(italic)
 
821
        # Bold
 
822
        if self.bold is None:
 
823
            bold = self.base_format.font().bold()
 
824
        else:
 
825
            bold = self.bold
 
826
        font.setBold(bold)
 
827
        # Underline
 
828
        if self.underline is None:
 
829
            underline = self.base_format.font().underline()
 
830
        else:
 
831
            underline = self.underline
 
832
        font.setUnderline(underline)
 
833
        self.current_format.setFont(font)
 
834
 
 
835
 
 
836
def inverse_color(color):
 
837
    color.setHsv(color.hue(), color.saturation(), 255-color.value())
 
838
    
 
839
class ConsoleFontStyle(object):
 
840
    def __init__(self, foregroundcolor, backgroundcolor, 
 
841
                 bold, italic, underline):
 
842
        self.foregroundcolor = foregroundcolor
 
843
        self.backgroundcolor = backgroundcolor
 
844
        self.bold = bold
 
845
        self.italic = italic
 
846
        self.underline = underline
 
847
        self.format = None
 
848
        
 
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)
 
865
    
 
866
class ConsoleBaseWidget(TextEditBaseWidget):
 
867
    """Console base widget"""
 
868
    BRACE_MATCHING_SCOPE = ('sol', 'eol')
 
869
    COLOR_PATTERN = re.compile('\x01?\x1b\[(.*?)m\x02?')
 
870
    
 
871
    def __init__(self, parent=None):
 
872
        TextEditBaseWidget.__init__(self, parent)
 
873
        
 
874
        self.light_background = True
 
875
 
 
876
        self.setMaximumBlockCount(300)
 
877
 
 
878
        # ANSI escape code handler
 
879
        self.ansi_handler = QtANSIEscapeCodeHandler()
 
880
                
 
881
        # Disable undo/redo (nonsense for a console widget...):
 
882
        self.setUndoRedoEnabled(False)
 
883
        
 
884
        self.connect(self, SIGNAL('userListActivated(int, const QString)'),
 
885
                     lambda user_id, text:
 
886
                     self.emit(SIGNAL('completion_widget_activated(QString)'),
 
887
                               text))
 
888
        
 
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)
 
905
        
 
906
    def set_light_background(self, state):
 
907
        self.light_background = state
 
908
        if state:
 
909
            self.set_palette(background=QColor(Qt.white),
 
910
                             foreground=QColor(Qt.darkGray))
 
911
        else:
 
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()
 
916
        
 
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)
 
922
 
 
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()
 
928
        if start < end:
 
929
            start = max([position_from, start])
 
930
        else:
 
931
            end = max([position_from, end])
 
932
        self.set_selection(start, end)
 
933
 
 
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)
 
942
 
 
943
    #------Python shell
 
944
    def insert_text(self, text):
 
945
        """Reimplement TextEditBaseWidget method"""
 
946
        self.textCursor().insertText(text, self.default_style.format)
 
947
        
 
948
    def paste(self):
 
949
        """Reimplement Qt method"""
 
950
        if self.has_selected_text():
 
951
            self.remove_selected_text()
 
952
        self.insert_text(QApplication.clipboard().text())
 
953
        
 
954
    def append_text_to_shell(self, text, error, prompt):
 
955
        """
 
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
 
959
        
 
960
        Handles error messages and show blue underlined links
 
961
        Handles ANSI color sequences
 
962
        Handles ANSI FF sequence
 
963
        """
 
964
        cursor = self.textCursor()
 
965
        cursor.movePosition(QTextCursor.End)
 
966
        while True:
 
967
            index = text.find(chr(12))
 
968
            if index == -1:
 
969
                break
 
970
            text = text[index+1:]
 
971
            self.clear()
 
972
        if error:
 
973
            is_traceback = False
 
974
            for text in text.splitlines(True):
 
975
                if text.startswith('  File') \
 
976
                and not text.startswith('  File "<'):
 
977
                    is_traceback = True
 
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)
 
982
                else:
 
983
                    # Show error/warning messages in red
 
984
                    cursor.insertText(text, self.error_style.format)
 
985
            if is_traceback:
 
986
                self.emit(SIGNAL('traceback_available()'))
 
987
        elif prompt:
 
988
            # Show prompt in green
 
989
            cursor.insertText(text, self.prompt_style.format)
 
990
        else:
 
991
            # Show other outputs in black
 
992
            last_end = 0
 
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)
 
1004
#            if segments:
 
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)
 
1012
    
 
1013
    def set_pythonshell_font(self, font=None):
 
1014
        """Python Shell only"""
 
1015
        if font is None:
 
1016
            font = QFont()
 
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)
999
1022
        
 
 
b'\\ No newline at end of file'