comparison frontends/src/primitivus/primitivus @ 223:86d249b6d9b7

Files reorganisation
author Goffi <goffi@goffi.org>
date Wed, 29 Dec 2010 01:06:29 +0100
parents frontends/primitivus/primitivus@3198bfd66daa
children fd9b7834d98a
comparison
equal deleted inserted replaced
222:3198bfd66daa 223:86d249b6d9b7
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """
5 Primitivus: a SAT frontend
6 Copyright (C) 2009, 2010 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 from quick_frontend.quick_app import QuickApp
24 from quick_frontend.quick_chat_list import QuickChatList
25 from quick_frontend.quick_contact_list import QuickContactList
26 from quick_frontend.quick_contact_management import QuickContactManagement
27 import urwid
28 from profile_manager import ProfileManager
29 from contact_list import ContactList
30 from chat import Chat
31 from gateways import GatewaysManager
32 from urwid_satext import sat_widgets
33 import logging
34 from logging import debug, info, error
35 import sys, os
36 from tools.jid import JID
37 from xmlui import XMLUI
38 from progress import Progress
39
40
41 ### logging configuration FIXME: put this elsewhere ###
42 logging.basicConfig(level=logging.CRITICAL, #TODO: configure it to put messages in a log file
43 format='%(message)s')
44 ###
45
46 const_APP_NAME = "Primitivus"
47 const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'),
48 ('title_focus', 'white,bold', 'light gray', 'standout,underline'),
49 ('selected', 'default', 'dark red'),
50 ('selected_focus', 'default,bold', 'dark red'),
51 ('default', 'default', 'default'),
52 ('default_focus', 'default,bold', 'default'),
53 ('alert', 'default,underline', 'default'),
54 ('alert_focus', 'default,bold,underline', 'default'),
55 ('date', 'light gray', 'default'),
56 ('my_nick', 'dark red,bold', 'default'),
57 ('other_nick', 'dark cyan,bold', 'default'),
58 ('menubar', 'light gray,bold', 'dark red'),
59 ('menubar_focus', 'light gray,bold', 'dark green'),
60 ('selected_menu', 'light gray,bold', 'dark green'),
61 ('menuitem', 'light gray,bold', 'dark red'),
62 ('menuitem_focus', 'light gray,bold', 'dark green'),
63 ('notifs', 'black,bold', 'yellow'),
64 ('notifs_focus', 'dark red', 'yellow'),
65 ('card_neutral', 'dark gray', 'white', 'standout,underline'),
66 ('card_neutral_selected', 'dark gray', 'dark green', 'standout,underline'),
67 ('card_special', 'brown', 'white', 'standout,underline'),
68 ('card_special_selected', 'brown', 'dark green', 'standout,underline'),
69 ('card_red', 'dark red', 'white', 'standout,underline'),
70 ('card_red_selected', 'dark red', 'dark green', 'standout,underline'),
71 ('card_black', 'black', 'white', 'standout,underline'),
72 ('card_black_selected', 'black', 'dark green', 'standout,underline'),
73 ('directory', 'dark cyan, bold', 'default'),
74 ('directory_focus', 'dark cyan, bold', 'dark green'),
75 ('separator', 'brown', 'default'),
76 ('warning', 'light red', 'default'),
77 ('progress_normal', 'default', 'black'),
78 ('progress_complete', 'default', 'light red'),
79 ]
80
81 class ChatList(QuickChatList):
82 """This class manage the list of chat windows"""
83
84 def __init__(self, host):
85 QuickChatList.__init__(self, host)
86
87 def createChat(self, target):
88 return Chat(target, self.host)
89
90 class PrimitivusApp(QuickApp):
91
92 def __init__(self):
93 self.CM = QuickContactManagement() #FIXME: not the best place
94 QuickApp.__init__(self)
95
96 ## main loop setup ##
97 self.main_widget = ProfileManager(self)
98 self.loop = urwid.MainLoop(self.main_widget, const_PALETTE, event_loop=urwid.GLibEventLoop(), input_filter=self.inputFilter, unhandled_input=self.keyHandler)
99
100 ##misc setup##
101 self.chat_wins=ChatList(self)
102 self.notBar = sat_widgets.NotificationBar()
103 urwid.connect_signal(self.notBar,'change',self.onNotification)
104 self.progress_wid = Progress(self)
105 urwid.connect_signal(self.notBar.progress,'click',lambda x:self.addWindow(self.progress_wid))
106 self.__saved_overlay = None
107
108 def debug(self):
109 """convenient method to reset screen and launch p(u)db"""
110 try:
111 import pudb
112 pudb.set_trace()
113 except:
114 import os,pdb
115 os.system('reset')
116 print 'Entered debug mode'
117 pdb.set_trace()
118
119 def writeLog(self, log, file_name='/tmp/primitivus_log'):
120 """method to write log in a temporary file, useful for debugging"""
121 with open(file_name, 'a') as f:
122 f.write(log+"\n")
123
124 def redraw(self):
125 """redraw the screen"""
126 self.loop.draw_screen()
127
128 def start(self):
129 self.i = 0
130 self.loop.set_alarm_in(0,lambda a,b: self.postInit())
131 self.loop.run()
132
133 def inputFilter(self, input, raw):
134 if self.__saved_overlay and input != ['ctrl s']:
135 return
136 for i in input:
137 if isinstance(i,tuple):
138 if i[0] == 'mouse press':
139 if i[1] == 4: #Mouse wheel up
140 input[input.index(i)] = 'up'
141 if i[1] == 5: #Mouse wheel down
142 input[input.index(i)] = 'down'
143 return input
144
145 def keyHandler(self, input):
146 if input == 'meta m':
147 """User want to (un)hide the menu roller"""
148 try:
149 if self.main_widget.header == None:
150 self.main_widget.header = self.menu_roller
151 else:
152 self.main_widget.header = None
153 except AttributeError:
154 pass
155 elif input == 'ctrl n':
156 """User wants to see next notification"""
157 self.notBar.showNext()
158 elif input == 'ctrl s':
159 """User wants to (un)hide overlay window"""
160 if isinstance(self.loop.widget,urwid.Overlay):
161 self.__saved_overlay = self.loop.widget
162 self.loop.widget = self.main_widget
163 else:
164 if self.__saved_overlay:
165 self.loop.widget = self.__saved_overlay
166 self.__saved_overlay = None
167
168 elif input == 'ctrl d' and 'D' in self.bridge.getVersion(): #Debug only for dev versions
169 self.debug()
170 elif input == 'f2': #user wants to (un)hide the contact_list
171 try:
172 center_widgets = self.center_part.widget_list
173 if self.contactList in center_widgets:
174 self.center_part.set_focus(0) #necessary as the focus change to the next object, we can go out of range if we are on the last object of self.center_part
175 center_widgets.remove(self.contactList)
176 del self.center_part.column_types[0]
177 else:
178 center_widgets.insert(0, self.contactList)
179 self.center_part.column_types.insert(0, ('weight', 2))
180 except AttributeError:
181 #The main widget is not built (probably in Profile Manager)
182 pass
183 elif input == 'window resize':
184 width,height = self.loop.screen_size
185 if height<=5 and width<=35:
186 if not 'save_main_widget' in dir(self):
187 self.save_main_widget = self.loop.widget
188 self.loop.widget = urwid.Filler(urwid.Text(_("Pleeeeasse, I can't even breathe !")))
189 else:
190 if 'save_main_widget' in dir(self):
191 self.loop.widget = self.save_main_widget
192 del self.save_main_widget
193 try:
194 return self.menu_roller.checkShortcuts(input)
195 except AttributeError:
196 return input
197
198 def __buildMenuRoller(self):
199 menu = sat_widgets.Menu(self.loop)
200 general = _("General")
201 menu.addMenu(general, _("Connect"), self.onConnectRequest)
202 menu.addMenu(general, _("Disconnect"), self.onDisconnectRequest)
203 menu.addMenu(general, _("Parameters"), self.onParam)
204 menu.addMenu(general, _("About"), self.onAboutRequest)
205 menu.addMenu(general, _("Exit"), self.onExitRequest, 'ctrl x')
206 contact = _("Contact")
207 menu.addMenu(contact, _("Add contact"), self.onAddContactRequest)
208 menu.addMenu(contact, _("Remove contact"), self.onRemoveContactRequest)
209 communication = _("Communication")
210 menu.addMenu(communication, _("Join room"), self.onJoinRoomRequest, 'meta j')
211 menu.addMenu(communication, _("Find Gateways"), self.onFindGatewaysRequest, 'meta g')
212 #additionals menus
213 #FIXME: do this in a more generic way (in quickapp)
214 add_menus = self.bridge.getMenus()
215 def add_menu_cb(menu):
216 category, item = menu
217 id = self.bridge.callMenu(category, item, "NORMAL", self.profile)
218 self.current_action_ids.add(id)
219 for new_menu in add_menus:
220 category,item,type = new_menu
221 assert(type=="NORMAL") #TODO: manage other types
222 menu.addMenu(unicode(category), unicode(item), add_menu_cb)
223
224 menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)])
225 return menu_roller
226
227 def __buildMainWidget(self):
228 self.contactList = ContactList(self, self.CM, on_click = self.contactSelected, on_change=lambda w: self.redraw())
229 #self.center_part = urwid.Columns([('weight',2,self.contactList),('weight',8,Chat('',self))])
230 self.center_part = urwid.Columns([('weight',2,self.contactList), ('weight',8,urwid.Filler(urwid.Text('')))])
231 self.editBar = sat_widgets.AdvancedEdit('> ')
232 self.editBar.setCompletionMethod(self._nick_completion)
233 urwid.connect_signal(self.editBar,'click',self.onTextEntered)
234 self.menu_roller = self.__buildMenuRoller()
235 self.main_widget = sat_widgets.FocusFrame(self.center_part, header=self.menu_roller, footer=self.editBar, focus_part='footer')
236 return self.main_widget
237
238 def _nick_completion(self, text, completion_data):
239 """Completion method which complete pseudo in group chat
240 for params, see AdvancedEdit"""
241 contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once
242 if contact:
243 chat = self.chat_wins[contact]
244 if chat.type != "group":
245 return text
246 space = text.rfind(" ")
247 start = text[space+1:]
248 nicks = list(chat.occupants)
249 nicks.sort()
250 try:
251 start_idx=nicks.index(completion_data['last_nick'])+1
252 if start_idx == len(nicks):
253 start_idx = 0
254 except (KeyError,ValueError):
255 start_idx = 0
256 for idx in range(start_idx,len(nicks)) + range(0,start_idx):
257 if nicks[idx].lower().startswith(start.lower()):
258 completion_data['last_nick'] = nicks[idx]
259 return text[:space+1] + nicks[idx] + (': ' if space < 0 else '')
260 return text
261
262
263
264 def plug_profile(self, profile_key='@DEFAULT@'):
265 self.loop.widget = self.__buildMainWidget()
266 QuickApp.plug_profile(self, profile_key)
267
268 def removePopUp(self, widget=None):
269 "Remove current pop-up, and if there is other in queue, show it"
270 self.loop.widget = self.main_widget
271 next_popup = self.notBar.getNextPopup()
272 if next_popup:
273 #we still have popup to show, we display it
274 self.showPopUp(next_popup)
275
276 def showPopUp(self, pop_up_widget, perc_width=40, perc_height=40):
277 "Show a pop-up window if possible, else put it in queue"
278 if not isinstance(self.loop.widget,urwid.Overlay):
279 display_widget = urwid.Overlay(pop_up_widget, self.main_widget, 'center', ('relative', perc_width), 'middle', ('relative', perc_height))
280 self.loop.widget = display_widget
281 self.redraw()
282 else:
283 self.notBar.addPopUp(pop_up_widget)
284
285 def notify(self, message):
286 """"Notify message to user via notification bar"""
287 self.notBar.addMessage(message)
288
289 def addWindow(self, widget):
290 """Display a window if possible,
291 else add it in the notification bar queue
292 @param widget: BoxWidget"""
293 assert(len(self.center_part.widget_list)<=2)
294 wid_idx = len(self.center_part.widget_list)-1
295 self.center_part.widget_list[wid_idx] = widget
296 self.menu_roller.removeMenu(_('Chat menu'))
297 self.contactList.unselectAll()
298 self.redraw()
299
300 def removeWindow(self):
301 """Remove window showed on the right column"""
302 #TODO: to a better Window management than this crappy hack
303 assert(len(self.center_part.widget_list)<=2)
304 wid_idx = len(self.center_part.widget_list)-1
305 self.center_part.widget_list[wid_idx] = urwid.Filler(urwid.Text(''))
306 self.center_part.set_focus(0)
307 self.redraw()
308
309 def addProgress (self, id, message):
310 """Follow a SàT progress bar
311 @param id: SàT id of the progression
312 @param message: message to show to identify the progression"""
313 self.progress_wid.addProgress(id, message)
314
315 def setProgress(self, percentage):
316 """Set the progression shown in notification bar"""
317 self.notBar.setProgress(percentage)
318
319 def contactSelected(self, contact_list):
320 contact = contact_list.get_contact()
321 if contact:
322 assert(len(self.center_part.widget_list)==2)
323 self.center_part.widget_list[1] = self.chat_wins[contact]
324 self.menu_roller.addMenu(_('Chat menu'), self.chat_wins[contact].getMenu())
325
326 def onTextEntered(self, editBar):
327 """Called when text is entered in the main edit bar"""
328 contact = self.contactList.get_contact() ###Based on the fact that there is currently only one contact selectableat once
329 if contact:
330 chat = self.chat_wins[contact]
331 self.bridge.sendMessage(contact,
332 editBar.get_edit_text(),
333 type = "groupchat" if chat.type == 'group' else "chat",
334 profile_key=self.profile)
335 editBar.set_edit_text('')
336
337 def newMessage(self, from_jid, msg, type, to_jid, profile):
338 if not self.check_profile(profile):
339 return
340 QuickApp.newMessage(self, from_jid, msg, type, to_jid, profile)
341 sender = JID(from_jid)
342 if JID(self.contactList.selected).short != sender.short:
343 self.contactList.putAlert(sender)
344
345 def _dialogOkCb(self, widget, data):
346 self.removePopUp()
347 answer_cb = data[0]
348 answer_data = [data[1]] if data[1] else []
349 answer_cb(True, *answer_data)
350
351 def _dialogCancelCb(self, widget, data):
352 self.removePopUp()
353 answer_cb = data[0]
354 answer_data = [data[1]] if data[1] else []
355 answer_cb(False, *answer_data)
356
357
358 def showDialog(self, message, title="", type="info", answer_cb = None, answer_data = None):
359 if type == 'info':
360 popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
361 flags = wx.OK | wx.ICON_INFORMATION
362 elif type == 'error':
363 popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
364 elif type == 'yes/no':
365 popup = sat_widgets.ConfirmDialog(unicode(message),
366 yes_cb=self._dialogOkCb, yes_value = (answer_cb, answer_data),
367 no_cb=self._dialogCancelCb, no_value = (answer_cb, answer_data))
368 else:
369 popup = sat_widgets.Alert(unicode(title), unicode(message), ok_cb=answer_cb or self.removePopUp) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
370 error(_('unmanaged dialog type: %s'), type)
371 self.showPopUp(popup)
372
373 def onNotification(self, notBar):
374 """Called when a new notification has been received"""
375 if not isinstance(self.main_widget, sat_widgets.FocusFrame):
376 #if we are not in the main configuration, we ignore the notifications bar
377 return
378 if isinstance(self.main_widget.footer,sat_widgets.AdvancedEdit):
379 if not self.notBar.canHide():
380 #the notification bar is not visible and has usefull informations, we show it
381 pile = urwid.Pile([self.notBar, self.editBar])
382 self.main_widget.footer = pile
383 else:
384 if not isinstance(self.main_widget.footer, urwid.Pile):
385 error(_("INTERNAL ERROR: Unexpected class for main widget's footer"))
386 assert(False)
387 if self.notBar.canHide():
388 #No notification left, we can hide the bar
389 self.main_widget.footer = self.editBar
390
391 def actionResult(self, type, id, data):
392 if not id in self.current_action_ids:
393 debug (_('unknown id, ignoring'))
394 return
395 if type == "SUPPRESS":
396 self.current_action_ids.remove(id)
397 elif type == "XMLUI":
398 self.current_action_ids.remove(id)
399 debug (_("XML user interface received"))
400 misc = {}
401 #FIXME FIXME FIXME: must clean all this crap !
402 title = _('Form')
403 if data['type'] == 'registration':
404 title = _('Registration')
405 misc['target'] = data['target']
406 misc['action_back'] = self.bridge.gatewayRegister
407 ui = XMLUI(self, title=title, xml_data = data['xml'], misc = misc)
408 if data['type'] == 'registration':
409 ui.show('popup')
410 else:
411 ui.show('window')
412 elif type == "ERROR":
413 self.current_action_ids.remove(id)
414 self.showPopUp(sat_widgets.Alert(_("Error"), unicode(data["message"]), ok_cb=self.removePopUp)) #FIXME: remove unicode here when DBus Bridge will no return dbus.String anymore
415 elif type == "RESULT":
416 self.current_action_ids.remove(id)
417 if self.current_action_ids_cb.has_key(id):
418 callback = self.current_action_ids_cb[id]
419 del self.current_action_ids_cb[id]
420 callback(data)
421 elif type == "DICT_DICT":
422 self.current_action_ids.remove(id)
423 if self.current_action_ids_cb.has_key(id):
424 callback = self.current_action_ids_cb[id]
425 del self.current_action_ids_cb[id]
426 callback(data)
427 else:
428 error (_("FIXME FIXME FIXME: type [%s] not implemented") % type)
429 raise NotImplementedError
430
431 ##DIALOGS CALLBACKS##
432 def onJoinRoom(self, button, edit):
433 self.removePopUp()
434 room_jid = JID(edit.get_edit_text())
435 if room_jid.is_valid():
436 self.bridge.joinMUC(room_jid.domain, room_jid.node, self.profiles[self.profile]['whoami'].node, self.profile)
437 else:
438 message = _("'%s' is an invalid JID !") % room_jid
439 error (message)
440 self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp))
441
442 def onAddContact(self, button, edit):
443 self.removePopUp()
444 jid=JID(edit.get_edit_text())
445 if jid.is_valid():
446 self.bridge.addContact(jid.short, profile_key=self.profile)
447 else:
448 message = _("'%s' is an invalid JID !") % jid
449 error (message)
450 self.showPopUp(sat_widgets.Alert(_("Error"), message, ok_cb=self.removePopUp))
451
452 def onRemoveContact(self, button):
453 self.removePopUp()
454 info(_("Unsubscribing %s presence"),self.contactList.get_contact())
455 self.bridge.delContact(self.contactList.get_contact(), profile_key=self.profile)
456
457 #MENU EVENTS#
458 def onConnectRequest(self, menu):
459 self.bridge.connect(self.profile)
460
461 def onDisconnectRequest(self, menu):
462 self.bridge.disconnect(self.profile)
463
464 def onParam(self, menu):
465 params = XMLUI(self,xml_data=self.bridge.getParamsUI(self.profile))
466 self.addWindow(params)
467
468 def onExitRequest(self, menu):
469 QuickApp.onExit(self)
470 raise urwid.ExitMainLoop()
471
472 def onJoinRoomRequest(self, menu):
473 """User wants to join a MUC room"""
474 pop_up_widget = sat_widgets.InputDialog(_("Entering a MUC room"), _("Please enter MUC's JID"), default_txt = 'room@muc_service.server.tld', cancel_cb=self.removePopUp, ok_cb=self.onJoinRoom)
475 self.showPopUp(pop_up_widget)
476
477 def onFindGatewaysRequest(self, e):
478 debug(_("Find Gateways request"))
479 id = self.bridge.findGateways(self.profiles[self.profile]['whoami'].domain, self.profile)
480 self.current_action_ids.add(id)
481 self.current_action_ids_cb[id] = self.onGatewaysFound
482
483 def onAddContactRequest(self, menu):
484 pop_up_widget = sat_widgets.InputDialog(_("Adding a contact"), _("Please enter new contact JID"), default_txt = 'name@server.tld', cancel_cb=self.removePopUp, ok_cb=self.onAddContact)
485 self.showPopUp(pop_up_widget)
486
487 def onRemoveContactRequest(self, menu):
488 contact = self.contactList.get_contact()
489 if not contact:
490 self.showPopUp(sat_widgets.Alert(_("Error"), _("You have not selected any contact to delete !"), ok_cb=self.removePopUp))
491 else:
492 pop_up_widget = sat_widgets.ConfirmDialog(_("Are you sure you want to delete the contact [%s] ?" % contact), yes_cb=self.onRemoveContact, no_cb=self.removePopUp)
493 self.showPopUp(pop_up_widget)
494
495 def onAboutRequest(self, menu):
496 self.showPopUp(sat_widgets.Alert(_("About"), const_APP_NAME + " v" + self.bridge.getVersion(), ok_cb=self.removePopUp))
497
498 #MISC CALLBACKS#
499
500 def onGatewaysFound(self, data):
501 """Called when SàT has found the server gateways"""
502 target = data['__private__']['target']
503 del data['__private__']
504 gatewayManager = GatewaysManager(self, data, server=target)
505 self.addWindow(gatewayManager)
506
507 sat = PrimitivusApp()
508 sat.start()
509