~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/manhole/ui/gtk2manhole.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.manhole.ui.test.test_gtk2manhole -*-
 
2
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
"""
 
6
Manhole client with a GTK v2.x front-end.
 
7
"""
 
8
 
 
9
__version__ = '$Revision: 1.9 $'[11:-2]
 
10
 
 
11
from twisted import copyright
 
12
from twisted.internet import reactor
 
13
from twisted.python import components, failure, log, util
 
14
from twisted.python.reflect import prefixedMethodNames
 
15
from twisted.spread import pb
 
16
from twisted.spread.ui import gtk2util
 
17
 
 
18
from twisted.manhole.service import IManholeClient
 
19
from zope.interface import implements
 
20
 
 
21
# The pygtk.require for version 2.0 has already been done by the reactor.
 
22
import gtk
 
23
 
 
24
import code, types, inspect
 
25
 
 
26
# TODO:
 
27
#  Make wrap-mode a run-time option.
 
28
#  Explorer.
 
29
#  Code doesn't cleanly handle opening a second connection.  Fix that.
 
30
#  Make some acknowledgement of when a command has completed, even if
 
31
#     it has no return value so it doesn't print anything to the console.
 
32
 
 
33
class OfflineError(Exception):
 
34
    pass
 
35
 
 
36
class ManholeWindow(components.Componentized, gtk2util.GladeKeeper):
 
37
    gladefile = util.sibpath(__file__, "gtk2manhole.glade")
 
38
 
 
39
    _widgets = ('input','output','manholeWindow')
 
40
 
 
41
    def __init__(self):
 
42
        self.defaults = {}
 
43
        gtk2util.GladeKeeper.__init__(self)
 
44
        components.Componentized.__init__(self)
 
45
 
 
46
        self.input = ConsoleInput(self._input)
 
47
        self.input.toplevel = self
 
48
        self.output = ConsoleOutput(self._output)
 
49
 
 
50
        # Ugh.  GladeKeeper actually isn't so good for composite objects.
 
51
        # I want this connected to the ConsoleInput's handler, not something
 
52
        # on this class.
 
53
        self._input.connect("key_press_event", self.input._on_key_press_event)
 
54
 
 
55
    def setDefaults(self, defaults):
 
56
        self.defaults = defaults
 
57
 
 
58
    def login(self):
 
59
        client = self.getComponent(IManholeClient)
 
60
        d = gtk2util.login(client, **self.defaults)
 
61
        d.addCallback(self._cbLogin)
 
62
        d.addCallback(client._cbLogin)
 
63
        d.addErrback(self._ebLogin)
 
64
 
 
65
    def _cbDisconnected(self, perspective):
 
66
        self.output.append("%s went away. :(\n" % (perspective,), "local")
 
67
        self._manholeWindow.set_title("Manhole")
 
68
 
 
69
    def _cbLogin(self, perspective):
 
70
        peer = perspective.broker.transport.getPeer()
 
71
        self.output.append("Connected to %s\n" % (peer,), "local")
 
72
        perspective.notifyOnDisconnect(self._cbDisconnected)
 
73
        self._manholeWindow.set_title("Manhole - %s" % (peer))
 
74
        return perspective
 
75
 
 
76
    def _ebLogin(self, reason):
 
77
        self.output.append("Login FAILED %s\n" % (reason.value,), "exception")
 
78
 
 
79
    def _on_aboutMenuItem_activate(self, widget, *unused):
 
80
        import sys
 
81
        from os import path
 
82
        self.output.append("""\
 
83
a Twisted Manhole client
 
84
  Versions:
 
85
    %(twistedVer)s
 
86
    Python %(pythonVer)s on %(platform)s
 
87
    GTK %(gtkVer)s / PyGTK %(pygtkVer)s
 
88
    %(module)s %(modVer)s
 
89
http://twistedmatrix.com/
 
90
""" % {'twistedVer': copyright.longversion,
 
91
       'pythonVer': sys.version.replace('\n', '\n      '),
 
92
       'platform': sys.platform,
 
93
       'gtkVer': ".".join(map(str, gtk.gtk_version)),
 
94
       'pygtkVer': ".".join(map(str, gtk.pygtk_version)),
 
95
       'module': path.basename(__file__),
 
96
       'modVer': __version__,
 
97
       }, "local")
 
