From c091e0c16fb1dbcf5c2a3789c8960e5384a480c0 Mon Sep 17 00:00:00 2001
From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13"
 <louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13>
Date: Wed, 27 Oct 2010 22:49:52 +0000
Subject: Command mode in roster tab, toggle offline contacts with 'o' and sort
 contacts by show

---
 src/contact.py       | 10 ++++--
 src/gui.py           |  6 +---
 src/multiuserchat.py |  4 ---
 src/roster.py        | 87 +++++++++++++++++++++++++++++++++++++++++++---------
 src/tab.py           | 66 +++++++++++++++++++++++++++++++++------
 src/user.py          |  1 -
 src/window.py        | 27 +++++++++++++---
 7 files changed, 159 insertions(+), 42 deletions(-)

diff --git a/src/contact.py b/src/contact.py
index 5fa0a17d..35a4a4a8 100644
--- a/src/contact.py
+++ b/src/contact.py
@@ -14,6 +14,11 @@
 # You should have received a copy of the GNU General Public License
 # along with Poezio.  If not, see <http://www.gnu.org/licenses/>.
 
+
+"""
+Defines the Resource and Contact classes
+"""
+
 from sleekxmpp.xmlstream.stanzabase import JID
 
 class Resource(object):
@@ -55,7 +60,7 @@ class Resource(object):
 class Contact(object):
     """
     This a way to gather multiple resources from the same bare JID.
-    This class contains zero or more esource class and useful methods
+    This class contains zero or more Resource object and useful methods
     to get the resource with the highest priority, etc
     """
     def __init__(self, bare_jid):
@@ -116,6 +121,7 @@ class Contact(object):
             if resource.get_jid().full == fulljid:
                 return resource
         return None
+
     def toggle_folded(self):
         """
         Fold if it's unfolded, and vice versa
@@ -148,7 +154,7 @@ class Contact(object):
 
     def get_resources(self):
         """
-        Return all resources
+        Return all resources, sorted by priority
         """
         compare_resources = lambda x: x.get_priority()
         return sorted(self._resources, key=compare_resources)
diff --git a/src/gui.py b/src/gui.py
index ec7480bd..8e14ca35 100644
--- a/src/gui.py
+++ b/src/gui.py
@@ -171,8 +171,6 @@ class Gui(object):
         self.refresh_window()
 
     def on_got_offline(self, presence):
-        from common import debug
-        debug('OFFLINE: %s\n' % presence)
         jid = presence['from']
         contact = self.roster.get_contact_by_jid(jid.bare)
         if not contact:
@@ -185,8 +183,6 @@ class Gui(object):
             self.refresh_window()
 
     def on_got_online(self, presence):
-        from common import debug
-        debug('ONLINE: %s\n' % presence)
         jid = presence['from']
         contact = self.roster.get_contact_by_jid(jid.bare)
         if not contact:
@@ -537,7 +533,7 @@ class Gui(object):
         Resize the whole screen
         """
         with resize_lock:
-            self.resize_timer = None
+           # self.resize_timer = None
             for tab in self.tabs:
                 tab.resize(self.stdscr)
             self.refresh_window()
diff --git a/src/multiuserchat.py b/src/multiuserchat.py
index 5800e7f9..311bce87 100644
--- a/src/multiuserchat.py
+++ b/src/multiuserchat.py
@@ -22,9 +22,6 @@ import sleekxmpp
 
 from xml.etree import cElementTree as ET
 
-
-from common import debug
-
 def send_private_message(xmpp, jid, line):
     """
     Send a private message
@@ -54,7 +51,6 @@ def change_show(xmpp, jid, own_nick, show, status):
         pres['type'] = show
     if status:
         pres['status'] = status
-    debug('Change presence: %s\n' % (pres))
     pres.send()
 
 def change_subject(xmpp, jid, subject):
diff --git a/src/roster.py b/src/roster.py
index 03a5f93a..7191e5ca 100644
--- a/src/roster.py
+++ b/src/roster.py
@@ -14,6 +14,12 @@
 # You should have received a copy of the GNU General Public License
 # along with Poezio.  If not, see <http://www.gnu.org/licenses/>.
 
+
+"""
+Defines the Roster and RosterGroup classes
+"""
+
+from config import config
 from contact import Contact, Resource
 
 class Roster(object):
@@ -29,9 +35,15 @@ class Roster(object):
         self._contacts[jid] = contact
 
     def get_contact_len(self):
+        """
+        Return the number of contacts in this group
+        """
         return len(self._contacts.keys())
 
     def get_contact_by_jid(self, jid):
+        """
+        Returns the contact with the given bare JID
+        """
         if jid in self._contacts:
             return self._contacts[jid]
         return None
@@ -42,15 +54,15 @@ class Roster(object):
         Add or remove RosterGroup if needed
         """
         # add the contact to each group he is in
