1""" The task of this perspective, is to save, load and init new games """ 2 3import asyncio 4import os 5import subprocess 6import tempfile 7 8from collections import defaultdict 9from io import StringIO 10 11from gi.repository import Gtk 12from gi.repository import GObject 13 14from pychess.Savers.ChessFile import LoadingError 15from pychess.Savers import epd, fen, pgn, olv, png, database, html, txt 16from pychess.System import conf 17from pychess.System.Log import log 18from pychess.System.protoopen import isWriteable 19from pychess.System.uistuff import GladeWidgets 20from pychess.System.prefix import addUserConfigPrefix 21from pychess.Savers.pgn import parseDateTag 22from pychess.Utils.const import UNFINISHED_STATES, ABORTED, ABORTED_AGREEMENT, LOCAL, ARTIFICIAL, MENU_ITEMS 23from pychess.Utils.Offer import Offer 24from pychess.widgets import gamewidget, mainwindow, new_notebook 25from pychess.widgets.gamenanny import game_nanny 26from pychess.perspectives import Perspective, perspective_manager, panel_name 27from pychess.widgets.pydock.PyDockTop import PyDockTop 28from pychess.widgets.pydock.__init__ import CENTER, EAST, SOUTH 29from pychess.ic.ICGameModel import ICGameModel 30 31enddir = {} 32savers = (pgn, epd, fen, olv, png, html, txt) # chessalpha2 is broken 33 34saveformats = Gtk.ListStore(str, str, GObject.TYPE_PYOBJECT) 35exportformats = Gtk.ListStore(str, str, GObject.TYPE_PYOBJECT) 36 37auto = _("Detect type automatically") 38saveformats.append([auto, "", None]) 39exportformats.append([auto, "", None]) 40 41for saver in savers: 42 label, ending = saver.__label__, saver.__ending__ 43 endstr = "(%s)" % ending 44 enddir[ending] = saver 45 if hasattr(saver, "load"): 46 saveformats.append([label, endstr, saver]) 47 else: 48 exportformats.append([label, endstr, saver]) 49 50 51class Games(GObject.GObject, Perspective): 52 __gsignals__ = { 53 'gmwidg_created': (GObject.SignalFlags.RUN_FIRST, None, (object, )) 54 } 55 56 def __init__(self): 57 GObject.GObject.__init__(self) 58 Perspective.__init__(self, "games", _("Games")) 59 60 self.notebooks = {} 61 self.first_run = True 62 self.gamewidgets = set() 63 self.gmwidg_cids = {} 64 self.board_cids = {} 65 self.notify_cids = defaultdict(list) 66 67 self.key2gmwidg = {} 68 self.key2cid = {} 69 70 self.dock = None 71 self.dockAlign = None 72 self.dockLocation = addUserConfigPrefix("pydock.xml") 73 74 @asyncio.coroutine 75 def generalStart(self, gamemodel, player0tup, player1tup, loaddata=None): 76 """ The player tuples are: 77 (The type af player in a System.const value, 78 A callable creating the player, 79 A list of arguments for the callable, 80 A preliminary name for the player) 81 82 If loaddata is specified, it should be a tuple of: 83 (A text uri or fileobj, 84 A Savers.something module with a load function capable of loading it, 85 An int of the game in file you want to load, 86 The position from where to start the game) 87 """ 88 89 log.debug("Games.generalStart: %s\n %s\n %s" % 90 (gamemodel, player0tup, player1tup)) 91 gmwidg = gamewidget.GameWidget(gamemodel, self) 92 self.gamewidgets.add(gmwidg) 93 self.gmwidg_cids[gmwidg] = gmwidg.connect("game_close_clicked", self.closeGame) 94 95 # worker.publish((gmwidg,gamemodel)) 96 self.attachGameWidget(gmwidg) 97 game_nanny.nurseGame(gmwidg, gamemodel) 98 log.debug("Games.generalStart: -> emit gmwidg_created: %s" % (gmwidg)) 99 self.emit("gmwidg_created", gmwidg) 100 log.debug("Games.generalStart: <- emit gmwidg_created: %s" % (gmwidg)) 101 102 # Initing players 103 104 def xxxset_name(none, player, key, alt): 105 player.setName(conf.get(key, alt)) 106 107 players = [] 108 for i, playertup in enumerate((player0tup, player1tup)): 109 type, func, args, prename = playertup 110 if type != LOCAL: 111 if type == ARTIFICIAL: 112 player = yield from func(*args) 113 else: 114 player = func(*args) 115 players.append(player) 116 # if type == ARTIFICIAL: 117 # def readyformoves (player, color): 118 # gmwidg.setTabText(gmwidg.display_text)) 119 # players[i].connect("readyForMoves", readyformoves, i) 120 else: 121 # Until PyChess has a proper profiles system, as discussed on the 122 # issue tracker, we need to give human players special treatment 123 player = func(gmwidg, *args) 124 players.append(player) 125 assert len(players) == 2 126 if player0tup[0] == ARTIFICIAL and player1tup[0] == ARTIFICIAL: 127 128 def emit_action(board, action, player, param, gmwidg): 129 if gmwidg.isInFront(): 130 gamemodel.curplayer.emit("offer", Offer(action, param=param)) 131 132 self.board_cids[gmwidg.board] = gmwidg.board.connect("action", emit_action, gmwidg) 133 134 log.debug("Games.generalStart: -> gamemodel.setPlayers(): %s" % 135 (gamemodel)) 136 gamemodel.setPlayers(players) 137 log.debug("Games.generalStart: <- gamemodel.setPlayers(): %s" % 138 (gamemodel)) 139 140 # Forward information from the engines 141 for playertup, tagname in ((player0tup, "WhiteElo"), (player1tup, "BlackElo")): 142 if playertup[0] == ARTIFICIAL: 143 elo = playertup[2][0].get("elo") 144 if elo: 145 gamemodel.tags[tagname] = elo 146 147 # Starting 148 if loaddata: 149 try: 150 uri, loader, gameno, position = loaddata 151 gamemodel.loadAndStart(uri, loader, gameno, position) 152 if position != gamemodel.ply and position != -1: 153 gmwidg.board.view.shown = position 154 except LoadingError as e: 155 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.WARNING, 156 buttons=Gtk.ButtonsType.OK) 157 d.set_markup(_("<big><b>Error loading game</big></b>")) 158 d.format_secondary_text(", ".join(str(a) for a in e.args)) 159 d.show() 160 d.destroy() 161 162 else: 163 if gamemodel.variant.need_initial_board: 164 for player in gamemodel.players: 165 player.setOptionInitialBoard(gamemodel) 166 log.debug("Games..generalStart: -> gamemodel.start(): %s" % 167 (gamemodel)) 168 gamemodel.emit("game_loaded", "") 169 gamemodel.start() 170 log.debug("Games.generalStart: <- gamemodel.start(): %s" % 171 (gamemodel)) 172 173 log.debug("Games.generalStart: returning gmwidg=%s\n gamemodel=%s" % 174 (gmwidg, gamemodel)) 175 176 ################################################################################ 177 # Saving # 178 ################################################################################ 179 180 def saveGame(self, game, position=None): 181 if not game.isChanged(): 182 return 183 if game.uri and isWriteable(game.uri): 184 self.saveGameSimple(game.uri, game, position=position) 185 else: 186 return self.saveGameAs(game, position=position) 187 188 def saveGameSimple(self, uri, game, position=None): 189 ending = os.path.splitext(uri)[1] 190 if not ending: 191 return 192 saver = enddir[ending[1:]] 193 game.save(uri, saver, append=False, position=position) 194 195 def saveGamePGN(self, game): 196 if conf.get("saveOwnGames") and not game.hasLocalPlayer(): 197 return True 198 199 filename = conf.get("autoSaveFormat") 200 filename = filename.replace("#n1", game.tags["White"]) 201 filename = filename.replace("#n2", game.tags["Black"]) 202 year, month, day = parseDateTag(game.tags["Date"]) 203 year = '' if year is None else str(year) 204 month = '' if month is None else str(month) 205 day = '' if day is None else str(day) 206 filename = filename.replace("#y", "%s" % year) 207 filename = filename.replace("#m", "%s" % month) 208 filename = filename.replace("#d", "%s" % day) 209 pgn_path = conf.get("autoSavePath") + "/" + filename + ".pgn" 210 append = True 211 try: 212 if not os.path.isfile(pgn_path): 213 # create new file 214 with open(pgn_path, "w"): 215 pass 216 base_offset = os.path.getsize(pgn_path) 217 218 # save to .sqlite 219 database_path = os.path.splitext(pgn_path)[0] + '.sqlite' 220 database.save(database_path, game, base_offset) 221 222 # save to .scout 223 from pychess.Savers.pgn import scoutfish_path 224 if scoutfish_path is not None: 225 pgn_text = pgn.save(StringIO(), game) 226 227 tmp = tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) 228 pgnfile = tmp.name 229 with tmp.file as f: 230 f.write(pgn_text) 231 232 # create new .scout from pgnfile we are importing 233 args = [scoutfish_path, "make", pgnfile, "%s" % base_offset] 234 output = subprocess.check_output(args, stderr=subprocess.STDOUT) 235 236 # append it to our existing one 237 if output.decode().find(u"Processing...done") > 0: 238 old_scout = os.path.splitext(pgn_path)[0] + '.scout' 239 new_scout = os.path.splitext(pgnfile)[0] + '.scout' 240 241 with open(old_scout, "ab") as file1, open(new_scout, "rb") as file2: 242 file1.write(file2.read()) 243 244 # TODO: do we realy want to update .bin ? It can be huge/slow! 245 246 # save to .pgn 247 game.save(pgn_path, pgn, append) 248 249 return True 250 except IOError: 251 return False 252 253 def saveGameAs(self, game, position=None, export=False): 254 savedialog, savecombo = get_save_dialog(export) 255 256 # Keep running the dialog until the user has canceled it or made an error 257 # free operation 258 title = _("Save Game") if not export else _("Export position") 259 savedialog.set_title(title) 260 while True: 261 filename = "%s-%s" % (game.players[0], game.players[1]) 262 savedialog.set_current_name(filename.replace(" ", "_")) 263 264 res = savedialog.run() 265 if res != Gtk.ResponseType.ACCEPT: 266 break 267 268 uri = savedialog.get_filename() 269 ending = os.path.splitext(uri)[1] 270 if ending.startswith("."): 271 ending = ending[1:] 272 append = False 273 274 index = savecombo.get_active() 275 if index == 0: 276 if ending not in enddir: 277 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.ERROR, 278 buttons=Gtk.ButtonsType.OK) 279 folder, file = os.path.split(uri) 280 d.set_markup(_("<big><b>Unknown file type '%s'</b></big>") % 281 ending) 282 d.format_secondary_text(_( 283 "Was unable to save '%(uri)s' as PyChess doesn't know the format '%(ending)s'.") % 284 {'uri': uri, 'ending': ending}) 285 d.run() 286 d.destroy() 287 continue 288 else: 289 saver = enddir[ending] 290 else: 291 format = exportformats[index] if export else saveformats[index] 292 saver = format[2] 293 if ending not in enddir or not saver == enddir[ending]: 294 uri += ".%s" % saver.__ending__ 295 296 if os.path.isfile(uri) and not os.access(uri, os.W_OK): 297 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.ERROR, 298 buttons=Gtk.ButtonsType.OK) 299 d.set_markup(_("<big><b>Unable to save file '%s'</b></big>") % uri) 300 d.format_secondary_text(_( 301 "You don't have the necessary rights to save the file.\n\ 302 Please ensure that you have given the right path and try again.")) 303 d.run() 304 d.destroy() 305 continue 306 307 if os.path.isfile(uri): 308 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.QUESTION) 309 d.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 310 _("_Replace"), Gtk.ResponseType.ACCEPT) 311 if saver.__append__: 312 d.add_buttons(Gtk.STOCK_ADD, 1) 313 d.set_title(_("File exists")) 314 folder, file = os.path.split(uri) 315 d.set_markup(_( 316 "<big><b>A file named '%s' already exists. Would you like to replace it?</b></big>") % file) 317 d.format_secondary_text(_( 318 "The file already exists in '%s'. If you replace it, its content will be overwritten.") % folder) 319 replaceRes = d.run() 320 d.destroy() 321 322 if replaceRes == 1: 323 append = True 324 elif replaceRes == Gtk.ResponseType.CANCEL: 325 continue 326 else: 327 print(repr(uri)) 328 329 try: 330 flip = self.cur_gmwidg().board.view.rotation > 0 331 game.save(uri, saver, append, position, flip) 332 except IOError as e: 333 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.ERROR) 334 d.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK) 335 d.set_title(_("Could not save the file")) 336 d.set_markup(_( 337 "<big><b>PyChess was not able to save the game</b></big>")) 338 d.format_secondary_text(_("The error was: %s") % ", ".join( 339 str(a) for a in e.args)) 340 d.run() 341 d.destroy() 342 continue 343 344 break 345 346 savedialog.destroy() 347 return res 348 349 ################################################################################ 350 # Closing # 351 ################################################################################ 352 def closeAllGames(self, gamewidgets): 353 log.debug("Games.closeAllGames") 354 response = None 355 changedPairs = [(gmwidg, gmwidg.gamemodel) for gmwidg in gamewidgets 356 if gmwidg.gamemodel.isChanged()] 357 if len(changedPairs) == 0: 358 response = Gtk.ResponseType.OK 359 360 elif len(changedPairs) == 1: 361 response = self.closeGame(changedPairs[0][0]) 362 else: 363 markup = "<big><b>" + ngettext("There is %d game with unsaved moves.", 364 "There are %d games with unsaved moves.", 365 len(changedPairs)) % len(changedPairs) + " " + \ 366 _("Save moves before closing?") + "</b></big>" 367 368 for gmwidg, game in changedPairs: 369 if not gmwidg.gamemodel.isChanged(): 370 response = Gtk.ResponseType.OK 371 else: 372 if conf.get("autoSave"): 373 x = self.saveGamePGN(game) 374 if x: 375 response = Gtk.ResponseType.OK 376 else: 377 response = None 378 markup = "<b><big>" + _("Unable to save to configured file. \ 379 Save the games before closing?") + "</big></b>" 380 break 381 382 if response is None: 383 widgets = GladeWidgets("saveGamesDialog.glade") 384 dialog = widgets["saveGamesDialog"] 385 heading = widgets["saveGamesDialogHeading"] 386 saveLabel = widgets["saveGamesDialogSaveLabel"] 387 treeview = widgets["saveGamesDialogTreeview"] 388 389 heading.set_markup(markup) 390 391 liststore = Gtk.ListStore(bool, str) 392 treeview.set_model(liststore) 393 renderer = Gtk.CellRendererToggle() 394 renderer.props.activatable = True 395 treeview.append_column(Gtk.TreeViewColumn("", renderer, active=0)) 396 treeview.append_column(Gtk.TreeViewColumn("", 397 Gtk.CellRendererText(), 398 text=1)) 399 for gmwidg, game in changedPairs: 400 liststore.append((True, "%s %s %s" % (game.players[0], _("vs."), game.players[1]))) 401 402 def callback(cell, path): 403 if path: 404 liststore[path][0] = not liststore[path][0] 405 saves = len(tuple(row for row in liststore if row[0])) 406 saveLabel.set_text(ngettext( 407 "_Save %d document", "_Save %d documents", saves) % saves) 408 saveLabel.set_use_underline(True) 409 410 renderer.connect("toggled", callback) 411 412 callback(None, None) 413 414 while True: 415 response = dialog.run() 416 if response == Gtk.ResponseType.YES: 417 for i in range(len(liststore) - 1, -1, -1): 418 checked, name = liststore[i] 419 if checked: 420 cgmwidg, cgame = changedPairs[i] 421 if self.saveGame(cgame) == Gtk.ResponseType.ACCEPT: 422 liststore.remove(liststore.get_iter((i, ))) 423 del changedPairs[i] 424 if cgame.status in UNFINISHED_STATES: 425 cgame.end(ABORTED, ABORTED_AGREEMENT) 426 cgame.terminate() 427 self.delGameWidget(cgmwidg) 428 else: 429 break 430 else: 431 break 432 else: 433 break 434 dialog.destroy() 435 436 if response not in (Gtk.ResponseType.DELETE_EVENT, 437 Gtk.ResponseType.CANCEL): 438 pairs = [(gmwidg, gmwidg.gamemodel) for gmwidg in gamewidgets] 439 for gmwidg, game in pairs: 440 if game.status in UNFINISHED_STATES: 441 game.end(ABORTED, ABORTED_AGREEMENT) 442 game.terminate() 443 if gmwidg.notebookKey in self.key2gmwidg: 444 self.delGameWidget(gmwidg) 445 446 return response 447 448 def closeGame(self, gmwidg): 449 log.debug("Games.closeGame") 450 response = None 451 if not gmwidg.gamemodel.isChanged(): 452 response = Gtk.ResponseType.OK 453 else: 454 markup = "<b><big>" + _("Save the current game before you close it?") + "</big></b>" 455 if conf.get("autoSave"): 456 x = self.saveGamePGN(gmwidg.gamemodel) 457 if x: 458 response = Gtk.ResponseType.OK 459 else: 460 markup = "<b><big>" + _("Unable to save to configured file. \ 461 Save the current game before you close it?") + "</big></b>" 462 463 if response is None: 464 d = Gtk.MessageDialog(mainwindow(), type=Gtk.MessageType.WARNING) 465 d.add_button(_("Close _without Saving"), Gtk.ResponseType.OK) 466 d.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) 467 if gmwidg.gamemodel.uri: 468 d.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.YES) 469 else: 470 d.add_button(Gtk.STOCK_SAVE_AS, Gtk.ResponseType.YES) 471 472 gmwidg.bringToFront() 473 474 d.set_markup(markup) 475 d.format_secondary_text(_( 476 "It is not possible later to continue the game,\nif you don't save it.")) 477 478 response = d.run() 479 d.destroy() 480 481 if response == Gtk.ResponseType.YES: 482 # Test if cancel was pressed in the save-file-dialog 483 if self.saveGame(gmwidg.gamemodel) != Gtk.ResponseType.ACCEPT: 484 response = Gtk.ResponseType.CANCEL 485 486 if response not in (Gtk.ResponseType.DELETE_EVENT, 487 Gtk.ResponseType.CANCEL): 488 if gmwidg.gamemodel.status in UNFINISHED_STATES: 489 gmwidg.gamemodel.end(ABORTED, ABORTED_AGREEMENT) 490 491 gmwidg.disconnect(self.gmwidg_cids[gmwidg]) 492 del self.gmwidg_cids[gmwidg] 493 494 for cid in self.notify_cids[gmwidg]: 495 conf.notify_remove(cid) 496 del self.notify_cids[gmwidg] 497 498 if gmwidg.board in self.board_cids: 499 gmwidg.board.disconnect(self.board_cids[gmwidg.board]) 500 del self.board_cids[gmwidg.board] 501 502 self.delGameWidget(gmwidg) 503 self.gamewidgets.remove(gmwidg) 504 gmwidg.gamemodel.terminate() 505 506 db_persp = perspective_manager.get_perspective("database") 507 if len(self.gamewidgets) == 0: 508 for widget in MENU_ITEMS: 509 if widget in ("copy_pgn", "copy_fen") and db_persp.preview_panel is not None: 510 continue 511 gamewidget.getWidgets()[widget].set_property('sensitive', False) 512 513 return response 514 515 def delGameWidget(self, gmwidg): 516 """ Remove the widget from the GUI after the game has been terminated """ 517 log.debug("Games.delGameWidget: starting %s" % repr(gmwidg)) 518 gmwidg.closed = True 519 gmwidg.emit("closed") 520 521 called_from_preferences = False 522 window_list = Gtk.Window.list_toplevels() 523 widgets = gamewidget.getWidgets() 524 for window in window_list: 525 if window.is_active() and window == widgets["preferences"]: 526 called_from_preferences = True 527 break 528 529 pageNum = gmwidg.getPageNumber() 530 headbook = self.getheadbook() 531 532 if gmwidg.notebookKey in self.key2gmwidg: 533 del self.key2gmwidg[gmwidg.notebookKey] 534 535 if gmwidg.notebookKey in self.key2cid: 536 headbook.disconnect(self.key2cid[gmwidg.notebookKey]) 537 del self.key2cid[gmwidg.notebookKey] 538 539 headbook.remove_page(pageNum) 540 for notebook in self.notebooks.values(): 541 notebook.remove_page(pageNum) 542 543 if headbook.get_n_pages() == 1 and conf.get("hideTabs"): 544 self.show_tabs(False) 545 546 if headbook.get_n_pages() == 0: 547 if not called_from_preferences: 548 # If the last (but not the designGW) gmwidg was closed 549 # and we are FICS-ing, present the FICS lounge 550 perspective_manager.disable_perspective("games") 551 552 gmwidg._del() 553 554 def init_layout(self): 555 perspective_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 556 perspective_manager.set_perspective_widget("games", perspective_widget) 557 558 self.notebooks = {"board": new_notebook("board"), 559 "buttons": new_notebook("buttons"), 560 "messageArea": new_notebook("messageArea")} 561 self.main_notebook = self.notebooks["board"] 562 for panel in self.sidePanels: 563 self.notebooks[panel_name(panel.__name__)] = new_notebook(panel_name(panel.__name__)) 564 565 # Initing headbook 566 567 align = gamewidget.createAlignment(4, 4, 0, 4) 568 align.set_property("yscale", 0) 569 570 headbook = Gtk.Notebook() 571 headbook.set_name("headbook") 572 headbook.set_scrollable(True) 573 align.add(headbook) 574 perspective_widget.pack_start(align, False, True, 0) 575 self.show_tabs(not conf.get("hideTabs")) 576 577 # Initing center 578 579 centerVBox = Gtk.VBox() 580 581 # The dock 582 583 self.dock = PyDockTop("main", self) 584 self.dockAlign = gamewidget.createAlignment(4, 4, 0, 4) 585 self.dockAlign.add(self.dock) 586 centerVBox.pack_start(self.dockAlign, True, True, 0) 587 self.dockAlign.show() 588 self.dock.show() 589 590 self.docks["board"] = (Gtk.Label(label="Board"), self.notebooks["board"], None) 591 for panel in self.sidePanels: 592 self.docks[panel_name(panel.__name__)][1] = self.notebooks[panel_name(panel.__name__)] 593 594 self.load_from_xml() 595 596 # Default layout of side panels 597 first_time_layout = False 598 if not os.path.isfile(self.dockLocation): 599 first_time_layout = True 600 leaf = self.dock.dock(self.docks["board"][1], 601 CENTER, 602 Gtk.Label(label=self.docks["board"][0]), 603 "board") 604 self.docks["board"][1].show_all() 605 leaf.setDockable(False) 606 607 # S 608 epanel = leaf.dock(self.docks["bookPanel"][1], SOUTH, self.docks["bookPanel"][0], 609 "bookPanel") 610 epanel.default_item_height = 45 611 epanel = epanel.dock(self.docks["engineOutputPanel"][1], CENTER, 612 self.docks["engineOutputPanel"][0], 613 "engineOutputPanel") 614 615 # NE 616 leaf = leaf.dock(self.docks["annotationPanel"][1], EAST, 617 self.docks["annotationPanel"][0], "annotationPanel") 618 leaf = leaf.dock(self.docks["historyPanel"][1], CENTER, 619 self.docks["historyPanel"][0], "historyPanel") 620 leaf = leaf.dock(self.docks["scorePanel"][1], CENTER, 621 self.docks["scorePanel"][0], "scorePanel") 622 623 # SE 624 leaf = leaf.dock(self.docks["chatPanel"][1], SOUTH, self.docks["chatPanel"][0], 625 "chatPanel") 626 leaf = leaf.dock(self.docks["commentPanel"][1], CENTER, 627 self.docks["commentPanel"][0], "commentPanel") 628 629 def unrealize(dock, notebooks): 630 # unhide the panel before saving so its configuration is saved correctly 631 self.notebooks["board"].get_parent().get_parent().zoomDown() 632 dock.saveToXML(self.dockLocation) 633 dock._del() 634 635 self.dock.connect("unrealize", unrealize, self.notebooks) 636 637 hbox = Gtk.HBox() 638 639 # Buttons 640 self.notebooks["buttons"].set_border_width(4) 641 hbox.pack_start(self.notebooks["buttons"], False, True, 0) 642 643 # The message area 644 # TODO: If you try to fix this first read issue #958 and 1018 645 align = gamewidget.createAlignment(0, 0, 0, 0) 646 # sw = Gtk.ScrolledWindow() 647 # port = Gtk.Viewport() 648 # port.add(self.notebooks["messageArea"]) 649 # sw.add(port) 650 # align.add(sw) 651 align.add(self.notebooks["messageArea"]) 652 hbox.pack_start(align, True, True, 0) 653 654 def ma_switch_page(notebook, gpointer, page_num): 655 notebook.props.visible = notebook.get_nth_page(page_num).\ 656 get_child().props.visible 657 658 self.notebooks["messageArea"].connect("switch-page", ma_switch_page) 659 centerVBox.pack_start(hbox, False, True, 0) 660 661 perspective_widget.pack_start(centerVBox, True, True, 0) 662 centerVBox.show_all() 663 perspective_widget.show_all() 664 665 perspective_manager.set_perspective_menuitems("games", self.menuitems, default=first_time_layout) 666 667 conf.notify_add("hideTabs", self.tabsCallback) 668 669 # Connecting headbook to other notebooks 670 671 def hb_switch_page(notebook, gpointer, page_num): 672 for notebook in self.notebooks.values(): 673 notebook.set_current_page(page_num) 674 675 gmwidg = self.key2gmwidg[self.getheadbook().get_nth_page(page_num)] 676 if isinstance(gmwidg.gamemodel, ICGameModel): 677 primary = "primary " + str(gmwidg.gamemodel.ficsgame.gameno) 678 gmwidg.gamemodel.connection.client.run_command(primary) 679 680 headbook.connect("switch-page", hb_switch_page) 681 682 if hasattr(headbook, "set_tab_reorderable"): 683 684 def page_reordered(widget, child, new_num, headbook): 685 old_num = self.notebooks["board"].page_num(self.key2gmwidg[child].boardvbox) 686 if old_num == -1: 687 log.error('Games and labels are out of sync!') 688 else: 689 for notebook in self.notebooks.values(): 690 notebook.reorder_child( 691 notebook.get_nth_page(old_num), new_num) 692 693 headbook.connect("page-reordered", page_reordered, headbook) 694 695 def adjust_divider(self, diff): 696 """ Try to move paned (containing board) divider to show/hide captured pieces """ 697 if self.dock is None: 698 return 699 child = self.dock.get_children()[0] 700 c1 = child.paned.get_child1() 701 if hasattr(c1, "paned"): 702 c1.paned.set_position(c1.paned.get_position() + diff) 703 else: 704 child.paned.set_position(child.paned.get_position() + diff) 705 706 def getheadbook(self): 707 if len(self.key2gmwidg) == 0: 708 return None 709 headbook = self.widget.get_children()[0].get_children()[0].get_child() 710 # to help StoryText create widget description 711 # headbook.get_tab_label_text = customGetTabLabelText 712 return headbook 713 714 def cur_gmwidg(self): 715 if len(self.key2gmwidg) == 0: 716 return None 717 headbook = self.getheadbook() 718 notebookKey = headbook.get_nth_page(headbook.get_current_page()) 719 return self.key2gmwidg[notebookKey] 720 721 def customGetTabLabelText(self, child): 722 gmwidg = self.key2gmwidg[child] 723 return gmwidg.display_text 724 725 def zoomToBoard(self, view_zoomed): 726 if not self.notebooks["board"].get_parent(): 727 return 728 if view_zoomed: 729 self.notebooks["board"].get_parent().get_parent().zoomUp() 730 else: 731 self.notebooks["board"].get_parent().get_parent().zoomDown() 732 733 def show_tabs(self, show): 734 head = self.getheadbook() 735 if head is None: 736 return 737 head.set_show_tabs(show) 738 739 def tabsCallback(self, widget): 740 head = self.getheadbook() 741 if not head: 742 return 743 if head.get_n_pages() == 1: 744 self.show_tabs(not conf.get("hideTabs")) 745 746 def attachGameWidget(self, gmwidg): 747 log.debug("attachGameWidget: %s" % gmwidg) 748 if self.first_run: 749 self.init_layout() 750 self.first_run = False 751 perspective_manager.activate_perspective("games") 752 753 gmwidg.panels = [panel.Sidepanel().load(gmwidg) for panel in self.sidePanels] 754 self.key2gmwidg[gmwidg.notebookKey] = gmwidg 755 headbook = self.getheadbook() 756 757 headbook.append_page(gmwidg.notebookKey, gmwidg.tabcontent) 758 gmwidg.notebookKey.show_all() 759 760 if hasattr(headbook, "set_tab_reorderable"): 761 headbook.set_tab_reorderable(gmwidg.notebookKey, True) 762 763 def callback(notebook, gpointer, page_num, gmwidg): 764 if notebook.get_nth_page(page_num) == gmwidg.notebookKey: 765 gmwidg.infront() 766 if gmwidg.gamemodel.players and gmwidg.gamemodel.isObservationGame(): 767 gmwidg.light_on_off(False) 768 text = gmwidg.game_info_label.get_text() 769 gmwidg.game_info_label.set_markup( 770 '<span color="black" weight="bold">%s</span>' % text) 771 772 self.key2cid[gmwidg.notebookKey] = headbook.connect_after("switch-page", callback, gmwidg) 773 gmwidg.infront() 774 775 align = gamewidget.createAlignment(0, 0, 0, 0) 776 align.show() 777 align.add(gmwidg.infobar) 778 self.notebooks["messageArea"].append_page(align, None) 779 self.notebooks["board"].append_page(gmwidg.boardvbox, None) 780 gmwidg.boardvbox.show_all() 781 for panel, instance in zip(self.sidePanels, gmwidg.panels): 782 self.notebooks[panel_name(panel.__name__)].append_page(instance, None) 783 instance.show_all() 784 self.notebooks["buttons"].append_page(gmwidg.stat_hbox, None) 785 gmwidg.stat_hbox.show_all() 786 787 if headbook.get_n_pages() == 1: 788 self.show_tabs(not conf.get("hideTabs")) 789 else: 790 # We should always show tabs if more than one exists 791 self.show_tabs(True) 792 793 headbook.set_current_page(-1) 794 795 widgets = gamewidget.getWidgets() 796 if headbook.get_n_pages() == 1 and not widgets["show_sidepanels"].get_active(): 797 self.zoomToBoard(True) 798 799 800def get_save_dialog(export=False): 801 savedialog = Gtk.FileChooserDialog( 802 "", mainwindow(), Gtk.FileChooserAction.SAVE, 803 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, 804 Gtk.ResponseType.ACCEPT)) 805 savedialog.set_current_folder(os.path.expanduser("~")) 806 807 # Add widgets to the savedialog 808 savecombo = Gtk.ComboBox() 809 savecombo.set_name("savecombo") 810 811 crt = Gtk.CellRendererText() 812 savecombo.pack_start(crt, True) 813 savecombo.add_attribute(crt, 'text', 0) 814 815 crt = Gtk.CellRendererText() 816 savecombo.pack_start(crt, False) 817 savecombo.add_attribute(crt, 'text', 1) 818 819 if export: 820 savecombo.set_model(exportformats) 821 else: 822 savecombo.set_model(saveformats) 823 824 savecombo.set_active(1) # pgn 825 savedialog.set_extra_widget(savecombo) 826 827 return savedialog, savecombo 828 829 830def get_open_dialog(): 831 opendialog = Gtk.FileChooserDialog( 832 _("Open chess file"), mainwindow(), Gtk.FileChooserAction.OPEN, 833 (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, 834 Gtk.ResponseType.OK)) 835 opendialog.set_show_hidden(True) 836 opendialog.set_select_multiple(True) 837 838 # All chess files filter 839 all_filter = Gtk.FileFilter() 840 all_filter.set_name(_("All Chess Files")) 841 opendialog.add_filter(all_filter) 842 opendialog.set_filter(all_filter) 843 844 # Specific filters and save formats 845 for ending, saver in enddir.items(): 846 label = saver.__label__ 847 endstr = "(%s)" % ending 848 f = Gtk.FileFilter() 849 f.set_name(label + " " + endstr) 850 if hasattr(saver, "load"): 851 f.add_pattern("*." + ending) 852 all_filter.add_pattern("*." + ending) 853 opendialog.add_filter(f) 854 855 return opendialog 856