98
 
 
99
    def _on_openMenuItem_activate(self, widget, userdata=None):
 
100
        self.login()
 
101
 
 
102
    def _on_manholeWindow_delete_event(self, widget, *unused):
 
103
        reactor.stop()
 
104
 
 
105
    def _on_quitMenuItem_activate(self, widget, *unused):
 
106
        reactor.stop()
 
107
 
 
108
    def on_reload_self_activate(self, *unused):
 
109
        from twisted.python import rebuild
 
110
        rebuild.rebuild(inspect.getmodule(self.__class__))
 
111
 
 
112
 
 
113
tagdefs = {
 
114
    'default': {"family": "monospace"},
 
115
    # These are message types we get from the server.
 
116
    'stdout': {"foreground": "black"},
 
117
    'stderr': {"foreground": "#AA8000"},
 
118
    'result': {"foreground": "blue"},
 
119
    'exception': {"foreground": "red"},
 
120
    # Messages generate locally.
 
121
    'local': {"foreground": "#008000"},
 
122
    'log': {"foreground": "#000080"},
 
123
    'command': {"foreground": "#666666"},
 
124
    }
 
125
 
 
126
# TODO: Factor Python console stuff back out to pywidgets.
 
127
 
 
128
class ConsoleOutput:
 
129
    _willScroll = None
 
130
    def __init__(self, textView):
 
131
        self.textView = textView
 
132
        self.buffer = textView.get_buffer()
 
133
 
 
134
        # TODO: Make this a singleton tag table.
 
135
        for name, props in tagdefs.iteritems():
 
136
            tag = self.buffer.create_tag(name)
 
137
            # This can be done in the constructor in newer pygtk (post 1.99.14)
 
138
            for k, v in props.iteritems():
 
139
                tag.set_property(k, v)
 
140
 
 
141
        self.buffer.tag_table.lookup("default").set_priority(0)
 
142
 
 
143
        self._captureLocalLog()
 
144
 
 
145
    def _captureLocalLog(self):
 
146
        return log.startLogging(_Notafile(self, "log"), setStdout=False)
 
147
 
 
148
    def append(self, text, kind=None):
 
149
        # XXX: It seems weird to have to do this thing with always applying
 
150
        # a 'default' tag.  Can't we change the fundamental look instead?
 
151
        tags = ["default"]
 
152
        if kind is not None:
 
153
            tags.append(kind)
 
154
 
 
155
        self.buffer.insert_with_tags_by_name(self.buffer.get_end_iter(),
 
156
                                             text, *tags)
 
157
        # Silly things, the TextView needs to update itself before it knows
 
158
        # where the bottom is.
 
159
        if self._willScroll is None:
 
160
            self._willScroll = gtk.idle_add(self._scrollDown)
 
161
 
 
162
    def _scrollDown(self, *unused):
 
163
        self.textView.scroll_to_iter(self.buffer.get_end_iter(), 0,
 
164
                                     True, 1.0, 1.0)
 
165
        self._willScroll = None
 
166
        return False
 
167
 
 
168
class History:
 
169
    def __init__(self, maxhist=10000):
 
170
        self.ringbuffer = ['']
 
171
        self.maxhist = maxhist
 
172
        self.histCursor = 0
 
173
 
 
174
    def append(self, htext):
 
175
        self.ringbuffer.insert(-1, htext)
 
176
        if len(self.ringbuffer) > self.maxhist:
 
177
            self.ringbuffer.pop(0)
 
178
        self.histCursor = len(self.ringbuffer) - 1
 
179
        self.ringbuffer[-1] = ''
 
