| # $Id: chatgui.py 4704 2014-01-16 05:30:46Z ming $ |
| # |
| # pjsua Python GUI Demo |
| # |
| # Copyright (C)2013 Teluu Inc. (http://www.teluu.com) |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| # |
| import sys |
| if sys.version_info[0] >= 3: # Python 3 |
| import tkinter as tk |
| from tkinter import ttk |
| from tkinter import messagebox as msgbox |
| else: |
| import Tkinter as tk |
| import ttk |
| import tkMessageBox as msgbox |
| |
| |
| class TextObserver: |
| def onSendMessage(self, msg): |
| pass |
| def onStartTyping(self): |
| pass |
| def onStopTyping(self): |
| pass |
| |
| class TextFrame(ttk.Frame): |
| def __init__(self, master, observer): |
| ttk.Frame.__init__(self, master) |
| self._observer = observer |
| self._isTyping = False |
| self._createWidgets() |
| |
| def _onSendMessage(self, event): |
| send_text = self._typingBox.get("1.0", tk.END).strip() |
| if send_text == '': |
| return |
| |
| self.addMessage('me: ' + send_text) |
| self._typingBox.delete("0.0", tk.END) |
| self._onTyping(None) |
| |
| # notify app for sending message |
| self._observer.onSendMessage(send_text) |
| |
| def _onTyping(self, event): |
| # notify app for typing indication |
| is_typing = self._typingBox.get("1.0", tk.END).strip() != '' |
| if is_typing != self._isTyping: |
| self._isTyping = is_typing |
| if is_typing: |
| self._observer.onStartTyping() |
| else: |
| self._observer.onStopTyping() |
| |
| def _createWidgets(self): |
| self.rowconfigure(0, weight=1) |
| self.rowconfigure(1, weight=0) |
| self.rowconfigure(2, weight=0) |
| self.columnconfigure(0, weight=1) |
| self.columnconfigure(1, weight=0) |
| |
| self._text = tk.Text(self, width=50, height=30, font=("Arial", "10")) |
| self._text.grid(row=0, column=0, sticky='nswe') |
| self._text.config(state=tk.DISABLED) |
| self._text.tag_config("info", foreground="darkgray", font=("Arial", "9", "italic")) |
| |
| scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self._text.yview) |
| self._text.config(yscrollcommand=scrl.set) |
| scrl.grid(row=0, column=1, sticky='nsw') |
| |
| self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10")) |
| self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0) |
| |
| self._statusBar = tk.Label(self, anchor='w', font=("Arial", "8", "italic")) |
| self._statusBar.grid(row=2, columnspan=2, sticky='we') |
| |
| self._typingBox.bind('<Return>', self._onSendMessage) |
| self._typingBox.bind("<Key>", self._onTyping) |
| self._typingBox.focus_set() |
| |
| def addMessage(self, msg, is_chat = True): |
| self._text.config(state=tk.NORMAL) |
| if is_chat: |
| self._text.insert(tk.END, msg+'\r\n') |
| else: |
| self._text.insert(tk.END, msg+'\r\n', 'info') |
| self._text.config(state=tk.DISABLED) |
| self._text.yview(tk.END) |
| |
| def setTypingIndication(self, who, is_typing): |
| if is_typing: |
| self._statusBar['text'] = "'%s' is typing.." % (who) |
| else: |
| self._statusBar['text'] = '' |
| |
| class AudioState: |
| NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5) |
| |
| class AudioObserver: |
| def onHangup(self, peer_uri): |
| pass |
| def onHold(self, peer_uri): |
| pass |
| def onUnhold(self, peer_uri): |
| pass |
| def onRxMute(self, peer_uri, is_muted): |
| pass |
| def onRxVol(self, peer_uri, vol_pct): |
| pass |
| def onTxMute(self, peer_uri, is_muted): |
| pass |
| |
| |
| class AudioFrame(ttk.Labelframe): |
| def __init__(self, master, peer_uri, observer): |
| ttk.Labelframe.__init__(self, master, text=peer_uri) |
| self.peerUri = peer_uri |
| self._observer = observer |
| self._initFrame = None |
| self._callFrame = None |
| self._rxMute = False |
| self._txMute = False |
| self._state = AudioState.NULL |
| |
| self._createInitWidgets() |
| self._createWidgets() |
| |
| def updateState(self, state): |
| if self._state == state: |
| return |
| |
| if state == AudioState.INITIALIZING: |
| self._callFrame.pack_forget() |
| self._initFrame.pack(fill=tk.BOTH) |
| self._btnCancel.pack(side=tk.TOP) |
| self._lblInitState['text'] = 'Intializing..' |
| |
| elif state == AudioState.CONNECTED: |
| self._initFrame.pack_forget() |
| self._callFrame.pack(fill=tk.BOTH) |
| else: |
| self._callFrame.pack_forget() |
| self._initFrame.pack(fill=tk.BOTH) |
| if state == AudioState.FAILED: |
| self._lblInitState['text'] = 'Failed' |
| else: |
| self._lblInitState['text'] = 'Normal cleared' |
| self._btnCancel.pack_forget() |
| |
| self._btnHold['text'] = 'Hold' |
| self._btnHold.config(state=tk.NORMAL) |
| self._rxMute = False |
| self._txMute = False |
| self.btnRxMute['text'] = 'Mute' |
| self.btnTxMute['text'] = 'Mute' |
| self.rxVol.set(5.0) |
| |
| # save last state |
| self._state = state |
| |
| def setStatsText(self, stats_str): |
| self.stat.config(state=tk.NORMAL) |
| self.stat.delete("0.0", tk.END) |
| self.stat.insert(tk.END, stats_str) |
| self.stat.config(state=tk.DISABLED) |
| |
| def _onHold(self): |
| self._btnHold.config(state=tk.DISABLED) |
| # notify app |
| if self._btnHold['text'] == 'Hold': |
| self._observer.onHold(self.peerUri) |
| self._btnHold['text'] = 'Unhold' |
| else: |
| self._observer.onUnhold(self.peerUri) |
| self._btnHold['text'] = 'Hold' |
| self._btnHold.config(state=tk.NORMAL) |
| |
| def _onHangup(self): |
| # notify app |
| self._observer.onHangup(self.peerUri) |
| |
| def _onRxMute(self): |
| # notify app |
| self._rxMute = not self._rxMute |
| self._observer.onRxMute(self.peerUri, self._rxMute) |
| self.btnRxMute['text'] = 'Unmute' if self._rxMute else 'Mute' |
| |
| def _onRxVol(self, event): |
| # notify app |
| vol = self.rxVol.get() |
| self._observer.onRxVol(self.peerUri, vol*10.0) |
| |
| def _onTxMute(self): |
| # notify app |
| self._txMute = not self._txMute |
| self._observer.onTxMute(self.peerUri, self._txMute) |
| self.btnTxMute['text'] = 'Unmute' if self._txMute else 'Mute' |
| |
| def _createInitWidgets(self): |
| self._initFrame = ttk.Frame(self) |
| #self._initFrame.pack(fill=tk.BOTH) |
| |
| |
| self._lblInitState = tk.Label(self._initFrame, font=("Arial", "12"), text='') |
| self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1) |
| |
| # Operation: cancel/kick |
| self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup) |
| self._btnCancel.pack(side=tk.TOP) |
| |
| def _createWidgets(self): |
| self._callFrame = ttk.Frame(self) |
| #self._callFrame.pack(fill=tk.BOTH) |
| |
| # toolbar |
| toolbar = ttk.Frame(self._callFrame) |
| toolbar.pack(side=tk.TOP, fill=tk.X) |
| self._btnHold = ttk.Button(toolbar, text='Hold', command=self._onHold) |
| self._btnHold.pack(side=tk.LEFT, fill=tk.Y) |
| #self._btnXfer = ttk.Button(toolbar, text='Transfer..') |
| #self._btnXfer.pack(side=tk.LEFT, fill=tk.Y) |
| self._btnHangUp = ttk.Button(toolbar, text='Hangup', command=self._onHangup) |
| self._btnHangUp.pack(side=tk.LEFT, fill=tk.Y) |
| |
| # volume tool |
| vol_frm = ttk.Frame(self._callFrame) |
| vol_frm.pack(side=tk.TOP, fill=tk.X) |
| |
| self.rxVolFrm = ttk.Labelframe(vol_frm, text='RX volume') |
| self.rxVolFrm.pack(side=tk.LEFT, fill=tk.Y) |
| |
| self.btnRxMute = ttk.Button(self.rxVolFrm, width=8, text='Mute', command=self._onRxMute) |
| self.btnRxMute.pack(side=tk.LEFT) |
| self.rxVol = tk.Scale(self.rxVolFrm, orient=tk.HORIZONTAL, from_=0.0, to=10.0, showvalue=1) #, tickinterval=10.0, showvalue=1) |
| self.rxVol.set(5.0) |
| self.rxVol.bind("<ButtonRelease-1>", self._onRxVol) |
| self.rxVol.pack(side=tk.LEFT) |
| |
| self.txVolFrm = ttk.Labelframe(vol_frm, text='TX volume') |
| self.txVolFrm.pack(side=tk.RIGHT, fill=tk.Y) |
| |
| self.btnTxMute = ttk.Button(self.txVolFrm, width=8, text='Mute', command=self._onTxMute) |
| self.btnTxMute.pack(side=tk.LEFT) |
| |
| # stat |
| self.stat = tk.Text(self._callFrame, width=10, height=2, bg='lightgray', relief=tk.FLAT, font=("Arial", "9")) |
| self.stat.insert(tk.END, 'stat here') |
| self.stat.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) |
| |
| |
| class ChatObserver(TextObserver, AudioObserver): |
| def onAddParticipant(self): |
| pass |
| def onStartAudio(self): |
| pass |
| def onStopAudio(self): |
| pass |
| def onCloseWindow(self): |
| pass |
| |
| class ChatFrame(tk.Toplevel): |
| """ |
| Room |
| """ |
| def __init__(self, observer): |
| tk.Toplevel.__init__(self) |
| self.protocol("WM_DELETE_WINDOW", self._onClose) |
| self._observer = observer |
| |
| self._text = None |
| self._text_shown = True |
| |
| self._audioEnabled = False |
| self._audioFrames = [] |
| self._createWidgets() |
| |
| def _createWidgets(self): |
| # toolbar |
| self.toolbar = ttk.Frame(self) |
| self.toolbar.pack(side=tk.TOP, fill=tk.BOTH) |
| |
| btnText = ttk.Button(self.toolbar, text='Show/hide text', command=self._onShowHideText) |
| btnText.pack(side=tk.LEFT, fill=tk.Y) |
| btnAudio = ttk.Button(self.toolbar, text='Start/stop audio', command=self._onStartStopAudio) |
| btnAudio.pack(side=tk.LEFT, fill=tk.Y) |
| |
| ttk.Separator(self.toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx = 4) |
| |
| btnAdd = ttk.Button(self.toolbar, text='Add participant..', command=self._onAddParticipant) |
| btnAdd.pack(side=tk.LEFT, fill=tk.Y) |
| |
| # media frame |
| self.media = ttk.Frame(self) |
| self.media.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) |
| |
| # create Text Chat frame |
| self.media_left = ttk.Frame(self.media) |
| self._text = TextFrame(self.media_left, self._observer) |
| self._text.pack(fill=tk.BOTH, expand=1) |
| self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) |
| |
| # create other media frame |
| self.media_right = ttk.Frame(self.media) |
| |
| def _arrangeMediaFrames(self): |
| if len(self._audioFrames) == 0: |
| self.media_right.pack_forget() |
| return |
| |
| self.media_right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1) |
| MAX_ROWS = 3 |
| row_num = 0 |
| col_num = 1 |
| for frm in self._audioFrames: |
| frm.grid(row=row_num, column=col_num, sticky='nsew', padx=5, pady=5) |
| row_num += 1 |
| if row_num >= MAX_ROWS: |
| row_num = 0 |
| col_num += 1 |
| |
| def _onShowHideText(self): |
| self.textShowHide(not self._text_shown) |
| |
| def _onAddParticipant(self): |
| self._observer.onAddParticipant() |
| |
| def _onStartStopAudio(self): |
| self._audioEnabled = not self._audioEnabled |
| if self._audioEnabled: |
| self._observer.onStartAudio() |
| else: |
| self._observer.onStopAudio() |
| self.enableAudio(self._audioEnabled) |
| |
| def _onClose(self): |
| self._observer.onCloseWindow() |
| |
| # APIs |
| |
| def bringToFront(self): |
| self.deiconify() |
| self.lift() |
| self._text._typingBox.focus_set() |
| |
| def textAddMessage(self, msg, is_chat = True): |
| self._text.addMessage(msg, is_chat) |
| |
| def textSetTypingIndication(self, who, is_typing = True): |
| self._text.setTypingIndication(who, is_typing) |
| |
| def addParticipant(self, participant_uri): |
| aud_frm = AudioFrame(self.media_right, participant_uri, self._observer) |
| self._audioFrames.append(aud_frm) |
| |
| def delParticipant(self, participant_uri): |
| for aud_frm in self._audioFrames: |
| if participant_uri == aud_frm.peerUri: |
| self._audioFrames.remove(aud_frm) |
| # need to delete aud_frm manually? |
| aud_frm.destroy() |
| return |
| |
| def textShowHide(self, show = True): |
| if show: |
| self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) |
| self._text._typingBox.focus_set() |
| else: |
| self.media_left.pack_forget() |
| self._text_shown = show |
| |
| def enableAudio(self, is_enabled = True): |
| if is_enabled: |
| self._arrangeMediaFrames() |
| else: |
| self.media_right.pack_forget() |
| self._audioEnabled = is_enabled |
| |
| def audioUpdateState(self, participant_uri, state): |
| for aud_frm in self._audioFrames: |
| if participant_uri == aud_frm.peerUri: |
| aud_frm.updateState(state) |
| break |
| if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1: |
| self.enableAudio(False) |
| else: |
| self.enableAudio(True) |
| |
| def audioSetStatsText(self, participant_uri, stats_str): |
| for aud_frm in self._audioFrames: |
| if participant_uri == aud_frm.peerUri: |
| aud_frm.setStatsText(stats_str) |
| break |
| |
| if __name__ == '__main__': |
| root = tk.Tk() |
| root.title("Chat") |
| root.columnconfigure(0, weight=1) |
| root.rowconfigure(0, weight=1) |
| |
| obs = ChatObserver() |
| dlg = ChatFrame(obs) |
| #dlg = TextFrame(root) |
| #dlg = AudioFrame(root) |
| |
| #dlg.pack(fill=tk.BOTH, expand=1) |
| root.mainloop() |