~ubuntu-branches/ubuntu/wily/sflphone/wily

« back to all changes in this revision

Viewing changes to daemon/libs/pjproject-2.2.1/pjsip-apps/src/pygui/chat.py

  • Committer: Package Import Robot
  • Author(s): Jonathan Riddell
  • Date: 2015-01-07 14:51:16 UTC
  • mfrom: (4.3.5 sid)
  • Revision ID: package-import@ubuntu.com-20150107145116-yxnafinf4lrdvrmx
Tags: 1.4.1-0.1ubuntu1
* Merge with Debian, remaining changes:
 - Drop soprano, nepomuk build-dep
* Drop ubuntu patches, now upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# $Id: chat.py 4757 2014-02-21 07:53:31Z nanang $
 
2
#
 
3
# pjsua Python GUI Demo
 
4
#
 
5
# Copyright (C)2013 Teluu Inc. (http://www.teluu.com)
 
6
#
 
7
# This program is free software; you can redistribute it and/or modify
 
8
# it under the terms of the GNU General Public License as published by
 
9
# the Free Software Foundation; either version 2 of the License, or
 
10
# (at your option) any later version.
 
11
#
 
12
# This program is distributed in the hope that it will be useful,
 
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
15
# GNU General Public License for more details.
 
16
#
 
17
# You should have received a copy of the GNU General Public License
 
18
# along with this program; if not, write to the Free Software
 
19
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 
20
#
 
21
import sys
 
22
if sys.version_info[0] >= 3: # Python 3
 
23
        import tkinter as tk
 
24
        from tkinter import ttk
 
25
else:
 
26
        import Tkinter as tk
 
27
        import ttk
 
28
 
 
29
import buddy
 
30
import call
 
31
import chatgui as gui
 
32
import endpoint as ep
 
33
import pjsua2 as pj
 
34
import re
 
35
 
 
36
SipUriRegex = re.compile('(sip|sips):([^:;>\@]*)@?([^:;>]*):?([^:;>]*)')
 
37
ConfIdx = 1
 
38
 
 
39
# Simple SIP uri parser, input URI must have been validated
 
40
def ParseSipUri(sip_uri_str):
 
41
        m = SipUriRegex.search(sip_uri_str)
 
42
        if not m:
 
43
                assert(0)
 
44
                return None
 
45
        
 
46
        scheme = m.group(1)
 
47
        user = m.group(2)
 
48
        host = m.group(3)
 
49
        port = m.group(4)
 
50
        if host == '':
 
51
                host = user
 
52
                user = ''
 
53
                
 
54
        return SipUri(scheme.lower(), user, host.lower(), port)
 
55
        
 
56
class SipUri:
 
57
        def __init__(self, scheme, user, host, port):
 
58
                self.scheme = scheme
 
59
                self.user = user
 
60
                self.host = host
 
61
                self.port = port
 
62
                
 
63
        def __cmp__(self, sip_uri):
 
64
                if self.scheme == sip_uri.scheme and self.user == sip_uri.user and self.host == sip_uri.host:
 
65
                        # don't check port, at least for now
 
66
                        return 0
 
67
                return -1
 
68
        
 
69
        def __str__(self):
 
70
                s = self.scheme + ':'
 
71
                if self.user: s += self.user + '@'
 
72
                s += self.host
 
73
                if self.port: s+= ':' + self.port
 
74
                return s
 
75
        
 
76
class Chat(gui.ChatObserver):
 
77
        def __init__(self, app, acc, uri, call_inst=None):
 
78
                self._app = app
 
79
                self._acc = acc
 
80
                self.title = ''
 
81
                
 
82
                global ConfIdx
 
83
                self.confIdx = ConfIdx
 
84
                ConfIdx += 1
 
85
                
 
86
                # each participant call/buddy instances are stored in call list
 
87
                # and buddy list with same index as in particpant list
 
88
                self._participantList = []      # list of SipUri
 
89
                self._callList = []             # list of Call
 
90
                self._buddyList = []            # list of Buddy
 
91
                
 
92
                self._gui = gui.ChatFrame(self)
 
