comparison frontends/wix/main_window.py @ 51:8c67ea98ab91

frontend improved to take into account new SàT features - quick_frontend: better use of contact management, it now manages nicks, avatars, and connected status - quick_frontend: disconnect and remove are now 2 separate methods for contact list - wix: new contact list using HTML items, and showing avatars. Groups are not showed for now - wix: contact status now use tuples, to keep order, human readable status and color of contact - wix: contact list is updated when avatar or nick is found - wix: fixed 'question' dialog, which is renamed in 'yes/no' - wix: action result are now ignored for unkwnown id - sortilege refactored to work again
author Goffi <goffi@goffi.org>
date Thu, 07 Jan 2010 00:17:27 +1100
parents 874de3020e1c
children 6455fb62ff83
comparison
equal deleted inserted replaced
50:daa1f01a5332 51:8c67ea98ab91
32 from tools.jid import JID 32 from tools.jid import JID
33 from logging import debug, info, error 33 from logging import debug, info, error
34 from quick_frontend.quick_chat_list import QuickChatList 34 from quick_frontend.quick_chat_list import QuickChatList
35 from quick_frontend.quick_contact_list import QuickContactList 35 from quick_frontend.quick_contact_list import QuickContactList
36 from quick_frontend.quick_app import QuickApp 36 from quick_frontend.quick_app import QuickApp
37 37 from quick_frontend.quick_contact_management import QuickContactManagement
38 from cgi import escape
39 import sys
40
41 IMAGE_DIR = sys.path[0]+'/images'
38 42
39 msgOFFLINE = "offline" 43 msgOFFLINE = "offline"
40 msgONLINE = "online" 44 msgONLINE = "online"
41 idCONNECT = 1 45 idCONNECT,\
42 idDISCONNECT = 2 46 idDISCONNECT,\
43 idEXIT = 3 47 idEXIT,\
44 idPARAM = 4 48 idPARAM,\
45 idADD_CONTACT = 5 49 idADD_CONTACT,\
46 idREMOVE_CONTACT = 6 50 idREMOVE_CONTACT,\
47 idSHOW_PROFILE = 7 51 idSHOW_PROFILE,\
48 idFIND_GATEWAYS = 8 52 idFIND_GATEWAYS = range(8)
49 const_DEFAULT_GROUP = "Unclassed" 53 const_DEFAULT_GROUP = "Unclassed"
50 const_STATUS = {"Online":"", 54 const_STATUS = [("", "Online", None),
51 "Want to discuss":"chat", 55 ("chat", "Free for chat", "green"),
52 "AFK":"away", 56 ("away", "AFK", "brown"),
53 "Do Not Disturb":"dnd", 57 ("dnd", "DND", "red"),
54 "Away":"xa"} 58 ("xa", "Away", "red")]
55 59
56 class ChatList(QuickChatList): 60 class ChatList(QuickChatList):
57 """This class manage the list of chat windows""" 61 """This class manage the list of chat windows"""
58 62
59 def __init__(self, host): 63 def __init__(self, host):
60 QuickChatList.__init__(self, host) 64 QuickChatList.__init__(self, host)
61 65
62 def createChat(self, name): 66 def createChat(self, target):
63 return Chat(name, self.host) 67 return Chat(target, self.host)
64 68
65 69
66 70 class ContactList(wx.SimpleHtmlListBox, QuickContactList):
67 class ContactList(wx.TreeCtrl, QuickContactList):
68 """Customized control to manage contacts.""" 71 """Customized control to manage contacts."""
69 72
70 def __init__(self, parent): 73 def __init__(self, parent, CM):
71 wx.TreeCtrl.__init__(self, parent, style = wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS) 74 wx.SimpleHtmlListBox.__init__(self, parent, -1)
72 QuickContactList.__init__(self) 75 QuickContactList.__init__(self, CM)
73 self.jid_ids={} 76 self.host = parent
74 self.groups={} 77 self.Bind(wx.EVT_LISTBOX_DCLICK, self.onActivated)
75 self.root=self.AddRoot("") 78
76 self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.onActivated, self) 79 def __find_idx(self, jid, reverse=False):
77 80 """Find indexes of given jid in contact list
78 #icons 81 @return: list of indexes"""
79 isz = (16,16) 82 result=[]
80 il = wx.ImageList(isz[0], isz[1]) 83 for i in range(self.GetCount()):
81 self.icon_online = il.Add(wx.ArtProvider_GetBitmap(wx.ART_TICK_MARK, wx.ART_OTHER, isz)) 84 if self.GetClientData(i).short == jid.short:
82 self.icon_unavailable = il.Add(wx.ArtProvider_GetBitmap(wx.ART_CROSS_MARK, wx.ART_OTHER, isz)) 85 result.append(i)
83 self.AssignImageList(il) 86 return result
84 87
85 self.__addNode(const_DEFAULT_GROUP) 88 def replace(self, jid):
86 89 debug("update %s" % jid)
87 def __addNode(self, label): 90 if not self.__find_idx(jid):
88 """Add an item container""" 91 self.add(jid)
89 ret=self.AppendItem(self.root, label)
90 self.SetPyData(ret, "[node]")
91 self.SetItemBold(ret)
92 self.groups[label]=ret
93
94 def replace(self, jid, name="", show="", status="", group=""):
95 debug("status = %s show = %s",status, show)
96 if not self.jid_ids.has_key(jid):
97 self.add(jid, name, show, status, group)
98 else: 92 else:
99 debug ("updating %s",jid) 93 for i in self.__find_idx(jid):
100 self.__presentItem(jid, name, show, status, group) 94 self.SetString(i, self.__presentItem(jid))
101 95
102 def __presentItem(self, jid, name, show, status, group): 96 def disconnect(self, jid):
97 self.remove(jid) #for now, we only show online contacts
98
99 def __presentItem(self, jid):
103 """Make a nice presentation of the contact in the list.""" 100 """Make a nice presentation of the contact in the list."""
104 id=self.jid_ids[jid] 101 name = self.CM.getAttr(jid,'name')
105 label= "%s [%s] \n %s" % ((name or jid), (show or "online"), status) 102 nick = self.CM.getAttr(jid,'nick')
106 self.SetItemText(id, label) 103 show = filter(lambda x:x[0]==self.CM.getAttr(jid,'show'), const_STATUS)[0]
107 104 #show[0]==shortcut
108 # icon 105 #show[1]==human readable
109 if not show or show=="chat": 106 #show[2]==color (or None)
110 self.SetItemImage(id, self.icon_online) 107 show_html = "<font color='%s'>[%s]</font>" % (show[2], show[1]) if show[2] else ""
111 else: 108 status = self.CM.getAttr(jid,'status') or ''
112 self.SetItemImage(id, self.icon_unavailable) 109 avatar = self.CM.getAttr(jid,'avatar') or IMAGE_DIR+'/empty_avatar.png'
113 110
114 #colour 111 #XXX: yes table I know :) but wxHTML* doesn't support CSS
115 if not show: 112 html = """
116 self.SetItemTextColour(id, wx.BLACK) 113 <table border='0'>
117 elif show=="chat": 114 <td>
118 self.SetItemTextColour(id, wx.GREEN) 115 <img height='64' width='64' src='%s' />
119 elif show=="away": 116 </td>
120 self.SetItemTextColour(id, wx.BLUE) 117 <td>
121 else: 118 <b>%s</b> %s<br />
122 self.SetItemTextColour(id, wx.RED) 119 <i>%s</i>
123 120 </td>
124 def add(self, jid, name="", show="", status="", group=""): 121 </table>
122 """ % (avatar,
123 escape(nick or name or jid.node or jid.short),
124 show_html,
125 escape(status))
126
127 return html
128
129 def add(self, jid):
125 """add a contact to the list""" 130 """add a contact to the list"""
126 debug ("adding %s",jid) 131 debug ("adding %s",jid)
127 dest_group=group or const_DEFAULT_GROUP 132 idx = self.Append(self.__presentItem(jid))
128 if not self.groups.has_key(dest_group): 133
129 self.__addNode(dest_group) 134 self.SetClientData(idx, jid)
130 self.jid_ids[jid]=self.AppendItem(self.groups[dest_group], "")
131 self.__presentItem(jid, name, show, status, group)
132 self.SetPyData(self.jid_ids[jid], "[contact]"+jid)
133 self.EnsureVisible(self.jid_ids[jid])
134 self.Refresh() #FIXME: Best way ?
135 135
136 def remove(self, jid): 136 def remove(self, jid):
137 """remove a contact from the list""" 137 """remove a contact from the list"""
138 debug ("removing %s",jid) 138 debug ("removing %s",jid)
139 self.Delete(self.jid_ids[jid]) 139 list_idx = self.__find_idx(jid)
140 del self.jid_ids[jid] 140 list_idx.reverse() #we me make some deletions, we have to reverse the order
141 self.Refresh() #FIXME: Best way ? 141 for i in list_idx:
142 self.Delete(i)
142 143
143 def onActivated(self, event): 144 def onActivated(self, event):
144 """Called when a contact is clicked or activated with keyboard.""" 145 """Called when a contact is clicked or activated with keyboard."""
145 if self.GetPyData(event.GetItem()).startswith("[contact]"): 146 data = self.getSelection()
146 self.onActivatedCB(self.GetPyData(event.GetItem())[9:]) 147 self.onActivatedCB(data)
147 else: 148 event.Skip()
148 event.Skip()
149 149
150 def getSelection(self): 150 def getSelection(self):
151 """Return the selected contact, or an empty string if there is not""" 151 """Return the selected contact, or an empty string if there is not"""
152 data = self.GetPyData(self.GetSelection()) 152 if self.GetSelection() == wx.NOT_FOUND:
153 if not data or not data.startswith("[contact]"): 153 return "" #FIXME: gof: à améliorer
154 return "" 154 data = self.GetClientData(self.GetSelection())
155 return JID(data[9:]) 155 return data
156 156
157 def registerActivatedCB(self, cb): 157 def registerActivatedCB(self, cb):
158 """Register a callback with manage contact activation.""" 158 """Register a callback with manage contact activation."""
159 self.onActivatedCB=cb 159 self.onActivatedCB=cb
160 160
161 class MainWindow(wx.Frame, QuickApp): 161 class MainWindow(wx.Frame, QuickApp):
162 """main app window""" 162 """main app window"""
163 163
164 def __init__(self): 164 def __init__(self):
165 wx.Frame.__init__(self,None, title="SAT Wix", size=(400,200)) 165 wx.Frame.__init__(self,None, title="SAT Wix", size=(400,200))
166 self.CM = QuickContactManagement() #gof:
166 167
167 168
168 #Frame elements 169 #Frame elements
169 self.contactList = ContactList(self) 170 self.contactList = ContactList(self, self.CM)
170 self.contactList.registerActivatedCB(self.onContactActivated) 171 self.contactList.registerActivatedCB(self.onContactActivated)
171 self.chat_wins=ChatList(self) 172 self.chat_wins=ChatList(self)
172 self.CreateStatusBar() 173 self.CreateStatusBar()
173 self.SetStatusText(msgOFFLINE) 174 self.SetStatusText(msgOFFLINE)
174 self.createMenus() 175 self.createMenus()
175 176
176 #ToolBar 177 #ToolBar
177 self.tools=self.CreateToolBar() 178 self.tools=self.CreateToolBar()
178 self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=const_STATUS.keys(), 179 self.statusBox = wx.ComboBox(self.tools, -1, "Online", choices=[status[1] for status in const_STATUS],
179 style=wx.CB_DROPDOWN | wx.CB_READONLY) 180 style=wx.CB_DROPDOWN | wx.CB_READONLY)
180 self.tools.AddControl(self.statusBox) 181 self.tools.AddControl(self.statusBox)
181 self.tools.AddSeparator() 182 self.tools.AddSeparator()
182 self.statusTxt=wx.TextCtrl(self.tools, -1, style = wx.TE_PROCESS_ENTER) 183 self.statusTxt=wx.TextCtrl(self.tools, -1, style = wx.TE_PROCESS_ENTER)
183 self.tools.AddControl(self.statusTxt) 184 self.tools.AddControl(self.statusTxt)
184 self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox) 185 self.Bind(wx.EVT_COMBOBOX, self.onStatusChange, self.statusBox)
185 self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt) 186 self.Bind(wx.EVT_TEXT_ENTER, self.onStatusChange, self.statusTxt)
186 self.tools.Disable() 187 self.tools.Disable()
187 188
188 #tray icon 189 #tray icon
189 ticon = wx.Icon("images/tray_icon.xpm", wx.BITMAP_TYPE_XPM) 190 ticon = wx.Icon(IMAGE_DIR+'/crystal/tray_icon.xpm', wx.BITMAP_TYPE_XPM)
190 self.tray_icon = wx.TaskBarIcon() 191 self.tray_icon = wx.TaskBarIcon()
191 self.tray_icon.SetIcon(ticon, "Wix jabber client") 192 self.tray_icon.SetIcon(ticon, "Wix jabber client")
192 wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick) 193 wx.EVT_TASKBAR_LEFT_UP(self.tray_icon, self.onTrayClick)
193 194
194 195
250 def showDialog(self, message, title="", type="info"): 251 def showDialog(self, message, title="", type="info"):
251 if type == 'info': 252 if type == 'info':
252 flags = wx.OK | wx.ICON_INFORMATION 253 flags = wx.OK | wx.ICON_INFORMATION
253 elif type == 'error': 254 elif type == 'error':
254 flags = wx.OK | wx.ICON_ERROR 255 flags = wx.OK | wx.ICON_ERROR
255 elif type == 'question': 256 elif type == 'yes/no':
256 flags = wx.OK | wx.ICON_QUESTION 257 flags = wx.YES_NO | wx.ICON_QUESTION
257 else: 258 else:
258 flags = wx.OK | wx.ICON_INFORMATION 259 flags = wx.OK | wx.ICON_INFORMATION
260 error('unmanaged dialog type: %s', type)
259 dlg = wx.MessageDialog(self, message, title, flags) 261 dlg = wx.MessageDialog(self, message, title, flags)
260 answer = dlg.ShowModal() 262 answer = dlg.ShowModal()
261 dlg.Destroy() 263 dlg.Destroy()
262 return True if (answer == wx.ID_YES or answer == wx.ID_OK) else False 264 return True if (answer == wx.ID_YES or answer == wx.ID_OK) else False
263 265
270 self.SetStatusText(msgOFFLINE) 272 self.SetStatusText(msgOFFLINE)
271 self.tools.Disable() 273 self.tools.Disable()
272 return 274 return
273 275
274 276
275 def presenceUpdate(self, jabber_id, type, show, status, priority): 277 def presenceUpdate(self, jabber_id, show, priority, statuses):
276 QuickApp.presenceUpdate(self, jabber_id, type, show, status, priority) 278 QuickApp.presenceUpdate(self, jabber_id, show, priority, statuses)
277 279
278 def askConfirmation(self, type, id, data): 280 def askConfirmation(self, type, id, data):
279 #TODO: refactor this in QuickApp 281 #TODO: refactor this in QuickApp
280 debug ("Confirmation asked") 282 debug ("Confirmation asked")
281 answer_data={} 283 answer_data={}
313 315
314 dlg.Destroy() 316 dlg.Destroy()
315 317
316 def actionResult(self, type, id, data): 318 def actionResult(self, type, id, data):
317 debug ("actionResult: type = [%s] id = [%s] data = [%s]" % (type, id, data)) 319 debug ("actionResult: type = [%s] id = [%s] data = [%s]" % (type, id, data))
320 if not id in self.current_action_ids:
321 debug ('unknown id, ignoring')
322 return
318 if type == "SUPPRESS": 323 if type == "SUPPRESS":
319 self.current_action_ids.remove(id) 324 self.current_action_ids.remove(id)
320 elif type == "SUCCESS": 325 elif type == "SUCCESS":
321 self.current_action_ids.remove(id) 326 self.current_action_ids.remove(id)
322 dlg = wx.MessageDialog(self, data["message"], 327 dlg = wx.MessageDialog(self, data["message"],
347 self.current_action_ids.remove(id) 352 self.current_action_ids.remove(id)
348 if self.current_action_ids_cb.has_key(id): 353 if self.current_action_ids_cb.has_key(id):
349 callback = self.current_action_ids_cb[id] 354 callback = self.current_action_ids_cb[id]
350 del self.current_action_ids_cb[id] 355 del self.current_action_ids_cb[id]
351 callback(data) 356 callback(data)
352 print ("Dict of dict found as result")
353 else: 357 else:
354 error ("FIXME FIXME FIXME: type [%s] not implemented" % type) 358 error ("FIXME FIXME FIXME: type [%s] not implemented" % type)
355 raise NotImplementedError 359 raise NotImplementedError
356 360
357 361
382 386
383 ### events ### 387 ### events ###
384 388
385 def onContactActivated(self, jid): 389 def onContactActivated(self, jid):
386 debug ("onContactActivated: %s", jid) 390 debug ("onContactActivated: %s", jid)
387 if self.chat_wins[jid].IsShown(): 391 if self.chat_wins[jid.short].IsShown():
388 self.chat_wins[jid].Hide() 392 self.chat_wins[jid.short].Hide()
389 else: 393 else:
390 self.chat_wins[jid].Show() 394 self.chat_wins[jid.short].Show()
391 395
392 def onConnectRequest(self, e): 396 def onConnectRequest(self, e):
393 self.bridge.connect() 397 self.bridge.connect()
394 398
395 def onDisconnectRequest(self, e): 399 def onDisconnectRequest(self, e):
396 self.bridge.disconnect() 400 self.bridge.disconnect()
397 401
398 def __updateStatus(self): 402 def __updateStatus(self):
399 show = const_STATUS[self.statusBox.GetValue()] 403 show = filter(lambda x:x[1] == self.statusBox.GetValue(), const_STATUS)[0][0]
400 status = self.statusTxt.GetValue() 404 status = self.statusTxt.GetValue()
401 self.bridge.setPresence(show=show, status=status) 405 self.bridge.setPresence(show=show, statuses={'default':status}) #FIXME: manage multilingual statuses
402 406
403 def onStatusChange(self, e): 407 def onStatusChange(self, e):
404 debug("Status change request") 408 debug("Status change request")
405 self.__updateStatus() 409 self.__updateStatus()
406 410
443 'Contact suppression', 447 'Contact suppression',
444 wx.YES_NO | wx.ICON_QUESTION 448 wx.YES_NO | wx.ICON_QUESTION
445 ) 449 )
446 450
447 if dlg.ShowModal() == wx.ID_YES: 451 if dlg.ShowModal() == wx.ID_YES:
448 info("Unsubsribing %s presence", target.short) 452 info("Unsubscribing %s presence", target.short)
449 self.bridge.delContact(target.short) 453 self.bridge.delContact(target.short)
450 454
451 dlg.Destroy() 455 dlg.Destroy()
452 456
453 def onShowProfile(self, e): 457 def onShowProfile(self, e):
474 def onFindGateways(self, e): 478 def onFindGateways(self, e):
475 debug("Find Gateways request") 479 debug("Find Gateways request")
476 id = self.bridge.findGateways(self.whoami.domain) 480 id = self.bridge.findGateways(self.whoami.domain)
477 self.current_action_ids.add(id) 481 self.current_action_ids.add(id)
478 self.current_action_ids_cb[id] = self.onGatewaysFound 482 self.current_action_ids_cb[id] = self.onGatewaysFound
479 print "Find Gateways id=", id
480 483
481 def onGatewaysFound(self, data): 484 def onGatewaysFound(self, data):
482 """Called when SàT has found the server gateways""" 485 """Called when SàT has found the server gateways"""
483 target = data['__private__']['target'] 486 target = data['__private__']['target']
484 del data['__private__'] 487 del data['__private__']