changeset 119:ded2431cea5a

Primitivus: chat window / text sending. Primitivus has now the most basics features \o/ - core: new getVersion method - primitivus: new debug key (C-d), only work if SàT is in dev version (D in version) - quick_app: new post_init method, used for automatique task like auto-plug - primitivus: lists now use genericList (Box) or List (Flow) - primitivus: List now manage correctly its size - primitivus: new FocusFrame widget which manage focus changing with 'tab' - primitivus: advancedEdit now manage 'click' signal - primitivus: contactList now manager 'change' and 'click' signals - primitivus: Chat window now working
author Goffi <goffi@goffi.org>
date Mon, 05 Jul 2010 19:13:36 +0800 (2010-07-05)
parents 76055a209ed9
children 1ca5f254ce41
files frontends/primitivus/chat.py frontends/primitivus/contact_list.py frontends/primitivus/custom_widgets.py frontends/primitivus/primitivus frontends/primitivus/profile_manager.py frontends/quick_frontend/quick_app.py frontends/quick_frontend/quick_contact_management.py frontends/sat_bridge_frontend/DBus.py frontends/wix/main_window.py sat.tac sat_bridge/DBus.py
diffstat 11 files changed, 280 insertions(+), 55 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/primitivus/chat.py	Mon Jul 05 19:13:36 2010 +0800
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Primitivus: a SAT frontend
+Copyright (C) 2009, 2010  Jérôme Poisson (goffi@goffi.org)
+
+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 3 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, see <http://www.gnu.org/licenses/>.
+"""
+
+import urwid
+from quick_frontend.quick_contact_list import QuickContactList
+from quick_frontend.quick_chat import QuickChat
+from custom_widgets import Password,List,InputDialog,ConfirmDialog,Alert,SelectableText
+
+
+class Chat(urwid.WidgetWrap, QuickChat):
+
+    def __init__(self, target, host, type='one2one'):
+        QuickChat.__init__(self, target, host, type)
+        self.content = urwid.SimpleListWalker([])
+        self.text_list = urwid.ListBox(self.content)
+        main_widget = urwid.LineBox(
+                      urwid.Frame(self.text_list, urwid.AttrMap(urwid.Text(str(target),'center'),'title'))
+                      )
+        urwid.WidgetWrap.__init__(self, main_widget)
+        self.setType(type)
+    
+    def setType(self, type):
+        QuickChat.setType(self, type)
+        if type == 'one2one':
+            self.historyPrint(profile=self.host.profile)
+
+    def printMessage(self, from_jid, msg, profile, timestamp=""):
+        self.content.append(SelectableText("[%s] " % from_jid + msg))
+        self.text_list.set_focus(len(self.content)-1)
+        self.host.redraw()
--- a/frontends/primitivus/contact_list.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/primitivus/contact_list.py	Mon Jul 05 19:13:36 2010 +0800
@@ -25,12 +25,12 @@
 
 
 class ContactList(urwid.WidgetWrap, QuickContactList):
+    signals = ['click','change']
 
-    def __init__(self, host, CM):
-        QuickContactList.__init__(self, CM)
+    def __init__(self, host, CM, on_click=None, on_change=None, user_data=None):
         self.host = host
                
-        self.list_wid = List([], style=['single'], align='left')
+        self.list_wid = List([], style=['single','no_first_select'], align='left', on_click=self.__contactClicked, on_change=on_change)
 
         #we now build the widget
         body_content = urwid.SimpleListWalker([self.list_wid])
@@ -38,21 +38,35 @@
         frame = urwid.Frame(frame_body,urwid.AttrMap(urwid.Text(_("Contacts"),align='center'),'title'))
         self.main_widget = urwid.LineBox(frame)
         urwid.WidgetWrap.__init__(self, self.main_widget)
+        if on_click:
+            urwid.connect_signal(self, 'click', on_click, user_data)
+        if on_change:
+            urwid.connect_signal(self, 'change', on_change, user_data)
+        QuickContactList.__init__(self, CM)
+
+    def __contactClicked(self, list_wid):
+        self._emit('click')
+
+    def get_contact(self):
+        """Return contact currently selected"""
+        return self.list_wid.getSelectedValue()
             
     def clear_contacts(self):
         """clear all the contact list"""
         self.list_wid.changeValues([])
 
     def replace(self, jid, groups=None):
-            """add a contact to the list if doesn't exist, else update it"""
-            contacts = self.list_wid.getValues()
-            if jid.short not in contacts:
-                contacts.append(jid.short)
-                contacts.sort()
-                self.list_wid.changeValues(contacts)
+        """add a contact to the list if doesn't exist, else update it"""
+        contacts = self.list_wid.getAllValues()
+        if jid.short not in contacts:
+            contacts.append(jid.short)
+            contacts.sort()
+            self.list_wid.changeValues(contacts)
+            self._emit('change')
     
     def disconnect(self, jid):
         """mark a contact disconnected"""