93
                self.addParticipant(uri, call_inst)
 
94
        
 
95
        def _updateGui(self):
 
96
                if self.isPrivate():
 
97
                        self.title = str(self._participantList[0])
 
98
                else:
 
99
                        self.title = 'Conference #%d (%d participants)' % (self.confIdx, len(self._participantList))
 
100
                self._gui.title(self.title)
 
101
                self._app.updateWindowMenu()
 
102
                
 
103
        def _getCallFromUriStr(self, uri_str, op = ''):
 
104
                uri = ParseSipUri(uri_str)
 
105
                if uri not in self._participantList:
 
106
                        print "=== %s cannot find participant with URI '%s'" % (op, uri_str)
 
107
                        return None
 
108
                idx = self._participantList.index(uri)
 
109
                if idx < len(self._callList):
 
110
                        return self._callList[idx]
 
111
                return None
 
112
        
 
113
        def _getActiveMediaIdx(self, thecall):
 
114
                ci = thecall.getInfo()
 
115
                for mi in ci.media:
 
116
                        if mi.type == pj.PJMEDIA_TYPE_AUDIO and \
 
117
                          (mi.status != pj.PJSUA_CALL_MEDIA_NONE and \
 
118
                           mi.status != pj.PJSUA_CALL_MEDIA_ERROR):
 
119
                                return mi.index
 
120
                return -1
 
121
                
 
122
        def _getAudioMediaFromUriStr(self, uri_str):
 
123
                c = self._getCallFromUriStr(uri_str)
 
124
                if not c: return None
 
125
 
 
126
                idx = self._getActiveMediaIdx(c)
 
127
                if idx < 0: return None
 
128
 
 
129
                m = c.getMedia(idx)
 
130
                am = pj.AudioMedia.typecastFromMedia(m)
 
131
                return am
 
132
                
 
133
        def _sendTypingIndication(self, is_typing, sender_uri_str=''):
 
134
                sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None
 
135
                type_ind_param = pj.SendTypingIndicationParam()
 
136
                type_ind_param.isTyping = is_typing
 
137
                for idx, p in enumerate(self._participantList):
 
138
                        # don't echo back to the original sender
 
139
                        if sender_uri and p == sender_uri:
 
140
                                continue
 
141
                                
 
142
                        # send via call, if any, or buddy
 
143
                        target = None
 
144
                        if self._callList[idx] and self._callList[idx].connected:
 
145
                                target = self._callList[idx]
 
146
                        else:
 
147
                                target = self._buddyList[idx]
 
148
                        assert(target)
 
149
                                
 
150
                        try:
 
151
                                target.sendTypingIndication(type_ind_param)
 
152
                        except:
 
153
                                pass
 
154
 
 
155
        def _sendInstantMessage(self, msg, sender_uri_str=''):
 
156
                sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None
 
157
                send_im_param = pj.SendInstantMessageParam()
 
158
                send_im_param.content = str(msg)
 
159
                for idx, p in enumerate(self._participantList):
 
160
                        # don't echo back to the original sender
 
161
                        if sender_uri and p == sender_uri:
 
162
                                continue
 
163
                                
 
164
                        # send via call, if any, or buddy
 
165
                        target = None
 
166
                        if self._callList[idx] and self._callList[idx].connected:
 
167
                                target = self._callList[idx]
 
168
                        else:
 
169
                                target = self._buddyList[idx]
 
170
                        assert(target)
 
171
                        
 
172
                        try:
 
173
                                target.sendInstantMessage(send_im_param)
 
174
                        except:
 
175
                                # error will be handled via Account::onInstantMessageStatus()
 
176
                                pass
 
177
 
 
178
        def isPrivate(self):
 
179
                return len(self._participantList) <= 1
 
180
                
 
181
        def isUriParticipant(self, uri):
 
182
                return uri in self._participantList
 
183
                
 
184
        def registerCall(self, uri_str, call_inst):
 
185
                uri = ParseSipUri(uri_str)
 
186
                try:
 
187
                        idx = self._participantList.index(uri)
 
188
                        bud = self._buddyList[idx]
 
