From 6fab04a6dc6dd673a07830185b932d97ff55913c Mon Sep 17 00:00:00 2001
From: "louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13"
 <louiz@4325f9fc-e183-4c21-96ce-0ab188b42d13>
Date: Sun, 31 Oct 2010 18:57:48 +0000
Subject: Basic search in the roster (based on contact JIDs)

---
 src/gui.py    | 51 +++++++++++++++++++++++++++++++++++++-----------
 src/roster.py | 13 ++++++++++---
 src/tab.py    | 36 ++++++++++++++++++++++++++++++++--
 src/window.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
 4 files changed, 140 insertions(+), 22 deletions(-)

(limited to 'src')

diff --git a/src/gui.py b/src/gui.py
index 7b60b4f1..f1d5eb0c 100644
--- a/src/gui.py
+++ b/src/gui.py
@@ -39,7 +39,7 @@ from config import config
 from tab import MucTab, InfoTab, PrivateTab, RosterInfoTab, ConversationTab
 from user import User
 from room import Room
-from roster import Roster, RosterGroup
+from roster import Roster, RosterGroup, roster
 from contact import Contact, Resource
 from message import Message
 from text_buffer import TextBuffer
@@ -81,7 +81,7 @@ class Gui(object):
             else RosterInfoTab(self.stdscr)
         default_tab.on_gain_focus()
         self.tabs = [default_tab]
-        self.roster = Roster()
+        # self.roster = Roster()
         # a unique buffer used to store global informations
         # that are displayed in almost all tabs, in an
         # information window.
@@ -150,6 +150,8 @@ class Gui(object):
         self.xmpp.add_event_handler("roster_update", self.on_roster_update)
         self.xmpp.add_event_handler("changed_status", self.on_presence)
 
+        # self.__debug_fill_roster()
+
     def grow_information_win(self):
         """
         """
@@ -172,7 +174,7 @@ class Gui(object):
 
     def on_got_offline(self, presence):
         jid = presence['from']
-        contact = self.roster.get_contact_by_jid(jid.bare)
+        contact = roster.get_contact_by_jid(jid.bare)
         if not contact:
             return
         resource = contact.get_resource_by_fulljid(jid.full)
@@ -184,7 +186,7 @@ class Gui(object):
 
     def on_got_online(self, presence):
         jid = presence['from']
-        contact = self.roster.get_contact_by_jid(jid.bare)
+        contact = roster.get_contact_by_jid(jid.bare)
         if not contact:
             # Todo, handle presence comming from contacts not in roster
             return
@@ -471,8 +473,7 @@ class Gui(object):
         """
         """
         jid = presence['from']
-        # contact = ros
-        contact = self.roster.get_contact_by_jid(jid.bare)
+        contact = roster.get_contact_by_jid(jid.bare)
         if not contact:
             return
         resource = contact.get_resource_by_fulljid(jid.full)
@@ -485,6 +486,34 @@ class Gui(object):
         if isinstance(self.current_tab(), RosterInfoTab):
             self.refresh_window()
 
+    def __debug_fill_roster(self):
+        for i in range(10):
+            jid = 'contact%s@fion%s.org'%(i,i)
+            contact = Contact(jid)
+            contact.set_ask('wat')
+            contact.set_subscription('both')
+            roster.add_contact(contact, jid)
+            contact.set_name('%s %s fion'%(i,i))
+            roster.edit_groups_of_contact(contact, ['hello'])
+        for i in range(10):
+            jid = 'test%s@bernard%s.org'%(i,i)
+            contact = Contact(jid)
+            contact.set_ask('wat')
+            contact.set_subscription('both')
+            roster.add_contact(contact, jid)
+            contact.set_name('%s test'%(i))
+            roster.edit_groups_of_contact(contact, ['hello'])
+        for i in range(10):
+            jid = 'pouet@top%s.org'%(i)
+            contact = Contact(jid)
+            contact.set_ask('wat')
+            contact.set_subscription('both')
+            roster.add_contact(contact, jid)
+            contact.set_name('%s oula'%(i))
+            roster.edit_groups_of_contact(contact, ['hello'])
+        if isinstance(self.current_tab(), RosterInfoTab):
+            self.refresh_window()
+
     def on_roster_update(self, iq):
         """
         A subscription changed, or we received a roster item
@@ -492,10 +521,10 @@ class Gui(object):
         """
         for item in iq.findall('{jabber:iq:roster}query/{jabber:iq:roster}item'):
             jid = item.attrib['jid']
-            contact = self.roster.get_contact_by_jid(jid)
+            contact = roster.get_contact_by_jid(jid)
             if not contact:
                 contact = Contact(jid)
