1""" This module handles the tabbed layout in PyChess """ 2 3import sys 4from collections import defaultdict 5 6from gi.repository import Gtk, GObject 7 8import pychess 9from .BoardControl import BoardControl 10from .ChessClock import ChessClock 11from .MenuItemsDict import MenuItemsDict 12from pychess.System import conf 13 14from pychess.System.Log import log 15from pychess.Utils.IconLoader import get_pixbuf 16from pychess.Utils.const import REMOTE, UNFINISHED_STATES, PAUSED, RUNNING, LOCAL, \ 17 WHITE, BLACK, ACTION_MENU_ITEMS, DRAW, UNDOABLE_STATES, HINT, SPY, WHITEWON, \ 18 MENU_ITEMS, BLACKWON, DROP, FAN_PIECES, TOOL_CHESSDB, TOOL_SCOUTFISH 19from pychess.Utils.GameModel import GameModel 20from pychess.Utils.Move import listToMoves 21from pychess.Utils.lutils import lmove 22from pychess.Utils.lutils.lmove import ParsingError 23from pychess.Utils.logic import playerHasMatingMaterial, isClaimableDraw 24from pychess.ic import get_infobarmessage_content, get_infobarmessage_content2 25from pychess.ic.FICSObjects import get_player_tooltip_text 26from pychess.ic.ICGameModel import ICGameModel 27from pychess.widgets import createImage, createAlignment, gtk_close 28from pychess.widgets.InfoBar import InfoBarNotebook, InfoBarMessage, InfoBarMessageButton 29from pychess.perspectives import perspective_manager 30 31 32light_on = get_pixbuf("glade/16x16/weather-clear.png") 33light_off = get_pixbuf("glade/16x16/weather-clear-night.png") 34 35widgets = None 36 37 38def setWidgets(w): 39 global widgets 40 widgets = w 41 pychess.widgets.main_window = widgets["main_window"] 42 43 44def getWidgets(): 45 return widgets 46 47 48class GameWidget(GObject.GObject): 49 50 __gsignals__ = { 51 'game_close_clicked': (GObject.SignalFlags.RUN_FIRST, None, ()), 52 'title_changed': (GObject.SignalFlags.RUN_FIRST, None, (str, )), 53 'closed': (GObject.SignalFlags.RUN_FIRST, None, ()), 54 } 55 56 def __init__(self, gamemodel, perspective): 57 GObject.GObject.__init__(self) 58 self.gamemodel = gamemodel 59 self.perspective = perspective 60 self.cids = {} 61 self.closed = False 62 63 # InfoBarMessage with rematch, undo or observe buttons 64 self.game_ended_message = None 65 66 self.tabcontent, white_label, black_label, self.game_info_label = self.initTabcontents() 67 self.boardvbox, self.board, self.infobar, self.clock = self.initBoardAndClock(self.gamemodel) 68 self.stat_hbox = self.initButtons(self.board) 69 70 self.player_name_labels = (white_label, black_label) 71 self.infobar.connect("hide", self.infobar_hidden) 72 73 self.notebookKey = Gtk.Alignment() 74 self.menuitems = MenuItemsDict() 75 76 self.gamemodel_cids = [ 77 self.gamemodel.connect_after("game_started", self.game_started), 78 self.gamemodel.connect_after("game_ended", self.game_ended), 79 self.gamemodel.connect_after("game_changed", self.game_changed), 80 self.gamemodel.connect("game_paused", self.game_paused), 81 self.gamemodel.connect("game_resumed", self.game_resumed), 82 self.gamemodel.connect("moves_undone", self.moves_undone), 83 self.gamemodel.connect("game_unended", self.game_unended), 84 self.gamemodel.connect("game_saved", self.game_saved), 85 self.gamemodel.connect("players_changed", self.players_changed), 86 self.gamemodel.connect("analyzer_added", self.analyzer_added), 87 self.gamemodel.connect("analyzer_removed", self.analyzer_removed), 88 self.gamemodel.connect("message_received", self.message_received), 89 ] 90 self.players_changed(self.gamemodel) 91 92 self.notify_cids = [conf.notify_add("showFICSgameno", self.on_show_fics_gameno), ] 93 94 if self.gamemodel.display_text: 95 if isinstance(self.gamemodel, ICGameModel) and conf.get("showFICSgameno"): 96 self.game_info_label.set_text("%s [%s]" % ( 97 self.display_text, self.gamemodel.ficsgame.gameno)) 98 else: 99 self.game_info_label.set_text(self.display_text) 100 if self.gamemodel.timed: 101 self.cids[self.gamemodel.timemodel] = self.gamemodel.timemodel.connect("zero_reached", self.zero_reached) 102 103 self.connections = defaultdict(list) 104 if isinstance(self.gamemodel, ICGameModel): 105 self.connections[self.gamemodel.connection.bm].append( 106 self.gamemodel.connection.bm.connect("player_lagged", self.player_lagged)) 107 self.connections[self.gamemodel.connection.bm].append( 108 self.gamemodel.connection.bm.connect("opp_not_out_of_time", self.opp_not_out_of_time)) 109 self.cids[self.board.view] = self.board.view.connect("shownChanged", self.shownChanged) 110 111 if isinstance(self.gamemodel, ICGameModel): 112 self.gamemodel.gmwidg_ready.set() 113 114 def _del(self): 115 if self.gamemodel.offline_lecture: 116 self.gamemodel.lecture_exit_event.set() 117 118 for obj in self.cids: 119 if obj.handler_is_connected(self.cids[obj]): 120 log.debug("GameWidget._del: disconnecting %s" % repr(obj)) 121 obj.disconnect(self.cids[obj]) 122 self.cids = {} 123 124 for obj in self.connections: 125 for handler_id in self.connections[obj]: 126 if obj.handler_is_connected(handler_id): 127 obj.disconnect(handler_id) 128 self.connections = {} 129 130 for cid in self.gamemodel_cids: 131 self.gamemodel.disconnect(cid) 132 133 for cid in self.notify_cids: 134 conf.notify_remove(cid) 135 136 self.board._del() 137 138 if self.game_ended_message is not None: 139 self.game_ended_message.callback = None 140 141 def on_show_fics_gameno(self, *args): 142 """ Checks the configuration / preferences to see if the FICS 143 game number should be displayed next to player names. 144 """ 145 if isinstance(self.gamemodel, ICGameModel) and conf.get("showFICSgameno"): 146 self.game_info_label.set_text(" [%s]" % self.gamemodel.ficsgame.gameno) 147 else: 148 self.game_info_label.set_text("") 149 150 def infront(self): 151 for menuitem in self.menuitems: 152 self.menuitems[menuitem].update() 153 154 for widget in MENU_ITEMS: 155 if widget in self.menuitems: 156 continue 157 elif widget == 'show_sidepanels' and isDesignGWShown(): 158 getWidgets()[widget].set_property('sensitive', False) 159 else: 160 getWidgets()[widget].set_property('sensitive', True) 161 162 # Change window title 163 getWidgets()['main_window'].set_title(self.display_text + (" - " if self.display_text != "" else "") + "PyChess") 164 165 def _update_menu_abort(self): 166 if self.gamemodel.hasEnginePlayer(): 167 self.menuitems["abort"].sensitive = True 168 self.menuitems["abort"].tooltip = "" 169 elif self.gamemodel.isObservationGame(): 170 self.menuitems["abort"].sensitive = False 171 elif isinstance(self.gamemodel, ICGameModel) and self.gamemodel.status in UNFINISHED_STATES: 172 if self.gamemodel.ply < 2: 173 self.menuitems["abort"].label = _("Abort") 174 self.menuitems["abort"].tooltip = \ 175 _("This game can be automatically aborted without rating loss because \ 176 there has not yet been two moves made") 177 else: 178 self.menuitems["abort"].label = _("Offer Abort") 179 self.menuitems["abort"].tooltip = \ 180 _("Your opponent must agree to abort the game because there \ 181 has been two or more moves made") 182 self.menuitems["abort"].sensitive = True 183 else: 184 self.menuitems["abort"].sensitive = False 185 self.menuitems["abort"].tooltip = "" 186 187 def _update_menu_adjourn(self): 188 self.menuitems["adjourn"].sensitive = \ 189 isinstance(self.gamemodel, ICGameModel) and \ 190 self.gamemodel.status in UNFINISHED_STATES and \ 191 not self.gamemodel.isObservationGame() and \ 192 not self.gamemodel.hasGuestPlayers() 193 194 if isinstance(self.gamemodel, ICGameModel) and \ 195 self.gamemodel.status in UNFINISHED_STATES and \ 196 not self.gamemodel.isObservationGame() and self.gamemodel.hasGuestPlayers(): 197 self.menuitems["adjourn"].tooltip = \ 198 _("This game can not be adjourned because one or both players are guests") 199 else: 200 self.menuitems["adjourn"].tooltip = "" 201 202 def _update_menu_draw(self): 203 self.menuitems["draw"].sensitive = self.gamemodel.status in UNFINISHED_STATES \ 204 and not self.gamemodel.isObservationGame() 205 206 def can_win(color): 207 if self.gamemodel.timed: 208 return playerHasMatingMaterial(self.gamemodel.boards[-1], color) and \ 209 self.gamemodel.timemodel.getPlayerTime(color) > 0 210 else: 211 return playerHasMatingMaterial(self.gamemodel.boards[-1], 212 color) 213 if isClaimableDraw(self.gamemodel.boards[-1]) or not \ 214 (can_win(self.gamemodel.players[0].color) or 215 can_win(self.gamemodel.players[1].color)): 216 self.menuitems["draw"].label = _("Claim Draw") 217 218 def _update_menu_resign(self): 219 self.menuitems["resign"].sensitive = self.gamemodel.status in UNFINISHED_STATES \ 220 and not self.gamemodel.isObservationGame() 221 222 def _update_menu_pause_and_resume(self): 223 def game_is_pausable(): 224 if self.gamemodel.isEngine2EngineGame() or \ 225 (self.gamemodel.hasLocalPlayer() and 226 (self.gamemodel.isLocalGame() or 227 (isinstance(self.gamemodel, ICGameModel) and 228 self.gamemodel.ply > 1))): 229 if sys.platform == "win32" and self.gamemodel.hasEnginePlayer( 230 ): 231 return False 232 else: 233 return True 234 else: 235 return False 236 237 self.menuitems["pause1"].sensitive = \ 238 self.gamemodel.status == RUNNING and game_is_pausable() 239 self.menuitems["resume1"].sensitive = \ 240 self.gamemodel.status == PAUSED and game_is_pausable() 241 # TODO: if IC game is over and game ended in adjournment 242 # and opponent is available, enable Resume 243 244 def _update_menu_undo(self): 245 if self.gamemodel.isObservationGame(): 246 self.menuitems["undo1"].sensitive = False 247 elif isinstance(self.gamemodel, ICGameModel): 248 if self.gamemodel.status in UNFINISHED_STATES and self.gamemodel.ply > 0: 249 self.menuitems["undo1"].sensitive = True 250 else: 251 self.menuitems["undo1"].sensitive = False 252 elif self.gamemodel.ply > 0 and self.gamemodel.status in UNDOABLE_STATES + (RUNNING,): 253 self.menuitems["undo1"].sensitive = True 254 else: 255 self.menuitems["undo1"].sensitive = False 256 257 def _update_menu_ask_to_move(self): 258 if self.gamemodel.isObservationGame(): 259 self.menuitems["ask_to_move"].sensitive = False 260 elif isinstance(self.gamemodel, ICGameModel): 261 self.menuitems["ask_to_move"].sensitive = False 262 elif self.gamemodel.waitingplayer.__type__ == LOCAL and self.gamemodel.status \ 263 in UNFINISHED_STATES and self.gamemodel.status != PAUSED: 264 self.menuitems["ask_to_move"].sensitive = True 265 else: 266 self.menuitems["ask_to_move"].sensitive = False 267 268 def _showHolding(self, holding): 269 figurines = ["", ""] 270 for color in (BLACK, WHITE): 271 for piece in holding[color].keys(): 272 count = holding[color][piece] 273 figurines[color] += " " if count == 0 else FAN_PIECES[color][ 274 piece] * count 275 print(figurines[BLACK] + " " + figurines[WHITE]) 276 277 def shownChanged(self, boardview, shown): 278 # Help crazyhouse testing 279 # if self.gamemodel.boards[-1].variant == CRAZYHOUSECHESS: 280 # holding = self.gamemodel.getBoardAtPly(shown, boardview.variation).board.holding 281 # self._showHolding(holding) 282 283 if self.gamemodel.timemodel.hasTimes and \ 284 (self.gamemodel.endstatus or self.gamemodel.status in (DRAW, WHITEWON, BLACKWON)) and \ 285 boardview.shownIsMainLine(): 286 wmovecount, color = divmod(shown + 1, 2) 287 bmovecount = wmovecount - 1 if color == WHITE else wmovecount 288 if self.gamemodel.timemodel.hasBWTimes(bmovecount, wmovecount): 289 self.clock.update(wmovecount, bmovecount) 290 291 self.on_shapes_changed(self.board) 292 293 def game_started(self, gamemodel): 294 if self.gamemodel.isLocalGame(): 295 self.menuitems["abort"].label = _("Abort") 296 self._update_menu_abort() 297 self._update_menu_adjourn() 298 self._update_menu_draw() 299 if self.gamemodel.isLocalGame(): 300 self.menuitems["pause1"].label = _("Pause") 301 self.menuitems["resume1"].label = _("Resume") 302 else: 303 self.menuitems["pause1"].label = _("Offer Pause") 304 self.menuitems["resume1"].label = _("Offer Resume") 305 self._update_menu_pause_and_resume() 306 self._update_menu_resign() 307 if self.gamemodel.isLocalGame(): 308 self.menuitems["undo1"].label = _("Undo") 309 else: 310 self.menuitems["undo1"].label = _("Offer Undo") 311 self._update_menu_undo() 312 self._update_menu_ask_to_move() 313 314 if isinstance(gamemodel, 315 ICGameModel) and not gamemodel.isObservationGame(): 316 for item in self.menuitems: 317 if item in self.menuitems.ANAL_MENU_ITEMS: 318 self.menuitems[item].sensitive = False 319 320 if not gamemodel.timed and not gamemodel.timemodel.hasTimes: 321 try: 322 self.boardvbox.remove(self.clock.get_parent()) 323 except TypeError: 324 # no clock 325 pass 326 327 def game_ended(self, gamemodel, reason): 328 for item in self.menuitems: 329 if item in self.menuitems.ANAL_MENU_ITEMS: 330 self.menuitems[item].sensitive = True 331 elif item not in self.menuitems.VIEW_MENU_ITEMS: 332 self.menuitems[item].sensitive = False 333 self._update_menu_undo() 334 self._set_arrow(HINT, None) 335 self._set_arrow(SPY, None) 336 return False 337 338 def game_changed(self, gamemodel, ply): 339 '''This runs when the game changes. It updates everything.''' 340 self._update_menu_abort() 341 self._update_menu_ask_to_move() 342 self._update_menu_draw() 343 self._update_menu_pause_and_resume() 344 self._update_menu_undo() 345 if isinstance(gamemodel, 346 ICGameModel): # on FICS game board change update allob 347 if gamemodel.connection is not None and not gamemodel.connection.ICC: 348 allob = 'allob ' + str(gamemodel.ficsgame.gameno) 349 gamemodel.connection.client.run_command(allob) 350 351 for analyzer_type in (HINT, SPY): 352 # only clear arrows if analyzer is examining the last position 353 if analyzer_type in gamemodel.spectators and \ 354 gamemodel.spectators[analyzer_type].board == gamemodel.boards[-1]: 355 self._set_arrow(analyzer_type, None) 356 self.name_changed(gamemodel.players[0]) # We may need to add * to name 357 358 if gamemodel.isObservationGame() and not self.isInFront(): 359 self.light_on_off(True) 360 361 # print(gamemodel.waitingplayer, gamemodel.waitingplayer.__type__) 362 if not gamemodel.isPlayingICSGame(): 363 self.clearMessages() 364 365 return False 366 367 def game_saved(self, gamemodel, uri): 368 '''Run when the game is saved. Will remove * from title.''' 369 self.name_changed(gamemodel.players[0]) # We may need to remove * in name 370 return False 371 372 def game_paused(self, gamemodel): 373 self._update_menu_pause_and_resume() 374 self._update_menu_undo() 375 self._update_menu_ask_to_move() 376 return False 377 378 def game_resumed(self, gamemodel): 379 self._update_menu_pause_and_resume() 380 self._update_menu_undo() 381 self._update_menu_ask_to_move() 382 return False 383 384 def moves_undone(self, gamemodel, moves): 385 self.game_changed(gamemodel, 0) 386 return False 387 388 def game_unended(self, gamemodel): 389 self._update_menu_abort() 390 self._update_menu_adjourn() 391 self._update_menu_draw() 392 self._update_menu_pause_and_resume() 393 self._update_menu_resign() 394 self._update_menu_undo() 395 self._update_menu_ask_to_move() 396 return False 397 398 def _set_arrow(self, analyzer_type, coordinates): 399 if self.gamemodel.isPlayingICSGame(): 400 return 401 402 if analyzer_type == HINT: 403 self.board.view._setGreenarrow(coordinates) 404 else: 405 self.board.view._setRedarrow(coordinates) 406 407 def _on_analyze(self, analyzer, analysis, analyzer_type): 408 if self.board.view.animating: 409 return 410 411 if not self.menuitems[analyzer_type + "_mode"].active: 412 return 413 414 if len(analysis) >= 1 and analysis[0] is not None: 415 ply, movstrs, score, depth, nps = analysis[0] 416 board = analyzer.board 417 try: 418 moves = listToMoves(board, movstrs, validate=True) 419 except ParsingError as e: 420 # ParsingErrors may happen when parsing "old" lines from 421 # analyzing engines, which haven't yet noticed their new tasks 422 log.debug("GameWidget._on_analyze(): Ignored (%s) from analyzer: ParsingError%s" % 423 (' '.join(movstrs), e)) 424 return 425 426 if moves and (self.gamemodel.curplayer.__type__ == LOCAL or 427 [player.__type__ for player in self.gamemodel.players] == [REMOTE, REMOTE] or 428 self.gamemodel.status not in UNFINISHED_STATES): 429 if moves[0].flag == DROP: 430 piece = lmove.FCORD(moves[0].move) 431 color = board.color if analyzer_type == HINT else 1 - board.color 432 cord0 = board.getHoldingCord(color, piece) 433 self._set_arrow(analyzer_type, (cord0, moves[0].cord1)) 434 else: 435 self._set_arrow(analyzer_type, moves[0].cords) 436 else: 437 self._set_arrow(analyzer_type, None) 438 return False 439 440 def analyzer_added(self, gamemodel, analyzer, analyzer_type): 441 self.cids[analyzer] = \ 442 analyzer.connect("analyze", self._on_analyze, analyzer_type) 443 # self.menuitems[analyzer_type + "_mode"].active = True 444 self.menuitems[analyzer_type + "_mode"].sensitive = True 445 return False 446 447 def analyzer_removed(self, gamemodel, analyzer, analyzer_type): 448 self._set_arrow(analyzer_type, None) 449 # self.menuitems[analyzer_type + "_mode"].active = False 450 self.menuitems[analyzer_type + "_mode"].sensitive = False 451 452 try: 453 if analyzer.handler_is_connected(self.cids[analyzer]): 454 analyzer.disconnect(self.cids[analyzer]) 455 del self.cids[analyzer] 456 except KeyError: 457 pass 458 459 return False 460 461 def show_arrow(self, analyzer, analyzer_type): 462 self.menuitems[analyzer_type + "_mode"].active = True 463 self._on_analyze(analyzer, analyzer.getAnalysis(), analyzer_type) 464 return False 465 466 def hide_arrow(self, analyzer, analyzer_type): 467 self.menuitems[analyzer_type + "_mode"].active = False 468 self._set_arrow(analyzer_type, None) 469 return False 470 471 def player_display_text(self, color, with_elo): 472 text = "" 473 if isinstance(self.gamemodel, ICGameModel): 474 if self.gamemodel.ficsplayers: 475 text = self.gamemodel.ficsplayers[color].name 476 if (self.gamemodel.connection.username == 477 self.gamemodel.ficsplayers[color].name) and \ 478 self.gamemodel.ficsplayers[color].isGuest(): 479 text += " (Player)" 480 else: 481 if self.gamemodel.players: 482 text = repr(self.gamemodel.players[color]) 483 if with_elo: 484 elo = self.gamemodel.tags.get("WhiteElo" if color == WHITE else "BlackElo") 485 if elo not in [None, '', '?', '0', 0]: 486 text += " (%s)" % str(elo) 487 return text 488 489 @property 490 def display_text(self): 491 if not self.gamemodel.players: 492 return "" 493 '''This will give you the name of the game.''' 494 vs = " - " 495 t = vs.join((self.player_display_text(WHITE, True), 496 self.player_display_text(BLACK, True))) 497 return t 498 499 def players_changed(self, gamemodel): 500 log.debug("GameWidget.players_changed: starting %s" % repr(gamemodel)) 501 for player in gamemodel.players: 502 self.name_changed(player) 503 # Notice that this may connect the same player many times. In 504 # normal use that shouldn't be a problem. 505 self.cids[player] = player.connect("name_changed", self.name_changed) 506 log.debug("GameWidget.players_changed: returning") 507 508 def name_changed(self, player): 509 log.debug("GameWidget.name_changed: starting %s" % repr(player)) 510 color = self.gamemodel.color(player) 511 512 if self.gamemodel is None: 513 return 514 name = self.player_display_text(color, False) 515 self.gamemodel.tags["White" if color == WHITE else "Black"] = name 516 self.player_name_labels[color].set_text(name) 517 if isinstance(self.gamemodel, ICGameModel) and \ 518 player.__type__ == REMOTE: 519 self.player_name_labels[color].set_tooltip_text( 520 get_player_tooltip_text(self.gamemodel.ficsplayers[color], 521 show_status=False)) 522 523 self.emit('title_changed', self.display_text) 524 log.debug("GameWidget.name_changed: returning") 525 526 def message_received(self, gamemodel, name, msg): 527 if gamemodel.isObservationGame() and not self.isInFront(): 528 text = self.game_info_label.get_text() 529 self.game_info_label.set_markup( 530 '<span color="red" weight="bold">%s</span>' % text) 531 532 def zero_reached(self, timemodel, color): 533 if self.gamemodel.status not in UNFINISHED_STATES: 534 return 535 536 if self.gamemodel.players[0].__type__ == LOCAL \ 537 and self.gamemodel.players[1].__type__ == LOCAL: 538 self.menuitems["call_flag"].sensitive = True 539 return 540 541 for player in self.gamemodel.players: 542 opplayercolor = BLACK if player == self.gamemodel.players[ 543 WHITE] else WHITE 544 if player.__type__ == LOCAL and opplayercolor == color: 545 log.debug("gamewidget.zero_reached: LOCAL player=%s, color=%s" % 546 (repr(player), str(color))) 547 self.menuitems["call_flag"].sensitive = True 548 break 549 550 def player_lagged(self, bm, player): 551 if player in self.gamemodel.ficsplayers: 552 content = get_infobarmessage_content( 553 player, _(" has lagged for 30 seconds"), 554 self.gamemodel.ficsgame.game_type) 555 556 def response_cb(infobar, response, message): 557 message.dismiss() 558 return False 559 560 message = InfoBarMessage(Gtk.MessageType.INFO, content, 561 response_cb) 562 message.add_button(InfoBarMessageButton(Gtk.STOCK_CLOSE, 563 Gtk.ResponseType.CANCEL)) 564 self.showMessage(message) 565 return False 566 567 def opp_not_out_of_time(self, bm): 568 if self.gamemodel is not None and self.gamemodel.remote_player.time <= 0: 569 content = get_infobarmessage_content2( 570 self.gamemodel.remote_ficsplayer, 571 _(" is lagging heavily but hasn't disconnected"), 572 _("Continue to wait for opponent, or try to adjourn the game?"), 573 gametype=self.gamemodel.ficsgame.game_type) 574 575 def response_cb(infobar, response, message): 576 if response == 2: 577 self.gamemodel.connection.client.run_command("adjourn") 578 message.dismiss() 579 return False 580 581 message = InfoBarMessage(Gtk.MessageType.QUESTION, content, 582 response_cb) 583 message.add_button(InfoBarMessageButton( 584 _("Wait"), Gtk.ResponseType.CANCEL)) 585 message.add_button(InfoBarMessageButton(_("Adjourn"), 2)) 586 self.showMessage(message) 587 return False 588 589 def on_game_close_clicked(self, button): 590 log.debug("gamewidget.on_game_close_clicked %s" % button) 591 self.emit("game_close_clicked") 592 593 def initTabcontents(self): 594 tabcontent = createAlignment(0, 0, 0, 0) 595 hbox = Gtk.HBox() 596 hbox.set_spacing(4) 597 hbox.pack_start(createImage(light_off), False, True, 0) 598 close_button = Gtk.Button() 599 close_button.set_property("can-focus", False) 600 close_button.add(createImage(gtk_close)) 601 close_button.set_relief(Gtk.ReliefStyle.NONE) 602 close_button.set_size_request(20, 18) 603 604 self.cids[close_button] = close_button.connect("clicked", self.on_game_close_clicked) 605 606 hbox.pack_end(close_button, False, True, 0) 607 text_hbox = Gtk.HBox() 608 white_label = Gtk.Label(label="") 609 text_hbox.pack_start(white_label, False, True, 0) 610 text_hbox.pack_start(Gtk.Label(label=" - "), False, True, 0) 611 black_label = Gtk.Label(label="") 612 text_hbox.pack_start(black_label, False, True, 0) 613 gameinfo_label = Gtk.Label(label="") 614 text_hbox.pack_start(gameinfo_label, False, True, 0) 615 # label.set_alignment(0,.7) 616 hbox.pack_end(text_hbox, True, True, 0) 617 tabcontent.add(hbox) 618 tabcontent.show_all() # Gtk doesn't show tab labels when the rest is 619 return tabcontent, white_label, black_label, gameinfo_label 620 621 def initBoardAndClock(self, gamemodel): 622 boardvbox = Gtk.VBox() 623 boardvbox.set_spacing(2) 624 infobar = InfoBarNotebook("gamewidget_infobar") 625 626 ccalign = createAlignment(0, 0, 0, 0) 627 cclock = ChessClock() 628 cclock.setModel(gamemodel.timemodel) 629 ccalign.add(cclock) 630 ccalign.set_size_request(-1, 32) 631 boardvbox.pack_start(ccalign, False, True, 0) 632 633 actionMenuDic = {} 634 for item in ACTION_MENU_ITEMS: 635 actionMenuDic[item] = widgets[item] 636 637 if self.gamemodel.offline_lecture: 638 preview = True 639 else: 640 preview = False 641 642 board = BoardControl(gamemodel, actionMenuDic, game_preview=preview) 643 boardvbox.pack_start(board, True, True, 0) 644 return boardvbox, board, infobar, cclock 645 646 def initButtons(self, board): 647 align = createAlignment(4, 0, 4, 0) 648 toolbar = Gtk.Toolbar() 649 650 firstButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_PREVIOUS) 651 firstButton.set_tooltip_text(_("Jump to initial position")) 652 toolbar.insert(firstButton, -1) 653 654 prevButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_REWIND) 655 prevButton.set_tooltip_text(_("Step back one move")) 656 toolbar.insert(prevButton, -1) 657 658 mainButton = Gtk.ToolButton(stock_id=Gtk.STOCK_GOTO_FIRST) 659 mainButton.set_tooltip_text(_("Go back to the main line")) 660 toolbar.insert(mainButton, -1) 661 662 upButton = Gtk.ToolButton(stock_id=Gtk.STOCK_GOTO_TOP) 663 upButton.set_tooltip_text(_("Go back to the parent line")) 664 toolbar.insert(upButton, -1) 665 666 nextButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_FORWARD) 667 nextButton.set_tooltip_text(_("Step forward one move")) 668 toolbar.insert(nextButton, -1) 669 670 lastButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_NEXT) 671 lastButton.set_tooltip_text(_("Jump to latest position")) 672 toolbar.insert(lastButton, -1) 673 674 filterButton = Gtk.ToolButton(stock_id=Gtk.STOCK_FIND) 675 filterButton.set_tooltip_text(_("Find position in current database")) 676 toolbar.insert(filterButton, -1) 677 678 self.saveButton = Gtk.ToolButton(stock_id=Gtk.STOCK_SAVE) 679 self.saveButton.set_tooltip_text(_("Save arrows/circles")) 680 toolbar.insert(self.saveButton, -1) 681 682 def on_clicked(button, func): 683 # Prevent moving in game while lesson not finished 684 if self.gamemodel.lesson_game and not self.gamemodel.solved: 685 return 686 else: 687 func() 688 689 self.cids[firstButton] = firstButton.connect("clicked", on_clicked, self.board.view.showFirst) 690 self.cids[prevButton] = prevButton.connect("clicked", on_clicked, self.board.view.showPrev) 691 self.cids[mainButton] = mainButton.connect("clicked", on_clicked, self.board.view.backToMainLine) 692 self.cids[upButton] = upButton.connect("clicked", on_clicked, self.board.view.backToParentLine) 693 self.cids[nextButton] = nextButton.connect("clicked", on_clicked, self.board.view.showNext) 694 self.cids[lastButton] = lastButton.connect("clicked", on_clicked, self.board.view.showLast) 695 self.cids[filterButton] = filterButton.connect("clicked", on_clicked, self.find_in_database) 696 self.cids[self.saveButton] = self.saveButton.connect("clicked", on_clicked, self.save_shapes_to_pgn) 697 698 self.on_shapes_changed(self.board) 699 self.board.connect("shapes_changed", self.on_shapes_changed) 700 701 tool_box = Gtk.Box() 702 tool_box.pack_start(toolbar, True, True, 0) 703 704 align.add(tool_box) 705 return align 706 707 def on_shapes_changed(self, boardcontrol): 708 self.saveButton.set_sensitive(boardcontrol.view.has_unsaved_shapes) 709 710 def find_in_database(self): 711 persp = perspective_manager.get_perspective("database") 712 if persp.chessfile is None: 713 dialogue = Gtk.MessageDialog(pychess.widgets.mainwindow(), 714 type=Gtk.MessageType.ERROR, 715 buttons=Gtk.ButtonsType.OK, 716 message_format=_("No database is currently opened.")) 717 dialogue.run() 718 dialogue.destroy() 719 return 720 721 view = self.board.view 722 shown_board = self.gamemodel.getBoardAtPly(view.shown, view.shown_variation_idx) 723 fen = shown_board.asFen() 724 725 tool, found = persp.chessfile.has_position(fen) 726 if not found: 727 dialogue = Gtk.MessageDialog(pychess.widgets.mainwindow(), 728 type=Gtk.MessageType.WARNING, 729 buttons=Gtk.ButtonsType.OK, 730 message_format=_("The position does not exist in the database.")) 731 dialogue.run() 732 dialogue.destroy() 733 else: 734 if tool == TOOL_CHESSDB: 735 persp.chessfile.set_fen_filter(fen) 736 elif tool == TOOL_SCOUTFISH: 737 dialogue = Gtk.MessageDialog(pychess.widgets.mainwindow(), 738 type=Gtk.MessageType.QUESTION, 739 buttons=Gtk.ButtonsType.YES_NO, 740 message_format=_("An approximate position has been found. Do you want to display it ?")) 741 response = dialogue.run() 742 dialogue.destroy() 743 if response != Gtk.ResponseType.YES: 744 return 745 746 persp.chessfile.set_scout_filter({'sub-fen': fen}) 747 else: 748 raise RuntimeError('Internal error') 749 persp.gamelist.ply = view.shown 750 persp.gamelist.load_games() 751 perspective_manager.activate_perspective("database") 752 753 def save_shapes_to_pgn(self): 754 view = self.board.view 755 shown_board = self.gamemodel.getBoardAtPly(view.shown, view.shown_variation_idx) 756 757 for child in shown_board.board.children: 758 if isinstance(child, str): 759 if child.lstrip().startswith("[%csl "): 760 shown_board.board.children.remove(child) 761 self.gamemodel.needsSave = True 762 elif child.lstrip().startswith("[%cal "): 763 shown_board.board.children.remove(child) 764 self.gamemodel.needsSave = True 765 766 if view.circles: 767 csl = [] 768 for circle in view.circles: 769 csl.append("%s%s" % (circle.color, repr(circle))) 770 shown_board.board.children = ["[%%csl %s]" % ",".join(csl)] + shown_board.board.children 771 self.gamemodel.needsSave = True 772 773 if view.arrows: 774 cal = [] 775 for arrow in view.arrows: 776 cal.append("%s%s%s" % (arrow[0].color, repr(arrow[0]), repr(arrow[1]))) 777 shown_board.board.children = ["[%%cal %s]" % ",".join(cal)] + shown_board.board.children 778 self.gamemodel.needsSave = True 779 780 view.saved_arrows = set() 781 view.saved_arrows |= view.arrows 782 783 view.saved_circles = set() 784 view.saved_circles |= view.circles 785 786 self.on_shapes_changed(self.board) 787 788 def light_on_off(self, on): 789 child = self.tabcontent.get_child() 790 if child: 791 child.remove(child.get_children()[0]) 792 if on: 793 # child.pack_start(createImage(light_on, True, True, 0), expand=False) 794 child.pack_start(createImage(light_on), True, True, 0) 795 else: 796 # child.pack_start(createImage(light_off, True, True, 0), expand=False) 797 child.pack_start(createImage(light_off), True, True, 0) 798 self.tabcontent.show_all() 799 800 def setLocked(self, locked): 801 """ Makes the board insensitive and turns off the tab ready indicator """ 802 log.debug("GameWidget.setLocked: %s locked=%s" % 803 (self.gamemodel.players, str(locked))) 804 self.board.setLocked(locked) 805 if not self.tabcontent.get_children(): 806 return 807 if len(self.tabcontent.get_child().get_children()) < 2: 808 log.warning( 809 "GameWidget.setLocked: Not removing last tabcontent child") 810 return 811 812 self.light_on_off(not locked) 813 814 log.debug("GameWidget.setLocked: %s: returning" % 815 self.gamemodel.players) 816 817 def bringToFront(self): 818 self.perspective.getheadbook().set_current_page(self.getPageNumber()) 819 820 def isInFront(self): 821 if not self.perspective.getheadbook(): 822 return False 823 return self.perspective.getheadbook().get_current_page() == self.getPageNumber() 824 825 def getPageNumber(self): 826 return self.perspective.getheadbook().page_num(self.notebookKey) 827 828 def infobar_hidden(self, infobar): 829 if self == self.perspective.cur_gmwidg(): 830 self.perspective.notebooks["messageArea"].hide() 831 832 def showMessage(self, message): 833 self.infobar.push_message(message) 834 if self == self.perspective.cur_gmwidg(): 835 self.perspective.notebooks["messageArea"].show() 836 837 def replaceMessages(self, message): 838 """ Replace all messages with message """ 839 if not self.closed: 840 self.infobar.clear_messages() 841 self.showMessage(message) 842 843 def clearMessages(self): 844 self.infobar.clear_messages() 845 if self == self.perspective.cur_gmwidg(): 846 self.perspective.notebooks["messageArea"].hide() 847 848 849# ############################################################################### 850# Handling of the special sidepanels-design-gamewidget used in preferences # 851# ############################################################################### 852 853designGW = None 854 855 856def showDesignGW(): 857 global designGW 858 perspective = perspective_manager.get_perspective("games") 859 designGW = GameWidget(GameModel(), perspective) 860 if isDesignGWShown(): 861 return 862 getWidgets()["show_sidepanels"].set_active(True) 863 getWidgets()["show_sidepanels"].set_sensitive(False) 864 perspective.attachGameWidget(designGW) 865 866 867def hideDesignGW(): 868 if isDesignGWShown(): 869 perspective = perspective_manager.get_perspective("games") 870 perspective.delGameWidget(designGW) 871 getWidgets()["show_sidepanels"].set_sensitive(True) 872 873 874def isDesignGWShown(): 875 perspective = perspective_manager.get_perspective("games") 876 return designGW in perspective.key2gmwidg.values() 877