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