1# -*- coding: UTF-8 -*-
2
3
4from math import floor, ceil, pi
5from time import time
6from io import StringIO
7
8import cairo
9from gi.repository import GLib, Gtk, Gdk, GObject, Pango, PangoCairo
10
11from pychess.Savers import pgn
12from pychess.System.prefix import addDataPrefix
13from pychess.System import conf
14from pychess.gfx import Pieces
15from pychess.Savers.pgn import comment_arrows_re, comment_circles_re
16from pychess.Utils.Cord import Cord
17from pychess.Utils.GameModel import GameModel
18from pychess.Utils.const import ASEAN_VARIANTS, DROP_VARIANTS, WAITING_TO_START, REMOTE, \
19    LOCAL, DRAW, WHITEWON, BLACKWON, ABORTED, KILLED, DROP, \
20    KING_CASTLE, QUEEN_CASTLE, WILDCASTLESHUFFLECHESS, \
21    WILDCASTLECHESS, PAWN, KNIGHT, SITTUYINCHESS, BLACK
22from pychess.Variants.blindfold import BlindfoldBoard, HiddenPawnsBoard, \
23    HiddenPiecesBoard, AllWhiteBoard
24from . import preferencesDialog
25from pychess.perspectives import perspective_manager
26
27# This file contains the class that is used to draw the board
28
29
30# util functions on rectangles to redner board used by the class
31
32
33def intersects(r_zero, r_one):
34    """ Takes two square and determines if they have an Intersection
35        Returns a boolean
36    """
37    w_zero = r_zero.width + r_zero.x
38    h_zero = r_zero.height + r_zero.y
39    w_one = r_one.width + r_one.x
40    h_one = r_one.height + r_one.y
41    return (w_one < r_one.x or w_one > r_zero.x) and \
42           (h_one < r_one.y or h_one > r_zero.y) and \
43           (w_zero < r_zero.x or w_zero > r_one.x) and \
44           (h_zero < r_zero.y or h_zero > r_one.y)
45
46
47def contains(r_zero, r_one):
48    """ Takes two squares and determines if square one is contained
49        within square zero
50        Returns a boolean
51    """
52    w_zero = r_zero.width + r_zero.x
53    h_zero = r_zero.height + r_zero.y
54    w_one = r_one.width + r_one.x
55    h_one = r_one.height + r_one.y
56    return \
57        r_zero.x <= r_one.x and w_zero >= w_one and \
58        r_zero.y <= r_one.y and h_zero >= h_one
59
60
61def union(r_zero, r_one):
62    """ Takes 2 rectangles and returns a rectangle that represents
63        the union of the two areas
64        Returns a Gdk.Rectangle
65    """
66    x_min = min(r_zero.x, r_one.x)
67    y_min = min(r_zero.y, r_one.y)
68    w_max = max(r_zero.x + r_zero.width, r_one.x + r_one.width) - x_min
69    h_max = max(r_zero.y + r_zero.height, r_one.y + r_one.height) - y_min
70    rct = Gdk.Rectangle()
71    rct.x, rct.y, rct.width, rct.height = (x_min, y_min, w_max, h_max)
72    return rct
73
74
75def join(r_zero, r_one):
76    """ Take(x, y, w, [h]) squares """
77
78    if not r_zero:
79        return r_one
80    if not r_one:
81        return r_zero
82    if not r_zero and not r_one:
83        return None
84
85    if len(r_zero) == 3:
86        r_zero = (r_zero[0], r_zero[1], r_zero[2], r_zero[2])
87    if len(r_one) == 3:
88        r_one = (r_one[0], r_one[1], r_one[2], r_one[2])
89
90    x_one = min(r_zero[0], r_one[0])
91    x_two = max(r_zero[0] + r_zero[2], r_one[0] + r_one[2])
92    y_one = min(r_zero[1], r_one[1])
93    y_two = max(r_zero[1] + r_zero[3], r_one[1] + r_one[3])
94
95    return (x_one, y_one, x_two - x_one, y_two - y_one)
96
97
98def rect(rectangle):
99    """
100        Takes a list of 3 variables x,y,height and generates a rectangle
101        rectangle(list) : contains screen locations
102        returns a Gdk.Rectangle
103    """
104    x_size, y_size = [int(floor(v)) for v in rectangle[:2]]
105    width = int(ceil(rectangle[2]))
106    if len(rectangle) == 4:
107        height = int(ceil(rectangle[3]))
108    else:
109        height = width
110    rct = Gdk.Rectangle()
111    rct.x, rct.y, rct.width, rct.height = (x_size, y_size, width, height)
112    return rct
113
114
115def matrixAround(rotated_matrix, anchor_x, anchor_y):
116    """
117    Description : Rotates a matrix through the hypotenuse so that the original
118    matrix becomes the inverse matrix and the inverse matrix becomes matrix
119    Returns a tuple representing the matrix and its inverse
120
121    """
122    corner = rotated_matrix[0]
123    side = rotated_matrix[1]
124    anchor_yside = anchor_y * side
125    anchor_xside = anchor_x * side
126    anchor_ycorner = anchor_y * (1 - corner)
127    anchor_xcorner = anchor_x * (1 - corner)
128    matrix = cairo.Matrix(corner, side, -side, corner,
129                          anchor_xcorner + anchor_yside,
130                          anchor_ycorner - anchor_xside)
131    invmatrix = cairo.Matrix(corner, -side, side, corner,
132                             anchor_xcorner - anchor_yside,
133                             anchor_ycorner + anchor_xside)
134    return matrix, invmatrix
135
136
137ANIMATION_TIME = 0.5
138
139# If this is true, the board is scaled so that everything fits inside the window
140# even if the board is rotated 45 degrees
141SCALE_ROTATED_BOARD = False
142
143CORD_PADDING = 1.5
144
145
146class BoardView(Gtk.DrawingArea):
147    """ Description The BoardView instance is used to render the board to screen and supports
148        event updates associated with the game
149    """
150
151    __gsignals__ = {  # Signals emitted by class
152        'shownChanged': (GObject.SignalFlags.RUN_FIRST, None, (int,))
153    }
154
155    def __init__(self, gamemodel=None, preview=False, setup_position=False):
156        GObject.GObject.__init__(self)
157
158        if gamemodel is None:
159            gamemodel = GameModel()
160        self.model = gamemodel
161
162        self.allwhite = self.model.variant == AllWhiteBoard
163        self.asean = self.model.variant.variant in ASEAN_VARIANTS
164        self.preview = preview
165        self.setup_position = setup_position
166        self.shown_variation_idx = 0  # the main variation is the first in gamemodel.variations list
167
168        self.model_cids = [
169            self.model.connect("game_started", self.gameStarted),
170            self.model.connect("game_changed", self.gameChanged),
171            self.model.connect("moves_undoing", self.movesUndoing),
172            self.model.connect("variation_undoing", self.variationUndoing),
173            self.model.connect("game_loading", self.gameLoading),
174            self.model.connect("game_loaded", self.gameLoaded),
175            self.model.connect("game_ended", self.gameEnded),
176        ]
177
178        self.board_style_name = None
179        self.board_frame_name = None
180
181        self.draw_cid = self.connect("draw", self.expose)
182        self.realize_cid = self.connect_after("realize", self.onRealized)
183        self.notify_cids = [
184            conf.notify_add("drawGrid", self.onDrawGrid),
185            conf.notify_add("showCords", self.onShowCords),
186            conf.notify_add("showCaptured", self.onShowCaptured),
187            conf.notify_add("faceToFace", self.onFaceToFace),
188            conf.notify_add("noAnimation", self.onNoAnimation),
189            conf.notify_add("autoRotate", self.onAutoRotate),
190            conf.notify_add("pieceTheme", self.onPieceTheme),
191            conf.notify_add("board_frame", self.onBoardFrame),
192            conf.notify_add("board_style", self.onBoardStyle),
193            conf.notify_add("lightcolour", self.onBoardColour),
194            conf.notify_add("darkcolour", self.onBoardColour),
195            conf.notify_add("activateSupportAlgorithm", self.onSupportAlgorithmActivation)
196        ]
197        self.RANKS = self.model.boards[0].RANKS
198        self.FILES = self.model.boards[0].FILES
199        self.FILES_FOR_HOLDING = 6
200
201        self.animation_start = time()
202        self.last_shown = None
203        self.deadlist = []
204
205        self.auto_update_shown = True
206
207        self.real_set_shown = True
208        # only false when self.shown set temporarily(change shown variation)
209        # to avoid redraw_misc in animation
210
211        self.padding = 0  # Set to self.pad when setcords is active
212        self.square = 0, 0, self.FILES, 1  # An object global variable with the current
213        # board size
214        self.pad = 0.06  # Padding applied only when setcords is active
215
216        self._selected = None
217        self._hover = None
218        self._active = None
219        self._premove0 = None
220        self._premove1 = None
221        self._redarrow = None
222        self._greenarrow = None
223        self._bluearrow = None
224
225        # this is an integer that contains the last move that is shown
226        self._shown = self.model.ply
227
228        self.no_frame = False
229        self._show_cords = False
230        self.show_cords = conf.get("showCords")
231
232        self._draw_grid = False
233        self.draw_grid = conf.get("drawGrid")
234
235        self._show_captured = None
236        if self.setup_position:
237            self.set_size_request(int(40 * (self.FILES + self.FILES_FOR_HOLDING)), 40 * self.RANKS)
238            self.redrawCanvas()
239
240        self.noAnimation = conf.get("noAnimation")
241        self.faceToFace = conf.get("faceToFace")
242        self.autoRotate = conf.get("autoRotate")
243        if conf.get("activateSupportAlgorithm"):
244            self.onSupportAlgorithmActivation()
245
246        self.onBoardColour()
247        self.onBoardStyle()
248        self.onBoardFrame()
249
250        self._show_enpassant = False
251
252        self.lastMove = None
253
254        self.matrix = cairo.Matrix()
255        self.matrix_pi = cairo.Matrix.init_rotate(pi)
256        self.invmatrix = cairo.Matrix().invert()
257        self.cord_matrices_state = (0, 0)
258        self._rotation = 0
259
260        self.drawcount = 0
261        self.drawtime = 0
262
263        self.got_started = False
264        self.animating = False
265
266        self.dragged_piece = None  # a piece being dragged by the user
267        self.premove_piece = None
268        self.premove_promotion = None
269
270        # right click circles and arrows
271        # Contains Cord object, and are the coordinates in which we need to draw the circles or arrows
272        self.arrows = set()
273        self.circles = set()
274        self.pre_arrow = None
275        self.pre_circle = None
276
277        # circles and arrows from .pgn comments
278        self.saved_circles = set()
279        self.saved_arrows = set()
280
281        # store in memory the last number of move to know whether or not a new turn has started,
282        # of we went back to history
283        self.last_shown = 0
284
285    def _del(self):
286        self.disconnect(self.draw_cid)
287        self.disconnect(self.realize_cid)
288        for cid in self.notify_cids:
289            conf.notify_remove(cid)
290        for cid in self.model_cids:
291            self.model.disconnect(cid)
292
293    def gameStarted(self, model):
294        if model.lesson_game:
295            self.shown = model.lowply
296
297        if self.noAnimation:
298            self.got_started = True
299            self.redrawCanvas()
300        else:
301            if model.moves:
302                self.lastMove = model.moves[-1]
303
304            for row in self.model.boards[-1].data:
305                for piece in row.values():  # row:
306                    if piece:
307                        piece.opacity = 0
308
309            self.got_started = True
310            self.startAnimation()
311
312        self.emit("shownChanged", self.shown)
313
314    def gameChanged(self, model, ply):
315        # Play sounds
316        if self.model.players and self.model.status != WAITING_TO_START:
317            move = model.moves[-1]
318            if move.is_capture(model.boards[-2]):
319                sound = "aPlayerCaptures"
320            else:
321                sound = "aPlayerMoves"
322
323            if model.boards[-1].board.isChecked():
324                sound = "aPlayerChecks"
325
326            if model.players[0].__type__ == REMOTE and \
327                    model.players[1].__type__ == REMOTE:
328                sound = "observedMoves"
329
330            preferencesDialog.SoundTab.playAction(sound)
331
332        # Auto updating self.shown can be disabled. Useful for loading games.
333        # If we are not at the latest game we are probably browsing the history,
334        # and we won't like auto updating.
335        if self.auto_update_shown and self.shown + 1 >= ply and self.shownIsMainLine():
336            self.shown = ply
337
338            # Rotate board
339            if self.autoRotate:
340                if self.model.players and self.model.curplayer.__type__ == LOCAL:
341                    self.rotation = self.model.boards[-1].color * pi
342
343    def movesUndoing(self, model, moves):
344        if self.shownIsMainLine():
345            self.shown = model.ply - moves
346        else:
347            # Go back to the mainline to let animation system work
348            board = model.getBoardAtPly(self.shown, self.shown_variation_idx)
349            while board not in model.variations[0]:
350                board = model.variations[self.shown_variation_idx][board.ply - model.lowply - 1]
351            self.shown = board.ply
352            self.shown_variation_idx = 0
353            self.shown = model.ply - moves
354        self.redrawCanvas()
355
356    def variationUndoing(self, model):
357        self.showPrev()
358
359    def gameLoading(self, model, uri):
360        self.auto_update_shown = False
361
362    def gameLoaded(self, model, uri):
363        self.auto_update_shown = True
364        self._shown = model.ply
365
366    def gameEnded(self, model, reason):
367        self.redrawCanvas()
368
369        if self.model.players:
370            sound = ""
371
372            if model.status == DRAW:
373                sound = "gameIsDrawn"
374            elif model.status == WHITEWON:
375                if model.players[0].__type__ == LOCAL:
376                    sound = "gameIsWon"
377                elif model.players[1].__type__ == LOCAL:
378                    sound = "gameIsLost"
379            elif model.status == BLACKWON:
380                if model.players[1].__type__ == LOCAL:
381                    sound = "gameIsWon"
382                elif model.players[0].__type__ == LOCAL:
383                    sound = "gameIsLost"
384            elif model.status in (ABORTED, KILLED):
385                sound = "gameIsLost"
386
387            if model.status in (DRAW, WHITEWON, BLACKWON, KILLED, ABORTED) and \
388                    model.players[0].__type__ == REMOTE and \
389                    model.players[1].__type__ == REMOTE:
390                sound = "oberservedEnds"
391
392            # This should never be false, unless status is set to UNKNOWN or
393            # something strange
394            if sound != "":
395                preferencesDialog.SoundTab.playAction(sound)
396
397    def onDrawGrid(self, *args):
398        """ Checks the configuration / preferences to see if the board
399            grid should be displayed.
400        """
401        self.draw_grid = conf.get("drawGrid")
402
403    def onShowCords(self, *args):
404        """ Checks the configuration / preferences to see if the board
405            co-ordinates should be displayed.
406        """
407        self.show_cords = conf.get("showCords")
408
409    def onShowCaptured(self, *args):
410        """ Check the configuration / preferences to see if
411            the captured pieces should be displayed
412        """
413        self._setShowCaptured(conf.get("showCaptured"), force_restore=True)
414
415    def onNoAnimation(self, *args):
416        """ Check the configuration / preferences to see if
417            no animation needed at all
418        """
419        self.noAnimation = conf.get("noAnimation")
420
421    def onFaceToFace(self, *args):
422        """ If the preference for pieces to be displayed facing each other
423            has been set then refresh the board
424        """
425        self.faceToFace = conf.get("faceToFace")
426        self.redrawCanvas()
427
428    def onAutoRotate(self, *args):
429        self.autoRotate = conf.get("autoRotate")
430
431    def onPieceTheme(self, *args):
432        """ If the preference to display another chess set has been
433            selected then refresh the board
434        """
435        self.redrawCanvas()
436
437    def onBoardColour(self, *args):
438        """ If the preference to display another set of board colours has been
439            selected then refresh the board
440        """
441        self.light_colour = conf.get("lightcolour")
442        self.dark_colour = conf.get("darkcolour")
443        self.redrawCanvas()
444
445    def onBoardStyle(self, *args):
446        """ If the preference to display another set of board colours has been
447            selected then refresh the board
448        """
449        board_style = conf.get("board_style")
450        self.colors_only = board_style == 0
451        if not self.colors_only:
452            # create dark and light square surfaces
453            board_style_name = preferencesDialog.board_items[board_style][1]
454            if self.board_style_name is None or self.board_style_name != board_style_name:
455                self.board_style_name = board_style_name
456                dark_png = addDataPrefix("boards/%s_d.png" % board_style_name)
457                light_png = addDataPrefix("boards/%s_l.png" % board_style_name)
458                self.dark_surface = cairo.ImageSurface.create_from_png(dark_png)
459                self.light_surface = cairo.ImageSurface.create_from_png(light_png)
460
461        self.redrawCanvas()
462
463    def onBoardFrame(self, *args):
464        board_frame = conf.get("board_frame")
465        self.no_frame = board_frame == 0
466        if not self.no_frame:
467            # create board frame surface
468            board_frame_name = preferencesDialog.board_items[board_frame][1]
469            if self.board_frame_name is None or self.board_frame_name != board_frame_name:
470                self.board_frame_name = board_frame_name
471                frame_png = addDataPrefix("boards/%s_d.png" % board_frame_name)
472                self.frame_surface = cairo.ImageSurface.create_from_png(frame_png)
473
474        if not self.show_cords and self.no_frame:
475            self.padding = 0.
476        else:
477            self.padding = self.pad
478
479        self.redrawCanvas()
480
481    def onSupportAlgorithmActivation(self, *args):
482        '''activation of the support algorithm'''
483        activation_wanted = conf.get("activateSupportAlgorithm")
484        self.model.support_algorithm.enableDisableAlgo(activation_wanted)
485
486    ###############################
487    #          Animation          #
488    ###############################
489
490    def paintBoxAround(self, move):
491        paint_box = self.cord2RectRelative(move.cord1)
492        if move.flag != DROP:
493            paint_box = join(paint_box, self.cord2RectRelative(move.cord0))
494        if move.flag in (KING_CASTLE, QUEEN_CASTLE):
495            board = self.model.boards[-1].board
496            color = board.color
497            wildcastle = \
498                Cord(board.ini_kings[color]).x == 3 and \
499                board.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS)
500            if move.flag == KING_CASTLE:
501                side = 0 if wildcastle else 1
502                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.ini_rooks[color][side])))
503                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.fin_rooks[color][side])))
504                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.fin_kings[color][side])))
505            else:
506                side = 1 if wildcastle else 0
507                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.ini_rooks[color][side])))
508                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.fin_rooks[color][side])))
509                paint_box = join(paint_box, self.cord2RectRelative(Cord(board.fin_kings[color][side])))
510        return paint_box
511
512    def setShownBoard(self, board):
513        """ Set shown to the index of the given board in board list.
514            If the board belongs to a different variationd,
515            adjust the shown variation index too.
516            If board is in the main line, reset the shown variation idx to 0(the main line).
517        """
518        if board in self.model.variations[self.shown_variation_idx]:
519            # if the board to be shown is in the current shown variation, we are ok
520            self.shown = \
521                self.model.variations[self.shown_variation_idx].index(board) + \
522                self.model.lowply
523        else:
524            # else we have to go back first
525            for vari in self.model.variations:
526                if board in vari:
527                    # Go back to the common board of variations to let animation system work
528                    board_in_vari = board
529                    while board_in_vari not in self.model.variations[self.shown_variation_idx]:
530                        board_in_vari = vari[board_in_vari.ply - self.model.lowply - 1]
531                    self.real_set_shown = False
532                    self.shown = board_in_vari.ply
533                    break
534            # swich to the new variation
535            self.shown_variation_idx = self.model.variations.index(vari)
536            self.real_set_shown = True
537            self.shown = \
538                self.model.variations[self.shown_variation_idx].index(board) + \
539                self.model.lowply
540
541    def shownIsMainLine(self):
542        return self.shown_variation_idx == 0
543
544    @property
545    def has_unsaved_shapes(self):
546        return self.saved_arrows != self.arrows or self.saved_circles != self.circles
547
548    def _getShown(self):
549        return self._shown
550
551    def _setShown(self, shown, old_variation_idx=None):
552        """
553            Adjust the index in current variation board list.
554            old_variation_index is used when variation was added
555            to the last played move and we want to step back.
556
557            This function is called before draw, so this is where we preprocess data before drawing
558            This function is called at each turn
559        """
560        assert shown >= 0
561        if shown < self.model.lowply:
562            shown = self.model.lowply
563
564        # This would cause IndexErrors later
565        if shown > self.model.variations[self.shown_variation_idx][-1].ply:
566            return
567
568        if old_variation_idx is None:
569            old_variation_idx = self.shown_variation_idx
570
571        self.redarrow = None
572        self.greenarrow = None
573        self.bluearrow = None
574
575        # remove all circles and arrows
576        need_redraw = False
577        if self.saved_circles:
578            self.saved_circles.clear()
579            need_redraw = True
580        if self.saved_arrows:
581            self.saved_arrows.clear()
582            need_redraw = True
583        if self.arrows:
584            self.arrows.clear()
585            need_redraw = True
586        if self.circles:
587            self.circles.clear()
588            need_redraw = True
589        if self.pre_arrow is not None:
590            self.pre_arrow = None
591            need_redraw = True
592        if self.pre_circle is not None:
593            self.pre_circle = None
594            need_redraw = True
595
596        if (self.shown != shown) and not self.setup_position:
597            algorithm = self.model.support_algorithm
598            x = shown - self.model.lowply
599            if x >= len(self.model.boards):
600                x = -1
601            algorithm.calculate_coordinate_in_danger(
602                self.model.boards[x], (shown % 2)
603            )
604            need_redraw = True
605
606        # search circles/arrows in move comments
607        board = self.model.getBoardAtPly(shown, self.shown_variation_idx).board
608        if board.children:
609            for child in board.children:
610                if isinstance(child, str):
611                    if "[%csl" in child:
612                        match = comment_circles_re.search(child)
613                        circles = match.groups()[0].split(",")
614                        for circle in circles:
615                            self.saved_circles.add(Cord(circle[1:3], color=circle[0]))
616                            self.circles.add(Cord(circle[1:3], color=circle[0]))
617                        need_redraw = True
618
619                    if "[%cal" in child:
620                        match = comment_arrows_re.search(child)
621                        arrows = match.groups()[0].split(",")
622                        for arrow in arrows:
623                            self.saved_arrows.add((Cord(arrow[1:3], color=arrow[0]), Cord(arrow[3:5])))
624                            self.arrows.add((Cord(arrow[1:3], color=arrow[0]), Cord(arrow[3:5])))
625                        need_redraw = True
626
627        if need_redraw:
628            self.redrawCanvas()
629
630        # If there is only one board, we don't do any animation, but simply
631        # redraw the entire board. Same if we are at first draw.
632        if len(self.model.boards) == 1 or self.shown < self.model.lowply:
633            self._shown = shown
634            if shown > self.model.lowply:
635                self.lastMove = self.model.getMoveAtPly(shown - 1, self.shown_variation_idx)
636            self.emit("shownChanged", self.shown)
637            self.redrawCanvas()
638            return
639
640        step = shown > self.shown and 1 or -1
641
642        deadset = set()
643        for i in range(self.shown, shown, step):
644            board = self.model.getBoardAtPly(i, old_variation_idx)
645            board1 = self.model.getBoardAtPly(i + step, self.shown_variation_idx)
646            if step == 1:
647                move = self.model.getMoveAtPly(i, self.shown_variation_idx)
648                moved, new, dead = board.simulateMove(board1, move)
649            else:
650                move = self.model.getMoveAtPly(i - 1, old_variation_idx)
651                moved, new, dead = board.simulateUnmove(board1, move)
652
653            # We need to ensure, that the piece coordinate is saved in the
654            # piece
655            for piece, cord0 in moved:
656                # Test if the piece already has a realcoord(has been dragged)
657                if (piece is not None) and piece.x is None:
658                    # We don't want newly restored pieces to flew from their
659                    # deadspot to their old position, as it doesn't work
660                    # vice versa
661                    if piece.opacity == 1:
662                        piece.x = cord0.x
663                        piece.y = cord0.y
664
665            for piece in dead:
666                deadset.add(piece)
667                # Reset the location of the piece to avoid a small visual
668                # jump, when it is at some other time waken to life.
669                piece.x = None
670                piece.y = None
671
672            for piece in new:
673                piece.opacity = 0
674
675        self.deadlist = []
676        for y_loc, row in enumerate(self.model.getBoardAtPly(self.shown, old_variation_idx).data):
677            for x_loc, piece in row.items():
678                if piece in deadset:
679                    self.deadlist.append((piece, x_loc, y_loc))
680
681        self._shown = shown
682        if self.real_set_shown:
683            board = self.model.getBoardAtPly(self.shown, self.shown_variation_idx)
684            if board in self.model.variations[0]:
685                self.shown_variation_idx = 0
686            else:
687                for vari in self.model.variations:
688                    if board in vari:
689                        # swich to the new variation
690                        self.shown_variation_idx = self.model.variations.index(vari)
691                        break
692            self.emit("shownChanged", self.shown)
693
694        self.animation_start = time()
695        self.animating = True
696
697        if self.lastMove:
698            paint_box = self.paintBoxAround(self.lastMove)
699            self.lastMove = None
700            self.redrawCanvas(rect(paint_box))
701
702        if self.shown > self.model.lowply:
703            self.lastMove = self.model.getMoveAtPly(self.shown - 1, self.shown_variation_idx)
704            paint_box = self.paintBoxAround(self.lastMove)
705            self.redrawCanvas(rect(paint_box))
706        else:
707            self.lastMove = None
708
709        self.runAnimation(redraw_misc=self.real_set_shown)
710        if not self.noAnimation:
711            while self.animating:
712                self.runAnimation()
713
714    shown = property(_getShown, _setShown)
715
716    def runAnimation(self, redraw_misc=False):
717        """
718            The animationsystem in pychess is very loosely inspired by the one of
719            chessmonk. The idea is, that every piece has a place in an array(the
720            board.data one) for where to be drawn. If a piece is to be animated, it
721            can set its x and y properties, to some cord(or part cord like 0.42 for
722            42% right to file 0). Each time runAnimation is run, it will set those x
723            and y properties a little closer to the location in the array. When it
724            has reached its final location, x and y will be set to None. _setShown,
725            which starts the animation, also sets a timestamp for the acceleration
726            to work properply.
727        """
728        if self.model is None:
729            return False
730
731        if not self.animating:
732            return False
733
734        paint_box = None
735
736        mod = min(1, (time() - self.animation_start) / ANIMATION_TIME)
737        board = self.model.getBoardAtPly(self.shown, self.shown_variation_idx)
738
739        for y_loc, row in enumerate(board.data):
740            for x_loc, piece in row.items():
741                if not piece:
742                    continue
743                if piece == self.dragged_piece:
744                    continue
745                if piece == self.premove_piece:
746                    # if premove move is being made, the piece will already be
747                    # sitting on the cord it needs to move to-
748                    # do not animate and reset premove to None
749                    if self.shown == self.premove_ply:
750                        piece.x = None
751                        piece.y = None
752                        self.setPremove(None, None, None, None)
753                        continue
754                    # otherwise, animate premove piece moving to the premove cord
755                    # rather than the cord it actually exists on
756                    elif self.premove0 and self.premove1:
757                        x_loc = self.premove1.x
758                        y_loc = self.premove1.y
759
760                if piece.x is not None:
761                    if not self.noAnimation:
762                        if piece.piece == KNIGHT:
763                            newx = piece.x + (x_loc - piece.x) * mod ** (1.5)
764                            newy = piece.y + (y_loc - piece.y) * mod
765                        else:
766                            newx = piece.x + (x_loc - piece.x) * mod
767                            newy = piece.y + (y_loc - piece.y) * mod
768                    else:
769                        newx, newy = x_loc, y_loc
770
771                    paint_box = join(paint_box, self.cord2RectRelative(piece.x,
772                                                                       piece.y))
773                    paint_box = join(paint_box, self.cord2RectRelative(newx, newy))
774
775                    if (newx <= x_loc <= piece.x or newx >= x_loc >= piece.x) and \
776                            (newy <= y_loc <= piece.y or newy >= y_loc >= piece.y) or \
777                            abs(newx - x_loc) < 0.005 and abs(newy - y_loc) < 0.005:
778                        paint_box = join(paint_box, self.cord2RectRelative(x_loc, y_loc))
779                        piece.x = None
780                        piece.y = None
781                    else:
782                        piece.x = newx
783                        piece.y = newy
784
785                if piece.opacity < 1:
786                    if piece.x is not None:
787                        px_loc = piece.x
788                        py_loc = piece.y
789                    else:
790                        px_loc = x_loc
791                        py_loc = y_loc
792
793                    if paint_box:
794                        paint_box = join(paint_box, self.cord2RectRelative(px_loc, py_loc))
795                    else:
796                        paint_box = self.cord2RectRelative(px_loc, py_loc)
797
798                    if not self.noAnimation:
799                        new_op = piece.opacity + (1 - piece.opacity) * mod
800                    else:
801                        new_op = 1
802
803                    if new_op >= 1 >= piece.opacity or abs(1 - new_op) < 0.005:
804                        piece.opacity = 1
805                    else:
806                        piece.opacity = new_op
807
808        ready = []
809        for i, dead in enumerate(self.deadlist):
810            piece, x_loc, y_loc = dead
811            if not paint_box:
812                paint_box = self.cord2RectRelative(x_loc, y_loc)
813            else:
814                paint_box = join(paint_box, self.cord2RectRelative(x_loc, y_loc))
815
816            if not self.noAnimation:
817                new_op = piece.opacity + (0 - piece.opacity) * mod
818            else:
819                new_op = 0
820
821            if new_op <= 0 <= piece.opacity or abs(0 - new_op) < 0.005:
822                ready.append(dead)
823            else:
824                piece.opacity = new_op
825
826        for dead in ready:
827            self.deadlist.remove(dead)
828
829        if paint_box:
830            self.redrawCanvas(rect(paint_box))
831
832        if self.noAnimation:
833            self.animating = False
834            return False
835        else:
836            if not paint_box:
837                self.animating = False
838            return paint_box and True or False
839
840    def startAnimation(self):
841        self.animation_start = time()
842        self.animating = True
843
844        self.runAnimation(redraw_misc=True)
845        if not self.noAnimation:
846            while self.animating:
847                self.runAnimation()
848
849    #############################
850    #          Drawing          #
851    #############################
852
853    def onRealized(self, widget):
854        padding = (1 - self.padding)
855        alloc = self.get_allocation()
856        square = float(min(alloc.width, alloc.height)) * padding
857        xc_loc = alloc.width / 2. - square / 2
858        yc_loc = alloc.height / 2. - square / 2
859        size = square / self.FILES
860        self.square = (xc_loc, yc_loc, square, size)
861
862    def expose(self, widget, ctx):
863        context = widget.get_window().cairo_create()
864
865        start = time()
866        rectangle = Gdk.Rectangle()
867        clip_ext = ctx.clip_extents()
868        rectangle.x, rectangle.y = clip_ext[0], clip_ext[1]
869        rectangle.width, rectangle.height = clip_ext[2] - clip_ext[0], clip_ext[3] - clip_ext[1]
870
871        if False:
872            import profile
873            profile.runctx("self.draw(context, rectangle)", locals(), globals(), "/tmp/pychessprofile")
874            from pstats import Stats
875            stats = Stats("/tmp/pychessprofile")
876            stats.sort_stats('cumulative')
877            stats.print_stats()
878        else:
879            self.draw(context, rectangle)
880            # self.drawcount += 1
881            # self.drawtime += time() - start
882            # if self.drawcount % 100 == 0:
883            #    print( "Average FPS: %0.3f - %d / %d" % \
884            #     (self.drawcount/self.drawtime, self.drawcount, self.drawtime))
885
886        return False
887
888    ############################################################################
889    #                            drawing functions                             #
890    ############################################################################
891
892    ###############################
893    #        redrawCanvas        #
894    ###############################
895
896    def redrawCanvas(self, rect=None):
897        if self.get_window():
898            if not rect:
899                alloc = self.get_allocation()
900                rect = Gdk.Rectangle()
901                rect.x, rect.y, rect.width, rect.height = (0, 0, alloc.width, alloc.height)
902            self.get_window().invalidate_rect(rect, True)
903            self.get_window().process_updates(True)
904
905    ###############################
906    #            draw             #
907    ###############################
908
909    # draw called each time we hover on a case. WARNING it only redraw the case
910    def draw(self, context, r):
911
912        # context.set_antialias(cairo.ANTIALIAS_NONE)
913        if self.shown < self.model.lowply:
914            print("exiting cause to lowlpy", self.shown, self.model.lowply)
915            return
916
917        alloc = self.get_allocation()
918
919        self.matrix, self.invmatrix = matrixAround(
920            self.matrix, alloc.width / 2., alloc.height / 2.)
921        cos_, sin_ = self.matrix[0], self.matrix[1]
922        context.transform(self.matrix)
923
924        square = float(min(alloc.width, alloc.height)) * (1 - self.padding)
925        if SCALE_ROTATED_BOARD:
926            square /= abs(cos_) + abs(sin_)
927        xc_loc = alloc.width / 2. - square / 2
928        yc_loc = alloc.height / 2. - square / 2
929        side = square / self.FILES
930        self.square = (xc_loc, yc_loc, square, side)
931
932        # draw all the different components by calling all the draw methods of this class
933        self.drawBoard(context, r)
934
935        self.drawSupportAlgorithm(context, r)
936
937        if min(alloc.width, alloc.height) > 32:
938            self.drawCords(context, r)
939
940        if self.got_started:
941            self.drawSpecial(context, r)
942            self.drawEnpassant(context, r)
943
944            self.drawCircles(context)
945            self.drawArrows(context)
946            self.drawPieces(context, r)
947            if not self.setup_position:
948                self.drawLastMove(context, r)
949
950        if self.model.status == KILLED:
951            pass
952            # self.drawCross(context, r)
953
954        # At this point we have real values of self.get_allocation()
955        # and can adjust board paned divider if needed
956        if self._show_captured is None:
957            self.showCaptured = conf.get("showCaptured")
958
959        # Unselect to mark redrawn areas - for debugging purposes
960        # context.transform(self.invmatrix)
961        # context.rectangle(r.x,r.y,r.width,r.height)
962        # dc = self.drawcount*50
963        # dc = dc % 1536
964        # c = dc % 256 / 255.
965        # if dc < 256:
966        #    context.set_source_rgb(1, ,c,0)
967        # elif dc < 512:
968        #    context.set_source_rgb(1-c,1, 0)
969        # elif dc < 768:
970        #    context.set_source_rgb(0, 1,c)
971        # elif dc < 1024:
972        #    context.set_source_rgb(0, 1-c,1)
973        # elif dc < 1280:
974        #    context.set_source_rgb(c,0, 1)
975        # elif dc < 1536:
976        #    context.set_source_rgb(1, 0, 1-c)
977        # context.stroke()
978
979    ###############################
980    #          drawCords          #
981    ###############################
982
983    def drawCords(self, context, rectangle):
984        thickness = 0.01
985        signsize = 0.02
986
987        if (not self.show_cords) and (not self.setup_position):
988            return
989
990        xc_loc, yc_loc, square, side = self.square
991
992        if rectangle is not None and contains(rect((xc_loc, yc_loc, square)), rectangle):
993            return
994
995        thick = thickness * square
996        sign_size = signsize * square
997
998        pangoScale = float(Pango.SCALE)
999        if self.no_frame:
1000            context.set_source_rgb(0.0, 0.0, 0.0)
1001        else:
1002            context.set_source_rgb(1.0, 1.0, 1.0)
1003
1004        def paint(inv):
1005            for num in range(self.RANKS):
1006                rank = inv and num + 1 or self.RANKS - num
1007                layout = self.create_pango_layout("%d" % rank)
1008                layout.set_font_description(
1009                    Pango.FontDescription("bold %d" % sign_size))
1010                width = layout.get_extents()[1].width / pangoScale
1011                height = layout.get_extents()[0].height / pangoScale
1012
1013                # Draw left side
1014                context.move_to(xc_loc - thick - width, side * num + yc_loc + height / 2 + thick * 3)
1015                PangoCairo.show_layout(context, layout)
1016
1017                file = inv and self.FILES - num or num + 1
1018                layout = self.create_pango_layout(chr(file + ord("A") - 1))
1019                layout.set_font_description(
1020                    Pango.FontDescription("bold %d" % sign_size))
1021
1022                # Draw bottom
1023                context.move_to(xc_loc + side * num + side / 2 - width / 2, yc_loc + square)
1024                PangoCairo.show_layout(context, layout)
1025
1026        matrix, invmatrix = matrixAround(self.matrix_pi, xc_loc + square / 2, yc_loc + square / 2)
1027        if self.rotation == 0:
1028            paint(False)
1029        else:
1030            context.transform(matrix)
1031            paint(True)
1032            context.transform(invmatrix)
1033
1034        if self.faceToFace:
1035            if self.rotation == 0:
1036                context.transform(matrix)
1037                paint(True)
1038                context.transform(invmatrix)
1039            else:
1040                paint(False)
1041
1042    def draw_image(self, context, image_surface, left, top, width, height):
1043        """ Draw a scaled image on a given context. """
1044
1045        # calculate scale
1046        image_width = image_surface.get_width()
1047        image_height = image_surface.get_height()
1048        width_ratio = float(width) / float(image_width)
1049        height_ratio = float(height) / float(image_height)
1050        scale_xy = min(width_ratio, height_ratio)
1051
1052        # scale image and add it
1053        context.save()
1054        context.translate(left, top)
1055        context.scale(scale_xy, scale_xy)
1056        context.set_source_surface(image_surface)
1057
1058        context.paint()
1059        context.restore()
1060
1061    def draw_frame(self, context, image_surface, left, top, width, height):
1062        """ Draw a repeated image pattern on a given context. """
1063
1064        pat = cairo.SurfacePattern(image_surface)
1065        pat.set_extend(cairo.EXTEND_REPEAT)
1066
1067        context.rectangle(left, top, width, height)
1068        context.set_source(pat)
1069
1070        context.fill()
1071
1072    ###############################
1073    #          drawBoard          #
1074    ###############################
1075
1076    def drawBoard(self, context, r):
1077        xc_loc, yc_loc, square, side = self.square
1078
1079        col = Gdk.RGBA()
1080        col.parse(self.light_colour)
1081        context.set_source_rgba(col.red, col.green, col.blue, col.alpha)
1082
1083        if self.model.variant.variant in ASEAN_VARIANTS:
1084            # just fill the whole board with light color
1085            if self.colors_only:
1086                context.rectangle(xc_loc, yc_loc, side * self.FILES, side * self.RANKS)
1087            else:
1088                self.draw_image(context, self.light_surface, xc_loc, yc_loc, side * self.FILES, side * self.RANKS)
1089            if self.colors_only:
1090                context.fill()
1091        else:
1092            # light squares
1093            for x_loc in range(self.FILES):
1094                for y_loc in range(self.RANKS):
1095                    if x_loc % 2 + y_loc % 2 != 1:
1096                        if self.colors_only:
1097                            context.rectangle(xc_loc + x_loc * side, yc_loc + y_loc * side, side, side)
1098                        else:
1099                            self.draw_image(context, self.light_surface, xc_loc + x_loc * side, yc_loc + y_loc * side,
1100                                            side, side)
1101            if self.colors_only:
1102                context.fill()
1103
1104        col = Gdk.RGBA()
1105        col.parse(self.dark_colour)
1106        context.set_source_rgba(col.red, col.green, col.blue, col.alpha)
1107
1108        if self.model.variant.variant in ASEAN_VARIANTS:
1109            # diagonals
1110            if self.model.variant.variant == SITTUYINCHESS:
1111                context.move_to(xc_loc, yc_loc)
1112                context.rel_line_to(square, square)
1113                context.move_to(xc_loc + square, yc_loc)
1114                context.rel_line_to(-square, square)
1115                context.stroke()
1116        else:
1117            # dark squares
1118            for x_loc in range(self.FILES):
1119                for y_loc in range(self.RANKS):
1120                    if x_loc % 2 + y_loc % 2 == 1:
1121                        if self.colors_only:
1122                            context.rectangle((xc_loc + x_loc * side), (yc_loc + y_loc * side), side, side)
1123                        else:
1124                            self.draw_image(context, self.dark_surface, (xc_loc + x_loc * side),
1125                                            (yc_loc + y_loc * side), side, side)
1126            if self.colors_only:
1127                context.fill()
1128
1129        if not self.no_frame:
1130            # board frame
1131            delta = side / 4
1132            # top
1133            self.draw_frame(context, self.frame_surface, xc_loc - delta, yc_loc - delta, self.FILES * side + delta * 2,
1134                            delta)
1135            # bottom
1136            self.draw_frame(context, self.frame_surface, xc_loc - delta, yc_loc + self.RANKS * side,
1137                            self.FILES * side + delta * 2, delta)
1138            # left
1139            self.draw_frame(context, self.frame_surface, xc_loc - delta, yc_loc, delta, self.FILES * side)
1140            # right
1141            self.draw_frame(context, self.frame_surface, xc_loc + self.FILES * side, yc_loc, delta, self.FILES * side)
1142
1143        if self.draw_grid:
1144            # grid lines between squares
1145            context.set_source_rgb(0.0, 0.0, 0.0)
1146            context.set_line_width(0.5 if r is None else 1.0)
1147
1148            for loc in range(self.FILES):
1149                context.move_to(xc_loc + side * loc, yc_loc)
1150                context.line_to(xc_loc + side * loc, yc_loc + self.FILES * side)
1151                context.move_to(xc_loc, yc_loc + side * loc)
1152                context.line_to(xc_loc + self.FILES * side, yc_loc + side * loc)
1153
1154            context.rectangle(xc_loc, yc_loc, self.FILES * side, self.RANKS * side)
1155            context.stroke()
1156
1157        context.set_source_rgba(col.red, col.green, col.blue, col.alpha)
1158
1159    ###############################
1160    #         drawPieces          #
1161    ###############################
1162
1163    def getCordMatrices(self, x_loc, y_loc, inv=False):
1164        square, side = self.square[2], self.square[3]
1165        rot_ = self.cord_matrices_state[1]
1166        if square != self.square or rot_ != self.rotation:
1167            self.cord_matrices = [None] * self.FILES * self.RANKS + [None] * self.FILES * 4
1168            self.cord_matrices_state = (self.square, self.rotation)
1169        c_loc = x_loc * self.FILES + y_loc
1170        if isinstance(c_loc, int) and self.cord_matrices[c_loc]:
1171            matrices = self.cord_matrices[c_loc]
1172        else:
1173            cx_loc, cy_loc = self.cord2Point(x_loc, y_loc)
1174            matrices = matrixAround(self.matrix, cx_loc + side / 2., cy_loc + side / 2.)
1175            matrices += (cx_loc, cy_loc)
1176            if isinstance(c_loc, int):
1177                self.cord_matrices[c_loc] = matrices
1178        return matrices
1179
1180    def __drawPiece(self, context, piece, x_loc, y_loc):
1181        # Maybe a premove was reset from another thread
1182        if piece is None:
1183            print("Trying to draw a None piece")
1184            return
1185        if self.model.variant == BlindfoldBoard:
1186            return
1187        elif self.model.variant == HiddenPawnsBoard:
1188            if piece.piece == PAWN:
1189                return
1190        elif self.model.variant == HiddenPiecesBoard:
1191            if piece.piece != PAWN:
1192                return
1193
1194        if piece.captured and not self.showCaptured:
1195            return
1196
1197        side = self.square[3]
1198
1199        if not self.faceToFace:
1200            matrix, invmatrix, cx_loc, cy_loc = self.getCordMatrices(x_loc, y_loc)
1201        else:
1202            cx_loc, cy_loc = self.cord2Point(x_loc, y_loc)
1203            if piece.color == BLACK:
1204                matrix, invmatrix = matrixAround((-1, 0), cx_loc + side / 2., cy_loc + side / 2.)
1205            else:
1206                matrix = invmatrix = cairo.Matrix(1, 0, 0, 1, 0, 0)
1207
1208        context.transform(invmatrix)
1209        Pieces.drawPiece(piece, context,
1210                         cx_loc + CORD_PADDING, cy_loc + CORD_PADDING,
1211                         side - CORD_PADDING * 2, allwhite=self.allwhite, asean=self.asean,
1212                         variant=self.model.variant.variant)
1213        context.transform(matrix)
1214
1215    def drawPieces(self, context, rectangle):
1216        pieces = self.model.getBoardAtPly(self.shown, self.shown_variation_idx)
1217
1218        style_ctxt = self.get_style_context()
1219
1220        col = style_ctxt.lookup_color("p_fg_color")[1]
1221        fg_n = (col.red, col.green, col.blue)
1222        fg_s = fg_n
1223
1224        col = style_ctxt.lookup_color("p_fg_active")[1]
1225        fg_a = (col.red, col.green, col.blue)
1226
1227        col = style_ctxt.lookup_color("p_fg_prelight")[1]
1228        fg_p = (col.red, col.green, col.blue)
1229
1230        fg_m = fg_n
1231
1232        # As default we use normal foreground for selected cords, as it looks
1233        # less confusing. However for some themes, the normal foreground is so
1234        # similar to the selected background, that we have to use the selected
1235        # foreground.
1236
1237        col = style_ctxt.lookup_color("p_bg_selected")[1]
1238        bg_sl = (col.red, col.green, col.blue)
1239
1240        col = style_ctxt.lookup_color("p_dark_selected")[1]
1241        bg_sd = (col.red, col.green, col.blue)
1242
1243        if min((fg_n[0] - bg_sl[0]) ** 2 + (fg_n[1] - bg_sl[1]) ** 2 + (fg_n[2] - bg_sl[2]) ** 2,
1244               (fg_n[0] - bg_sd[0]) ** 2 + (fg_n[1] - bg_sd[1]) ** 2 + (fg_n[2] - bg_sd[2]) ** 2) < 0.2:
1245            col = style_ctxt.lookup_color("p_fg_selected")[1]
1246            fg_s = (col.red, col.green, col.blue)
1247
1248        # Draw dying pieces(Found in self.deadlist)
1249        for piece, x_loc, y_loc in self.deadlist:
1250            context.set_source_rgba(fg_n[0], fg_n[1], fg_n[2], piece.opacity)
1251            self.__drawPiece(context, piece, x_loc, y_loc)
1252
1253        # Draw pieces reincarnating(With opacity < 1)
1254        for y_loc, row in enumerate(pieces.data):
1255            for x_loc, piece in row.items():
1256                if not piece or piece.opacity == 1:
1257                    continue
1258                if piece.x:
1259                    x_loc, y_loc = piece.x, piece.y
1260                context.set_source_rgba(fg_n[0], fg_n[1], fg_n[2], piece.opacity)
1261                self.__drawPiece(context, piece, x_loc, y_loc)
1262
1263        # Draw standing pieces(Only those who intersect drawn area)
1264        for y_loc, row in enumerate(pieces.data):
1265            for x_loc, piece in row.items():
1266                if piece == self.premove_piece:
1267                    continue
1268                if not piece or piece.x is not None or piece.opacity < 1:
1269                    continue
1270                if rectangle is not None and not intersects(rect(self.cord2RectRelative(x_loc, y_loc)), rectangle):
1271                    continue
1272                if Cord(x_loc, y_loc) == self.selected:
1273                    context.set_source_rgb(*fg_s)
1274                elif Cord(x_loc, y_loc) == self.active:
1275                    context.set_source_rgb(*fg_a)
1276                elif Cord(x_loc, y_loc) == self.hover:
1277                    context.set_source_rgb(*fg_p)
1278                else:
1279                    context.set_source_rgb(*fg_n)
1280
1281                self.__drawPiece(context, piece, x_loc, y_loc)
1282
1283        # Draw moving or dragged pieces(Those with piece.x and piece.y != None)
1284        context.set_source_rgb(*fg_p)
1285        for y_loc, row in enumerate(pieces.data):
1286            for x_loc, piece in row.items():
1287                if not piece or piece.x is None or piece.opacity < 1:
1288                    continue
1289                self.__drawPiece(context, piece, piece.x, piece.y)
1290
1291        # Draw standing premove piece
1292        context.set_source_rgb(*fg_m)
1293        if self.premove_piece and self.premove_piece.x is None and self.premove0 and self.premove1:
1294            self.__drawPiece(context, self.premove_piece, self.premove1.x, self.premove1.y)
1295
1296    ###############################
1297    #         drawSpecial         #
1298    ###############################
1299
1300    def drawSpecial(self, context, redrawn):
1301
1302        light_blue = (0.550, 0.775, 0.950, 0.8)
1303        dark_blue = (0.475, 0.700, 0.950, 0.5)
1304
1305        used = []
1306        for cord, state in ((self.active, "_active"),
1307                            (self.selected, "_selected"),
1308                            (self.premove0, "_selected"),
1309                            (self.premove1, "_selected"),
1310                            (self.hover, "_prelight")):
1311            if not cord:
1312                continue
1313            if cord in used:
1314                continue
1315            # Ensure that same cord, if having multiple "tasks", doesn't get
1316            # painted more than once
1317            used.append(cord)
1318
1319            bounding = self.cord2RectRelative(cord)
1320            if not intersects(rect(bounding), redrawn):
1321                continue
1322
1323            board = self.model.getBoardAtPly(self.shown, self.shown_variation_idx)
1324            if board[cord] is None and (cord.x < 0 or cord.x > self.FILES - 1):
1325                continue
1326
1327            side = self.square[3]
1328            x_loc, y_loc = self.cord2Point(cord)
1329            context.rectangle(x_loc, y_loc, side, side)
1330            if cord == self.premove0 or cord == self.premove1:
1331                if self.isLight(cord):
1332                    context.set_source_rgba(*light_blue)
1333                else:
1334                    context.set_source_rgba(*dark_blue)
1335            else:
1336                style_ctxt = self.get_style_context()
1337                if self.isLight(cord):
1338                    # bg
1339                    found, color = style_ctxt.lookup_color("p_bg" + state)
1340                else:
1341                    # dark
1342                    found, color = style_ctxt.lookup_color("p_dark" + state)
1343                if not found:
1344                    print("color not found in boardview.py:", "p_dark" + state)
1345                red, green, blue, alpha = color.red, color.green, color.blue, color.alpha
1346                context.set_source_rgba(red, green, blue, alpha)
1347            context.fill()
1348
1349    def color2rgba(self, color):
1350        if color == "R":
1351            rgba = (.643, 0, 0, 0.8)
1352        elif color == "B":
1353            rgba = (.204, .396, .643, 0.8)
1354        elif color == "Y":
1355            rgba = (.961, .475, 0, 0.8)
1356        else:
1357            # light_green
1358            rgba = (0.337, 0.612, 0.117, 0.8)
1359        return rgba
1360
1361    def drawCircles(self, context):
1362        radius = self.square[3] / 2.0
1363        context.set_line_width(4)
1364
1365        for cord in self.circles:
1366            rgba = self.color2rgba(cord.color)
1367            context.set_source_rgb(*rgba[:3])
1368            x_loc, y_loc = self.cord2Point(cord)
1369            context.new_sub_path()
1370            context.arc(x_loc + radius, y_loc + radius, radius - 3, 0, 2 * pi)
1371            context.stroke()
1372
1373        if self.pre_circle is not None:
1374            rgba = self.color2rgba(self.pre_circle.color)
1375            context.set_source_rgb(*rgba[:3])
1376            x_loc, y_loc = self.cord2Point(self.pre_circle)
1377            context.new_sub_path()
1378            context.arc(x_loc + radius, y_loc + radius, radius - 3, 0, 2 * pi)
1379            context.stroke()
1380
1381        arw = 0.15  # Arrow width
1382        arhw = 0.6  # Arrow head width
1383        arhh = 0.6  # Arrow head height
1384        arsw = 0.0  # Arrow stroke width
1385        for arrow_cords in self.arrows:
1386            rgba = self.color2rgba(arrow_cords[0].color)
1387            self.__drawArrow(context, arrow_cords, arw, arhw, arhh, arsw, rgba, rgba)
1388        if self.pre_arrow is not None:
1389            rgba = self.color2rgba(self.pre_arrow[0].color)
1390            self.__drawArrow(context, self.pre_arrow, arw, arhw, arhh, arsw, rgba, rgba)
1391
1392    ###############################
1393    #        drawLastMove         #
1394    ###############################
1395
1396    def drawLastMove(self, context, redrawn):
1397        if not self.lastMove:
1398            return
1399        if self.shown <= self.model.lowply:
1400            return
1401        show_board = self.model.getBoardAtPly(self.shown, self.shown_variation_idx)
1402        last_board = self.model.getBoardAtPly(self.shown - 1, self.shown_variation_idx)
1403        capture = self.lastMove.is_capture(last_board)
1404
1405        mark_width = 0.27  # Width of marker
1406        padding_last = 0.155  # Padding on last cord
1407        padding_curr = 0.085  # Padding on current cord
1408        stroke_width = 0.02  # Stroke width
1409
1410        side = self.square[3]
1411
1412        context.save()
1413        context.set_line_width(stroke_width * side)
1414
1415        dic0 = {-1: 1 - padding_last, 1: padding_last}
1416        dic1 = {-1: 1 - padding_curr, 1: padding_curr}
1417        matrix_scaler = ((1, 1), (-1, 1), (-1, -1), (1, -1))
1418
1419        light_yellow = (.929, .831, 0, 0.8)
1420        dark_yellow = (.769, .627, 0, 0.5)
1421        light_orange = (.961, .475, 0, 0.8)
1422        dark_orange = (.808, .361, 0, 0.5)
1423        light_green = (0.337, 0.612, 0.117, 0.8)
1424        dark_green = (0.237, 0.512, 0.17, 0.5)
1425
1426        if self.lastMove.flag in (KING_CASTLE, QUEEN_CASTLE):
1427            ksq0 = last_board.board.kings[last_board.color]
1428            ksq1 = show_board.board.kings[last_board.color]
1429            wildcastle = \
1430                Cord(last_board.board.ini_kings[last_board.color]).x == 3 and \
1431                last_board.variant in (WILDCASTLECHESS, WILDCASTLESHUFFLECHESS)
1432            if self.lastMove.flag == KING_CASTLE:
1433                side = 0 if wildcastle else 1
1434                rsq0 = show_board.board.ini_rooks[last_board.color][side]
1435                rsq1 = show_board.board.fin_rooks[last_board.color][side]
1436            else:
1437                side = 1 if wildcastle else 0
1438                rsq0 = show_board.board.ini_rooks[last_board.color][side]
1439                rsq1 = show_board.board.fin_rooks[last_board.color][side]
1440            cord_pairs = [[Cord(ksq0), Cord(ksq1)], [Cord(rsq0), Cord(rsq1)]]
1441        else:
1442            cord_pairs = [[self.lastMove.cord0, self.lastMove.cord1]]
1443
1444        for [cord0, cord1] in cord_pairs:
1445            if cord0 is not None:
1446                rel = self.cord2RectRelative(cord0)
1447                if intersects(rect(rel), redrawn):
1448                    rectangle = self.cord2Rect(cord0)
1449                    for scaler in matrix_scaler:
1450                        context.move_to(
1451                            rectangle[0] + (dic0[scaler[0]] + mark_width * scaler[0]) * rectangle[2],
1452                            rectangle[1] + (dic0[scaler[1]] + mark_width * scaler[1]) * rectangle[2])
1453                        context.rel_line_to(
1454                            0, -mark_width * rectangle[2] * scaler[1])
1455                        context.rel_curve_to(0, mark_width * rectangle[2] * scaler[1] / 2.0,
1456                                             -mark_width * rectangle[2] * scaler[0] / 2.0,
1457                                             mark_width * rectangle[2] * scaler[1],
1458                                             -mark_width * rectangle[2] * scaler[0],
1459                                             mark_width * rectangle[2] * scaler[1])
1460                        context.close_path()
1461
1462                    context.set_source_rgba(*light_yellow)
1463                    context.fill_preserve()
1464                    context.set_source_rgba(*dark_yellow)
1465                    context.stroke()
1466
1467            rel = self.cord2RectRelative(cord1)
1468            if intersects(rect(rel), redrawn):
1469                rectangle = self.cord2Rect(cord1)
1470
1471                for scaler in matrix_scaler:
1472                    context.move_to(
1473                        rectangle[0] + dic1[scaler[0]] * rectangle[2],
1474                        rectangle[1] + dic1[scaler[1]] * rectangle[2])
1475                    context.rel_line_to(
1476                        mark_width * rectangle[2] * scaler[0], 0)
1477                    context.rel_curve_to(
1478                        -mark_width * rectangle[2] * scaler[0] / 2.0, 0,
1479                        -mark_width * rectangle[2] * scaler[0], mark_width * rectangle[2] * scaler[1] / 2.0,
1480                        -mark_width * rectangle[2] * scaler[0], mark_width * rectangle[2] * scaler[1])
1481                    context.close_path()
1482
1483                if capture:
1484                    context.set_source_rgba(*light_orange)
1485                    context.fill_preserve()
1486                    context.set_source_rgba(*dark_orange)
1487                    context.stroke()
1488                elif cord0 is None:  # DROP move
1489                    context.set_source_rgba(*light_green)
1490                    context.fill_preserve()
1491                    context.set_source_rgba(*dark_green)
1492                    context.stroke()
1493                else:
1494                    context.set_source_rgba(*light_yellow)
1495                    context.fill_preserve()
1496                    context.set_source_rgba(*dark_yellow)
1497                    context.stroke()
1498
1499    ###############################
1500    #         drawArrows          #
1501    ###############################
1502
1503    def __drawArrow(self, context, cords, aw, ahw, ahh, asw, fillc, strkc):
1504        context.save()
1505
1506        lvx = cords[1].x - cords[0].x
1507        lvy = cords[0].y - cords[1].y
1508        hypotenuse = float((lvx ** 2 + lvy ** 2) ** .5)
1509        vec_x = lvx / hypotenuse
1510        vec_y = lvy / hypotenuse
1511        v1x = -vec_y
1512        v1y = vec_x
1513
1514        rectangle = self.cord2Rect(cords[0])
1515
1516        px_loc = rectangle[0] + rectangle[2] / 2.0
1517        py_loc = rectangle[1] + rectangle[2] / 2.0
1518        ax_loc = v1x * rectangle[2] * aw / 2
1519        ay_loc = v1y * rectangle[2] * aw / 2
1520        context.move_to(px_loc + ax_loc, py_loc + ay_loc)
1521
1522        p1x = px_loc + (lvx - vec_x * ahh) * rectangle[2]
1523        p1y = py_loc + (lvy - vec_y * ahh) * rectangle[2]
1524        context.line_to(p1x + ax_loc, p1y + ay_loc)
1525
1526        lax = v1x * rectangle[2] * ahw / 2
1527        lay = v1y * rectangle[2] * ahw / 2
1528        context.line_to(p1x + lax, p1y + lay)
1529
1530        context.line_to(px_loc + lvx * rectangle[2], py_loc + lvy * rectangle[2])
1531        context.line_to(p1x - lax, p1y - lay)
1532        context.line_to(p1x - ax_loc, p1y - ay_loc)
1533        context.line_to(px_loc - ax_loc, py_loc - ay_loc)
1534        context.close_path()
1535
1536        context.set_source_rgba(*fillc)
1537        context.fill_preserve()
1538        context.set_line_join(cairo.LINE_JOIN_ROUND)
1539        context.set_line_width(asw * rectangle[2])
1540        context.set_source_rgba(*strkc)
1541        context.stroke()
1542
1543        context.restore()
1544
1545    def drawArrows(self, context):
1546        arw = 0.3  # Arrow width
1547        arhw = 0.72  # Arrow head width
1548        arhh = 0.64  # Arrow head height
1549        arsw = 0.08  # Arrow stroke width
1550
1551        if self.bluearrow:
1552            self.__drawArrow(context, self.bluearrow, arw, arhw, arhh, arsw,
1553                             (.447, .624, .812, 0.9), (.204, .396, .643, 1))
1554
1555        if self.greenarrow:
1556            self.__drawArrow(context, self.greenarrow, arw, arhw, arhh, arsw,
1557                             (.54, .886, .2, 0.9), (.306, .604, .024, 1))
1558
1559        if self.redarrow:
1560            self.__drawArrow(context, self.redarrow, arw, arhw, arhh, arsw,
1561                             (.937, .16, .16, 0.9), (.643, 0, 0, 1))
1562
1563    ###############################
1564    #        drawEnpassant        #
1565    ###############################
1566
1567    def drawEnpassant(self, context, redrawn):
1568        if not self.showEnpassant:
1569            return
1570        enpassant = self.model.boards[-1].enpassant
1571        if not enpassant:
1572            return
1573
1574        context.set_source_rgb(0, 0, 0)
1575        side = self.square[3]
1576        x_loc, y_loc = self.cord2Point(enpassant)
1577        if not intersects(rect((x_loc, y_loc, side, side)), redrawn):
1578            return
1579
1580        x_loc, y_loc = self.cord2Point(enpassant)
1581        crr = context
1582        crr.set_font_size(side / 2.)
1583        fdescent, fheight = crr.font_extents()[1], crr.font_extents()[2]
1584        chars = "en"
1585        xbearing, width = crr.text_extents(chars)[0], crr.text_extents(chars)[2]
1586        crr.move_to(x_loc + side / 2. - xbearing - width / 2.0 - 1,
1587                    side / 2. + y_loc - fdescent + fheight / 2.)
1588        crr.show_text(chars)
1589
1590    ###############################
1591    #          drawCross          #
1592    ###############################
1593
1594    def drawCross(self, context, redrawn):
1595        xc_loc, yc_loc, square, side = self.square
1596
1597        context.move_to(xc_loc, yc_loc)
1598        context.rel_line_to(square, square)
1599        context.move_to(xc_loc + square, yc_loc)
1600        context.rel_line_to(-square, square)
1601
1602        context.set_line_cap(cairo.LINE_CAP_SQUARE)
1603        context.set_source_rgba(0, 0, 0, 0.65)
1604        context.set_line_width(side)
1605        context.stroke_preserve()
1606
1607        context.set_source_rgba(1, 0, 0, 0.8)
1608        context.set_line_width(side / 2.)
1609        context.stroke()
1610
1611    ###############################
1612    #     drawSupportAlgorithm    #
1613    ###############################
1614
1615    # Here is how we implemented representation of support algorithm :
1616    # piece not protected -> yellow circle
1617    # piece attacked and not protected -> red circle
1618    # see SupportAlgorithm in file utils.DecisionSupportAlgorithm for more details on the algorithm
1619
1620    def drawSupportAlgorithm(self, context, redrawn):
1621        radius = self.square[3] / 2.0
1622        context.set_line_width(4)
1623
1624        algorithm = self.model.support_algorithm
1625        coord_attacked_not_protected = algorithm.coordinate_in_danger
1626
1627        for cord in coord_attacked_not_protected:
1628            rgba = self.color2rgba(cord.color)
1629            context.set_source_rgb(*rgba[:3])
1630            x_loc, y_loc = self.cord2Point(cord)
1631            context.new_sub_path()
1632            context.arc(x_loc + radius, y_loc + radius, radius - 3, 0, 2 * pi)
1633            context.stroke()
1634
1635    ############################################################################
1636    #                                Attributes                                #
1637    ############################################################################
1638
1639    ###############################
1640    #          Cord vars          #
1641    ###############################
1642
1643    def _setSelected(self, cord):
1644        self._active = None
1645        if self._selected == cord:
1646            return
1647        if self._selected:
1648            rectangle = rect(self.cord2RectRelative(self._selected))
1649            if cord:
1650                rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1651        elif cord:
1652            rectangle = rect(self.cord2RectRelative(cord))
1653        self._selected = cord
1654        self.redrawCanvas(rectangle)
1655
1656    def _getSelected(self):
1657        return self._selected
1658
1659    selected = property(_getSelected, _setSelected)
1660
1661    def _setHover(self, cord):
1662        if self._hover == cord:
1663            return
1664        if self._hover:
1665            rectangle = rect(self.cord2RectRelative(self._hover))
1666            # convert r from tuple to rect
1667            # tmpr = r
1668            # r = Gdk.Rectangle()
1669            # r.x, r.y, r.width, r.height = tmpr
1670            # if cord: r = r.union(rect(self.cord2RectRelative(cord)))
1671            if cord:
1672                rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1673        elif cord:
1674            rectangle = rect(self.cord2RectRelative(cord))
1675            # convert r from tuple to rect
1676            # tmpr = r
1677            # r = Gdk.Rectangle()
1678            # r.x, r.y, r.width, r.height = tmpr
1679        self._hover = cord
1680        self.redrawCanvas(rectangle)
1681
1682    def _getHover(self):
1683        return self._hover
1684
1685    hover = property(_getHover, _setHover)
1686
1687    def _setActive(self, cord):
1688        if self._active == cord:
1689            return
1690        if self._active:
1691            rectangle = rect(self.cord2RectRelative(self._active))
1692            if cord:
1693                rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1694        elif cord:
1695            rectangle = rect(self.cord2RectRelative(cord))
1696        self._active = cord
1697        self.redrawCanvas(rectangle)
1698
1699    def _getActive(self):
1700        return self._active
1701
1702    active = property(_getActive, _setActive)
1703
1704    def _setPremove0(self, cord):
1705        if self._premove0 == cord:
1706            return
1707        if self._premove0:
1708            rectangle = rect(self.cord2RectRelative(self._premove0))
1709            if cord:
1710                rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1711        elif cord:
1712            rectangle = rect(self.cord2RectRelative(cord))
1713        self._premove0 = cord
1714        self.redrawCanvas(rectangle)
1715
1716    def _getPremove0(self):
1717        return self._premove0
1718
1719    premove0 = property(_getPremove0, _setPremove0)
1720
1721    def _setPremove1(self, cord):
1722        if self._premove1 == cord:
1723            return
1724        if self._premove1:
1725            rectangle = rect(self.cord2RectRelative(self._premove1))
1726            if cord:
1727                rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1728        elif cord:
1729            rectangle = rect(self.cord2RectRelative(cord))
1730        self._premove1 = cord
1731        self.redrawCanvas(rectangle)
1732
1733    def _getPremove1(self):
1734        return self._premove1
1735
1736    premove1 = property(_getPremove1, _setPremove1)
1737
1738    ################################
1739    #          Arrow vars          #
1740    ################################
1741
1742    def _setRedarrow(self, cords):
1743        if cords == self._redarrow:
1744            return
1745        paint_cords = []
1746        if cords:
1747            paint_cords += cords
1748        if self._redarrow:
1749            paint_cords += self._redarrow
1750        rectangle = rect(self.cord2RectRelative(paint_cords[0]))
1751        for cord in paint_cords[1:]:
1752            rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1753        self._redarrow = cords
1754        self.redrawCanvas(rectangle)
1755
1756    def _getRedarrow(self):
1757        return self._redarrow
1758
1759    redarrow = property(_getRedarrow, _setRedarrow)
1760
1761    def _setGreenarrow(self, cords):
1762        if cords == self._greenarrow:
1763            return
1764        paint_cords = []
1765        if cords:
1766            paint_cords += cords
1767        if self._greenarrow:
1768            paint_cords += self._greenarrow
1769        rectangle = rect(self.cord2RectRelative(paint_cords[0]))
1770        for cord in paint_cords[1:]:
1771            rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1772        self._greenarrow = cords
1773        self.redrawCanvas(rectangle)
1774
1775    def _getGreenarrow(self):
1776        return self._greenarrow
1777
1778    greenarrow = property(_getGreenarrow, _setGreenarrow)
1779
1780    def _setBluearrow(self, cords):
1781        if cords == self._bluearrow:
1782            return
1783        paint_cords = []
1784        if cords:
1785            paint_cords += cords
1786        if self._bluearrow:
1787            paint_cords += self._bluearrow
1788        rectangle = rect(self.cord2RectRelative(paint_cords[0]))
1789        for cord in paint_cords[1:]:
1790            rectangle = union(rectangle, rect(self.cord2RectRelative(cord)))
1791        self._bluearrow = cords
1792        self.redrawCanvas(rectangle)
1793
1794    def _getBluearrow(self):
1795        return self._bluearrow
1796
1797    bluearrow = property(_getBluearrow, _setBluearrow)
1798
1799    ################################
1800    #          Other vars          #
1801    ################################
1802
1803    def _setRotation(self, radians):
1804        if not conf.get("fullAnimation"):
1805            self._rotation = radians
1806            self.next_rotation = radians
1807            self.matrix = cairo.Matrix.init_rotate(radians)
1808            self.redrawCanvas()
1809        else:
1810            if hasattr(self, "next_rotation") and \
1811                    self.next_rotation != self.rotation:
1812                return
1813            self.next_rotation = radians
1814            oldr = self.rotation
1815            start = time()
1816
1817            def rotate():
1818                amount = (time() - start) / ANIMATION_TIME
1819                if amount > 1:
1820                    amount = 1
1821                    next = False
1822                    self.animating = False
1823                else:
1824                    next = True
1825                self._rotation = new = oldr + amount * (radians - oldr)
1826                self.matrix = cairo.Matrix.init_rotate(new)
1827                self.redrawCanvas()
1828                return next
1829
1830            self.animating = True
1831            GLib.idle_add(rotate)
1832
1833    def _getRotation(self):
1834        return self._rotation
1835
1836    rotation = property(_getRotation, _setRotation)
1837
1838    def _setDrawGrid(self, draw_grid):
1839        self._draw_grid = draw_grid
1840        self.redrawCanvas()
1841
1842    def _getDrawGrid(self):
1843        return self._draw_grid
1844
1845    draw_grid = property(_getDrawGrid, _setDrawGrid)
1846
1847    def _setShowCords(self, show_cords):
1848        if not show_cords and self.no_frame:
1849            self.padding = 0.
1850        else:
1851            self.padding = self.pad
1852        self._show_cords = show_cords
1853        self.redrawCanvas()
1854
1855    def _getShowCords(self):
1856        return self._show_cords
1857
1858    show_cords = property(_getShowCords, _setShowCords)
1859
1860    def _setShowCaptured(self, show_captured, force_restore=False):
1861        self._show_captured = show_captured or self.model.variant.variant in DROP_VARIANTS
1862
1863        alloc = self.get_allocation()
1864        size = alloc.height / self.RANKS
1865
1866        persp = perspective_manager.get_perspective("games")
1867        if self._show_captured:
1868            needed_width = size * (self.FILES + self.FILES_FOR_HOLDING) + self.padding * 2
1869            if alloc.width < needed_width:
1870                persp.adjust_divider(needed_width - alloc.width)
1871        elif force_restore:
1872            needed_width = size * self.FILES + self.padding * 2
1873            if alloc.width > needed_width:
1874                persp.adjust_divider(needed_width - alloc.width)
1875
1876        self.redrawCanvas()
1877
1878    def _getShowCaptured(self):
1879        return False if self.preview else self._show_captured
1880
1881    showCaptured = property(_getShowCaptured, _setShowCaptured)
1882
1883    def _setShowEnpassant(self, show_enpassant):
1884        if self._show_enpassant == show_enpassant:
1885            return
1886        if self.model:
1887            enpascord = self.model.boards[-1].enpassant
1888            if enpascord:
1889                rectangle = rect(self.cord2RectRelative(enpascord))
1890                self.redrawCanvas(rectangle)
1891        self._show_enpassant = show_enpassant
1892
1893    def _getShowEnpassant(self):
1894        return self._show_enpassant
1895
1896    showEnpassant = property(_getShowEnpassant, _setShowEnpassant)
1897
1898    ###########################
1899    #          Other          #
1900    ###########################
1901
1902    def cord2Rect(self, cord, y_loc=None):
1903        if y_loc is None:
1904            x_loc, y_loc = cord.x, cord.y
1905        else:
1906            x_loc = cord
1907        xc_loc, yc_loc, side = self.square[0], self.square[1], self.square[3]
1908        return ((xc_loc + (x_loc * side)), (yc_loc + (self.RANKS - 1 - y_loc) * side), side)
1909
1910    def cord2Point(self, cord, y_loc=None):
1911        point = self.cord2Rect(cord, y_loc)
1912        return point[:2]
1913
1914    def cord2RectRelative(self, cord, y_loc=None):
1915        """ Like cord2Rect, but gives you bounding rect in case board is beeing
1916            Rotated """
1917        if isinstance(cord, tuple):
1918            cx_loc, cy_loc, square = cord
1919        else:
1920            cx_loc, cy_loc, square = self.cord2Rect(cord, y_loc)
1921        x_zero, y_zero = self.matrix.transform_point(cx_loc, cy_loc)
1922        x_one, y_one = self.matrix.transform_point(cx_loc + square, cy_loc)
1923        x_two, y_two = self.matrix.transform_point(cx_loc, cy_loc + square)
1924        x_three, y_three = self.matrix.transform_point(cx_loc + square, cy_loc + square)
1925        x_loc = min(x_zero, x_one, x_two, x_three)
1926        y_loc = min(y_zero, y_one, y_two, y_three)
1927        square = max(y_zero, y_one, y_two, y_three) - y_loc
1928        return (x_loc, y_loc, square)
1929
1930    def isLight(self, cord):
1931        """ Description: Given a board co-ordinate it returns True
1932            if the square at that co-ordinate is light
1933            Return : Boolean
1934        """
1935        if self.model.variant.variant in ASEAN_VARIANTS:
1936            return False
1937        x_loc, y_loc = cord.cords
1938        return (x_loc % 2 + y_loc % 2) == 1
1939
1940    def showFirst(self):
1941        if self.model.examined and self.model.noTD:
1942            self.model.goFirst()
1943        else:
1944            self.shown = self.model.lowply
1945            self.shown_variation_idx = 0
1946
1947    def showPrev(self, step=1):
1948        # If prev board belongs to a higher level variation
1949        # we have to update shown_variation_idx
1950        old_variation_idx = None
1951        if not self.shownIsMainLine():
1952            board = self.model.getBoardAtPly(self.shown - step, self.shown_variation_idx)
1953            for vari in self.model.variations:
1954                if board in vari:
1955                    break
1956            # swich to the new variation
1957            old_variation_idx = self.shown_variation_idx
1958            self.shown_variation_idx = self.model.variations.index(vari)
1959
1960        if self.model.examined and self.model.noTD:
1961            self.model.goPrev(step)
1962        else:
1963            if self.shown > self.model.lowply:
1964                if self.shown - step > self.model.lowply:
1965                    self._setShown(self.shown - step, old_variation_idx)
1966                else:
1967                    self._setShown(self.model.lowply, old_variation_idx)
1968
1969    def showNext(self, step=1):
1970        if self.model.examined and self.model.noTD:
1971            self.model.goNext(step)
1972        else:
1973            maxply = self.model.variations[self.shown_variation_idx][-1].ply
1974            if self.shown < maxply:
1975                if self.shown + step < maxply:
1976                    self.shown += step
1977                else:
1978                    self.shown = maxply
1979
1980    def showLast(self):
1981        if self.model.examined and self.model.noTD:
1982            self.model.goLast()
1983        else:
1984            maxply = self.model.variations[self.shown_variation_idx][-1].ply
1985            self.shown = maxply
1986
1987    def backToMainLine(self):
1988        if self.model.examined and self.model.noTD:
1989            self.model.backToMainLine()
1990        else:
1991            while not self.shownIsMainLine():
1992                self.showPrev()
1993
1994    def backToParentLine(self):
1995        if self.model.examined and self.model.noTD:
1996            self.model.backToMainLine()
1997        else:
1998            varline = self.shown_variation_idx
1999            while True:
2000                self.showPrev()
2001                if self.shownIsMainLine() or self.shown_variation_idx != varline:
2002                    break
2003
2004    def setPremove(self, premove_piece, premove0, premove1, premove_ply, promotion=None):
2005        self.premove_piece = premove_piece
2006        self.premove0 = premove0
2007        self.premove1 = premove1
2008        self.premove_ply = premove_ply
2009        self.premove_promotion = promotion
2010
2011    def copy_pgn(self):
2012        output = StringIO()
2013        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
2014        clipboard.set_text(pgn.save(output, self.model), -1)
2015
2016    def copy_fen(self):
2017        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
2018        fen = self.model.getBoardAtPly(self.shown, self.shown_variation_idx).asFen()
2019        clipboard.set_text(fen, -1)
2020