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