189
                        self._callList[idx] = call_inst
 
190
                        call_inst.chat = self
 
191
                        call_inst.peerUri = bud.cfg.uri
 
192
                except:
 
193
                        assert(0) # idx must be found!
 
194
                
 
195
        def showWindow(self, show_text_chat = False):
 
196
                self._gui.bringToFront()
 
197
                if show_text_chat:
 
198
                        self._gui.textShowHide(True)
 
199
                
 
200
        def addParticipant(self, uri, call_inst=None):
 
201
                # avoid duplication
 
202
                if self.isUriParticipant(uri): return
 
203
                
 
204
                uri_str = str(uri)
 
205
                
 
206
                # find buddy, create one if not found (e.g: for IM/typing ind),
 
207
                # it is a temporary one and not really registered to acc
 
208
                bud = None
 
209
                try:
 
210
                        bud = self._acc.findBuddy(uri_str)
 
211
                except:
 
212
                        bud = buddy.Buddy(None)
 
213
                        bud_cfg = pj.BuddyConfig()
 
214
                        bud_cfg.uri = uri_str
 
215
                        bud_cfg.subscribe = False
 
216
                        bud.create(self._acc, bud_cfg)
 
217
                        bud.cfg = bud_cfg
 
218
                        bud.account = self._acc
 
219
                        
 
220
                # update URI from buddy URI
 
221
                uri = ParseSipUri(bud.cfg.uri)
 
222
                
 
223
                # add it
 
224
                self._participantList.append(uri)
 
225
                self._callList.append(call_inst)
 
226
                self._buddyList.append(bud)
 
227
                self._gui.addParticipant(str(uri))
 
228
                self._updateGui()
 
229
        
 
230
        def kickParticipant(self, uri):
 
231
                if (not uri) or (uri not in self._participantList):
 
232
                        assert(0)
 
233
                        return
 
234
                
 
235
                idx = self._participantList.index(uri)
 
236
                del self._participantList[idx]
 
237
                del self._callList[idx]
 
238
                del self._buddyList[idx]
 
239
                self._gui.delParticipant(str(uri))
 
240
                
 
241
                if self._participantList:
 
242
                        self._updateGui()
 
243
                else:
 
244
                        self.onCloseWindow()
 
245
                        
 
246
        def addMessage(self, from_uri_str, msg):
 
247
                if from_uri_str:
 
248
                        # print message on GUI
 
249
                        msg = from_uri_str + ': ' + msg
 
250
                        self._gui.textAddMessage(msg)
 
251
                        # now relay to all participants
 
252
                        self._sendInstantMessage(msg, from_uri_str)
 
253
                else:
 
254
                        self._gui.textAddMessage(msg, False)
 
255
                        
 
256
        def setTypingIndication(self, from_uri_str, is_typing):
 
257
                # notify GUI
 
258
                self._gui.textSetTypingIndication(from_uri_str, is_typing)
 
259
                # now relay to all participants
 
260
                self._sendTypingIndication(is_typing, from_uri_str)
 
261
                
 
262
        def startCall(self):
 
263
                self._gui.enableAudio()
 
264
                call_param = pj.CallOpParam()
 
265
                call_param.opt.audioCount = 1
 
266
                call_param.opt.videoCount = 0
 
267
                fails = []
 
268
                for idx, p in enumerate(self._participantList):
 
269
                        # just skip if call is instantiated
 
270
                        if self._callList[idx]:
 
271
                                continue
 
272
                        
 
273
                        uri_str = str(p)
 
274
                        c = call.Call(self._acc, uri_str, self)
 
275
                        self._callList[idx] = c
 
276
                        self._gui.audioUpdateState(uri_str, gui.AudioState.INITIALIZING)
 
277
                        
 
278
                        try:
 
279
                                c.makeCall(uri_str, call_param)
 
280
                        except:
 
281
                                self._callList[idx] = None
 
282
                                self._gui.audioUpdateState(uri_str, gui.AudioState.FAILED)
 
283
                                fails.append(p)
 
284
                                
 
285
                for p in fails:
 
286
                        # kick participants with call failure, but spare the last (avoid zombie chat)
 
