1from gi.repository import Gtk, GObject, Gdk, Pango
2
3from pychess.Utils.IconLoader import load_icon
4from pychess.widgets.InfoPanel import Panel
5
6TYPE_PERSONAL, TYPE_CHANNEL, TYPE_GUEST, \
7    TYPE_ADMIN, TYPE_COMP, TYPE_BLINDFOLD = range(6)
8
9add_icon = load_icon(16, "gtk-add", "list-add")
10remove_icon = load_icon(16, "gtk-remove", "list-remove")
11
12
13def cmp(x, y):
14    return (x > y) - (x < y)
15
16
17class TextImageTree(Gtk.TreeView):
18    """ :Description: Defines a tree with two columns.
19        The first one has text. The second one a clickable stock_icon
20    """
21
22    __gsignals__ = {
23        'activated': (GObject.SignalFlags.RUN_FIRST, None, (str, str, int)),
24        'selected': (GObject.SignalFlags.RUN_FIRST, None, (str, int))
25    }
26
27    def __init__(self, icon):
28        GObject.GObject.__init__(self)
29        self.id2iter = {}
30
31        pm = Gtk.ListStore(str, str, int, str)
32        self.sort_model = Gtk.TreeModelSort(model=pm)
33        self.set_model(self.sort_model)
34        self.idSet = set()
35
36        self.set_headers_visible(False)
37        self.set_tooltip_column(3)
38        self.set_search_column(1)
39        self.sort_model.set_sort_column_id(1, Gtk.SortType.ASCENDING)
40        self.sort_model.set_sort_func(1, self.compareFunction, 1)
41
42        # First column
43        crp = Gtk.CellRendererPixbuf()
44        crp.props.pixbuf = icon
45        self.rightcol = Gtk.TreeViewColumn("", crp)
46        self.append_column(self.rightcol)
47
48        # Second column
49        crt = Gtk.CellRendererText()
50        crt.props.ellipsize = Pango.EllipsizeMode.END
51        self.leftcol = Gtk.TreeViewColumn("", crt, text=1)
52        self.leftcol.set_expand(True)
53        self.append_column(self.leftcol)
54
55        # Mouse
56        self.pressed = None
57        self.stdcursor = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)
58        self.linkcursor = Gdk.Cursor.new(Gdk.CursorType.HAND2)
59        self.connect("button_press_event", self.button_press)
60        self.connect("button_release_event", self.button_release)
61        self.connect("motion_notify_event", self.motion_notify)
62        self.connect("leave_notify_event", self.leave_notify)
63
64        # Selection
65        self.get_selection().connect("changed", self.selection_changed)
66
67    def addRow(self, grp_id, text, grp_type):
68        """ :Description: Takes a player or a channel identified by grp_id and adds
69            them to the correct group defined by grp_type
70            :return: None
71        """
72        if grp_id in self.id2iter:
73            return
74        model = self.sort_model.get_model()
75        m_iter = model.append([grp_id, text, grp_type, GObject.markup_escape_text(text)])
76        self.id2iter[grp_id] = m_iter
77        self.idSet.add(grp_id)
78
79    def removeRow(self, grp_id):
80        """ :Description: Takes a player or channel identified by grp_id and removes them from
81            the data model.
82            :return: None
83        """
84        try:
85            m_iter = self.id2iter[grp_id]
86        except KeyError:
87            return
88        model = self.sort_model.get_model()
89        model.remove(m_iter)
90        del self.id2iter[grp_id]
91        self.idSet.remove(grp_id)
92
93    def selectRow(self, grp_id):
94        """ :Description: Takes a grp_id and finds the row associated with this id then
95            sets this row to be the focus ie selected
96
97            :returns: None
98        """
99        m_iter = self.id2iter[grp_id]
100        m_iter = self.sort_model.convert_child_iter_to_iter(m_iter)[1]
101        sel = self.get_selection()
102        sel.select_iter(m_iter)
103
104    def __contains__(self, grp_id):
105        """ :Description: Checks to see if a grp_id in a member of the id set
106            :returns: boolean
107        """
108        return grp_id in self.idSet
109
110    def button_press(self, widget, event):
111        path_col_pos = self.get_path_at_pos(int(event.x), int(event.y))
112        if path_col_pos and path_col_pos[1] == self.rightcol:
113            self.pressed = path_col_pos[0]
114
115    def button_release(self, widget, event):
116        path_col_pos = self.get_path_at_pos(int(event.x), int(event.y))
117        if path_col_pos and path_col_pos[1] == self.rightcol:
118            if self.pressed == path_col_pos[0]:
119                model = self.sort_model
120                m_iter = model.get_iter(self.pressed)
121                grp_id = model.get_value(m_iter, 0)
122                text = model.get_value(m_iter, 1)
123                grp_type = model.get_value(m_iter, 2)
124                self.emit("activated", grp_id, text, grp_type)
125        self.pressed = None
126
127    def motion_notify(self, widget, event):
128        path_col_pos = self.get_path_at_pos(int(event.x), int(event.y))
129        if path_col_pos and path_col_pos[1] == self.rightcol:
130            self.get_window().set_cursor(self.linkcursor)
131        else:
132            self.get_window().set_cursor(self.stdcursor)
133
134    def leave_notify(self, widget, event):
135        self.get_window().set_cursor(self.stdcursor)
136
137    def selection_changed(self, selection):
138        model, m_iter = selection.get_selected()
139        if m_iter:
140            grp_id = model.get_value(m_iter, 0)
141            grp_type = model.get_value(m_iter, 2)
142            self.emit("selected", grp_id, grp_type)
143
144    def compareFunction(self, treemodel, iter0, iter1, column):
145        val0 = treemodel.get_value(iter0, column).split(":")[0]
146        val1 = treemodel.get_value(iter1, column).split(":")[0]
147        if val0.isdigit() and val1.isdigit():
148            return cmp(int(val0), int(val1))
149        return cmp(val0, val1)
150
151
152class ChannelsPanel(Gtk.ScrolledWindow, Panel):
153
154    __gsignals__ = {
155        'conversationAdded': (GObject.SignalFlags.RUN_FIRST, None,
156                              (str, str, int)),
157        'conversationRemoved': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
158        'conversationSelected': (GObject.SignalFlags.RUN_FIRST, None, (str, ))
159    }
160
161    def __init__(self, connection):
162        GObject.GObject.__init__(self)
163        self.connection = connection
164
165        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
166        vbox = Gtk.VBox()
167        self.add_with_viewport(vbox)
168        self.get_child().set_shadow_type(Gtk.ShadowType.NONE)
169
170        self.joinedList = TextImageTree(remove_icon)
171        self.joinedList.connect("activated", self.onRemove)
172        self.joinedList.connect("selected", self.onSelect)
173        vbox.pack_start(self.joinedList, True, True, 0)
174
175        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
176        expander = Gtk.Expander.new(_("Friends"))
177        vbox.pack_start(expander, False, True, 0)
178        self.friendsList = TextImageTree(add_icon)
179        self.friendsList.connect("activated", self.onAdd)
180        self.friendsList.fixed_height_mode = True
181        connection.cm.connect("privateMessage", self.onPersonMessage)
182        connection.cm.connect("channelsListed", self.onChannelsListed)
183        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
184        expander.add(self.friendsList)
185        self.channels = {}
186
187        expander = Gtk.Expander.new(_("Admin"))
188        vbox.pack_start(expander, False, True, 0)
189        self.adminList = TextImageTree(add_icon)
190        self.adminList.connect("activated", self.onAdd)
191        self.adminList.fixed_height_mode = True
192        connection.cm.connect("privateMessage", self.onPersonMessage)
193        connection.cm.connect("channelsListed", self.onChannelsListed)
194        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
195        expander.add(self.adminList)
196
197        expander = Gtk.Expander.new(_("More channels"))
198        vbox.pack_start(expander, False, True, 0)
199        self.channelsList = TextImageTree(add_icon)
200        self.channelsList.connect("activated", self.onAdd)
201        self.channelsList.fixed_height_mode = True
202        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
203        expander.add(self.channelsList)
204
205        expander = Gtk.Expander.new(_("More players"))
206        vbox.pack_start(expander, False, True, 0)
207        self.playersList = TextImageTree(add_icon)
208        self.playersList.connect("activated", self.onAdd)
209        self.playersList.fixed_height_mode = True
210        connection.cm.connect("privateMessage", self.onPersonMessage)
211        connection.cm.connect("channelsListed", self.onChannelsListed)
212        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
213        expander.add(self.playersList)
214
215        expander = Gtk.Expander.new(_("Computers"))
216        vbox.pack_start(expander, False, True, 0)
217        self.compList = TextImageTree(add_icon)
218        self.compList.connect("activated", self.onAdd)
219        self.compList.fixed_height_mode = True
220        connection.cm.connect("privateMessage", self.onPersonMessage)
221        connection.cm.connect("channelsListed", self.onChannelsListed)
222        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
223        expander.add(self.compList)
224
225        expander = Gtk.Expander.new(_("BlindFold"))
226        vbox.pack_start(expander, False, True, 0)
227        self.blindList = TextImageTree(add_icon)
228        self.blindList.connect("activated", self.onAdd)
229        self.blindList.fixed_height_mode = True
230        connection.cm.connect("privateMessage", self.onPersonMessage)
231        connection.cm.connect("channelsListed", self.onChannelsListed)
232        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
233        expander.add(self.blindList)
234
235        expander = Gtk.Expander.new(_("Guests"))
236        vbox.pack_start(expander, False, True, 0)
237        self.guestList = TextImageTree(add_icon)
238        self.guestList.connect("activated", self.onAdd)
239        self.guestList.fixed_height_mode = True
240        connection.cm.connect("privateMessage", self.onPersonMessage)
241        connection.cm.connect("channelsListed", self.onChannelsListed)
242        vbox.pack_start(Gtk.Separator.new(0), False, False, 2)
243        expander.add(self.guestList)
244
245        self.channels = {}
246        self.highlighted = {}
247
248    def change_fg_colour(self, lc, cell, model, m_iter, data):
249        """
250        :Description: Changes the foreground colour of a cell
251
252        :param lc: :class:`Gtk.TreeViewColumn` The column we are interested in
253        :param cell: :class:`Gtk.CellRenderer` The cell we want to change
254        :param model: :class:`Gtk.TreeModel`
255        :param iter: :class:`Gtk.TreeIter`
256        :param data: :py:class:`dict` (key=int,value=bool) value is true if channel already highlighted
257        :return: None
258        """
259
260        for chan in data:
261            if model[m_iter][0] == chan:
262                if data[chan]:
263                    cell.set_property('foreground_rgba',
264                                      Gdk.RGBA(0.9, 0.2, 0.2, 1))
265                else:
266                    cell.set_property('foreground_rgba', Gdk.RGBA(0, 0, 0, 1))
267
268    def channel_Highlight(self, a, channel, grp_type, b):
269        """
270        :Description: Highlights a channel ( that is **not** in focus ) that has received an update and
271        changes it's foreground colour to represent change in contents
272
273        :param a: not used
274        :param channel: **(str)** The channel the message is intended for
275        :param grp_type: either TYPE_CHANNEL or TYPE_PERSONAL
276        :param b:  not used
277        :return: None
278        """
279        j_list = self.joinedList
280        leftcol = j_list.leftcol  # treeViewColumn
281
282        cur_iter = j_list.get_selection().get_selected()[1]  # Selected iter
283        if grp_type == TYPE_PERSONAL:
284            channel = "person" + channel.lower()
285        tmp_iter = j_list.id2iter[channel]
286        tmp_iter = j_list.sort_model.convert_child_iter_to_iter(tmp_iter)[1]  # channel iter
287        j_list.get_selection().select_iter(tmp_iter)
288        cell = leftcol.get_cells()[0]
289        j_list.get_selection().select_iter(cur_iter)
290        self.highlighted[channel] = True
291        if cur_iter != tmp_iter:
292            # iter = tmp_iter
293            leftcol.set_cell_data_func(cell,
294                                       self.change_fg_colour,
295                                       func_data=self.highlighted)
296
297    def start(self):
298        self.channels = self.connection.cm.getChannels()
299        if self.channels:
300            self._addChannels(self.channels)
301        for player in list(self.connection.players.values()):
302            grp_id = self.compileId(player.name, TYPE_PERSONAL)
303            if str(player.name) in self.connection.notify_users:
304                self.friendsList.addRow(
305                    grp_id, player.name + player.display_titles(), TYPE_PERSONAL)
306            elif player.online and ('(B)' in player.display_titles()):
307                self.blindList.addRow(
308                    grp_id, player.name + player.display_titles(), TYPE_BLINDFOLD)
309            elif player.online and ('(C)' in player.display_titles()):
310                self.compList.addRow(grp_id, player.name + player.display_titles(),
311                                     TYPE_COMP)
312            elif player.online and ('Guest' in str(player.name)):
313                self.guestList.addRow(
314                    grp_id, player.name + player.display_titles(), TYPE_GUEST)
315            elif player.online:
316                self.playersList.addRow(
317                    grp_id, player.name + player.display_titles(), TYPE_PERSONAL)
318
319        def addPlayer(players, new_players):
320            for player in new_players:
321                # print("Player : %s : %s" % (str(player.name),player.display_titles()))
322                if str(player.name) in self.connection.notify_users:
323                    self.friendsList.addRow(
324                        self.compileId(player.name, TYPE_PERSONAL),
325                        player.name + player.display_titles(), TYPE_PERSONAL)
326                elif '(C)' in str(player.display_titles()):
327                    self.compList.addRow(
328                        self.compileId(player.name, TYPE_COMP),
329                        player.name + player.display_titles(), TYPE_COMP)
330                elif '(B)' in str(player.display_titles()):
331                    self.blindList.addRow(
332                        self.compileId(player.name, TYPE_BLINDFOLD),
333                        player.name + player.display_titles(), TYPE_BLINDFOLD)
334                elif 'Guest' in str(player.name):
335                    self.guestList.addRow(
336                        self.compileId(player.name, TYPE_GUEST),
337                        player.name + player.display_titles(), TYPE_GUEST)
338                else:
339                    self.playersList.addRow(
340                        self.compileId(player.name, TYPE_PERSONAL),
341                        player.name + player.display_titles(), TYPE_PERSONAL)
342            return False
343
344        self.connection.players.connect("FICSPlayerEntered", addPlayer)
345
346        def removePlayer(players, player):
347            if (str(player.name) in list(self.connection.notify_users)):
348                self.friendsList.removeRow(self.compileId(player.name,
349                                                          TYPE_PERSONAL))
350            else:
351                self.playersList.removeRow(self.compileId(player.name,
352                                                          TYPE_PERSONAL))
353            return False
354
355        self.connection.players.connect("FICSPlayerExited", removePlayer)
356
357    def _addChannels(self, channels):
358        for grp_id, name in channels:
359            grp_id = self.compileId(grp_id, TYPE_CHANNEL)
360            self.channelsList.addRow(grp_id, str(grp_id) + ": " + name, TYPE_CHANNEL)
361
362        for grp_id, name in channels:
363            if grp_id in self.connection.cm.getJoinedChannels():
364                grp_id = self.compileId(grp_id, TYPE_CHANNEL)
365                if grp_id.isdigit():
366                    self.onAdd(self.channelsList, grp_id, str(grp_id) + ": " + name,
367                               TYPE_CHANNEL)
368                else:
369                    self.onAdd(self.channelsList, grp_id, name, TYPE_CHANNEL)
370
371    def onChannelsListed(self, cm, channels):
372        if not self.channels:
373            self.channels = channels
374            self._addChannels(channels)
375
376    def compileId(self, grp_id, type):
377        if type == TYPE_CHANNEL:
378            # FIXME: We can't really add stuff to the grp_id, as panels use it to
379            # identify the channel
380            assert not grp_id.startswith("person"), "Oops, this is a problem"
381        else:
382            grp_id = "person" + grp_id.lower()
383        return grp_id
384
385    def onAdd(self, grp_list, grp_id, text, grp_type):
386        if grp_id in grp_list:
387            grp_list.removeRow(grp_id)
388        self.joinedList.addRow(grp_id, text, grp_type)
389        self.emit('conversationAdded', grp_id, text, grp_type)
390        if grp_type == TYPE_CHANNEL:
391            self.connection.cm.joinChannel(grp_id)
392        self.joinedList.selectRow(grp_id)
393
394    def onRemove(self, joined_list, grp_id, text, grp_type):
395        joined_list.removeRow(grp_id)
396        if grp_type == TYPE_CHANNEL:
397            self.channelsList.addRow(grp_id, text, grp_type)
398        elif grp_type == TYPE_PERSONAL:
399            self.playersList.addRow(grp_id, text, grp_type)
400        elif grp_type == TYPE_COMP:
401            self.compList.addRow(grp_id, text, grp_type)
402        elif grp_type == TYPE_ADMIN:
403            self.adminList.addRow(grp_id, text, grp_type)
404        elif grp_type == TYPE_GUEST:
405            self.guestList.addRow(grp_id, text, grp_type)
406        elif grp_type == TYPE_BLINDFOLD:
407            self.blindList.addRow(grp_id, text, grp_type)
408
409        self.emit('conversationRemoved', grp_id)
410        if grp_type == TYPE_CHANNEL:
411            self.connection.cm.removeChannel(grp_id)
412
413    def onSelect(self, joined_list, grp_id, grp_type):
414        self.emit('conversationSelected', grp_id)
415        joined_list.get_selection().get_selected()[1]  # Selected iter
416        cell = joined_list.leftcol.get_cells()[0]
417        self.highlighted[grp_id] = False
418        joined_list.leftcol.set_cell_data_func(cell,
419                                               self.change_fg_colour,
420                                               func_data=self.highlighted)
421
422    def onPersonMessage(self, cm, name, title, isadmin, text):
423        if not self.compileId(name, TYPE_PERSONAL) in self.joinedList:
424            grp_id = self.compileId(name, TYPE_PERSONAL)
425            self.onAdd(self.playersList, grp_id, name, TYPE_PERSONAL)
426