180
 
 
181
    def move(self, prevnext=1):
 
182
        '''
 
183
        Return next/previous item in the history, stopping at top/bottom.
 
184
        '''
 
185
        hcpn = self.histCursor + prevnext
 
186
        if hcpn >= 0 and hcpn < len(self.ringbuffer):
 
187
            self.histCursor = hcpn
 
188
            return self.ringbuffer[hcpn]
 
189
        else:
 
190
            return None
 
191
 
 
192
    def histup(self, textbuffer):
 
193
        if self.histCursor == len(self.ringbuffer) - 1:
 
194
            si, ei = textbuffer.get_start_iter(), textbuffer.get_end_iter()
 
195
            self.ringbuffer[-1] = textbuffer.get_text(si,ei)
 
196
        newtext = self.move(-1)
 
197
        if newtext is None:
 
198
            return
 
199
        textbuffer.set_text(newtext)
 
200
 
 
201
    def histdown(self, textbuffer):
 
202
        newtext = self.move(1)
 
203
        if newtext is None:
 
204
            return
 
205
        textbuffer.set_text(newtext)
 
206
 
 
207
 
 
208
class ConsoleInput:
 
209
    toplevel, rkeymap = None, None
 
210
    __debug = False
 
211
 
 
212
    def __init__(self, textView):
 
213
        self.textView=textView
 
214
        self.rkeymap = {}
 
215
        self.history = History()
 
216
        for name in prefixedMethodNames(self.__class__, "key_"):
 
217
            keysymName = name.split("_")[-1]
 
218
            self.rkeymap[getattr(gtk.keysyms, keysymName)] = keysymName
 
219
 
 
220
    def _on_key_press_event(self, entry, event):
 
221
        stopSignal = False
 
222
        ksym = self.rkeymap.get(event.keyval, None)
 
223
 
 
224
        mods = []
 
225
        for prefix, mask in [('ctrl', gtk.gdk.CONTROL_MASK), ('shift', gtk.gdk.SHIFT_MASK)]:
 
226
            if event.state & mask:
 
227
                mods.append(prefix)
 
228
 
 
229
        if mods:
 
230
            ksym = '_'.join(mods + [ksym])
 
231
 
 
232
        if ksym:
 
233
            rvalue = getattr(
 
234
                self, 'key_%s' % ksym, lambda *a, **kw: None)(entry, event)
 
235
 
 
236
        if self.__debug:
 
237
            print ksym
 
238
        return rvalue
 
239
 
 
240
    def getText(self):
 
241
        buffer = self.textView.get_buffer()
 
242
        iter1, iter2 = buffer.get_bounds()
 
243
        text = buffer.get_text(iter1, iter2, False)
 
244
        return text
 
245
 
 
246
    def setText(self, text):
 
247
        self.textView.get_buffer().set_text(text)
 
248
 
 
249
    def key_Return(self, entry, event):
 
250
        text = self.getText()
 
251
        # Figure out if that Return meant "next line" or "execute."
 
252
        try:
 
253
            c = code.compile_command(text)
 
254
        except SyntaxError, e:
 
255
            # This could conceivably piss you off if the client's python
 
256
            # doesn't accept keywords that are known to the manhole's
 
257
            # python.
 
258
            point = buffer.get_iter_at_line_offset(e.lineno, e.offset)
 
259
            buffer.place(point)
 
260
            # TODO: Componentize!
 
261
            self.toplevel.output.append(str(e), "exception")
 
262
        except (OverflowError, ValueError), e:
 
263
            self.toplevel.output.append(str(e), "exception")
 
264
        else:
 
265
            if c is not None:
 
266
                self.sendMessage()
 
267
                # Don't insert Return as a newline in the buffer.
 
268
                self.history.append(text)
 
269
                self.clear()
 
270
                # entry.emit_stop_by_name("key_press_event")
 
271
                return True
 
272
            else:
 
273
                # not a complete code block
 
274
                return False
 
