blob: c39e682aeaccc0a7c62676d5de04a32c6b73d19e [file] [log] [blame]
Alexandre Lision0e143012014-01-22 11:02:46 -05001# $Id: chatgui.py 4704 2014-01-16 05:30:46Z ming $
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#
21import sys
22if sys.version_info[0] >= 3: # Python 3
23 import tkinter as tk
24 from tkinter import ttk
25 from tkinter import messagebox as msgbox
26else:
27 import Tkinter as tk
28 import ttk
29 import tkMessageBox as msgbox
30
31
32class TextObserver:
33 def onSendMessage(self, msg):
34 pass
35 def onStartTyping(self):
36 pass
37 def onStopTyping(self):
38 pass
39
40class TextFrame(ttk.Frame):
41 def __init__(self, master, observer):
42 ttk.Frame.__init__(self, master)
43 self._observer = observer
44 self._isTyping = False
45 self._createWidgets()
46
47 def _onSendMessage(self, event):
48 send_text = self._typingBox.get("1.0", tk.END).strip()
49 if send_text == '':
50 return
51
52 self.addMessage('me: ' + send_text)
53 self._typingBox.delete("0.0", tk.END)
54 self._onTyping(None)
55
56 # notify app for sending message
57 self._observer.onSendMessage(send_text)
58
59 def _onTyping(self, event):
60 # notify app for typing indication
61 is_typing = self._typingBox.get("1.0", tk.END).strip() != ''
62 if is_typing != self._isTyping:
63 self._isTyping = is_typing
64 if is_typing:
65 self._observer.onStartTyping()
66 else:
67 self._observer.onStopTyping()
68
69 def _createWidgets(self):
70 self.rowconfigure(0, weight=1)
71 self.rowconfigure(1, weight=0)
72 self.rowconfigure(2, weight=0)
73 self.columnconfigure(0, weight=1)
74 self.columnconfigure(1, weight=0)
75
76 self._text = tk.Text(self, width=50, height=30, font=("Arial", "10"))
77 self._text.grid(row=0, column=0, sticky='nswe')
78 self._text.config(state=tk.DISABLED)
79 self._text.tag_config("info", foreground="darkgray", font=("Arial", "9", "italic"))
80
81 scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self._text.yview)
82 self._text.config(yscrollcommand=scrl.set)
83 scrl.grid(row=0, column=1, sticky='nsw')
84
85 self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10"))
86 self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0)
87
88 self._statusBar = tk.Label(self, anchor='w', font=("Arial", "8", "italic"))
89 self._statusBar.grid(row=2, columnspan=2, sticky='we')
90
91 self._typingBox.bind('<Return>', self._onSendMessage)
92 self._typingBox.bind("<Key>", self._onTyping)
93 self._typingBox.focus_set()
94
95 def addMessage(self, msg, is_chat = True):
96 self._text.config(state=tk.NORMAL)
97 if is_chat:
98 self._text.insert(tk.END, msg+'\r\n')
99 else:
100 self._text.insert(tk.END, msg+'\r\n', 'info')
101 self._text.config(state=tk.DISABLED)
102 self._text.yview(tk.END)
103
104 def setTypingIndication(self, who, is_typing):
105 if is_typing:
106 self._statusBar['text'] = "'%s' is typing.." % (who)
107 else:
108 self._statusBar['text'] = ''
109
110class AudioState:
111 NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5)
112
113class AudioObserver:
114 def onHangup(self, peer_uri):
115 pass
116 def onHold(self, peer_uri):
117 pass
118 def onUnhold(self, peer_uri):
119 pass
120 def onRxMute(self, peer_uri, is_muted):
121 pass
122 def onRxVol(self, peer_uri, vol_pct):
123 pass
124 def onTxMute(self, peer_uri, is_muted):
125 pass
126
127
128class AudioFrame(ttk.Labelframe):
129 def __init__(self, master, peer_uri, observer):
130 ttk.Labelframe.__init__(self, master, text=peer_uri)
131 self.peerUri = peer_uri
132 self._observer = observer
133 self._initFrame = None
134 self._callFrame = None
135 self._rxMute = False
136 self._txMute = False
137 self._state = AudioState.NULL
138
139 self._createInitWidgets()
140 self._createWidgets()
141
142 def updateState(self, state):
143 if self._state == state:
144 return
145
146 if state == AudioState.INITIALIZING:
147 self._callFrame.pack_forget()
148 self._initFrame.pack(fill=tk.BOTH)
149 self._btnCancel.pack(side=tk.TOP)
150 self._lblInitState['text'] = 'Intializing..'
151
152 elif state == AudioState.CONNECTED:
153 self._initFrame.pack_forget()
154 self._callFrame.pack(fill=tk.BOTH)
155 else:
156 self._callFrame.pack_forget()
157 self._initFrame.pack(fill=tk.BOTH)
158 if state == AudioState.FAILED:
159 self._lblInitState['text'] = 'Failed'
160 else:
161 self._lblInitState['text'] = 'Normal cleared'
162 self._btnCancel.pack_forget()
163
164 self._btnHold['text'] = 'Hold'
165 self._btnHold.config(state=tk.NORMAL)
166 self._rxMute = False
167 self._txMute = False
168 self.btnRxMute['text'] = 'Mute'
169 self.btnTxMute['text'] = 'Mute'
170 self.rxVol.set(5.0)
171
172 # save last state
173 self._state = state
174
175 def setStatsText(self, stats_str):
176 self.stat.config(state=tk.NORMAL)
177 self.stat.delete("0.0", tk.END)
178 self.stat.insert(tk.END, stats_str)
179 self.stat.config(state=tk.DISABLED)
180
181 def _onHold(self):
182 self._btnHold.config(state=tk.DISABLED)
183 # notify app
184 if self._btnHold['text'] == 'Hold':
185 self._observer.onHold(self.peerUri)
186 self._btnHold['text'] = 'Unhold'
187 else:
188 self._observer.onUnhold(self.peerUri)
189 self._btnHold['text'] = 'Hold'
190 self._btnHold.config(state=tk.NORMAL)
191
192 def _onHangup(self):
193 # notify app
194 self._observer.onHangup(self.peerUri)
195
196 def _onRxMute(self):
197 # notify app
198 self._rxMute = not self._rxMute
199 self._observer.onRxMute(self.peerUri, self._rxMute)
200 self.btnRxMute['text'] = 'Unmute' if self._rxMute else 'Mute'
201
202 def _onRxVol(self, event):
203 # notify app
204 vol = self.rxVol.get()
205 self._observer.onRxVol(self.peerUri, vol*10.0)
206
207 def _onTxMute(self):
208 # notify app
209 self._txMute = not self._txMute
210 self._observer.onTxMute(self.peerUri, self._txMute)
211 self.btnTxMute['text'] = 'Unmute' if self._txMute else 'Mute'
212
213 def _createInitWidgets(self):
214 self._initFrame = ttk.Frame(self)
215 #self._initFrame.pack(fill=tk.BOTH)
216
217
218 self._lblInitState = tk.Label(self._initFrame, font=("Arial", "12"), text='')
219 self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1)
220
221 # Operation: cancel/kick
222 self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup)
223 self._btnCancel.pack(side=tk.TOP)
224
225 def _createWidgets(self):
226 self._callFrame = ttk.Frame(self)
227 #self._callFrame.pack(fill=tk.BOTH)
228
229 # toolbar
230 toolbar = ttk.Frame(self._callFrame)
231 toolbar.pack(side=tk.TOP, fill=tk.X)
232 self._btnHold = ttk.Button(toolbar, text='Hold', command=self._onHold)
233 self._btnHold.pack(side=tk.LEFT, fill=tk.Y)
234 #self._btnXfer = ttk.Button(toolbar, text='Transfer..')
235 #self._btnXfer.pack(side=tk.LEFT, fill=tk.Y)
236 self._btnHangUp = ttk.Button(toolbar, text='Hangup', command=self._onHangup)
237 self._btnHangUp.pack(side=tk.LEFT, fill=tk.Y)
238
239 # volume tool
240 vol_frm = ttk.Frame(self._callFrame)
241 vol_frm.pack(side=tk.TOP, fill=tk.X)
242
243 self.rxVolFrm = ttk.Labelframe(vol_frm, text='RX volume')
244 self.rxVolFrm.pack(side=tk.LEFT, fill=tk.Y)
245
246 self.btnRxMute = ttk.Button(self.rxVolFrm, width=8, text='Mute', command=self._onRxMute)
247 self.btnRxMute.pack(side=tk.LEFT)
248 self.rxVol = tk.Scale(self.rxVolFrm, orient=tk.HORIZONTAL, from_=0.0, to=10.0, showvalue=1) #, tickinterval=10.0, showvalue=1)
249 self.rxVol.set(5.0)
250 self.rxVol.bind("<ButtonRelease-1>", self._onRxVol)
251 self.rxVol.pack(side=tk.LEFT)
252
253 self.txVolFrm = ttk.Labelframe(vol_frm, text='TX volume')
254 self.txVolFrm.pack(side=tk.RIGHT, fill=tk.Y)
255
256 self.btnTxMute = ttk.Button(self.txVolFrm, width=8, text='Mute', command=self._onTxMute)
257 self.btnTxMute.pack(side=tk.LEFT)
258
259 # stat
260 self.stat = tk.Text(self._callFrame, width=10, height=2, bg='lightgray', relief=tk.FLAT, font=("Arial", "9"))
261 self.stat.insert(tk.END, 'stat here')
262 self.stat.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
263
264
265class ChatObserver(TextObserver, AudioObserver):
266 def onAddParticipant(self):
267 pass
268 def onStartAudio(self):
269 pass
270 def onStopAudio(self):
271 pass
272 def onCloseWindow(self):
273 pass
274
275class ChatFrame(tk.Toplevel):
276 """
277 Room
278 """
279 def __init__(self, observer):
280 tk.Toplevel.__init__(self)
281 self.protocol("WM_DELETE_WINDOW", self._onClose)
282 self._observer = observer
283
284 self._text = None
285 self._text_shown = True
286
287 self._audioEnabled = False
288 self._audioFrames = []
289 self._createWidgets()
290
291 def _createWidgets(self):
292 # toolbar
293 self.toolbar = ttk.Frame(self)
294 self.toolbar.pack(side=tk.TOP, fill=tk.BOTH)
295
296 btnText = ttk.Button(self.toolbar, text='Show/hide text', command=self._onShowHideText)
297 btnText.pack(side=tk.LEFT, fill=tk.Y)
298 btnAudio = ttk.Button(self.toolbar, text='Start/stop audio', command=self._onStartStopAudio)
299 btnAudio.pack(side=tk.LEFT, fill=tk.Y)
300
301 ttk.Separator(self.toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx = 4)
302
303 btnAdd = ttk.Button(self.toolbar, text='Add participant..', command=self._onAddParticipant)
304 btnAdd.pack(side=tk.LEFT, fill=tk.Y)
305
306 # media frame
307 self.media = ttk.Frame(self)
308 self.media.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
309
310 # create Text Chat frame
311 self.media_left = ttk.Frame(self.media)
312 self._text = TextFrame(self.media_left, self._observer)
313 self._text.pack(fill=tk.BOTH, expand=1)
314 self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
315
316 # create other media frame
317 self.media_right = ttk.Frame(self.media)
318
319 def _arrangeMediaFrames(self):
320 if len(self._audioFrames) == 0:
321 self.media_right.pack_forget()
322 return
323
324 self.media_right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1)
325 MAX_ROWS = 3
326 row_num = 0
327 col_num = 1
328 for frm in self._audioFrames:
329 frm.grid(row=row_num, column=col_num, sticky='nsew', padx=5, pady=5)
330 row_num += 1
331 if row_num >= MAX_ROWS:
332 row_num = 0
333 col_num += 1
334
335 def _onShowHideText(self):
336 self.textShowHide(not self._text_shown)
337
338 def _onAddParticipant(self):
339 self._observer.onAddParticipant()
340
341 def _onStartStopAudio(self):
342 self._audioEnabled = not self._audioEnabled
343 if self._audioEnabled:
344 self._observer.onStartAudio()
345 else:
346 self._observer.onStopAudio()
347 self.enableAudio(self._audioEnabled)
348
349 def _onClose(self):
350 self._observer.onCloseWindow()
351
352 # APIs
353
354 def bringToFront(self):
355 self.deiconify()
356 self.lift()
357 self._text._typingBox.focus_set()
358
359 def textAddMessage(self, msg, is_chat = True):
360 self._text.addMessage(msg, is_chat)
361
362 def textSetTypingIndication(self, who, is_typing = True):
363 self._text.setTypingIndication(who, is_typing)
364
365 def addParticipant(self, participant_uri):
366 aud_frm = AudioFrame(self.media_right, participant_uri, self._observer)
367 self._audioFrames.append(aud_frm)
368
369 def delParticipant(self, participant_uri):
370 for aud_frm in self._audioFrames:
371 if participant_uri == aud_frm.peerUri:
372 self._audioFrames.remove(aud_frm)
373 # need to delete aud_frm manually?
374 aud_frm.destroy()
375 return
376
377 def textShowHide(self, show = True):
378 if show:
379 self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
380 self._text._typingBox.focus_set()
381 else:
382 self.media_left.pack_forget()
383 self._text_shown = show
384
385 def enableAudio(self, is_enabled = True):
386 if is_enabled:
387 self._arrangeMediaFrames()
388 else:
389 self.media_right.pack_forget()
390 self._audioEnabled = is_enabled
391
392 def audioUpdateState(self, participant_uri, state):
393 for aud_frm in self._audioFrames:
394 if participant_uri == aud_frm.peerUri:
395 aud_frm.updateState(state)
396 break
397 if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1:
398 self.enableAudio(False)
399 else:
400 self.enableAudio(True)
401
402 def audioSetStatsText(self, participant_uri, stats_str):
403 for aud_frm in self._audioFrames:
404 if participant_uri == aud_frm.peerUri:
405 aud_frm.setStatsText(stats_str)
406 break
407
408if __name__ == '__main__':
409 root = tk.Tk()
410 root.title("Chat")
411 root.columnconfigure(0, weight=1)
412 root.rowconfigure(0, weight=1)
413
414 obs = ChatObserver()
415 dlg = ChatFrame(obs)
416 #dlg = TextFrame(root)
417 #dlg = AudioFrame(root)
418
419 #dlg.pack(fill=tk.BOTH, expand=1)
420 root.mainloop()