1import sys
2import traceback
3import re
4
5from PyQt4.QtCore import QObject, Qt
6from PyQt4.QtGui import QTextCursor, qApp, QApplication, QPlainTextEdit
7
8
9from highlighter import PythonHighlighter,  QtScriptHighlighter
10
11
12
13
14from PyQt4.QtScript import (
15    QScriptEngine, QScriptValue, QScriptValueIterator)
16
17
18class OutputWidget(QPlainTextEdit):
19
20
21    def __init__(self, parent=None, readonly=True, max_rows=1000, echo=True):
22        QPlainTextEdit.__init__(self, parent)
23        self.echo = echo
24        self.setReadOnly(readonly)
25        self.document().setMaximumBlockCount(max_rows)
26        self.attach()
27
28
29    def attach(self):
30        sys.stdout = sys.stderr = self
31
32
33    def __del__(self):
34        self.detach()
35
36
37    def detach(self):
38        sys.stdout = sys.__stdout__
39        sys.stderr = sys.__stderr__
40
41
42    def write(self, s):
43        if self.echo:
44            sys.__stdout__.write(s)
45        doc = self.document()
46        cursor = QTextCursor(doc)
47        cursor.clearSelection()
48        cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
49        cursor.insertText(s)
50        cursor.movePosition(QTextCursor.End, QTextCursor.MoveAnchor)
51        cursor.clearSelection()
52        self.ensureCursorVisible()
53        qApp.processEvents()
54
55
56    def writelines(self, lines):
57        self.write("\n".join(lines))
58
59
60
61class ConsoleWidget(OutputWidget):
62
63
64    def __init__(self, parent=None, ps1="?", ps2=">"):
65        OutputWidget.__init__(self, parent, readonly=False)
66        self.setTabChangesFocus(False)
67        self.ps1 = ps1
68        self.ps2 = ps2
69        self.history_index = 0
70        self.history = [""]
71        self.tab_state = -1
72        print self.ps1,
73
74
75    def focusInEvent(self, event):
76        self.attach()
77        OutputWidget.focusInEvent(self, event)
78
79
80    def mousePressEvent(self, event):
81        self.setFocus()
82
83
84    def push(self, line):
85        return True
86
87
88    def keyPressEvent(self, event):
89        def remove_line():
90            cursor = self.textCursor()
91            cursor.select(QTextCursor.BlockUnderCursor)
92            cursor.removeSelectedText()
93        key = event.key()
94        modifiers = event.modifiers()
95        l = len(self.ps1)
96        line = unicode(self.document().end().previous().text())
97        ps1orps2, line = line[:l-1], line[l:]
98
99
100        if not key in [Qt.Key_Tab, Qt.Key_Backtab] and \
101                len(event.text()):
102            self.tab_state = -1
103        if key == Qt.Key_Up:
104            if self.history_index + 1 < len(self.history):
105                self.history_index += 1
106            remove_line()
107            print
108            print ps1orps2, self.history[self.history_index],
109        elif key == Qt.Key_Down:
110            if self.history_index > 0:
111                self.history_index -= 1
112            remove_line()
113            print
114            print ps1orps2, self.history[self.history_index],
115        elif key == Qt.Key_Tab:
116            if modifiers & Qt.ControlModifier:
117                print " " * 4,
118            else:
119                self.tab_state += 1
120                remove_line()
121                print
122                print ps1orps2,
123                print self.completer.complete(line, self.tab_state) or line,
124        elif key == Qt.Key_Backtab:
125            if self.tab_state >= 0:
126                self.tab_state -= 1
127            remove_line()
128            print
129            print ps1orps2,
130            print self.completer.complete(line, self.tab_state) or line,
131        elif key in [Qt.Key_Backspace, Qt.Key_Left]:
132            if self.textCursor().columnNumber()  > len(ps1orps2) + 1:
133                return OutputWidget.keyPressEvent(self, event)
134        elif key == Qt.Key_Return:
135            self.moveCursor(QTextCursor.EndOfLine,  QTextCursor.MoveAnchor)
136            print
137            if self.push(line):
138                print self.ps2,
139            else:
140                print self.ps1,
141            if line and line != self.history[self.history_index]:
142                self.history.insert(1, line)
143            self.history_index = 0
144        else:
145            return OutputWidget.keyPressEvent(self, event)
146
147
148
149class PythonInterpreter(object):
150
151
152    def __init__(self, name="<pyqtshell>", locals=None):
153        self.name = name
154        self.locals = locals or {}
155        self.locals["__name__"] = self.name
156        self.lines = []
157
158
159    def run(self, source, locals=None):
160        if locals == None:
161                locals = self.locals
162        code = compile(source, self.name, "exec")
163        try:
164                exec code in locals
165        except:
166                self.showtraceback()
167        try:
168            Scripter.activeWindow.redraw = True
169            Scripter.activeWindow.update()
170        except: pass
171
172
173    def push(self, line):
174        if self.lines:
175            if line:
176                self.lines.append(line)
177                return 1 # want more!
178            else:
179                line = "\n".join(self.lines) + "\n"
180        else:
181            if not line:
182                return 0
183        try:
184            code = compile(line, self.name, "single")
185            self.lines = []
186        except SyntaxError, why:
187            if why[0] == "unexpected EOF while parsing":
188                self.lines.append(line)
189                return 1 # want more!
190            else:
191                self.showtraceback()
192        except:
193            self.showtraceback()
194        else:
195            try:
196                exec code in self.locals
197            except:
198                self.showtraceback()
199            try:
200                Scripter.activeWindow.redraw = True
201                Scripter.activeWindow.update()
202            except: pass
203        return 0
204
205
206    def showtraceback(self):
207        self.lines = []
208        if sys.exc_type == SyntaxError: # and len(sys.exc_value) == 2:
209            print "  File \"%s\", line %d" % (self.name, sys.exc_value[1][1])
210            print " " * (sys.exc_value[1][2] + 2) + "^"
211            print str(sys.exc_type) + ":", sys.exc_value[0]
212        else:
213            traceback.print_tb(sys.exc_traceback, None)
214            print sys.exc_type.__name__ + ":", sys.exc_value
215
216
217
218
219class PythonCompleter(object):
220
221
222    def __init__(self, namespace):
223        self.namespace = namespace
224
225
226    def complete(self, text, state):
227        if state == 0:
228            if "." in text:
229                self.matches = self.attr_matches(text)
230            else:
231                self.matches = self.global_matches(text)
232        try:
233            return self.matches[state]
234        except IndexError:
235            return None
236
237
238    def global_matches(self, text):
239        import keyword, __builtin__
240        matches = []
241        n = len(text)
242        for list in [keyword.kwlist,
243                     __builtin__.__dict__,
244                     self.namespace]:
245            for word in list:
246                if word[:n] == text and word != "__builtins__":
247                    matches.append(word)
248        return matches
249
250
251    def attr_matches(self, text):
252        def get_class_members(cls):
253            ret = dir(cls)
254            if hasattr(cls,'__bases__'):
255                for base in cls.__bases__:
256                    ret = ret + get_class_members(base)
257            return ret
258        import re
259        m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text)
260        if not m:
261            return
262        expr, attr = m.group(1, 3)
263        object = eval(expr, self.namespace)
264        words = dir(object)
265        if hasattr(object,'__class__'):
266            words.append('__class__')
267            words = words + get_class_members(object.__class__)
268        matches = []
269        n = len(attr)
270        for word in words:
271            if word[:n] == attr and word != "__builtins__":
272                matches.append("%s.%s" % (expr, word))
273        return matches
274
275
276
277
278
279
280
281class PythonConsole(ConsoleWidget):
282
283
284    def __init__(self, parent=None, namespace=None):
285        ConsoleWidget.__init__(self, parent, ps1=">>> ", ps2="... ")
286        self.highlighter = PythonHighlighter(self)
287        self.inter = PythonInterpreter(locals=namespace)
288        self.namespace = self.inter.locals
289        self.completer = PythonCompleter(self.namespace)
290        #print "Python", sys.version
291        #print "Autocomplete with (Shift+)Tab, insert spaces with Ctrl+Tab"
292        self.push("pass")
293
294
295    def push(self, line):
296        return self.inter.push(line)
297
298
299    def clear(self):
300	doc = self.document()
301	doc.setPlainText(self.ps1)
302
303
304
305
306class QtScriptInterpreter(object):
307
308
309    def __init__(self, locals):
310        self.locals = locals
311        self.engine = self.newEngine()
312        self.code = ""
313        self.state = 0
314
315
316    def newEngine(self):
317        engine = QScriptEngine()
318        ns = engine.globalObject()
319        for name, value in self.locals.items():
320            if isinstance(value, QObject):
321                value = engine.newQObject(value)
322            elif callable(value):
323                value = engine.newFunction(value)
324            ns.setProperty(name, value)
325        return engine
326
327
328    def execute(self, code):
329        self.execute_code(code, self.engine)
330
331
332    def execute_code(self, code, engine=None):
333        engine = engine or self.newEngine()
334        result = engine.evaluate(code)
335        try:
336            Scripter.activeWindow.redraw = True
337            Scripter.activeWindow.update()
338        except: pass
339        if engine.hasUncaughtException():
340            bt = engine.uncaughtExceptionBacktrace()
341            print "Traceback:"
342            print "\n".join(["  %s" % l for l in list(bt)])
343            print engine.uncaughtException().toString()
344        else:
345            if not result.isUndefined():
346                print result.toString()
347
348
349    def push(self, line):
350        if not line.strip():
351            return self.state
352        self.code = self.code + line + "\n"
353        if self.engine.canEvaluate(self.code):
354            self.execute(self.code)
355            self.code = ""
356            self.state = 0
357        else:
358            self.state = 1
359        return self.state
360
361
362js_words = [
363 'break',
364 'for',
365 'throw',
366 'case',
367 'function',
368 'try',
369 'catch',
370 'if',
371 'typeof',
372 'continue',
373 'in',
374 'var',
375 'default',
376 'instanceof',
377 'void',
378 'delete',
379 'new',
380 'undefined',
381 'do',
382 'return',
383 'while',
384 'else',
385 'switch',
386 'with',
387 'finally',
388 'this',
389 'NaN',
390 'Infinity',
391 'undefined',
392 'print',
393 'parseInt',
394 'parseFloat',
395 'isNaN',
396 'isFinite',
397 'decodeURI',
398 'decodeURIComponent',
399 'encodeURI',
400 'encodeURIComponent',
401 'escape',
402 'unescape',
403 'version',
404 'gc',
405 'Object',
406 'Function',
407 'Number',
408 'Boolean',
409 'String',
410 'Date',
411 'Array',
412 'RegExp',
413 'Error',
414 'EvalError',
415 'RangeError',
416 'ReferenceError',
417 'SyntaxError',
418 'TypeError',
419 'URIError',
420 'eval',
421 'Math',
422 'Enumeration',
423 'Variant',
424 'QObject',
425 'QMetaObject']
426
427
428
429class QtScriptCompleter(object):
430
431
432    def __init__(self, engine):
433        self.engine = engine
434
435
436    def complete(self, text, state):
437        if state == 0:
438            if "." in text:
439                self.matches = self.attr_matches(text)
440            else:
441                self.matches = self.global_matches(text)
442        try:
443            return self.matches[state]
444        except IndexError:
445            return None
446
447
448
449    def attr_matches(self, text):
450        return []
451
452
453
454    def iter_obj(self, obj):
455        it = QScriptValueIterator(self.engine.globalObject())
456        while it.hasNext():
457            yield str(it.name())
458            it.next()
459
460
461    def global_matches(self, text):
462        words = list(self.iter_obj(self.engine.globalObject()))
463        words.extend(js_words)
464        l = []
465        n = len(text)
466        for w in words:
467            if w[:n] == text:
468                l.append(w)
469        return l
470
471
472
473
474
475class QtScriptConsole(ConsoleWidget):
476
477
478    def __init__(self, parent=None, namespace=None):
479        ConsoleWidget.__init__(self, parent, ps1=">>> ", ps2="... ")
480        self.highlighter = QtScriptHighlighter(self)
481        namespace = namespace or {}
482        def console_print(context, engine):
483            for i in range(context.argumentCount()):
484                print context.argument(i).toString(),
485            print
486            return QScriptValue()
487        def dir_context(context, engine):
488            if context.argumentCount() == 0:
489                obj = context.thisObject()
490            else:
491                obj = context.argument(0)
492            l = []
493            it = QScriptValueIterator(obj)
494            while it.hasNext():
495                it.next()
496                l.append(str(it.name()))
497            return QScriptValue(engine, repr(l))
498        namespace["print"] = console_print
499        namespace["dir"] = dir_context
500        namespace["Application"] = qApp
501        try:
502            namespace["Scripter"] = Scripter.qt
503        except: pass
504        self.inter = QtScriptInterpreter(namespace)
505        self.completer = QtScriptCompleter(self.inter.engine)
506
507
508
509    def push(self, line):
510        return self.inter.push(line)
511
512
513
514if __name__ == "__main__":
515    app = QApplication(sys.argv)
516    o = QtScriptConsole()
517    #o = PythonConsole()
518    o.resize(640,480)
519    o.attach()
520    o.show()
521    app.exec_()
522