-        if not len(groups):
+        # If the contact hasn't any group, we put her in
+        # the virtual default 'none' group
+        if not len(groups): 
             groups = ['none']
         for group in groups:
-            if group in contact._groups:
-                continue
-            else:
+            if group not in contact._groups:
                 # create the group if it doesn't exist yet
                 contact._groups.append(group)
-                self.add_contact_to_group(group, contact)
+            self.add_contact_to_group(group, contact)
         # remove the contact from each group he is not in
         for group in contact._groups:
             if group not in groups:
@@ -60,7 +72,7 @@ class Roster(object):
     def remove_contact_from_group(self, group_name, contact):
         """
         Remove the contact from the group.
-        Remove also the group if this makes it empty
+        Delete the group if this makes it empty
         """
         for group in self._roster_groups:
             if group.name == group_name:
@@ -76,28 +88,40 @@ class Roster(object):
         """
         for group in self._roster_groups:
             if group.name == group_name:
-                group.add_contact(contact)
+                if not group.has_contact(contact):
+                    group.add_contact(contact)
                 return
         new_group = RosterGroup(group_name)
         self._roster_groups.append(new_group)
         new_group.add_contact(contact)
 
     def get_groups(self):
+        """
+        Returns the list of groups
+        """
         return self._roster_groups
 
     def __len__(self):
         """
         Return the number of line that would be printed
+        for the whole roster
         """
-        l = 0
+        length = 0
         for group in self._roster_groups:
-            l += 1
+            if group.get_nb_connected_contacts() == 0:
+                continue
+            length += 1              # One for the group's line itself
             if not group.folded:
                 for contact in group.get_contacts():
-                    l += 1
+                    # We do not count the offline contacts (depending on config)
+                    if config.get('roster_show_offline', 'false') == 'false' and\
+                            contact.get_nb_resources() == 0:
+                        continue
+                    length += 1      # One for the contact's line
                     if not contact._folded:
-                        l += contact.get_nb_resources()
-        return l
+                        # One for each resource, if the contact is unfolded
+                        length += contact.get_nb_resources()
+        return length
 
     def __repr__(self):
         ret = '== Roster:\nContacts:\n'
@@ -108,6 +132,13 @@ class Roster(object):
             ret += '%s\n' % (group,)
         return ret + '\n'
 
+PRESENCE_PRIORITY = {'unavailable': 0,
+                     'xa': 1,
+                     'away': 2,
+                     'dnd': 3,
+                     '': 4,
+                     'available': 4}
+
 class RosterGroup(object):
     """
     A RosterGroup is a group containing contacts
@@ -122,6 +153,15 @@ class RosterGroup(object):
     def is_empty(self):
         return len(self._contacts) == 0
 
