1from gi.repository import Gtk, Gdk, GdkPixbuf
2
3from pychess.ic.FICSObjects import FICSSoughtMatch, FICSChallenge, \
4    get_seek_tooltip_text, get_challenge_tooltip_text
5from pychess.ic import TYPE_BLITZ, TYPE_LIGHTNING, TYPE_STANDARD, RATING_TYPES, \
6    TYPE_BULLET, TYPE_ONE_MINUTE, TYPE_THREE_MINUTE, TYPE_FIVE_MINUTE, \
7    TYPE_FIFTEEN_MINUTE, TYPE_FORTYFIVE_MINUTE, \
8    get_infobarmessage_content
9from pychess.perspectives.fics.ParrentListSection import ParrentListSection, cmp, \
10    SEPARATOR, ACCEPT, ASSESS, FOLLOW, CHAT, CHALLENGE, FINGER, ARCHIVED
11from pychess.Utils.IconLoader import get_pixbuf
12from pychess.System import conf, uistuff
13from pychess.System.Log import log
14from pychess.System.prefix import addDataPrefix
15from pychess.widgets import mainwindow
16from pychess.widgets.preferencesDialog import SoundTab
17from pychess.widgets.InfoBar import InfoBarMessage, InfoBarMessageButton
18
19__title__ = _("Seeks / Challenges")
20
21__icon__ = addDataPrefix("glade/manseek.svg")
22
23__desc__ = _("Handle seeks and challenges")
24
25
26class Sidepanel(ParrentListSection):
27
28    def load(self, widgets, connection, lounge):
29        self.widgets = widgets
30        self.connection = connection
31        self.lounge = lounge
32        self.infobar = lounge.infobar
33
34        __widget__ = lounge.seek_list
35
36        self.messages = {}
37        self.seeks = {}
38        self.challenges = {}
39        self.seekPix = get_pixbuf("glade/seek.png")
40        self.chaPix = get_pixbuf("glade/challenge.png")
41        self.manSeekPix = get_pixbuf("glade/manseek.png")
42
43        self.widgets["seekExpander"].set_vexpand(False)
44
45        self.tv = self.widgets["seektreeview"]
46        self.store = Gtk.ListStore(FICSSoughtMatch, GdkPixbuf.Pixbuf,
47                                   GdkPixbuf.Pixbuf, str, int, str, str, str,
48                                   int, Gdk.RGBA, str)
49
50        self.seek_filter = self.store.filter_new()
51        self.seek_filter.set_visible_func(self.seek_filter_func)
52
53        self.filter_toggles = {}
54        self.filter_buttons = ("standard_toggle1", "blitz_toggle1", "lightning_toggle1", "variant_toggle1", "computer_toggle1")
55        for widget in self.filter_buttons:
56            uistuff.keep(self.widgets[widget], widget)
57            self.widgets[widget].connect("toggled", self.on_filter_button_toggled)
58            initial = conf.get(widget)
59            self.filter_toggles[widget] = initial
60            self.widgets[widget].set_active(initial)
61
62        self.model = self.seek_filter.sort_new_with_model()
63        self.tv.set_model(self.model)
64
65        self.tv.set_model(self.model)
66        self.addColumns(self.tv,
67                        "FICSSoughtMatch",
68                        "",
69                        "",
70                        _("Name"),
71                        _("Rating"),
72                        _("Rated"),
73                        _("Type"),
74                        _("Clock"),
75                        "gametime",
76                        "textcolor",
77                        "tooltip",
78                        hide=[0, 8, 9, 10],
79                        pix=[1, 2])
80        self.tv.set_search_column(3)
81        self.tv.set_tooltip_column(10, )
82        for i in range(0, 2):
83            self.tv.get_model().set_sort_func(i, self.pixCompareFunction,
84                                              i + 1)
85        for i in range(2, 8):
86            self.tv.get_model().set_sort_func(i, self.compareFunction, i)
87        try:
88            self.tv.set_search_position_func(self.lowLeftSearchPosFunc, None)
89        except AttributeError:
90            # Unknow signal name is raised by gtk < 2.10
91            pass
92        for num in range(2, 7):
93            column = self.tv.get_column(num)
94            for cellrenderer in column.get_cells():
95                column.add_attribute(cellrenderer, "foreground_rgba", 9)
96        self.selection = self.tv.get_selection()
97        self.lastSeekSelected = None
98        self.selection.set_select_function(self.selectFunction, True)
99        self.selection.connect("changed", self.onSelectionChanged)
100        self.widgets["clearSeeksButton"].connect("clicked",
101                                                 self.onClearSeeksClicked)
102        self.widgets["acceptButton"].connect("clicked", self.on_accept)
103        self.widgets["declineButton"].connect("clicked", self.onDeclineClicked)
104        self.tv.connect("row-activated", self.row_activated)
105        self.tv.connect('button-press-event', self.button_press_event)
106
107        self.connection.seeks.connect("FICSSeekCreated", self.onAddSeek)
108        self.connection.seeks.connect("FICSSeekRemoved", self.onRemoveSeek)
109        self.connection.challenges.connect("FICSChallengeIssued",
110                                           self.onChallengeAdd)
111        self.connection.challenges.connect("FICSChallengeRemoved",
112                                           self.onChallengeRemove)
113        self.connection.glm.connect("our-seeks-removed",
114                                    self.our_seeks_removed)
115        self.connection.glm.connect("assessReceived", self.onAssessReceived)
116        self.connection.bm.connect("playGameCreated", self.onPlayingGame)
117        self.connection.bm.connect("curGameEnded", self.onCurGameEnded)
118
119        def get_sort_order(modelsort):
120            identity, order = modelsort.get_sort_column_id()
121            if identity is None or identity < 0:
122                identity = 0
123            else:
124                identity += 1
125            if order == Gtk.SortType.DESCENDING:
126                identity = -1 * identity
127            return identity
128
129        def set_sort_order(modelsort, value):
130            if value != 0:
131                order = Gtk.SortType.ASCENDING if value > 0 else Gtk.SortType.DESCENDING
132                modelsort.set_sort_column_id(abs(value) - 1, order)
133
134        uistuff.keep(self.model, "seektreeview_sort_order_col", get_sort_order,
135                     lambda modelsort, value: set_sort_order(modelsort, value))
136
137        self.createLocalMenu((ACCEPT, ASSESS, CHALLENGE, CHAT, FOLLOW, SEPARATOR, FINGER, ARCHIVED))
138        self.assess_sent = False
139
140        return __widget__
141
142    def seek_filter_func(self, model, iter, data):
143        sought_match = model[iter][0]
144        is_computer = sought_match.player.isComputer()
145        is_standard = sought_match.game_type.rating_type in (TYPE_STANDARD, TYPE_FIFTEEN_MINUTE, TYPE_FORTYFIVE_MINUTE) and not is_computer
146        is_blitz = sought_match.game_type.rating_type in (TYPE_BLITZ, TYPE_THREE_MINUTE, TYPE_FIVE_MINUTE) and not is_computer
147        is_lightning = sought_match.game_type.rating_type in (TYPE_LIGHTNING, TYPE_BULLET, TYPE_ONE_MINUTE) and not is_computer
148        is_variant = sought_match.game_type.rating_type in RATING_TYPES[9:] and not is_computer
149        return (
150            self.filter_toggles["computer_toggle1"] and is_computer) or (
151            self.filter_toggles["standard_toggle1"] and is_standard) or (
152            self.filter_toggles["blitz_toggle1"] and is_blitz) or (
153            self.filter_toggles["lightning_toggle1"] and is_lightning) or (
154            self.filter_toggles["variant_toggle1"] and is_variant)
155
156    def on_filter_button_toggled(self, widget):
157        for button in self.filter_buttons:
158            self.filter_toggles[button] = self.widgets[button].get_active()
159        self.seek_filter.refilter()
160
161    def onAssessReceived(self, glm, assess):
162        if self.assess_sent:
163            self.assess_sent = False
164            dialog = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.INFO,
165                                       buttons=Gtk.ButtonsType.OK)
166            dialog.set_title(_("Assess"))
167            dialog.set_markup(_("Effect on ratings by the possible outcomes"))
168            grid = Gtk.Grid()
169            grid.set_column_homogeneous(True)
170            grid.set_row_spacing(12)
171            grid.set_row_spacing(12)
172            name0 = Gtk.Label()
173            name0.set_markup("<b>%s</b>" % assess["names"][0])
174            name1 = Gtk.Label()
175            name1.set_markup("<b>%s</b>" % assess["names"][1])
176            grid.attach(Gtk.Label(label=""), 0, 0, 1, 1)
177            grid.attach(name0, 1, 0, 1, 1)
178            grid.attach(name1, 2, 0, 1, 1)
179            grid.attach(Gtk.Label(assess["type"]), 0, 1, 1, 1)
180            grid.attach(Gtk.Label(assess["oldRD"][0]), 1, 1, 1, 1)
181            grid.attach(Gtk.Label(assess["oldRD"][1]), 2, 1, 1, 1)
182            grid.attach(Gtk.Label(_("Win:")), 0, 2, 1, 1)
183            grid.attach(Gtk.Label(assess["win"][0]), 1, 2, 1, 1)
184            grid.attach(Gtk.Label(assess["win"][1]), 2, 2, 1, 1)
185            grid.attach(Gtk.Label(_("Draw:")), 0, 3, 1, 1)
186            grid.attach(Gtk.Label(assess["draw"][0]), 1, 3, 1, 1)
187            grid.attach(Gtk.Label(assess["draw"][1]), 2, 3, 1, 1)
188            grid.attach(Gtk.Label(_("Loss:")), 0, 4, 1, 1)
189            grid.attach(Gtk.Label(assess["loss"][0]), 1, 4, 1, 1)
190            grid.attach(Gtk.Label(assess["loss"][1]), 2, 4, 1, 1)
191            grid.attach(Gtk.Label(_("New RD:")), 0, 5, 1, 1)
192            grid.attach(Gtk.Label(assess["newRD"][0]), 1, 5, 1, 1)
193            grid.attach(Gtk.Label(assess["newRD"][1]), 2, 5, 1, 1)
194            grid.show_all()
195            dialog.get_message_area().add(grid)
196            dialog.run()
197            dialog.destroy()
198
199    def getSelectedPlayer(self):
200        model, sel_iter = self.tv.get_selection().get_selected()
201        if sel_iter is not None:
202            sought = model.get_value(sel_iter, 0)
203            return sought.player
204
205    def textcolor_normal(self):
206        style_ctxt = self.tv.get_style_context()
207        return style_ctxt.get_color(Gtk.StateFlags.NORMAL)
208
209    def textcolor_selected(self):
210        style_ctxt = self.tv.get_style_context()
211        return style_ctxt.get_color(Gtk.StateFlags.INSENSITIVE)
212
213    def selectFunction(self, selection, model, path, is_selected, data):
214        if model[path][9] == self.textcolor_selected():
215            return False
216        else:
217            return True
218
219    def __isAChallengeOrOurSeek(self, row):
220        sought = row[0]
221        textcolor = row[9]
222        red0, green0, blue0 = textcolor.red, textcolor.green, textcolor.blue
223        selected = self.textcolor_selected()
224        red1, green1, blue1 = selected.red, selected.green, selected.blue
225        if (isinstance(sought, FICSChallenge)) or (red0 == red1 and green0 == green1 and
226                                                   blue0 == blue1):
227            return True
228        else:
229            return False
230
231    def compareFunction(self, model, iter0, iter1, column):
232        row0 = list(model[model.get_path(iter0)])
233        row1 = list(model[model.get_path(iter1)])
234        is_ascending = True if self.tv.get_column(column - 1).get_sort_order() is \
235            Gtk.SortType.ASCENDING else False
236        if self.__isAChallengeOrOurSeek(
237                row0) and not self.__isAChallengeOrOurSeek(row1):
238            if is_ascending:
239                return -1
240            else:
241                return 1
242        elif self.__isAChallengeOrOurSeek(
243                row1) and not self.__isAChallengeOrOurSeek(row0):
244            if is_ascending:
245                return 1
246            else:
247                return -1
248        elif column == 7:
249            return self.timeCompareFunction(model, iter0, iter1, column)
250        else:
251            value0 = row0[column]
252            value0 = value0.lower() if isinstance(value0, str) else value0
253            value1 = row1[column]
254            value1 = value1.lower() if isinstance(value1, str) else value1
255            return cmp(value0, value1)
256
257    def __updateActiveSeeksLabel(self):
258        count = len(self.seeks) + len(self.challenges)
259        self.widgets["activeSeeksLabel"].set_text(_("Active seeks: %d") %
260                                                  count)
261
262    def onAddSeek(self, seeks, seek):
263        log.debug("%s" % seek,
264                  extra={"task": (self.connection.username, "onAddSeek")})
265        pix = self.seekPix if seek.automatic else self.manSeekPix
266        textcolor = self.textcolor_selected() if seek.player.name == self.connection.getUsername() \
267            else self.textcolor_normal()
268        seek_ = [seek, seek.player.getIcon(gametype=seek.game_type), pix,
269                 seek.player.name + seek.player.display_titles(),
270                 seek.player_rating, seek.display_rated,
271                 seek.game_type.display_text, seek.display_timecontrol,
272                 seek.sortable_time, textcolor, get_seek_tooltip_text(seek)]
273
274        if textcolor == self.textcolor_selected():
275            txi = self.store.prepend(seek_)
276            self.tv.scroll_to_cell(self.store.get_path(txi))
277            self.widgets["clearSeeksButton"].set_sensitive(True)
278        else:
279            txi = self.store.append(seek_)
280        self.seeks[hash(seek)] = txi
281        self.__updateActiveSeeksLabel()
282
283    def onRemoveSeek(self, seeks, seek):
284        log.debug("%s" % seek,
285                  extra={"task": (self.connection.username, "onRemoveSeek")})
286        try:
287            treeiter = self.seeks[hash(seek)]
288        except KeyError:
289            # We ignore removes we haven't added, as it seems fics sends a
290            # lot of removes for games it has never told us about
291            return
292        if self.store.iter_is_valid(treeiter):
293            self.store.remove(treeiter)
294        del self.seeks[hash(seek)]
295        self.__updateActiveSeeksLabel()
296
297    def onChallengeAdd(self, challenges, challenge):
298        log.debug("%s" % challenge,
299                  extra={"task": (self.connection.username, "onChallengeAdd")})
300        SoundTab.playAction("aPlayerChecks")
301
302        # TODO: differentiate between challenges and manual-seek-accepts
303        # (wait until seeks are comparable FICSSeek objects to do this)
304        # Related: http://code.google.com/p/pychess/issues/detail?id=206
305        if challenge.adjourned:
306            text = _(" would like to resume your adjourned <b>%(time)s</b> " +
307                     "<b>%(gametype)s</b> game.") % \
308                {"time": challenge.display_timecontrol,
309                 "gametype": challenge.game_type.display_text}
310        else:
311            text = _(" challenges you to a <b>%(time)s</b> %(rated)s <b>%(gametype)s</b> game") \
312                % {"time": challenge.display_timecontrol,
313                   "rated": challenge.display_rated.lower(),
314                   "gametype": challenge.game_type.display_text}
315            if challenge.color:
316                text += _(" where <b>%(player)s</b> plays <b>%(color)s</b>.") \
317                    % {"player": challenge.player.name,
318                       "color": _("white") if challenge.color == "white" else _("black")}
319            else:
320                text += "."
321        content = get_infobarmessage_content(challenge.player,
322                                             text,
323                                             gametype=challenge.game_type)
324
325        def callback(infobar, response, message):
326            if response == Gtk.ResponseType.ACCEPT:
327                self.connection.om.acceptIndex(challenge.index)
328            elif response == Gtk.ResponseType.NO:
329                self.connection.om.declineIndex(challenge.index)
330            message.dismiss()
331            return False
332
333        message = InfoBarMessage(Gtk.MessageType.QUESTION, content, callback)
334        message.add_button(InfoBarMessageButton(
335            _("Accept"), Gtk.ResponseType.ACCEPT))
336        message.add_button(InfoBarMessageButton(
337            _("Decline"), Gtk.ResponseType.NO))
338        message.add_button(InfoBarMessageButton(Gtk.STOCK_CLOSE,
339                                                Gtk.ResponseType.CANCEL))
340        self.messages[hash(challenge)] = message
341        self.infobar.push_message(message)
342
343        txi = self.store.prepend(
344            [challenge, challenge.player.getIcon(gametype=challenge.game_type),
345             self.chaPix, challenge.player.name +
346             challenge.player.display_titles(), challenge.player_rating,
347             challenge.display_rated, challenge.game_type.display_text,
348             challenge.display_timecontrol, challenge.sortable_time,
349             self.textcolor_normal(), get_challenge_tooltip_text(challenge)])
350        self.challenges[hash(challenge)] = txi
351        self.__updateActiveSeeksLabel()
352        self.widgets["seektreeview"].scroll_to_cell(self.store.get_path(txi))
353
354    def onChallengeRemove(self, challenges, challenge):
355        log.debug(
356            "%s" % challenge,
357            extra={"task": (self.connection.username, "onChallengeRemove")})
358        try:
359            txi = self.challenges[hash(challenge)]
360        except KeyError:
361            pass
362        else:
363            if self.store.iter_is_valid(txi):
364                self.store.remove(txi)
365            del self.challenges[hash(challenge)]
366
367        try:
368            message = self.messages[hash(challenge)]
369        except KeyError:
370            pass
371        else:
372            message.dismiss()
373            del self.messages[hash(challenge)]
374        self.__updateActiveSeeksLabel()
375
376    def our_seeks_removed(self, glm):
377        self.widgets["clearSeeksButton"].set_sensitive(False)
378
379    def onDeclineClicked(self, button):
380        model, sel_iter = self.tv.get_selection().get_selected()
381        if sel_iter is None:
382            return
383        sought = model.get_value(sel_iter, 0)
384        self.connection.om.declineIndex(sought.index)
385
386        try:
387            message = self.messages[hash(sought)]
388        except KeyError:
389            pass
390        else:
391            message.dismiss()
392            del self.messages[hash(sought)]
393
394    def onClearSeeksClicked(self, button):
395        self.connection.client.run_command("unseek")
396        self.widgets["clearSeeksButton"].set_sensitive(False)
397
398    def row_activated(self, treeview, path, view_column):
399        model, sel_iter = self.tv.get_selection().get_selected()
400        if sel_iter is None:
401            return
402        sought = model.get_value(sel_iter, 0)
403        if self.lastSeekSelected is None or \
404                sought.index != self.lastSeekSelected.index:
405            return
406        if path != model.get_path(sel_iter):
407            return
408        self.on_accept(None)
409
410    def onSelectionChanged(self, selection):
411        model, sel_iter = selection.get_selected()
412        sought = None
413        a_seek_is_selected = False
414        selection_is_challenge = False
415        if sel_iter is not None:
416            a_seek_is_selected = True
417            sought = model.get_value(sel_iter, 0)
418            if isinstance(sought, FICSChallenge):
419                selection_is_challenge = True
420
421            # # select sought owner on players tab to let challenge him using right click menu
422            # if sought.player in self.lounge.players_tab.players:
423                # # we have to undo the iter conversion that was introduced by the filter and sort model
424                # iter0 = self.lounge.players_tab.players[sought.player]["ti"]
425                # filtered_model = self.lounge.players_tab.player_filter
426                # is_ok, iter1 = filtered_model.convert_child_iter_to_iter(iter0)
427                # sorted_model = self.lounge.players_tab.model
428                # is_ok, iter2 = sorted_model.convert_child_iter_to_iter(iter1)
429                # players_selection = self.lounge.players_tab.tv.get_selection()
430                # players_selection.select_iter(iter2)
431                # self.lounge.players_tab.tv.scroll_to_cell(sorted_model.get_path(iter2))
432            # else:
433                # print(sought.player, "not in self.lounge.players_tab.players")
434
435        self.lastSeekSelected = sought
436        self.widgets["acceptButton"].set_sensitive(a_seek_is_selected)
437        self.widgets["declineButton"].set_sensitive(selection_is_challenge)
438
439    def _clear_messages(self):
440        for message in self.messages.values():
441            message.dismiss()
442        self.messages.clear()
443
444    def onPlayingGame(self, bm, game):
445        self._clear_messages()
446        self.widgets["seekListContent"].set_sensitive(False)
447        self.widgets["clearSeeksButton"].set_sensitive(False)
448        self.__updateActiveSeeksLabel()
449
450    def onCurGameEnded(self, bm, game):
451        self.widgets["seekListContent"].set_sensitive(True)
452