ConsoleEditArea.patch

Nathan Woodrow, 2011-01-14 04:51 AM

Download (20.5 KB)

View differences:

python/console.py (working copy)
15 15
- supports expressions that span through more lines
16 16
- has command history, accessible using up/down keys
17 17
- supports pasting of commands
18
- python syntax highlighing (portions of code from http://diotavelli.net/PyQtWiki/Python%20syntax%20highlighting)
18 19

  
19 20
TODO:
20 21
- configuration - init commands, font, ...
21
- python code highlighting
22 22

  
23 23
"""
24 24

  
......
29 29
import traceback
30 30
import code
31 31

  
32
EDIT_LINE, ERROR, OUTPUT, INIT = range(4)
32 33

  
33 34
_init_commands = ["from qgis.core import *", "import qgis.utils"]
34 35

  
35 36
_console = None
36 37

  
38
def format(color, style = ''):
39
    """Return a QTextCharFormat with the given attributes.
40
    """
41
    _color = QColor()
42
    _color.setNamedColor(color)
43

  
44
    _format = QTextCharFormat()
45
    _format.setForeground(_color)
46
    if 'bold' in style:
47
        _format.setFontWeight(QFont.Bold)
48
    if 'italic' in style:
49
        _format.setFontItalic(True)
50

  
51
    return _format
52

  
53
# Syntax styles that can be shared by all languages
54
STYLES = {
55
    'keyword': format('blue'),
56
    'operator': format('red'),
57
    'brace': format('darkGray'),
58
    'defclass': format('black', 'bold'),
59
    'string': format('magenta'),
60
    'string2': format('darkMagenta'),
61
    'comment': format('darkGreen', 'italic'),
62
    'self': format('black', 'italic'),
63
    'numbers': format('brown'),
64
}
65

  
37 66
def show_console():
38
  """ called from QGIS to open the console """
39
  global _console
40
  if _console is None:
41
    _console = PythonConsole(iface.mainWindow())
42
    _console.show() # force show even if it was restored as hidden
43
  else:
44
    _console.setVisible(not _console.isVisible())
45
  # set focus to the edit box so the user can start typing
46
  if _console.isVisible():
47
    _console.activateWindow()
48
    _console.edit.setFocus()
67
    """ called from QGIS to open the console """
68
    global _console
69
    if _console is None:
70
        _console = PythonConsole(iface.mainWindow())
71
        _console.show() # force show even if it was restored as hidden
72
    else:
73
        _console.setVisible(not _console.isVisible())
74
    # set focus to the edit box so the user can start typing
75
    if _console.isVisible():
76
        _console.activateWindow()
77
        _console.edit.setFocus()
49 78

  
50
  
79

  
51 80
_old_stdout = sys.stdout
52 81
_console_output = None
53 82

  
54 83

  
55 84
def clearConsole():
56
  global _console
57
  if _console is None:
58
    return
59
  _console.edit.clearConsole()
85
    global _console
86
    if _console is None:
87
        return
88
    _console.edit.clearConsole()
60 89

  
61

  
62 90
# hook for python console so all output will be redirected
63 91
# and then shown in console
64 92
def console_displayhook(obj):
65
  global _console_output
66
  _console_output = obj
93
    global _console_output
94
    _console_output = obj
67 95

  
68 96
class QgisOutputCatcher:
69
  def __init__(self):
70
    self.data = ''
71
  def write(self, stuff):
72
    self.data += stuff
73
  def get_and_clean_data(self):
74
    tmp = self.data
75
    self.data = ''
76
    return tmp
77
  def flush(self):
78
    pass
97
    def __init__(self):
98
        self.data = ''
99
    def write(self, stuff):
100
        self.data += stuff
101
    def get_and_clean_data(self):
102
        tmp = self.data
103
        self.data = ''
104
        return tmp
105
    def flush(self):
106
        pass
79 107

  
80 108
sys.stdout = QgisOutputCatcher()
81 109

  
82 110
class PythonConsole(QDockWidget):
83
  def __init__(self, parent=None):
84
    QDockWidget.__init__(self, parent)
85
    self.setObjectName("Python Console")
86
    self.setAllowedAreas(Qt.BottomDockWidgetArea)
87
    self.widget = QWidget()
88
    self.l = QVBoxLayout(self.widget)
89
    self.edit = PythonEdit()
90
    self.l.addWidget(self.edit)
91
    self.setWidget(self.widget)
92
    self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console"))
93
    # try to restore position from stored main window state
94
    if not iface.mainWindow().restoreDockWidget(self):
95
      iface.mainWindow().addDockWidget(Qt.BottomDockWidgetArea, self)
111
    def __init__(self, parent = None):
112
        QDockWidget.__init__(self, parent)
113
        self.setObjectName("Python Console")
114
        self.setAllowedAreas(Qt.AllDockWidgetAreas)
115

  
116
        self.widget = QWidget()
117
        self.splitter = QSplitter(Qt.Vertical)
118

  
119
        self.layout = QVBoxLayout(self.widget)
120
        self.edit = PythonEdit()
121
        self.editarea = PythonTextArea()
122
        
123
        self.highlight = PythonHighlighter(self.editarea.document())
124
        self.edithighlight = PythonHighlighter(self.edit.document())
125

  
126
        self.splitter.addWidget(self.edit)
127
        self.splitter.addWidget(self.editarea)
128
        self.layout.addWidget(self.splitter)
129
        self.setWidget(self.widget)
130

  
131
        self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console"))
132
        # try to restore position from stored main window state
133
        if not iface.mainWindow().restoreDockWidget(self):
134
            iface.mainWindow().addDockWidget(Qt.BottomDockWidgetArea, self)
135

  
136

  
137
    def sizeHint(self):
138
        return QSize(500, 300)
139

  
140
    def closeEvent(self, event):
141
        QWidget.closeEvent(self, event)
142

  
143
class PythonTextArea(QTextEdit):
144
    def __init_(self, parent = None):
145
        QTextEdit.__init__(self, parent)
146
        
147
    def keyPressEvent(self, e):
148
        if e.modifiers() & Qt.ControlModifier:
149
            if e.key() == Qt.Key_Return:
150
                lines = QStringList()
151
                lines = self.toPlainText().split("\n")
152
                #run each command in the edit area
153
                for line in lines:
154
                    _console.edit.insertPlainText(line)
155
                    _console.edit.runCommand(unicode(line))
156
            else:
157
                QTextEdit.keyPressEvent(self, e)
158
        elif e.key() == Qt.Key_Tab:
159
                self.insertPlainText(" " * 4)
160
        else:
161
            QTextEdit.keyPressEvent(self, e)
162

  
163

  
164

  
165
class PythonHighlighter (QSyntaxHighlighter):
166
    #Syntax highlighter for the Python language.
96 167
    
168
    # Python keywords
169
    keywords = [
170
        'and', 'assert', 'break', 'class', 'continue', 'def',
171
        'del', 'elif', 'else', 'except', 'exec', 'finally',
172
        'for', 'from', 'global', 'if', 'import', 'in',
173
        'is', 'lambda', 'not', 'or', 'pass', 'print',
174
        'raise', 'return', 'try', 'while', 'yield',
175
        'None', 'True', 'False',
176
    ]
97 177

  
98
  def sizeHint(self):
99
    return QSize(500,300)
178
    # Python operators
179
    operators = [
180
        '=',
181
        # Comparison
182
        '==', '!=', '<', '<=', '>', '>=',
183
        # Arithmetic
184
        '\+', '-', '\*', '/', '//', '\%', '\*\*',
185
        # In-place
186
        '\+=', '-=', '\*=', '/=', '\%=',
187
        # Bitwise
188
        '\^', '\|', '\&', '\~', '>>', '<<',
189
    ]
100 190

  
101
  def closeEvent(self, event):
102
    QWidget.closeEvent(self, event)
191
    # Python braces
192
    braces = [
193
        '\{', '\}', '\(', '\)', '\[', '\]',
194
    ]
103 195

  
196
    def __init__(self, document):
197
        QSyntaxHighlighter.__init__(self, document)
104 198

  
105
class ConsoleHighlighter(QSyntaxHighlighter):
106
  EDIT_LINE, ERROR, OUTPUT, INIT = range(4)
107
  def __init__(self, doc):
108
    QSyntaxHighlighter.__init__(self,doc)
109
    formats = { self.OUTPUT    : Qt.black,
110
		self.ERROR     : Qt.red,
111
		self.EDIT_LINE : Qt.darkGreen,
112
		self.INIT      : Qt.gray }
113
    self.f = {}
114
    for tag, color in formats.iteritems():
115
      self.f[tag] = QTextCharFormat()
116
      self.f[tag].setForeground(color)
199
        # Multi-line strings (expression, flag, style)
200
        # FIXME: The triple-quotes in these two lines will mess up the
201
        # syntax highlighting from this point onward
202
        self.tri_single = (QRegExp("'''"), 1, STYLES['string2'])
203
        self.tri_double = (QRegExp('"""'), 2, STYLES['string2'])
204
        
205
        rules = []
117 206

  
118
  def highlightBlock(self, txt):
119
    size = txt.length()
120
    state = self.currentBlockState()
121
    if state == self.OUTPUT or state == self.ERROR or state == self.INIT:
122
      self.setFormat(0,size, self.f[state])
123
    # highlight prompt only
124
    if state == self.EDIT_LINE:
125
      self.setFormat(0,3, self.f[self.EDIT_LINE])
207
        # Keyword, operator, and brace rules
208
        rules += [(r'\b%s\b' % w, 0, STYLES['keyword'])
209
            for w in PythonHighlighter.keywords]
210
        
211
        rules += [(r'%s' % o, 0, STYLES['operator'])
212
            for o in PythonHighlighter.operators]
213
        
214
        rules += [(r'%s' % b, 0, STYLES['brace'])
215
            for b in PythonHighlighter.braces]
126 216

  
217
        # All other rules
218
        rules += [
219
            # 'self'
220
            (r'\bself\b', 0, STYLES['self']),
127 221

  
222
            # Double-quoted string, possibly containing escape sequences
223
            (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
224
            # Single-quoted string, possibly containing escape sequences
225
            (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),
226

  
227
            # 'def' followed by an identifier
228
            (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']),
229
            # 'class' followed by an identifier
230
            (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']),
231

  
232
            # From '#' until a newline
233
            (r'#[^\n]*', 0, STYLES['comment']),
234

  
235
            # Numeric literals
236
            (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']),
237
            (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
238
            (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']),
239
        ]
240

  
241
        # Build a QRegExp for each pattern
242
        self.rules = [(QRegExp(pat), index, fmt)
243
            for (pat, index, fmt) in rules]
244

  
245

  
246
    def highlightBlock(self, text):
247
        """Apply syntax highlighting to the given block of text.
248
        """   
249

  
250
        # Do other syntax formatting
251
        for expression, nth, format in self.rules:
252
            index = expression.indexIn(text, 0)
253

  
254
            while index >= 0:
255
                # We actually want the index of the nth match
256
                index = expression.pos(nth)
257
                length = expression.cap(nth).length()
258
                self.setFormat(index, length, format)
259
                index = expression.indexIn(text, index + length)
260

  
261
        self.setCurrentBlockState(0)
262
        
263
        # Do multi-line strings
264
        in_multiline = self.match_multiline(text, *self.tri_single)
265
        if not in_multiline:
266
            in_multiline = self.match_multiline(text, *self.tri_double)
267

  
268

  
269
    def match_multiline(self, text, delimiter, in_state, style):
270
        """Do highlighting of multi-line strings. ``delimiter`` should be a
271
        ``QRegExp`` for triple-single-quotes or triple-double-quotes, and
272
        ``in_state`` should be a unique integer to represent the corresponding
273
        state changes when inside those strings. Returns True if we're still
274
        inside a multi-line string when this function is finished.
275
        """
276
        # If inside triple-single quotes, start at 0
277
        if self.previousBlockState() == in_state:
278
            start = 0
279
            add = 0
280
        # Otherwise, look for the delimiter on this line
281
        else:
282
            start = delimiter.indexIn(text)
283
            # Move past this match
284
            add = delimiter.matchedLength()
285

  
286
        # As long as there's a delimiter match on this line...
287
        while start >= 0:
288
            # Look for the ending delimiter
289
            end = delimiter.indexIn(text, start + add)
290
            # Ending delimiter on this line?
291
            if end >= add:
292
                length = end - start + add + delimiter.matchedLength()
293
                self.setCurrentBlockState(0)
294
            # No; multi-line string
295
            else:
296
                self.setCurrentBlockState(in_state)
297
                length = text.length() - start + add
298
            # Apply formatting
299
            self.setFormat(start, length, style)
300
            # Look for the next match
301
            start = delimiter.indexIn(text, start + length)
302

  
303
        # Return True if still inside a multi-line string, False otherwise
304
        if self.currentBlockState() == in_state:
305
            return True
306
        else:
307
            return False
308

  
309

  
310

  
128 311
class PythonEdit(QTextEdit, code.InteractiveInterpreter):
312
    def __init__(self, parent = None):
313
        QTextEdit.__init__(self, parent)
314
        code.InteractiveInterpreter.__init__(self, locals = None)
129 315

  
130
  def __init__(self,parent=None):
131
    QTextEdit.__init__(self, parent)
132
    code.InteractiveInterpreter.__init__(self, locals=None)
316
        self.setTextInteractionFlags(Qt.TextEditorInteraction)
317
        self.setAcceptDrops(False)
318
        self.setMinimumSize(30, 30)
319
        self.setUndoRedoEnabled(False)
320
        self.setAcceptRichText(False)
321
        monofont = QFont("Monospace")
322
        monofont.setStyleHint(QFont.TypeWriter)
323
        self.setFont(monofont)
133 324

  
134
    self.setTextInteractionFlags(Qt.TextEditorInteraction)
135
    self.setAcceptDrops(False)
136
    self.setMinimumSize(30, 30)
137
    self.setUndoRedoEnabled(False)
138
    self.setAcceptRichText(False)
139
    monofont = QFont("Monospace")
140
    monofont.setStyleHint(QFont.TypeWriter)
141
    self.setFont(monofont)
325
        self.buffer = []
142 326

  
143
    self.buffer = []
144
    
145
    self.insertInitText()
327
        self.insertInitText()
146 328

  
147
    for line in _init_commands:
148
      self.runsource(line)
329
        for line in _init_commands:
330
            self.runsource(line)
149 331

  
150
    self.displayPrompt(False)
332
        self.displayPrompt(False)
151 333

  
152
    self.history = QStringList()
153
    self.historyIndex = 0
334
        self.history = QStringList()
335
        self.historyIndex = 0
154 336

  
155
    self.high = ConsoleHighlighter(self)
156
    
157
  def insertInitText(self):
158
    self.insertTaggedText(QCoreApplication.translate("PythonConsole", "To access Quantum GIS environment from this console\n"
159
                          "use qgis.utils.iface object (instance of QgisInterface class).\n\n"),
160
                          ConsoleHighlighter.INIT)
337
    def insertInitText(self):
338
        self.insertTaggedText(QCoreApplication.translate("PythonConsole", "#To access Quantum GIS environment from this console\n"
339
                              "#use qgis.utils.iface object (instance of QgisInterface class).\n\n"),
340
                              INIT)
161 341

  
162 342

  
163
  def clearConsole(self):
164
    self.clear()
165
    self.insertInitText()
343
    def clearConsole(self):
344
        self.clear()
345
        self.insertInitText()
166 346

  
167
  def displayPrompt(self, more=False):
168
    self.currentPrompt = "... " if more else ">>> "
169
    self.currentPromptLength = len(self.currentPrompt)
170
    self.insertTaggedLine(self.currentPrompt, ConsoleHighlighter.EDIT_LINE)
171
    self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
347
    def displayPrompt(self, more = False):
348
        self.currentPrompt = "... " if more else ">>> "
349
        self.currentPromptLength = len(self.currentPrompt)
350
        self.insertTaggedLine(self.currentPrompt, EDIT_LINE)
351
        self.moveCursor(QTextCursor.End, QTextCursor.MoveAnchor)
172 352

  
173
  def isCursorInEditionZone(self):
174
    cursor = self.textCursor()
175
    pos = cursor.position()
176
    block = self.document().lastBlock()
177
    last = block.position() + self.currentPromptLength
178
    return pos >= last
353
    def isCursorInEditionZone(self):
354
        cursor = self.textCursor()
355
        pos = cursor.position()
356
        block = self.document().lastBlock()
357
        last = block.position() + self.currentPromptLength
358
        return pos >= last
179 359

  
180
  def currentCommand(self):
181
    block = self.cursor.block()
182
    text = block.text()
183
    return text.right(text.length()-self.currentPromptLength)
360
    def currentCommand(self):
361
        block = self.cursor.block()
362
        text = block.text()
363
        return text.right(text.length() - self.currentPromptLength)
184 364

  
185
  def showPrevious(self):
365
    def showPrevious(self):
186 366
        if self.historyIndex < len(self.history) and not self.history.isEmpty():
187 367
            self.cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor)
188 368
            self.cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor)
......
194 374
            else:
195 375
                self.insertPlainText(self.history[self.historyIndex])
196 376

  
197
  def showNext(self):
377
    def showNext(self):
198 378
        if  self.historyIndex > 0 and not self.history.isEmpty():
199 379
            self.cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor)
200 380
            self.cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor)
......
206 386
            else:
207 387
                self.insertPlainText(self.history[self.historyIndex])
208 388

  
209
  def updateHistory(self, command):
389
    def updateHistory(self, command):
210 390
        if isinstance(command, QStringList):
211 391
            for line in command:
212 392
                self.history.append(line)
......
216 396
                self.history.append(command)
217 397
        self.historyIndex = len(self.history)
218 398

  
219
  def keyPressEvent(self, e):
399
    def keyPressEvent(self, e):
220 400
        self.cursor = self.textCursor()
221 401
        # if the cursor isn't in the edition zone, don't do anything except Ctrl+C
222 402
        if not self.isCursorInEditionZone():
......
259 439
            elif e.key() == Qt.Key_End:
260 440
                anchor = QTextCursor.KeepAnchor if e.modifiers() & Qt.ShiftModifier else QTextCursor.MoveAnchor
261 441
                self.cursor.movePosition(QTextCursor.EndOfBlock, anchor, 1)
442
            elif e.key() == Qt.Key_Tab:
443
                self.insertPlainText(" " * 4)
262 444
            # use normal operation for all remaining keys
263 445
            else:
264 446
                QTextEdit.keyPressEvent(self, e)
265 447
        self.setTextCursor(self.cursor)
266 448
        self.ensureCursorVisible()
267 449

  
268
  def insertFromMimeData(self, source):
450
    def insertFromMimeData(self, source):
269 451
        self.cursor = self.textCursor()
270 452
        self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor, 1)
271 453
        self.setTextCursor(self.cursor)
......
273 455
            pasteList = QStringList()
274 456
            pasteList = source.text().split("\n")
275 457
            for line in pasteList:
276
		self.insertPlainText(line)
277
		self.runCommand(unicode(line))
458
                self.insertPlainText(line)
459
                self.runCommand(unicode(line))
278 460

  
279
  def entered(self):
280
    self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
281
    self.setTextCursor(self.cursor)
282
    self.runCommand( unicode(self.currentCommand()) )
461
    def entered(self):
462
        self.cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
463
        self.setTextCursor(self.cursor)
464
        self.runCommand(unicode(self.currentCommand()))
283 465

  
284
  def insertTaggedText(self, txt, tag):
466
    def insertTaggedText(self, txt, tag):
285 467

  
286
    if len(txt) > 0 and txt[-1] == '\n': # remove trailing newline to avoid one more empty line
287
      txt = txt[0:-1]
468
        if len(txt) > 0 and txt[-1] == '\n': # remove trailing newline to avoid one more empty line
469
            txt = txt[0:-1]
288 470

  
289
    c = self.textCursor()
290
    for line in txt.split('\n'):
291
      b = c.block()
292
      b.setUserState(tag)
293
      c.insertText(line)
294
      c.insertBlock()
471
        c = self.textCursor()
472
        for line in txt.split('\n'):
473
            b = c.block()
474
            b.setUserState(tag)
475
            c.insertText(line)
476
            c.insertBlock()
295 477

  
296
  def insertTaggedLine(self, txt, tag):
297
    c = self.textCursor()
298
    b = c.block()
299
    b.setUserState(tag)
300
    c.insertText(txt)
478
    def insertTaggedLine(self, txt, tag):
479
        c = self.textCursor()
480
        b = c.block()
481
        b.setUserState(tag)
482
        c.insertText(txt)
301 483

  
302
  def runCommand(self, cmd):
484
    def runCommand(self, cmd):
303 485

  
304
    self.updateHistory(cmd)
486
        self.updateHistory(cmd)
305 487

  
306
    self.insertPlainText("\n")
488
        self.insertPlainText("\n")
307 489

  
308
    self.buffer.append(cmd)
309
    src = "\n".join(self.buffer)
310
    more = self.runsource(src, "<input>")
311
    if not more:
312
      self.buffer = []
490
        self.buffer.append(cmd)
491
        src = "\n".join(self.buffer)
492
        more = self.runsource(src, "<input>")
493
        if not more:
494
            self.buffer = []
313 495

  
314
    output = sys.stdout.get_and_clean_data()
315
    if output:
316
      self.insertTaggedText(output, ConsoleHighlighter.OUTPUT)
317
    self.displayPrompt(more)
496
        output = sys.stdout.get_and_clean_data()
497
        if output:
498
            self.insertTaggedText(output, OUTPUT)
499
        self.displayPrompt(more)
318 500

  
319
  def write(self, txt):
320
    """ reimplementation from code.InteractiveInterpreter """
321
    self.insertTaggedText(txt, ConsoleHighlighter.ERROR)
501
    def write(self, txt):
502
        """ reimplementation from code.InteractiveInterpreter """
503
        self.insertTaggedText(txt, ERROR)
322 504

  
323 505
if __name__ == '__main__':
324
  a = QApplication(sys.argv)
325
  show_console()
326
  a.exec_()
506
    a = QApplication(sys.argv)
507
    show_console()
508
    a.exec_()