+        #self.host.debug()
         self.remove(jid)
     
     def remove(self, jid):
--- a/frontends/primitivus/custom_widgets.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/primitivus/custom_widgets.py	Mon Jul 05 19:13:36 2010 +0800
@@ -38,6 +38,7 @@
 
 class AdvancedEdit(urwid.Edit):
     """Edit box with some custom improvments"""
+    signals = urwid.Edit.signals + ['click']
 
     def keypress(self, size, key):
         #TODO: insert mode is not managed yet
@@ -53,6 +54,8 @@
             pos = before.rstrip().rfind(" ")+1
             self.set_edit_text(before[:pos] + self.edit_text[self.edit_pos:])
             self.set_edit_pos(pos)
+        elif key == 'enter':
+            self._emit('click')
         return super(AdvancedEdit, self).keypress(size, key) 
        
 
@@ -73,9 +76,9 @@
         @param invisible: don't emit change signal if True"""
         assert(type(selected)==bool)
         self.__selected=selected
+        self._invalidate()
         if not invisible:
             self._emit("change", self.__selected)
-        self._invalidate()
    
     def getState(self):
         return self.__selected
@@ -101,15 +104,32 @@
             attr+="_focus"
         return urwid.Text((attr,self.text), align=self.align)
 
-class List(urwid.WidgetWrap):
-    signals = ['change']
+class GenericList(urwid.WidgetWrap):
+    signals = ['click','change']
 
-    def __init__(self, options, style=[], align='left', on_state_change=None, user_data=None):
+    def __init__(self, options, style=[], align='left', on_click=None, on_change=None, user_data=None):
+        """
+        Widget managing list of string and their selection
+        @param options: list of strings used for options
+        @param style: list of string:
+            - 'single' if only one must be selected
+            - 'no_first_select' nothing selected when list is first displayed 
+            - 'can_select_none' if we can select nothing
+        @param align: alignement of text inside the list
+        @param on_click: method called when click signal is emited
+        @param user_data: data sent to the callback for click signal
+        """
         self.single = 'single' in style
+        self.no_first_select = 'no_first_select' in style
+        self.can_select_none = 'can_select_none' in style
         self.align = align
+        self.first_display = True
         
-        if on_state_change:
-            urwid.connect_signal(self, 'change', on_state_change, user_data)
+        if on_click:
+            urwid.connect_signal(self, 'click', on_click, user_data)
+        
+        if on_change:
+            urwid.connect_signal(self, 'change', on_change, user_data)
         
         self.content = urwid.SimpleListWalker([])
         self.list_box = urwid.ListBox(self.content)
@@ -118,14 +138,14 @@
 
     def __onStateChange(self, widget, selected):
         if self.single:
-            if not selected:
+            if not selected and not self.can_select_none:
                 #if in single mode, it's forbidden to unselect a value
                 widget.setState(True, invisible=True)
                 return
-            else:
+            if selected:
                 self.unselectAll(invisible=True)
                 widget.setState(True, invisible=True)
