1# $Id$ 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=("Courier", "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() 421