275
 
 
276
        return False
 
277
 
 
278
    def key_Up(self, entry, event):
 
279
        # if I'm at the top, previous history item.
 
280
        textbuffer = self.textView.get_buffer()
 
281
        if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == 0:
 
282
            self.history.histup(textbuffer)
 
283
            return True
 
284
        return False
 
285
 
 
286
    def key_Down(self, entry, event):
 
287
        textbuffer = self.textView.get_buffer()
 
288
        if textbuffer.get_iter_at_mark(textbuffer.get_insert()).get_line() == (
 
289
            textbuffer.get_line_count() - 1):
 
290
            self.history.histdown(textbuffer)
 
291
            return True
 
292
        return False
 
293
 
 
294
    key_ctrl_p = key_Up
 
295
    key_ctrl_n = key_Down
 
296
 
 
297
    def key_ctrl_shift_F9(self, entry, event):
 
298
        if self.__debug:
 
299
            import pdb; pdb.set_trace()
 
300
 
 
301
    def clear(self):
 
302
        buffer = self.textView.get_buffer()
 
303
        buffer.delete(*buffer.get_bounds())
 
304
 
 
305
    def sendMessage(self):
 
306
        buffer = self.textView.get_buffer()
 
307
        iter1, iter2 = buffer.get_bounds()
 
308
        text = buffer.get_text(iter1, iter2, False)
 
309
        self.toplevel.output.append(pythonify(text), 'command')
 
310
        # TODO: Componentize better!
 
311
        try:
 
312
            return self.toplevel.getComponent(IManholeClient).do(text)
 
313
        except OfflineError:
 
314
            self.toplevel.output.append("Not connected, command not sent.\n",
 
315
                                        "exception")
 
316
 
 
317
 
 
318
def pythonify(text):
 
319
    '''
 
320
    Make some text appear as though it was typed in at a Python prompt.
 
321
    '''
 
322
    lines = text.split('\n')
 
323
    lines[0] = '>>> ' + lines[0]
 
324
    return '\n... '.join(lines) + '\n'
 
325
 
 
326
class _Notafile:
 
327
    """Curry to make failure.printTraceback work with the output widget."""
 
328
    def __init__(self, output, kind):
 
329
        self.output = output
 
330
        self.kind = kind
 
331
 
 
332
    def write(self, txt):
 
333
        self.output.append(txt, self.kind)
 
334
 
 
335
    def flush(self):
 
336
        pass
 
337
 
 
338
class ManholeClient(components.Adapter, pb.Referenceable):
 
339
    implements(IManholeClient)
 
340
 
 
341
    capabilities = {
 
342
#        "Explorer": 'Set',
 
343
        "Failure": 'Set'
 
344
        }
 
345
 
 
346
    def _cbLogin(self, perspective):
 
347
        self.perspective = perspective
 
348
        perspective.notifyOnDisconnect(self._cbDisconnected)
 
349
        return perspective
 
350
 
 
351
    def remote_console(self, messages):
 
352
        for kind, content in messages:
 
353
            if isinstance(content, types.StringTypes):
 
354
                self.original.output.append(content, kind)
 
355
            elif (kind == "exception") and isinstance(content, failure.Failure):
 
356
                content.printTraceback(_Notafile(self.original.output,
 
357
                                                 "exception"))
 
358
            else:
 
359
                self.original.output.append(str(content), kind)
 
360
 
 
361
    def remote_receiveExplorer(self, xplorer):
 
362
        pass
 
363
 
 
364
    def remote_listCapabilities(self):
 
365
        return self.capabilities
 
366
 
 
367
    def _cbDisconnected(self, perspective):
 
368
        self.perspective = None
 
369
 
 
370
    def do(self, text):
 
371
        if self.perspective is None:
 
372
            raise OfflineError
 
373
        return self.perspective.callRemote("do", text)
 
374
 
 
375
components.registerAdapter(ManholeClient, ManholeWindow, IManholeClient)