-        self._emit("change")
+        self._emit("click")
 
 
     def unselectAll(self, invisible=False):
@@ -136,45 +156,109 @@
 
     def deleteValue(self, value):
         """Delete the first value equal to the param given"""
-        try:
-            self.content.remove(value)
-        except ValueError:
-            raise ValuError("%s ==> %s" %  (str(value),str(self.content)))
+        for widget in self.content:
+            if widget.getValue() == value:
+                self.content.remove(widget)
+                self._emit('change')
+                return
+        raise ValueError("%s ==> %s" %  (str(value),str(self.content)))
 
-    def getValue(self):
+    def getSelectedValue(self):
         """Convenience method to get the value selected as a string in single mode, or None"""
-        values = self.getValues()
+        values = self.getSelectedValues()
         return values[0] if values else None
 
-    def getValues(self):
+    def getAllValues(self):
+        """Return values of all items"""
+        return [widget.getValue() for widget in self.content]
+
+    def getSelectedValues(self):
+        """Return values of selected items"""
         result = []
         for widget in self.content:
             if widget.getState():
                 result.append(widget.getValue())
         return result
 
+    def getDisplayWidget(self):
+        return self.list_box
+
     def changeValues(self, new_values):
         """Change all value in one shot"""
-        widgets = [SelectableText(option, self.align) for option in new_values]
-        for widget in widgets:
+        if not self.first_display:
+            old_selected = self.getSelectedValues()
+        widgets = []
+        for option in new_values:
+            widget = SelectableText(option, self.align)
+            if not self.first_display and option in old_selected:
+                widget.setState(True)
+            widgets.append(widget)
             urwid.connect_signal(widget, 'change', self.__onStateChange)
         self.content[:] = widgets
-        if self.single and new_values:
+        if self.first_display and self.single and new_values and not self.no_first_select:
             self.content[0].setState(True)
-        display_widget = urwid.BoxAdapter(self.list_box, min(len(new_values),5) or 1)
+        display_widget = self.getDisplayWidget()
         self._set_w(display_widget)
-        
+        self._emit('change')
+        self.first_display = False 
+
     def selectValue(self, value):
         self.unselectAll()
         idx = 0
         for widget in self.content:
-            if widget.getValue() == value:
+            if widget.getSelectedValue() == value:
                 widget.setState(True)
                 self.list_box.set_focus(idx)
                 return
             idx+=1
 
-class genericDialog(urwid.WidgetWrap):
+class List(urwid.FlowWidget):
+    """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'"""
+    signals = ['click','change']
+
+    def __init__(self, options, style=[], max_height=5, align='left', on_click=None, on_change=None, user_data=None):
+        self.genericList = GenericList(options, style, align, on_click, on_change, user_data)
+        self.max_height = max_height 
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        return self.displayWidget(size,True).keypress(size, key)
+        
+    def unselectAll(self, invisible=False):
+        return self.genericList.unselectAll(invisible)
+    
+    def deleteValue(self, value):
+        return self.genericList.deleteValue(value)
+
+    def getSelectedValue(self):
+        return self.genericList.getSelectedValue()
+
+    def getAllValues(self):
+        return self.genericList.getAllValues()
+
+    def getSelectedValues(self):
+        return self.genericList.getSelectedValues()
+
+    def changeValues(self, new_values):
+        return self.genericList.changeValues(new_values)
+
+    def selectValue(self, value):
+        return self.genericList.selectValue(value)
+
+    def render(self, size, focus=False):
+        return self.displayWidget(size, focus).render(size, focus)
+    
+    def rows(self, size, focus=False):
+        return self.displayWidget(size, focus).rows(size, focus)
+
+    def displayWidget(self, size, focus):
+        list_size = sum([wid.rows(size, focus) for wid in self.genericList.content])
+        height = min(list_size,self.max_height) 
+        return urwid.BoxAdapter(self.genericList, height)
+
+class GenericDialog(urwid.WidgetWrap):
 
     def __init__(self, widgets_lst, title, style=[], **kwargs):
         frame_header = urwid.AttrMap(urwid.Text(title,'center'),'title')
