Mercurial > libervia-backend
annotate frontends/sortilege/sortilege @ 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 | c4badbf3dd97 |
children | 6455fb62ff83 |
rev | line source |
---|---|
0 | 1 #!/usr/bin/python |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 sortilege: a SAT frontend | |
6 Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org) | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 | |
23 import curses | |
24 import pdb | |
25 from window import Window | |
26 from editbox import EditBox | |
27 from statusbar import StatusBar | |
28 from chat import Chat | |
29 from tools.jid import JID | |
30 import logging | |
31 from logging import debug, info, error | |
32 import locale | |
33 import sys, os | |
34 import gobject | |
35 import time | |
36 from curses import ascii | |
37 import locale | |
38 from signal import signal, SIGWINCH | |
39 import fcntl | |
40 import struct | |
41 import termios | |
42 from boxsizer import BoxSizer | |
43 from quick_frontend.quick_chat_list import QuickChatList | |
44 from quick_frontend.quick_contact_list import QuickContactList | |
45 from quick_frontend.quick_app import QuickApp | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
46 from quick_frontend.quick_contact_management import QuickContactManagement |
0 | 47 |
48 ### logging configuration FIXME: put this elsewhere ### | |
49 logging.basicConfig(level=logging.CRITICAL, #TODO: configure it top put messages in a log file | |
50 format='%(message)s') | |
51 ### | |
52 | |
53 const_APP_NAME = "Sortilège" | |
54 const_CONTACT_WIDTH = 30 | |
55 | |
56 def ttysize(): | |
57 """This function return term size. | |
58 Comes from Donn Cave from python list mailing list""" | |
59 buf = 'abcdefgh' | |
60 buf = fcntl.ioctl(0, termios.TIOCGWINSZ, buf) | |
61 row, col, rpx, cpx = struct.unpack('hhhh', buf) | |
62 return row, col | |
63 | |
64 def C(k): | |
65 """return the value of Ctrl+key""" | |
66 return ord(ascii.ctrl(k)) | |
67 | |
68 class ChatList(QuickChatList): | |
69 """This class manage the list of chat windows""" | |
70 | |
71 def __init__(self, host): | |
72 QuickChatList.__init__(self, host) | |
73 self.sizer=host.sizer | |
74 | |
75 def createChat(self, name): | |
76 chat = Chat(name, self.host) | |
77 self.sizer.appendColum(0,chat) | |
78 self.sizer.update() | |
79 return chat | |
80 | |
81 | |
82 class ContactList(Window, QuickContactList): | |
83 | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
84 def __init__(self, host, CM): |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
85 QuickContactList.__init__(self, CM) |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
86 self.host = host |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
87 self.jid_list = [] |
0 | 88 self.__index=0 #indicate which contact is selected (ie: where we are) |
89 Window.__init__(self, stdscr, stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0, True, "Contact List", code=code) | |
90 | |
91 def resize(self, height, width, y, x): | |
92 Window.resize(self, height, width, y, x) | |
93 self.update() | |
94 | |
95 def resizeAdapt(self): | |
96 """Adapt window size to stdscr size. | |
97 Must be called when stdscr is resized.""" | |
98 self.resize(stdscr.getmaxyx()[0]-2,const_CONTACT_WIDTH,0,0) | |
99 self.update() | |
100 | |
101 def registerEnterCB(self, CB): | |
102 self.__enterCB=CB | |
103 | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
104 def replace(self, jid): |
0 | 105 """add a contact to the list""" |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
106 name = self.CM.getAttr(jid,'name') |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
107 self.jid_list.append(jid.short) |
0 | 108 self.update() |
109 | |
110 def indexUp(self): | |
111 """increment select contact index""" | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
112 if self.__index < len(self.jid_list)-1: #we dont want to select a missing contact |
0 | 113 self.__index = self.__index + 1 |
114 self.update() | |
115 | |
116 def indexDown(self): | |
117 """decrement select contact index""" | |
118 if self.__index > 0: | |
119 self.__index = self.__index - 1 | |
120 self.update() | |
121 | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
122 def disconnect(self, jid): |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
123 """for now, we just remove the contact""" |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
124 self.remove(jid) |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
125 |
0 | 126 def remove(self, jid): |
127 """remove a contact from the list""" | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
128 self.jid_list.remove(jid.short) |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
129 if self.__index >= len(self.jid_list) and self.__index > 0: #if select index is out of border, we put it on the last contact |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
130 self.__index = len(self.jid_list)-1 |
0 | 131 self.update() |
132 | |
133 def update(self): | |
134 """redraw all the window""" | |
135 if self.isHidden(): | |
136 return | |
137 Window.update(self) | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
138 self.jid_list.sort() |
0 | 139 begin=0 if self.__index<self.rHeight else self.__index-self.rHeight+1 |
140 idx=0 | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
141 for item in self.jid_list[begin:self.rHeight+begin]: |
0 | 142 attr = curses.A_REVERSE if ( self.isActive() and (idx+begin) == self.__index ) else 0 |
143 centered = item.center(self.rWidth) ## it's nicer in the center :) | |
144 self.addYXStr(idx, 0, centered, attr) | |
145 idx = idx + 1 | |
146 | |
147 self.noutrefresh() | |
148 | |
149 def handleKey(self, k): | |
150 if k == curses.KEY_UP: | |
151 self.indexDown() | |
152 elif k == curses.KEY_DOWN: | |
153 self.indexUp() | |
154 elif k == ascii.NL: | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
155 if not self.jid_list: |
0 | 156 return |
157 try: | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
158 self.__enterCB(self.jid_list[self.__index]) |
0 | 159 except NameError: |
160 pass # TODO: thrown an error here | |
161 | |
162 class SortilegeApp(QuickApp): | |
163 | |
164 def __init__(self): | |
165 #debug(const_APP_NAME+" init...") | |
166 | |
167 ## unicode support ## | |
168 locale.setlocale(locale.LC_ALL, '') | |
169 global code | |
170 code = locale.getpreferredencoding() | |
171 self.code=code | |
172 | |
173 ## main loop setup ## | |
174 self.loop=gobject.MainLoop() | |
175 gobject.io_add_watch(0, gobject.IO_IN, self.loopCB) | |
176 | |
177 ## misc init stuff ## | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
178 self.CM = QuickContactManagement() |
0 | 179 self.listWins=[] |
180 self.chatParams={'timestamp':True, | |
181 'color':True, | |
182 'short_nick':False} | |
183 | |
184 def start(self): | |
185 curses.wrapper(self.start_curses) | |
186 | |
187 def start_curses(self, win): | |
188 global stdscr | |
189 stdscr = win | |
190 self.stdscr = stdscr | |
191 curses.raw() #we handle everything ourself | |
192 curses.curs_set(False) | |
193 stdscr.nodelay(True) | |
194 | |
195 ## colours ## | |
196 self.color(True) | |
197 | |
198 ## windows ## | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
199 self.contactList = ContactList(self, self.CM) |
0 | 200 self.editBar = EditBox(stdscr, "> ", self.code) |
201 self.editBar.activate(False) | |
202 self.statusBar = StatusBar(stdscr, self.code) | |
203 self.statusBar.hide(True) | |
204 self.addWin(self.contactList) | |
205 self.addWin(self.editBar) | |
206 self.addWin(self.statusBar) | |
207 self.sizer=BoxSizer(stdscr) | |
208 self.sizer.appendRow(self.contactList) | |
209 self.sizer.appendRow(self.statusBar) | |
210 self.sizer.appendRow(self.editBar) | |
211 self.currentChat=None | |
212 | |
213 self.contactList.registerEnterCB(self.onContactChoosed) | |
214 self.editBar.registerEnterCB(self.onTextEntered) | |
215 | |
216 self.chat_wins=ChatList(self) | |
217 | |
218 QuickApp.__init__(self) #XXX: yes it's an unusual place for the constructor of a parent class, but the init order is important | |
219 | |
220 signal (SIGWINCH, self.onResize) #we manage SIGWINCH ourselves, because the loop is not called otherwise | |
221 | |
222 #last but not least, we adapt windows' sizes | |
223 self.sizer.update() | |
224 self.editBar.replace_cur() | |
225 curses.doupdate() | |
226 | |
227 self.loop.run() | |
228 | |
229 def addWin(self, win): | |
230 self.listWins.append(win) | |
231 | |
232 def color(self, activate=True): | |
233 if activate: | |
234 debug ("Activation des couleurs") | |
235 curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) | |
236 curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) | |
237 else: | |
238 debug ("Desactivation des couleurs") | |
239 curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) | |
240 curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) | |
241 | |
242 | |
243 def showChat(self, chat): | |
244 debug ("show chat") | |
245 if self.currentChat: | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
246 debug ("hiding %s", self.currentChat) |
0 | 247 self.chat_wins[self.currentChat].hide() |
248 self.currentChat=chat | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
249 debug ("showing %s", self.currentChat) |
0 | 250 self.chat_wins[self.currentChat].show() |
251 self.chat_wins[self.currentChat].update() | |
252 | |
253 | |
254 ### EVENTS ### | |
255 | |
256 def onContactChoosed(self, jid_txt): | |
257 """Called when a contact is selected in contact list.""" | |
258 jid=JID(jid_txt) | |
259 debug ("contact choose: %s", jid) | |
260 self.showChat(jid.short) | |
261 self.statusBar.remove_item(jid.short) | |
262 if len(self.statusBar)==0: | |
263 self.statusBar.hide() | |
264 self.sizer.update() | |
265 | |
266 | |
267 def onTextEntered(self, text): | |
268 jid=JID(self.whoami) | |
269 self.bridge.sendMessage(self.currentChat, text) | |
270 | |
271 def showDialog(self, message, title, type="info"): | |
272 if type==question: | |
273 raise NotImplementedError | |
274 pass | |
275 | |
276 | |
51
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
277 def presenceUpdate(self, jabber_id, show, priority, statuses): |
8c67ea98ab91
frontend improved to take into account new SàT features
Goffi <goffi@goffi.org>
parents:
32
diff
changeset
|
278 QuickApp.presenceUpdate(self, jabber_id, show, priority, statuses) |
0 | 279 self.editBar.replace_cur() |
280 curses.doupdate() | |
281 | |
22
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
282 def askConfirmation(self, type, id, data): |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
283 #FIXME |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
284 info ("FIXME: askConfirmation not implemented") |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
285 |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
286 def actionResult(self, type, id, data): |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
287 #FIXME |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
288 info ("FIXME: actionResult not implemented") |
bb72c29f3432
added action cb mechanism for buttons. Tested with a temporary new user registration button.
Goffi <goffi@goffi.org>
parents:
0
diff
changeset
|
289 |
0 | 290 def newMessage(self, from_jid, msg, type, to_jid): |
291 QuickApp.newMessage(self, from_jid, msg, type, to_jid) | |
292 sender=JID(from_jid) | |
293 addr=JID(to_jid) | |
294 win = addr if sender.short == self.whoami.short else sender #FIXME: duplicate code with QuickApp | |
295 if (self.currentChat==None): | |
296 self.currentChat=win.short | |
297 self.showChat(win.short) | |
298 | |
299 # we show the window in the status bar | |
300 if not self.currentChat == win.short: | |
301 self.statusBar.add_item(win.short) | |
302 self.statusBar.show() | |
303 self.sizer.update() | |
304 self.statusBar.update() | |
305 | |
306 self.editBar.replace_cur() | |
307 curses.doupdate() | |
308 | |
309 def onResize(self, sig, stack): | |
310 """Called on SIGWINCH. | |
311 resize the screen and launch the loop""" | |
312 height, width = ttysize() | |
313 curses.resizeterm(height, width) | |
314 gobject.idle_add(self.callOnceLoop) | |
315 | |
316 def callOnceLoop(self): | |
317 """Call the loop and return false (for not being called again by gobject mainloop). | |
318 Usefull for calling loop when there is no input in stdin""" | |
319 self.loopCB() | |
320 return False | |
321 | |
322 def __key_handling(self, k): | |
323 """Handle key and transmit to active window.""" | |
324 | |
325 ### General keys, handled _everytime_ ### | |
326 if k == C('x'): | |
327 if os.getenv('TERM')=='screen': | |
328 os.system('screen -X remove') | |
329 else: | |
330 self.loop.quit() | |
331 | |
332 ## windows navigation | |
333 elif k == C('l') and not self.contactList.isHidden(): | |
334 """We go to the contact list""" | |
335 self.contactList.activate(not self.contactList.isActive()) | |
336 if self.currentChat: | |
337 self.editBar.activate(not self.contactList.isActive()) | |
338 | |
339 elif k == curses.KEY_F2: | |
340 self.contactList.hide(not self.contactList.isHidden()) | |
341 if self.contactList.isHidden(): | |
342 self.contactList.activate(False) #TODO: auto deactivation when hiding ? | |
343 if self.currentChat: | |
344 self.editBar.activate(True) | |
345 self.sizer.update() | |
346 | |
347 ## Chat Params ## | |
348 elif k == C('c'): | |
349 self.chatParams["color"] = not self.chatParams["color"] | |
350 self.color(self.chatParams["color"]) | |
351 elif k == C('t'): | |
352 self.chatParams["timestamp"] = not self.chatParams["timestamp"] | |
353 self.chat_wins[self.currentChat].update() | |
354 elif k == C('s'): | |
355 self.chatParams["short_nick"] = not self.chatParams["short_nick"] | |
356 self.chat_wins[self.currentChat].update() | |
357 | |
358 ## misc ## | |
359 elif k == curses.KEY_RESIZE: | |
360 stdscr.erase() | |
361 height, width = stdscr.getmaxyx() | |
362 if height<5 and width<35: | |
363 stdscr.addstr("Pleeeeasse, I can't even breathe !") | |
364 else: | |
365 for win in self.listWins: | |
366 win.resizeAdapt() | |
367 for win in self.chat_wins.keys(): | |
368 self.chat_wins[win].resizeAdapt() | |
369 self.sizer.update() # FIXME: everything need to be managed by the sizer | |
370 | |
371 ## we now throw the key to win handlers ## | |
372 else: | |
373 for win in self.listWins: | |
374 if win.isActive(): | |
375 win.handleKey(k) | |
376 if self.currentChat: | |
377 self.chat_wins[self.currentChat].handleKey(k) | |
378 | |
379 def loopCB(self, source="", cb_condition=""): | |
380 """This callback is called by the main loop""" | |
381 #pressed = self.contactList.window.getch() | |
382 pressed = stdscr.getch() | |
383 if pressed != curses.ERR: | |
384 self.__key_handling(pressed) | |
385 self.editBar.replace_cur() | |
386 curses.doupdate() | |
387 | |
388 | |
389 return True | |
390 | |
391 | |
392 sat = SortilegeApp() | |
393 sat.start() |