-                self.roster.add_contact(contact, jid)
+                roster.add_contact(contact, jid)
             if 'ask' in item.attrib:
                 contact.set_ask(item.attrib['ask'])
             else:
@@ -507,7 +536,7 @@ class Gui(object):
             if item.attrib['subscription']:
                 contact.set_subscription(item.attrib['subscription'])
             groups = item.findall('{jabber:iq:roster}group')
-            self.roster.edit_groups_of_contact(contact, [group.text for group in groups])
+            roster.edit_groups_of_contact(contact, [group.text for group in groups])
         if isinstance(self.current_tab(), RosterInfoTab):
             self.refresh_window()
 
@@ -602,7 +631,7 @@ class Gui(object):
         Refresh everything
         """
         self.current_tab().set_color_state(theme.COLOR_TAB_CURRENT)
-        self.current_tab().refresh(self.tabs, self.information_buffer, self.roster)
+        self.current_tab().refresh(self.tabs, self.information_buffer, roster)
         self.doupdate()
 
     def open_new_room(self, room, nick, focus=True):
@@ -738,7 +767,7 @@ class Gui(object):
         own_nick = room.own_nick
         r = Room(complete_jid, own_nick) # PrivateRoom here
         new_tab = PrivateTab(self.stdscr, r, self.information_win_size)
-        # insert it in the rooms
+        # insert it in the tabs
         if self.current_tab().nb == 0:
             self.tabs.append(new_tab)
         else:
diff --git a/src/roster.py b/src/roster.py
index 7191e5ca..09bc7b6b 100644
--- a/src/roster.py
+++ b/src/roster.py
@@ -24,6 +24,9 @@ from contact import Contact, Resource
 
 class Roster(object):
     def __init__(self):
+        self._contact_filter = None # A tuple(function, *args)
+                                    # function to filter contacts,
+                                    # on search, for example
         self._contacts = {}     # key = bare jid; value = Contact()
         self._roster_groups = []
 
@@ -112,7 +115,7 @@ class Roster(object):
                 continue
             length += 1              # One for the group's line itself
             if not group.folded:
-                for contact in group.get_contacts():
+                for contact in group.get_contacts(self._contact_filter):
                     # We do not count the offline contacts (depending on config)
                     if config.get('roster_show_offline', 'false') == 'false' and\
                             contact.get_nb_resources() == 0:
@@ -178,7 +181,7 @@ class RosterGroup(object):
         assert contact not in self._contacts
         self._contacts.append(contact)
 
-    def get_contacts(self):
+    def get_contacts(self, contact_filter):
         def compare_contact(a):
             if not a.get_highest_priority_resource():
                 return 0
@@ -186,7 +189,9 @@ class RosterGroup(object):
             if show not in PRESENCE_PRIORITY:
                 return 5
             return PRESENCE_PRIORITY[show]
-        return sorted(self._contacts, key=compare_contact, reverse=True)
+        contact_list = self._contacts if not contact_filter\
+            else [contact for contact in self._contacts if contact_filter[0](contact, contact_filter[1])]
+        return sorted(contact_list, key=compare_contact, reverse=True)
 
     def toggle_folded(self):
         self.folded = not self.folded
@@ -203,3 +208,5 @@ class RosterGroup(object):
             if contact.get_highest_priority_resource():
                 l += 1
         return l
+
+roster = Roster()
diff --git a/src/tab.py b/src/tab.py
index 04d55f80..e6a8cc41 100644
--- a/src/tab.py
+++ b/src/tab.py
@@ -29,7 +29,7 @@ import window
 import theme
 import curses
 from config import config
-from roster import RosterGroup
+from roster import RosterGroup, roster
 from contact import Contact, Resource
 
 class Tab(object):
@@ -395,6 +395,7 @@ class RosterInfoTab(Tab):
             "KEY_UP": self.move_cursor_up,
             "KEY_DOWN": self.move_cursor_down,
             "o": self.toggle_offline_show,
+            "^F": self.start_search,
             }
         Tab.__init__(self, stdscr)
         self.name = "Roster"
@@ -439,11 +440,14 @@ class RosterInfoTab(Tab):
     def on_input(self, key):
         if self.input.input_mode:
             ret = self.input.do_command(key)
+            roster._contact_filter = (jid_and_name_match, self.input.text)
             # if the input is empty, go back to command mode
-            if self.input.is_empty():
+            if self.input.is_empty() and not self.input._instructions:
                 self.input.input_mode = False
                 curses.curs_set(0)
                 self.input.rewrite_text()
+            if self.input._instructions:
+                return True
             return ret
         if key in self.single_key_commands:
             return self.single_key_commands[key]()
@@ -458,6 +462,7 @@ class RosterInfoTab(Tab):
         else:
             config.set_and_save(option, 'false')
         return True
+
     def on_slash(self):
         """
         '/' is pressed, we enter "input mode"