@@ -200,19 +284,35 @@
 
 
 
-class InputDialog(genericDialog):
+class InputDialog(GenericDialog):
 
     def __init__(self, title, instrucions, style=['OK/CANCEL'], **kwargs):
         instr_wid = urwid.Text(instrucions+':')
         edit_box = urwid.Edit()
-        genericDialog.__init__(self, [instr_wid,edit_box], title, style, ok_value=edit_box, **kwargs)
+        GenericDialog.__init__(self, [instr_wid,edit_box], title, style, ok_value=edit_box, **kwargs)
 
-class ConfirmDialog(genericDialog):
+class ConfirmDialog(GenericDialog):
 
     def __init__(self, title, style=['YES/NO'], **kwargs):
-        genericDialog.__init__(self, [], title, style, yes_value=None, **kwargs)
+        GenericDialog.__init__(self, [], title, style, yes_value=None, **kwargs)
 
-class Alert(genericDialog):
+class Alert(GenericDialog):
 
     def __init__(self, title, message, style=['OK'], **kwargs):
-        genericDialog.__init__(self, [urwid.Text(message, 'center')], title, style, ok_value=None, **kwargs)
+        GenericDialog.__init__(self, [urwid.Text(message, 'center')], title, style, ok_value=None, **kwargs)
+
+class FocusFrame(urwid.Frame):
+    """Frame which manage "tab" key"""
+
+    def keypress(self, size, key):
+        if key == 'tab':
+            focus_list = ('header','body','footer')
+            focus_idx = focus_list.index(self.focus_part)
+            for i in range(2):
+                focus_idx = (focus_idx + 1) % len(focus_list)
+                focus_name = focus_list[focus_idx]
+                widget = getattr(self,'_'+focus_name)
+                if widget!=None and widget.selectable():
+                    self.set_focus(focus_name)
+
+        return urwid.Frame.keypress(self, size, key)
--- a/frontends/primitivus/primitivus	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/primitivus/primitivus	Mon Jul 05 19:13:36 2010 +0800
@@ -27,7 +27,8 @@
 import urwid
 from profile_manager import ProfileManager
 from contact_list import ContactList
