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