@@ -501,10 +506,27 @@ class RosterInfoTab(Tab):
                 isinstance(selected_row, Contact):
             selected_row.toggle_folded()
             return True
+
     def on_enter(self):
         selected_row = self.roster_win.get_selected_row()
         return selected_row
 
+    def start_search(self):
+        """
+        Start the search. The input should appear with a short instruction
+        in it.
+        """
+        curses.curs_set(1)
+        roster._contact_filter = (jid_and_name_match, self.input.text)
+        self.input.input_mode = True
+        self.input.start_command(self.on_search_terminate, self.on_search_terminate, '[search]')
+        return True
+
+    def on_search_terminate(self, txt):
+        curses.curs_set(0)
+        roster._contact_filter = None
+        return True
+
     def just_before_refresh(self):
         return
 
@@ -578,3 +600,13 @@ class ConversationTab(Tab):
 
     def just_before_refresh(self):
         return
+
+def jid_and_name_match(contact, txt):
+    """
+    A function used to know if a contact in the roster should
+    be shown in the roster
+    """
+    # TODO: search in nickname, and use libdiff
+    if txt in contact.get_bare_jid():
+        return True
+    return False
diff --git a/src/window.py b/src/window.py
index eb2c157b..1c3f32d9 100644
--- a/src/window.py
+++ b/src/window.py
@@ -28,7 +28,7 @@ from config import config
 from threading import Lock
 
 from contact import Contact, Resource
-from roster import RosterGroup
+from roster import RosterGroup, roster
 
 from message import Line
 from tab import MIN_WIDTH, MIN_HEIGHT
@@ -546,8 +546,8 @@ class Input(Win):
             'M-f': self.jump_word_right,
             "KEY_BACKSPACE": self.key_backspace,
             '^?': self.key_backspace,
-            '^J': self.get_text,
-            '\n': self.get_text,
+            '^J': self.on_enter,
+            '\n': self.on_enter,
             }
 
         Win.__init__(self, height, width, y, x, stdscr)
@@ -564,6 +564,51 @@ class Input(Win):
         self.hit_list = [] # current possible completion (normal)
         self.last_completion = None # Contains the last nickname completed,
                                     # if last key was a tab
+        # These are used when the user is entering a comand
+        self._on_cancel = None
+        self._on_terminate = None
+        self._instructions = ""  # a string displayed before the input, read-only
+
+    def on_enter(self):
+        """
+        Called when Enter is pressed
+        """
+        if not self._instructions:
+            return self.get_text()
+        self.on_terminate()
+        return True
+
+    def start_command(self, on_cancel, on_terminate, instructions):
+        """
+        Start a command, with an instruction, and two callbacks.
+        on_terminate is called when the command is successfull
+        on_cancel is called when the command is canceled
+        """
+        assert isinstance(instructions, str)
+        self._on_cancel = on_cancel
+        self._on_terminate = on_terminate
+        self._instructions = instructions
+
+    def cancel_command(self):
+        """
+        Call it to cancel the current command
+        """
+        self._on_cancel()
+        self._on_cancel = None
+        self._on_terminate = None
+        self._instructions = ''
+        return self.get_text()
+
+    def on_terminate(self):
+        """
+        Call it to terminate the command. Returns the content of the input
+        """
+        txt = self.get_text()
+        self._on_terminate(txt)
+        self._on_terminate = None
+        self._on_cancel = None
+        self._instructions = ''
+        return txt
 
     def is_empty(self):
         return len(self.text) == 0
@@ -881,11 +926,17 @@ class Input(Win):
         with g_lock:
             self.clear_text()
             if self.input_mode:
+                self.addstr(self._instructions, curses.color_pair(theme.COLOR_INFORMATION_BAR))
+                if self._instructions:
+                    self.addstr(' ')
                 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…
+            cursor_pos = self.pos
+            if self._instructions:
+                cursor_pos += 1 + len(self._instructions)
+            self.addstr(0, cursor_pos, '') # WTF, this works but .move() doesn't…
             self._refresh()
 
     def refresh(self):
@@ -992,11 +1043,10 @@ class RosterWin(Win):
                 y += 1
                 if group.folded:
                     continue
-                for contact in group.get_contacts():
+                for contact in group.get_contacts(roster._contact_filter):
                     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:
-- 
cgit v1.2.3