287
                        if not self.isPrivate():
 
288
                                self.kickParticipant(p)
 
289
                        
 
290
        def stopCall(self):
 
291
                for idx, p in enumerate(self._participantList):
 
292
                        self._gui.audioUpdateState(str(p), gui.AudioState.DISCONNECTED)
 
293
                        c = self._callList[idx]
 
294
                        if c:
 
295
                                c.hangup(pj.CallOpParam())
 
296
 
 
297
        def updateCallState(self, thecall, info = None):
 
298
                # info is optional here, just to avoid calling getInfo() twice (in the caller and here)
 
299
                if not info: info = thecall.getInfo()
 
300
                
 
301
                if info.state < pj.PJSIP_INV_STATE_CONFIRMED:
 
302
                        self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.INITIALIZING)
 
303
                elif info.state == pj.PJSIP_INV_STATE_CONFIRMED:
 
304
                        self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.CONNECTED)
 
305
                        if not self.isPrivate():
 
306
                                # inform peer about conference participants
 
307
                                conf_welcome_str  = '\n---\n'
 
308
                                conf_welcome_str += 'Welcome to the conference, participants:\n'
 
309
                                conf_welcome_str += '%s (host)\n' % (self._acc.cfg.idUri)
 
310
                                for p in self._participantList:
 
311
                                        conf_welcome_str += '%s\n' % (str(p))
 
312
                                conf_welcome_str += '---\n'
 
313
                                send_im_param = pj.SendInstantMessageParam()
 
314
                                send_im_param.content = conf_welcome_str
 
315
                                try:
 
316
                                        thecall.sendInstantMessage(send_im_param)
 
317
                                except:
 
318
                                        pass
 
319
                                        
 
320
                                # inform others, including self
 
321
                                msg = "[Conf manager] %s has joined" % (thecall.peerUri)
 
322
                                self.addMessage(None, msg)
 
323
                                self._sendInstantMessage(msg, thecall.peerUri)
 
324
                                
 
325
                elif info.state == pj.PJSIP_INV_STATE_DISCONNECTED:
 
326
                        if info.lastStatusCode/100 != 2:
 
327
                                self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.FAILED)
 
328
                        else:
 
329
                                self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED)
 
330
                        
 
331
                        # reset entry in the callList
 
332
                        try:
 
333
                                idx = self._callList.index(thecall)
 
334
                                if idx >= 0: self._callList[idx] = None
 
335
                        except:
 
336
                                pass
 
337
                        
 
338
                        self.addMessage(None, "Call to '%s' disconnected: %s" % (thecall.peerUri, info.lastReason))
 
339
                        
 
340
                        # kick the disconnected participant, but the last (avoid zombie chat)
 
341
                        if not self.isPrivate():
 
342
                                self.kickParticipant(ParseSipUri(thecall.peerUri))
 
343
                                
 
344
                                # inform others, including self
 
345
                                msg = "[Conf manager] %s has left" % (thecall.peerUri)
 
346
                                self.addMessage(None, msg)
 
347
                                self._sendInstantMessage(msg, thecall.peerUri)
 
348
 
 
349
        def updateCallMediaState(self, thecall, info = None):
 
350
                # info is optional here, just to avoid calling getInfo() twice (in the caller and here)
 
351
                if not info: info = thecall.getInfo()
 
352
                
 
353
                med_idx = self._getActiveMediaIdx(thecall)
 
354
                if (med_idx < 0):
 
355
                        self._gui.audioSetStatsText(thecall.peerUri, 'No active media')
 
356
                        return
 
357
 
 
358
                si = thecall.getStreamInfo(med_idx)
 
359
                dir_str = ''
 
360
                if si.dir == 0:
 
361
                        dir_str = 'inactive'
 
362
                else:
 
363
                        if si.dir & pj.PJMEDIA_DIR_ENCODING:
 
364
                                dir_str += 'send '
 
365
                        if si.dir & pj.PJMEDIA_DIR_DECODING:
 
366
                                dir_str += 'receive '
 
367
                stats_str  = "Direction   : %s\n" % (dir_str)
 