-from custom_widgets import AdvancedEdit
+from chat import Chat
+from custom_widgets import AdvancedEdit,FocusFrame
 import pdb
 """from window import Window
 from editbox import EditBox
@@ -60,6 +61,15 @@
                  ('default_focus', 'default,bold', 'default'),
                  ]
             
+class ChatList(QuickChatList):
+    """This class manage the list of chat windows"""
+    
+    def __init__(self, host):
+        QuickChatList.__init__(self, host)
+    
+    def createChat(self, target):
+        return Chat(target, self.host)
+
 class PrimitivusApp(QuickApp):
     
     def __init__(self):
@@ -70,17 +80,38 @@
         self.main_widget = ProfileManager(self)
         self.loop = urwid.MainLoop(self.main_widget, const_PALETTE, event_loop=urwid.GLibEventLoop(), unhandled_input=self.key_handler)
 
+        ##misc setup##
+        self.chat_wins=ChatList(self)
+    
+    def debug(self):
+        """convenience method to reset screen and launch pdb"""
+        import os
+        os.system('reset')
+        print 'Entered debug mode'
+        pdb.set_trace()
+
+    def redraw(self):
+        """redraw the screen"""
+        self.loop.draw_screen()
+
     def start(self):
+        self.i = 0
+        self.loop.set_alarm_in(0,lambda a,b: self.postInit())
         self.loop.run()
 
     def key_handler(self, input):
         if input in ('q', 'Q') or input == 'ctrl x':
             raise urwid.ExitMainLoop()
-
+        elif input == 'ctrl d' and 'D' in self.bridge.getVersion(): #Debug only for dev versions
+            self.debug()
+  
     def __buildMainWidget(self):
-        self.contactList = ContactList(self, self.CM)
-        self.center_part = urwid.Columns([self.contactList])
-        self.main_widget = urwid.Frame(self.center_part, footer=AdvancedEdit('> '), focus_part='footer')
+        self.contactList = ContactList(self, self.CM, on_click = self.contactSelected, on_change=lambda w: self.redraw())
+        #self.center_part = urwid.Columns([('weight',2,self.contactList),('weight',8,Chat('',self))])
+        self.center_part = urwid.Columns([('weight',2,self.contactList), ('weight',8,urwid.Filler(urwid.Text('')))])
+        editBar = AdvancedEdit('> ')
+        urwid.connect_signal(editBar,'click',self.onTextEntered)
+        self.main_widget = FocusFrame(self.center_part, footer=editBar, focus_part='footer')
         return self.main_widget
 
     def plug_profile(self, profile_key='@DEFAULT@'):
@@ -94,6 +125,20 @@
         display_widget = urwid.Overlay(pop_up_widget, self.main_widget, 'center', ('relative', 40), 'middle', ('relative', 40))
         self.loop.widget = display_widget
 
+    def contactSelected(self, contact_list):
+        contact = contact_list.get_contact()
+        if contact:
+            assert(len(self.center_part.widget_list)==2)
+            #self.center_part.widget_list.append(self.chat_wins[contact])
+            #self.center_part.column_types.append(('weight',8))
+            self.center_part.widget_list[1] = self.chat_wins[contact]
+
+    def onTextEntered(self, editBar):
+        contact = self.contactList.get_contact()
+        if contact:
+            self.bridge.sendMessage(contact, editBar.get_edit_text(), profile_key=self.profile)
+            editBar.set_edit_text('')
+
 sat = PrimitivusApp()
 sat.start()
 
--- a/frontends/primitivus/profile_manager.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/primitivus/profile_manager.py	Mon Jul 05 19:13:36 2010 +0800
@@ -20,7 +20,7 @@
 """
 
 import urwid
-from custom_widgets import Password,List,InputDialog,ConfirmDialog,Alert
+from custom_widgets import Password,List,InputDialog,ConfirmDialog,Alert,FocusFrame
 
 
 class ProfileManager(urwid.WidgetWrap):
@@ -35,7 +35,7 @@
         self.login_wid = urwid.Edit(_('Login:'),align='center')
         self.pass_wid = Password(_('Password:'),align='center')
         
-        self.list_profile = List(profiles, style=['single'], align='center', on_state_change=self.onProfileChange)
+        self.list_profile = List(profiles, style=['single'], align='center', on_click=self.onProfileChange)
 
         #toto = urwid.Padding(urwid.Text('toto'), align='center')
 
@@ -74,7 +74,7 @@
         self.host.removePopUp()
 
     def deleteProfile(self, button):
-        profile_name = self.list_profile.getValue()
+        profile_name = self.list_profile.getSelectedValue()
         if profile_name:
             self.host.bridge.deleteProfile(profile_name)
             self.__refillProfiles()
@@ -86,11 +86,11 @@
         self.host.showPopUp(pop_up_widget)
 
     def onDeleteProfile(self, e):
-        pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
+        pop_up_widget = ConfirmDialog(_("Are you sure you want to delete the profile %s ?") % self.list_profile.getSelectedValue(), no_cb=self.cancelDialog, yes_cb=self.deleteProfile)
         self.host.showPopUp(pop_up_widget)
 
     def onProfileChange(self, list_wid):
-        profile_name = list_wid.getValue()
+        profile_name = list_wid.getSelectedValue()
         if profile_name:
             jabberID = self.host.bridge.getParamA("JabberID", "Connection", profile_key=profile_name)
             password = self.host.bridge.getParamA("Password", "Connection", profile_key=profile_name)
@@ -98,7 +98,7 @@
             self.pass_wid.set_edit_text(password)
         
     def onConnectProfile(self, button):