+    def has_contact(self, contact):
+        """
+        Return a bool, telling if the contact
+        is already in the group
+        """
+        if contact in self._contacts:
+            return True
+        return False
+
     def remove_contact(self, contact):
         """
         Remove a Contact object to the list
@@ -139,10 +179,27 @@ class RosterGroup(object):
         self._contacts.append(contact)
 
     def get_contacts(self):
-        return self._contacts
+        def compare_contact(a):
+            if not a.get_highest_priority_resource():
+                return 0
+            show = a.get_highest_priority_resource().get_presence()
+            if show not in PRESENCE_PRIORITY:
+                return 5
+            return PRESENCE_PRIORITY[show]
+        return sorted(self._contacts, key=compare_contact, reverse=True)
+
+    def toggle_folded(self):
+        self.folded = not self.folded
 
     def __repr__(self):
         return '<Roster_group: %s; %s>' % (self.name, self._contacts)
 
-    def toggle_folded(self):
-        self.folded = not self.folded
+    def __len__(self):
+        return len(self._contacts)
+
+    def get_nb_connected_contacts(self):
+        l = 0
+        for contact in self._contacts:
+            if contact.get_highest_priority_resource():
+                l += 1
+        return l
diff --git a/src/tab.py b/src/tab.py
index 35e71efe..04d55f80 100644
--- a/src/tab.py
+++ b/src/tab.py
@@ -28,6 +28,7 @@ MIN_HEIGHT = 16
 import window
 import theme
 import curses
+from config import config
 from roster import RosterGroup
 from contact import Contact, Resource
 
@@ -357,7 +358,10 @@ class PrivateTab(Tab):
 
     def on_gain_focus(self):
         self._room.set_color_state(theme.COLOR_TAB_CURRENT)
-        curses.curs_set(1)
+        if not self.input.input_mode:
+            curses.curs_set(1)
+        else:
+            curses.curs_set(0)
 
     def on_scroll_up(self):
         self._room.scroll_up(self.text_win.height-1)
@@ -382,6 +386,16 @@ class RosterInfoTab(Tab):
     A tab, splitted in two, containing the roster and infos
     """
     def __init__(self, stdscr):
+        self.single_key_commands = {
+            "^J": self.on_enter,
+            "^M": self.on_enter,
+            "\n": self.on_enter,
+            ' ': self.on_space,
+            "/": self.on_slash,
+            "KEY_UP": self.move_cursor_up,
+            "KEY_DOWN": self.move_cursor_down,
+            "o": self.toggle_offline_show,
+            }
         Tab.__init__(self, stdscr)
         self.name = "Roster"
         roster_width = self.width//2
@@ -391,7 +405,7 @@ class RosterInfoTab(Tab):
         self.info_win = window.TextWin(self.height-2, info_width, 0, roster_width+1, stdscr, self.visible)
         self.roster_win = window.RosterWin(self.height-2-3, roster_width, 0, 0, stdscr, self.visible)
         self.contact_info_win = window.ContactInfoWin(3, roster_width, self.height-2-3, 0, stdscr, self.visible)
-        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible)
+        self.input = window.Input(1, self.width, self.height-1, 0, stdscr, self.visible, False, "Enter commands with “/”. “o”: toggle offline show")
         self.set_color_state(theme.COLOR_TAB_NORMAL)
 
     def resize(self, stdscr):
@@ -423,12 +437,34 @@ class RosterInfoTab(Tab):
         self._color_state = color
 
     def on_input(self, key):
-        if key in ('\n', '^J', '^M') and self.input.is_empty():
-            return self.on_enter()
-        if key == ' ':
-            return self.on_space()
-        # In writting mode
-        # return self.input.do_command(key)
+        if self.input.input_mode:
+            ret = self.input.do_command(key)
+            # if the input is empty, go back to command mode
+            if self.input.is_empty():
+                self.input.input_mode = False
+                curses.curs_set(0)
+                self.input.rewrite_text()
+            return ret
+        if key in self.single_key_commands:
+            return self.single_key_commands[key]()
+
+    def toggle_offline_show(self):
+        """
+        Show or hide offline contacts
+        """
+        option = 'roster_show_offline'
+        if config.get(option, 'false') == 'false':
+            config.set_and_save(option, 'true')
+        else:
+            config.set_and_save(option, 'false')
+        return True
+    def on_slash(self):
+        """
+        '/' is pressed, we enter "input mode"
+        """
+        self.input.input_mode = True
+        curses.curs_set(1)
+        self.on_input("/") # we add the slash
 
     def on_lose_focus(self):
         self._color_state = theme.COLOR_TAB_NORMAL
@@ -440,11 +476,21 @@ class RosterInfoTab(Tab):
     def add_message(self):
         return False
 
-    def on_scroll_down(self):
+    def move_cursor_down(self):
         self.roster_win.move_cursor_down()
+        return True
 