368
                stats_str += "Audio codec : %s (%sHz)" % (si.codecName, si.codecClockRate)
 
369
                self._gui.audioSetStatsText(thecall.peerUri, stats_str)
 
370
                m = pj.AudioMedia.typecastFromMedia(thecall.getMedia(med_idx))
 
371
                
 
372
                # make conference
 
373
                for c in self._callList:
 
374
                        if c == thecall:
 
375
                                continue
 
376
                        med_idx = self._getActiveMediaIdx(c)
 
377
                        if med_idx < 0:
 
378
                                continue
 
379
                        mm = pj.AudioMedia.typecastFromMedia(c.getMedia(med_idx))
 
380
                        m.startTransmit(mm)
 
381
                        mm.startTransmit(m)
 
382
 
 
383
                        
 
384
        # ** callbacks from GUI (ChatObserver implementation) **
 
385
        
 
386
        # Text
 
387
        def onSendMessage(self, msg):
 
388
                self._sendInstantMessage(msg)
 
389
 
 
390
        def onStartTyping(self):
 
391
                self._sendTypingIndication(True)
 
392
                
 
393
        def onStopTyping(self):
 
394
                self._sendTypingIndication(False)
 
395
                
 
396
        # Audio
 
397
        def onHangup(self, peer_uri_str):
 
398
                c = self._getCallFromUriStr(peer_uri_str, "onHangup()")
 
399
                if not c: return
 
400
                call_param = pj.CallOpParam()
 
401
                c.hangup(call_param)
 
402
 
 
403
        def onHold(self, peer_uri_str):
 
404
                c = self._getCallFromUriStr(peer_uri_str, "onHold()")
 
405
                if not c: return
 
406
                call_param = pj.CallOpParam()
 
407
                c.setHold(call_param)
 
408
 
 
409
        def onUnhold(self, peer_uri_str):
 
410
                c = self._getCallFromUriStr(peer_uri_str, "onUnhold()")
 
411
                if not c: return
 
412
                
 
413
                call_param = pj.CallOpParam()
 
414
                call_param.opt.audioCount = 1
 
415
                call_param.opt.videoCount = 0
 
416
                call_param.opt.flag |= pj.PJSUA_CALL_UNHOLD
 
417
                c.reinvite(call_param)
 
418
                
 
419
        def onRxMute(self, peer_uri_str, mute):
 
420
                am = self._getAudioMediaFromUriStr(peer_uri_str)
 
421
                if not am: return
 
422
                if mute:
 
423
                        am.stopTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia())
 
424
                        self.addMessage(None, "Muted audio from '%s'" % (peer_uri_str))
 
425
                else:
 
426
                        am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia())
 
427
                        self.addMessage(None, "Unmuted audio from '%s'" % (peer_uri_str))
 
428
                
 
429
        def onRxVol(self, peer_uri_str, vol_pct):
 
430
                am = self._getAudioMediaFromUriStr(peer_uri_str)
 
431
                if not am: return
 
432
                # pjsua volume range = 0:mute, 1:no adjustment, 2:100% louder
 
433
                am.adjustRxLevel(vol_pct/50.0)
 
434
                self.addMessage(None, "Adjusted volume level audio from '%s'" % (peer_uri_str))
 
435
                        
 
436
        def onTxMute(self, peer_uri_str, mute):
 
437
                am = self._getAudioMediaFromUriStr(peer_uri_str)
 
438
                if not am: return
 
439
                if mute:
 
440
                        ep.Endpoint.instance.audDevManager().getCaptureDevMedia().stopTransmit(am)
 
441
                        self.addMessage(None, "Muted audio to '%s'" % (peer_uri_str))
 
442
                else:
 
443
                        ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am)
 
444
                        self.addMessage(None, "Unmuted audio to '%s'" % (peer_uri_str))
 
445
 
 
446
        # Chat room
 
447
        def onAddParticipant(self):
 
448
                buds = []
 
449
                dlg = AddParticipantDlg(None, self._app, buds)
 
450
                if dlg.doModal():
 
451
                        for bud in buds:
 