-        profile_name = self.list_profile.getValue()
+        profile_name = self.list_profile.getSelectedValue()
         if not profile_name:
             pop_up_widget = Alert(_('No profile selected'), _('You need to create and select a profile before connecting'), ok_cb=self.cancelDialog)
             self.host.showPopUp(pop_up_widget)
--- a/frontends/quick_frontend/quick_app.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/quick_frontend/quick_app.py	Mon Jul 05 19:13:36 2010 +0800
@@ -71,6 +71,14 @@
         """Tell if the profile is currently followed by the application"""
         return profile in self.profiles.keys()
 
+    def postInit(self):
+        """Must be called after __init__, do all automatic task (auto plug profile)"""
+        if self.options.profile:
+            if not self.bridge.getProfileName(self.options.profile): 
+                error(_("Trying to plug an unknown profile (%s)" % self.options.profile))
+            else:
+                self.plug_profile(self.options.profile)
+
     def check_options(self):
         """Check command line options"""
         usage=_("""
--- a/frontends/quick_frontend/quick_contact_management.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/quick_frontend/quick_contact_management.py	Mon Jul 05 19:13:36 2010 +0800
@@ -80,7 +80,8 @@
                 #no more resource available: the contact seems really disconnected
                 del self.__contactlist[entity.short]
         except KeyError:
-            pass
+            error(_('INTERNAL ERROR: Key error'))
+            raise
 
     def update(self, entity, key, value):
         """Update attribute of contact
--- a/frontends/sat_bridge_frontend/DBus.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/sat_bridge_frontend/DBus.py	Mon Jul 05 19:13:36 2010 +0800
@@ -40,6 +40,9 @@
         elif iface == "request":
             self.db_req_iface.connect_to_signal(functionName, handler)
 
+    def getVersion(self):
+        return self.db_req_iface.getVersion()
+
     def getProfileName(self, profile_key='@DEFAULT@'):
         return self.db_req_iface.getProfileName(profile_key)
 
--- a/frontends/wix/main_window.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/frontends/wix/main_window.py	Mon Jul 05 19:13:36 2010 +0800
@@ -99,8 +99,8 @@
         #events
         self.Bind(wx.EVT_CLOSE, self.onClose, self)
 
+        
         QuickApp.__init__(self)
-        #self.plug_profile() #gof:
         
         #menus
         self.createMenus()
@@ -111,10 +111,9 @@
         self.profile_pan = ProfileManager(self) 
         #self.profile_pan.Hide()  #gof:
         self.sizer.Add(self.profile_pan, 1, flag=wx.EXPAND)
-        if self.options.profile: #TODO: move this to quick_app
-            self.plug_profile(self.options.profile)
-                
-       
+      
+        self.postInit()
+
         self.Show()
 
     def plug_profile(self, profile_key='@DEFAULT@'):
--- a/sat.tac	Sat Jul 03 13:56:44 2010 +0800
+++ b/sat.tac	Mon Jul 05 19:13:36 2010 +0800
@@ -332,6 +332,7 @@
         self.server_features=[]  #XXX: temp dic, need to be transfered into self.memory in the future
 
         self.bridge=DBusBridge()
+        self.bridge.register("getVersion", lambda: self.get_const('client_version'))
         self.bridge.register("getProfileName", self.memory.getProfileName)
         self.bridge.register("getProfilesList", self.memory.getProfilesList)
         self.bridge.register("createProfile", self.memory.createProfile)
--- a/sat_bridge/DBus.py	Sat Jul 03 13:56:44 2010 +0800
+++ b/sat_bridge/DBus.py	Mon Jul 05 19:13:36 2010 +0800
@@ -105,6 +105,12 @@
 
     ### methods ###    
 
+    
+    @dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
+                         in_signature='', out_signature='s')
+    def getVersion(self):
+        return self.cb["getVersion"]()
+    
     @dbus.service.method(const_INT_PREFIX+const_REQ_SUFFIX,
                          in_signature='s', out_signature='s')
     def getProfileName(self, profile_key):