-    def on_scroll_up(self):
+    def move_cursor_up(self):
         self.roster_win.move_cursor_up()
+        return True
+
+    def on_scroll_down(self):
+        # Scroll info win
+        pass
+
+    def on_scroll_up(self):
+        # Scroll info down
+        pass
 
     def on_info_win_size_changed(self, _, __):
         pass
diff --git a/src/user.py b/src/user.py
index 1ed2a9f1..b2b8790c 100644
--- a/src/user.py
+++ b/src/user.py
@@ -31,7 +31,6 @@ class User(object):
     keep trace of an user in a Room
     """
     def __init__(self, nick, affiliation, show, status, role):
-        from common import debug
         self.last_talked = datetime(1, 1, 1) # The oldest possible time
         self.update(affiliation, show, status, role)
         self.change_nick(nick)
diff --git a/src/window.py b/src/window.py
index 0e628a8c..eb2c157b 100644
--- a/src/window.py
+++ b/src/window.py
@@ -518,8 +518,11 @@ class TextWin(Win):
 class Input(Win):
     """
     The line where text is entered
+    It can be in input mode or in commmand mode.
+    Command mode means that single_key_commands can be entered, handled
+    by the Tab object, while this input just displays an help text.
     """
-    def __init__(self, height, width, y, x, stdscr, visible):
+    def __init__(self, height, width, y, x, stdscr, visible, input_mode=True, help_text=''):
         self.key_func = {
             "KEY_LEFT": self.key_left,
             "M-D": self.key_left,
@@ -548,6 +551,8 @@ class Input(Win):
             }
 
         Win.__init__(self, height, width, y, x, stdscr)
+        self.input_mode = input_mode
+        self.help_text = help_text # the text displayed in command_mode
         self.visible = visible
         self.history = []
         self.text = ''
@@ -875,8 +880,12 @@ class Input(Win):
         """
         with g_lock:
             self.clear_text()
-            self.addstr(self.text[self.line_pos:self.line_pos+self.width-1])
-            self.addstr(0, self.pos, '') # WTF, this works but .move() doesn't...
+            if self.input_mode:
+                self.addstr(self.text[self.line_pos:self.line_pos+self.width-1])
+            else:
+                self.addstr(self.help_text, curses.color_pair(theme.COLOR_INFORMATION_BAR))
+                self.finish_line(theme.COLOR_INFORMATION_BAR)
+            self.addstr(0, self.pos, '') # WTF, this works but .move() doesn't…
             self._refresh()
 
     def refresh(self):
@@ -928,7 +937,7 @@ class RosterWin(Win):
                   'chat':theme.COLOR_STATUS_CHAT,
                   'unavailable':theme.COLOR_STATUS_UNAVAILABLE
                   }
-    # subscription_char = {'both': '
+
     def __init__(self, height, width, y, x, parent_win, visible):
         self.visible = visible
         Win.__init__(self, height, width, y, x, parent_win)
@@ -967,10 +976,14 @@ class RosterWin(Win):
             return
         with g_lock:
             self.roster_len = len(roster)
+            while self.roster_len and self.pos >= self.roster_len:
+                self.move_cursor_up()
             self._win.erase()
             self.draw_roster_information(roster)
             y = 1
             for group in roster.get_groups():
+                if group.get_nb_connected_contacts() == 0:
+                    continue    # Ignore empty groups
                 # This loop is really REALLY ugly :^)
                 if y-1 == self.pos:
                     self.selected_row = group
@@ -980,6 +993,10 @@ class RosterWin(Win):
                 if group.folded:
                     continue
                 for contact in group.get_contacts():
+                    if config.get('roster_show_offline', 'false') == 'false' and\
+                            contact.get_nb_resources() == 0:
+                        continue
+
                     if y-1 == self.pos:
                         self.selected_row = contact
                     if y-self.start_pos+1 == self.height:
@@ -1007,7 +1024,7 @@ class RosterWin(Win):
     def draw_plus(self, y):
         """
         Draw the indicator that shows that
-        the list is longer that what is displayed
+        the list is longer than what is displayed
         """
         self.addstr(y, self.width-5, '++++', curses.color_pair(42))
 
-- 
cgit v1.2.3