452
                                uri = ParseSipUri(bud.cfg.uri)
 
453
                                self.addParticipant(uri)
 
454
                        if not self.isPrivate():
 
455
                                self.startCall()
 
456
                                
 
457
        def onStartAudio(self):
 
458
                self.startCall()
 
459
 
 
460
        def onStopAudio(self):
 
461
                self.stopCall()
 
462
                
 
463
        def onCloseWindow(self):
 
464
                self.stopCall()
 
465
                # will remove entry from list eventually destroy this chat?
 
466
                if self in self._acc.chatList: self._acc.chatList.remove(self)
 
467
                self._app.updateWindowMenu()
 
468
                # destroy GUI
 
469
                self._gui.destroy()
 
470
 
 
471
 
 
472
class AddParticipantDlg(tk.Toplevel):
 
473
        """
 
474
        List of buddies
 
475
        """
 
476
        def __init__(self, parent, app, bud_list):
 
477
                tk.Toplevel.__init__(self, parent)
 
478
                self.title('Add participants..')
 
479
                self.transient(parent)
 
480
                self.parent = parent
 
481
                self._app = app
 
482
                self.buddyList = bud_list
 
483
                
 
484
                self.isOk = False
 
485
                
 
486
                self.createWidgets()
 
487
        
 
488
        def doModal(self):
 
489
                if self.parent:
 
490
                        self.parent.wait_window(self)
 
491
                else:
 
492
                        self.wait_window(self)
 
493
                return self.isOk
 
494
                
 
495
        def createWidgets(self):
 
496
                # buddy list
 
497
                list_frame = ttk.Frame(self)
 
498
                list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1, padx=20, pady=20)
 
499
                #scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=list_frame.yview)
 
500
                #list_frame.config(yscrollcommand=scrl.set)
 
501
                #scrl.pack(side=tk.RIGHT, fill=tk.Y)
 
502
                
 
503
                # draw buddy list
 
504
                self.buddies = []
 
505
                for acc in self._app.accList:
 
506
                        self.buddies.append((0, acc.cfg.idUri))
 
507
                        for bud in acc.buddyList:
 
508
                                self.buddies.append((1, bud))
 
509
                
 
510
                self.bud_var = []
 
511
                for idx,(flag,bud) in enumerate(self.buddies):
 
512
                        self.bud_var.append(tk.IntVar())
 
513
                        if flag==0:
 
514
                                s = ttk.Separator(list_frame, orient=tk.HORIZONTAL)
 
515
                                s.pack(fill=tk.X)
 
516
                                l = tk.Label(list_frame, anchor=tk.W, text="Account '%s':" % (bud))
 
517
                                l.pack(fill=tk.X)
 
518
                        else:
 
519
                                c = tk.Checkbutton(list_frame, anchor=tk.W, text=bud.cfg.uri, variable=self.bud_var[idx])
 
520
                                c.pack(fill=tk.X)
 
521
                s = ttk.Separator(list_frame, orient=tk.HORIZONTAL)
 
522
                s.pack(fill=tk.X)
 
523
 
 
524
                # Ok/cancel buttons
 
525
                tail_frame = ttk.Frame(self)
 
526
                tail_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
 
527
                
 
528
                btnOk = ttk.Button(tail_frame, text='Ok', default=tk.ACTIVE, command=self.onOk)
 
529
                btnOk.pack(side=tk.LEFT, padx=20, pady=10)
 
530
                btnCancel = ttk.Button(tail_frame, text='Cancel', command=self.onCancel)
 
531
                btnCancel.pack(side=tk.RIGHT, padx=20, pady=10)
 
532
                
 
533
        def onOk(self):
 
534
                self.buddyList[:] = []
 
535
                for idx,(flag,bud) in enumerate(self.buddies):
 
536
                        if not flag: continue
 
537
                        if self.bud_var[idx].get() and not (bud in self.buddyList):
 
538
                                self.buddyList.append(bud)
 
539
                        
 
540
                self.isOk = True
 
541
                self.destroy()
 
542
                
 
543
        def onCancel(self):
 
544
                self.destroy()