Alexandre Lision | 0e14301 | 2014-01-22 11:02:46 -0500 | [diff] [blame] | 1 | # $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 | # |
| 21 | import sys |
| 22 | if sys.version_info[0] >= 3: # Python 3 |
| 23 | import tkinter as tk |
| 24 | from tkinter import ttk |
| 25 | from tkinter import messagebox as msgbox |
| 26 | else: |
| 27 | import Tkinter as tk |
| 28 | import ttk |
| 29 | import tkMessageBox as msgbox |
| 30 | |
| 31 | |
| 32 | class TextObserver: |
| 33 | def onSendMessage(self, msg): |
| 34 | pass |
| 35 | def onStartTyping(self): |
| 36 | pass |
| 37 | def onStopTyping(self): |
| 38 | pass |
| 39 | |
| 40 | class 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 | |
| 110 | class AudioState: |
| 111 | NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5) |
| 112 | |
| 113 | class 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 | |
| 128 | class 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 | |
| 265 | class 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 | |
| 275 | class 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 | |
| 408 | if __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() |