chatgui.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. #
  2. # pjsua Python GUI Demo
  3. #
  4. # Copyright (C)2013 Teluu Inc. (http://www.teluu.com)
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  19. #
  20. import sys
  21. if sys.version_info[0] >= 3: # Python 3
  22. import tkinter as tk
  23. from tkinter import ttk
  24. from tkinter import messagebox as msgbox
  25. else:
  26. import Tkinter as tk
  27. import ttk
  28. import tkMessageBox as msgbox
  29. class TextObserver:
  30. def onSendMessage(self, msg):
  31. pass
  32. def onStartTyping(self):
  33. pass
  34. def onStopTyping(self):
  35. pass
  36. class TextFrame(ttk.Frame):
  37. def __init__(self, master, observer):
  38. ttk.Frame.__init__(self, master)
  39. self._observer = observer
  40. self._isTyping = False
  41. self._createWidgets()
  42. def _onSendMessage(self, event):
  43. send_text = self._typingBox.get("1.0", tk.END).strip()
  44. if send_text == '':
  45. return
  46. self.addMessage('me: ' + send_text)
  47. self._typingBox.delete("0.0", tk.END)
  48. self._onTyping(None)
  49. # notify app for sending message
  50. self._observer.onSendMessage(send_text)
  51. def _onTyping(self, event):
  52. # notify app for typing indication
  53. is_typing = self._typingBox.get("1.0", tk.END).strip() != ''
  54. if is_typing != self._isTyping:
  55. self._isTyping = is_typing
  56. if is_typing:
  57. self._observer.onStartTyping()
  58. else:
  59. self._observer.onStopTyping()
  60. def _createWidgets(self):
  61. self.rowconfigure(0, weight=1)
  62. self.rowconfigure(1, weight=0)
  63. self.rowconfigure(2, weight=0)
  64. self.columnconfigure(0, weight=1)
  65. self.columnconfigure(1, weight=0)
  66. self._text = tk.Text(self, width=50, height=30, font=("Arial", "10"))
  67. self._text.grid(row=0, column=0, sticky='nswe')
  68. self._text.config(state=tk.DISABLED)
  69. self._text.tag_config("info", foreground="darkgray", font=("Arial", "9", "italic"))
  70. scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self._text.yview)
  71. self._text.config(yscrollcommand=scrl.set)
  72. scrl.grid(row=0, column=1, sticky='nsw')
  73. self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10"))
  74. self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0)
  75. self._statusBar = tk.Label(self, anchor='w', font=("Arial", "8", "italic"))
  76. self._statusBar.grid(row=2, columnspan=2, sticky='we')
  77. self._typingBox.bind('<Return>', self._onSendMessage)
  78. self._typingBox.bind("<Key>", self._onTyping)
  79. self._typingBox.focus_set()
  80. def addMessage(self, msg, is_chat = True):
  81. self._text.config(state=tk.NORMAL)
  82. if is_chat:
  83. self._text.insert(tk.END, msg+'\r\n')
  84. else:
  85. self._text.insert(tk.END, msg+'\r\n', 'info')
  86. self._text.config(state=tk.DISABLED)
  87. self._text.yview(tk.END)
  88. def setTypingIndication(self, who, is_typing):
  89. if is_typing:
  90. self._statusBar['text'] = "'%s' is typing.." % (who)
  91. else:
  92. self._statusBar['text'] = ''
  93. class AudioState:
  94. NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5)
  95. class AudioObserver:
  96. def onHangup(self, peer_uri):
  97. pass
  98. def onHold(self, peer_uri):
  99. pass
  100. def onUnhold(self, peer_uri):
  101. pass
  102. def onRxMute(self, peer_uri, is_muted):
  103. pass
  104. def onRxVol(self, peer_uri, vol_pct):
  105. pass
  106. def onTxMute(self, peer_uri, is_muted):
  107. pass
  108. class AudioFrame(ttk.Labelframe):
  109. def __init__(self, master, peer_uri, observer):
  110. ttk.Labelframe.__init__(self, master, text=peer_uri)
  111. self.peerUri = peer_uri
  112. self._observer = observer
  113. self._initFrame = None
  114. self._callFrame = None
  115. self._rxMute = False
  116. self._txMute = False
  117. self._state = AudioState.NULL
  118. self._createInitWidgets()
  119. self._createWidgets()
  120. def updateState(self, state):
  121. if self._state == state:
  122. return
  123. if state == AudioState.INITIALIZING:
  124. self._callFrame.pack_forget()
  125. self._initFrame.pack(fill=tk.BOTH)
  126. self._btnCancel.pack(side=tk.TOP)
  127. self._lblInitState['text'] = 'Intializing..'
  128. elif state == AudioState.CONNECTED:
  129. self._initFrame.pack_forget()
  130. self._callFrame.pack(fill=tk.BOTH)
  131. else:
  132. self._callFrame.pack_forget()
  133. self._initFrame.pack(fill=tk.BOTH)
  134. if state == AudioState.FAILED:
  135. self._lblInitState['text'] = 'Failed'
  136. else:
  137. self._lblInitState['text'] = 'Normal cleared'
  138. self._btnCancel.pack_forget()
  139. self._btnHold['text'] = 'Hold'
  140. self._btnHold.config(state=tk.NORMAL)
  141. self._rxMute = False
  142. self._txMute = False
  143. self.btnRxMute['text'] = 'Mute'
  144. self.btnTxMute['text'] = 'Mute'
  145. self.rxVol.set(5.0)
  146. # save last state
  147. self._state = state
  148. def setStatsText(self, stats_str):
  149. self.stat.config(state=tk.NORMAL)
  150. self.stat.delete("0.0", tk.END)
  151. self.stat.insert(tk.END, stats_str)
  152. self.stat.config(state=tk.DISABLED)
  153. def _onHold(self):
  154. self._btnHold.config(state=tk.DISABLED)
  155. # notify app
  156. if self._btnHold['text'] == 'Hold':
  157. self._observer.onHold(self.peerUri)
  158. self._btnHold['text'] = 'Unhold'
  159. else:
  160. self._observer.onUnhold(self.peerUri)
  161. self._btnHold['text'] = 'Hold'
  162. self._btnHold.config(state=tk.NORMAL)
  163. def _onHangup(self):
  164. # notify app
  165. self._observer.onHangup(self.peerUri)
  166. def _onRxMute(self):
  167. # notify app
  168. self._rxMute = not self._rxMute
  169. self._observer.onRxMute(self.peerUri, self._rxMute)
  170. self.btnRxMute['text'] = 'Unmute' if self._rxMute else 'Mute'
  171. def _onRxVol(self, event):
  172. # notify app
  173. vol = self.rxVol.get()
  174. self._observer.onRxVol(self.peerUri, vol*10.0)
  175. def _onTxMute(self):
  176. # notify app
  177. self._txMute = not self._txMute
  178. self._observer.onTxMute(self.peerUri, self._txMute)
  179. self.btnTxMute['text'] = 'Unmute' if self._txMute else 'Mute'
  180. def _createInitWidgets(self):
  181. self._initFrame = ttk.Frame(self)
  182. #self._initFrame.pack(fill=tk.BOTH)
  183. self._lblInitState = tk.Label(self._initFrame, font=("Arial", "12"), text='')
  184. self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1)
  185. # Operation: cancel/kick
  186. self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup)
  187. self._btnCancel.pack(side=tk.TOP)
  188. def _createWidgets(self):
  189. self._callFrame = ttk.Frame(self)
  190. #self._callFrame.pack(fill=tk.BOTH)
  191. # toolbar
  192. toolbar = ttk.Frame(self._callFrame)
  193. toolbar.pack(side=tk.TOP, fill=tk.X)
  194. self._btnHold = ttk.Button(toolbar, text='Hold', command=self._onHold)
  195. self._btnHold.pack(side=tk.LEFT, fill=tk.Y)
  196. #self._btnXfer = ttk.Button(toolbar, text='Transfer..')
  197. #self._btnXfer.pack(side=tk.LEFT, fill=tk.Y)
  198. self._btnHangUp = ttk.Button(toolbar, text='Hangup', command=self._onHangup)
  199. self._btnHangUp.pack(side=tk.LEFT, fill=tk.Y)
  200. # volume tool
  201. vol_frm = ttk.Frame(self._callFrame)
  202. vol_frm.pack(side=tk.TOP, fill=tk.X)
  203. self.rxVolFrm = ttk.Labelframe(vol_frm, text='RX volume')
  204. self.rxVolFrm.pack(side=tk.LEFT, fill=tk.Y)
  205. self.btnRxMute = ttk.Button(self.rxVolFrm, width=8, text='Mute', command=self._onRxMute)
  206. self.btnRxMute.pack(side=tk.LEFT)
  207. self.rxVol = tk.Scale(self.rxVolFrm, orient=tk.HORIZONTAL, from_=0.0, to=10.0, showvalue=1) #, tickinterval=10.0, showvalue=1)
  208. self.rxVol.set(5.0)
  209. self.rxVol.bind("<ButtonRelease-1>", self._onRxVol)
  210. self.rxVol.pack(side=tk.LEFT)
  211. self.txVolFrm = ttk.Labelframe(vol_frm, text='TX volume')
  212. self.txVolFrm.pack(side=tk.RIGHT, fill=tk.Y)
  213. self.btnTxMute = ttk.Button(self.txVolFrm, width=8, text='Mute', command=self._onTxMute)
  214. self.btnTxMute.pack(side=tk.LEFT)
  215. # stat
  216. self.stat = tk.Text(self._callFrame, width=10, height=2, bg='lightgray', relief=tk.FLAT, font=("Courier", "9"))
  217. self.stat.insert(tk.END, 'stat here')
  218. self.stat.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
  219. class ChatObserver(TextObserver, AudioObserver):
  220. def onAddParticipant(self):
  221. pass
  222. def onStartAudio(self):
  223. pass
  224. def onStopAudio(self):
  225. pass
  226. def onCloseWindow(self):
  227. pass
  228. class ChatFrame(tk.Toplevel):
  229. """
  230. Room
  231. """
  232. def __init__(self, observer):
  233. tk.Toplevel.__init__(self)
  234. self.protocol("WM_DELETE_WINDOW", self._onClose)
  235. self._observer = observer
  236. self._text = None
  237. self._text_shown = True
  238. self._audioEnabled = False
  239. self._audioFrames = []
  240. self._createWidgets()
  241. def _createWidgets(self):
  242. # toolbar
  243. self.toolbar = ttk.Frame(self)
  244. self.toolbar.pack(side=tk.TOP, fill=tk.BOTH)
  245. btnText = ttk.Button(self.toolbar, text='Show/hide text', command=self._onShowHideText)
  246. btnText.pack(side=tk.LEFT, fill=tk.Y)
  247. btnAudio = ttk.Button(self.toolbar, text='Start/stop audio', command=self._onStartStopAudio)
  248. btnAudio.pack(side=tk.LEFT, fill=tk.Y)
  249. ttk.Separator(self.toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx = 4)
  250. btnAdd = ttk.Button(self.toolbar, text='Add participant..', command=self._onAddParticipant)
  251. btnAdd.pack(side=tk.LEFT, fill=tk.Y)
  252. # media frame
  253. self.media = ttk.Frame(self)
  254. self.media.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
  255. # create Text Chat frame
  256. self.media_left = ttk.Frame(self.media)
  257. self._text = TextFrame(self.media_left, self._observer)
  258. self._text.pack(fill=tk.BOTH, expand=1)
  259. self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
  260. # create other media frame
  261. self.media_right = ttk.Frame(self.media)
  262. def _arrangeMediaFrames(self):
  263. if len(self._audioFrames) == 0:
  264. self.media_right.pack_forget()
  265. return
  266. self.media_right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1)
  267. MAX_ROWS = 3
  268. row_num = 0
  269. col_num = 1
  270. for frm in self._audioFrames:
  271. frm.grid(row=row_num, column=col_num, sticky='nsew', padx=5, pady=5)
  272. row_num += 1
  273. if row_num >= MAX_ROWS:
  274. row_num = 0
  275. col_num += 1
  276. def _onShowHideText(self):
  277. self.textShowHide(not self._text_shown)
  278. def _onAddParticipant(self):
  279. self._observer.onAddParticipant()
  280. def _onStartStopAudio(self):
  281. self._audioEnabled = not self._audioEnabled
  282. if self._audioEnabled:
  283. self._observer.onStartAudio()
  284. else:
  285. self._observer.onStopAudio()
  286. self.enableAudio(self._audioEnabled)
  287. def _onClose(self):
  288. self._observer.onCloseWindow()
  289. # APIs
  290. def bringToFront(self):
  291. self.deiconify()
  292. self.lift()
  293. self._text._typingBox.focus_set()
  294. def textAddMessage(self, msg, is_chat = True):
  295. self._text.addMessage(msg, is_chat)
  296. def textSetTypingIndication(self, who, is_typing = True):
  297. self._text.setTypingIndication(who, is_typing)
  298. def addParticipant(self, participant_uri):
  299. aud_frm = AudioFrame(self.media_right, participant_uri, self._observer)
  300. self._audioFrames.append(aud_frm)
  301. def delParticipant(self, participant_uri):
  302. for aud_frm in self._audioFrames:
  303. if participant_uri == aud_frm.peerUri:
  304. self._audioFrames.remove(aud_frm)
  305. # need to delete aud_frm manually?
  306. aud_frm.destroy()
  307. return
  308. def textShowHide(self, show = True):
  309. if show:
  310. self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
  311. self._text._typingBox.focus_set()
  312. else:
  313. self.media_left.pack_forget()
  314. self._text_shown = show
  315. def enableAudio(self, is_enabled = True):
  316. if is_enabled:
  317. self._arrangeMediaFrames()
  318. else:
  319. self.media_right.pack_forget()
  320. self._audioEnabled = is_enabled
  321. def audioUpdateState(self, participant_uri, state):
  322. for aud_frm in self._audioFrames:
  323. if participant_uri == aud_frm.peerUri:
  324. aud_frm.updateState(state)
  325. break
  326. if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1:
  327. self.enableAudio(False)
  328. else:
  329. self.enableAudio(True)
  330. def audioSetStatsText(self, participant_uri, stats_str):
  331. for aud_frm in self._audioFrames:
  332. if participant_uri == aud_frm.peerUri:
  333. aud_frm.setStatsText(stats_str)
  334. break
  335. if __name__ == '__main__':
  336. root = tk.Tk()
  337. root.title("Chat")
  338. root.columnconfigure(0, weight=1)
  339. root.rowconfigure(0, weight=1)
  340. obs = ChatObserver()
  341. dlg = ChatFrame(obs)
  342. #dlg = TextFrame(root)
  343. #dlg = AudioFrame(root)
  344. #dlg.pack(fill=tk.BOTH, expand=1)
  345. root.mainloop()