1# -*- coding: UTF-8 -*-
2from io import StringIO
3
4from gi.repository import Gtk, GObject
5
6from pychess.compat import create_task
7from pychess.Utils.const import DRAW, LOCAL, WHITE, BLACK, WAITING_TO_START, reprResult, \
8    UNDOABLE_STATES, FIRST_PAGE, PREV_PAGE, NEXT_PAGE
9from pychess.Players.Human import Human
10from pychess.Utils.GameModel import GameModel
11from pychess.perspectives import perspective_manager
12from pychess.Variants import variants
13from pychess.Database.model import game, event, site, pl1, pl2
14from pychess.widgets import newGameDialog
15from pychess.Savers import pgn
16
17
18cols = (game.c.id, pl1.c.name, game.c.white_elo, pl2.c.name, game.c.black_elo,
19        game.c.result, game.c.date, event.c.name, site.c.name, game.c.round,
20        game.c.ply_count, game.c.eco, game.c.time_control, game.c.variant, game.c.fen)
21
22
23class GameList(Gtk.TreeView):
24    def __init__(self, persp):
25        GObject.GObject.__init__(self)
26        self.persp = persp
27
28        self.records = []
29        self.preview_cid = None
30
31        # GTK_SELECTION_BROWSE - exactly one item is always selected
32        self.get_selection().set_mode(Gtk.SelectionMode.BROWSE)
33
34        self.liststore = Gtk.ListStore(int, str, str, str, str, str, str, str,
35                                       str, str, str, str, str, str, str)
36        self.modelsort = Gtk.TreeModelSort(self.liststore)
37
38        self.modelsort.set_sort_column_id(0, Gtk.SortType.ASCENDING)
39        self.modelsort.connect("sort-column-changed", self.sort_column_changed)
40        self.set_model(self.modelsort)
41        self.get_selection().set_mode(Gtk.SelectionMode.BROWSE)
42        self.set_headers_visible(True)
43        self.set_rules_hint(True)
44        self.set_search_column(1)
45
46        titles = (_("Id"), _("White"), _("W Elo"), _("Black"), _("B Elo"),
47                  _("Result"), _("Date"), _("Event"), _("Site"), _("Round"),
48                  _("Length"), "ECO", _("Time control"), _("Variant"), "FEN")
49
50        for i, title in enumerate(titles):
51            r = Gtk.CellRendererText()
52            column = Gtk.TreeViewColumn(title, r, text=i)
53            column.set_resizable(True)
54            column.set_reorderable(True)
55            column.set_sort_column_id(i)
56            self.append_column(column)
57
58        self.connect("row-activated", self.row_activated)
59
60        self.set_cursor(0)
61        self.columns_autosize()
62        self.gamemodel = GameModel()
63        self.ply = 0
64
65        # buttons
66        toolbar = Gtk.Toolbar()
67
68        firstButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_PREVIOUS)
69        firstButton.set_tooltip_text(_("First games"))
70        toolbar.insert(firstButton, -1)
71
72        prevButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_REWIND)
73        prevButton.set_tooltip_text(_("Previous games"))
74        toolbar.insert(prevButton, -1)
75
76        nextButton = Gtk.ToolButton(stock_id=Gtk.STOCK_MEDIA_FORWARD)
77        nextButton.set_tooltip_text(_("Next games"))
78        toolbar.insert(nextButton, -1)
79
80        firstButton.connect("clicked", self.on_first_clicked)
81        prevButton.connect("clicked", self.on_prev_clicked)
82        nextButton.connect("clicked", self.on_next_clicked)
83
84        limit_combo = Gtk.ComboBoxText()
85        for limit in ("100", "500", "1000", "5000"):
86            limit_combo.append_text(limit)
87        limit_combo.set_active(0)
88
89        toolitem = Gtk.ToolItem.new()
90        toolitem.add(limit_combo)
91        toolbar.insert(toolitem, -1)
92        limit_combo.connect("changed", self.on_limit_combo_changed)
93
94        sw = Gtk.ScrolledWindow()
95        sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
96        sw.add(self)
97
98        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
99
100        self.box.pack_start(sw, True, True, 0)
101        self.box.pack_start(toolbar, False, False, 0)
102        self.box.show_all()
103
104    def on_first_clicked(self, button):
105        self.load_games(direction=FIRST_PAGE)
106
107    def on_prev_clicked(self, button):
108        self.load_games(direction=PREV_PAGE)
109
110    def on_next_clicked(self, button):
111        self.load_games(direction=NEXT_PAGE)
112
113    def on_limit_combo_changed(self, combo):
114        text = combo.get_active_text()
115        if text is not None:
116            self.persp.chessfile.limit = int(text)
117            self.load_games(direction=FIRST_PAGE)
118
119    def sort_column_changed(self, treesortable):
120        sort_column_id, order = treesortable.get_sort_column_id()
121        if sort_column_id is None:
122            self.modelsort.set_sort_column_id(0, Gtk.SortType.ASCENDING)
123            sort_column_id, order = 0, Gtk.SortType.ASCENDING
124
125        self.set_search_column(sort_column_id)
126        is_desc = order == Gtk.SortType.DESCENDING
127        self.persp.chessfile.set_tag_order(cols[sort_column_id], is_desc)
128        self.load_games(direction=FIRST_PAGE)
129
130    def load_games(self, direction=FIRST_PAGE):
131        selection = self.get_selection()
132        if selection is not None and self.preview_cid is not None and \
133                selection.handler_is_connected(self.preview_cid):
134            with GObject.signal_handler_block(selection, self.preview_cid):
135                self.liststore.clear()
136        else:
137            self.liststore.clear()
138
139        add = self.liststore.append
140
141        self.records = []
142        records, plys = self.persp.chessfile.get_records(direction)
143        for i, rec in enumerate(records):
144            game_id = rec["Id"]
145            offs = rec["Offset"]
146            wname = rec["White"]
147            bname = rec["Black"]
148            welo = rec["WhiteElo"]
149            belo = rec["BlackElo"]
150            result = rec["Result"]
151            result = "½-½" if result == DRAW else reprResult[result] if result else "*"
152            event = "" if rec["Event"] is None else rec["Event"].replace("?", "")
153            site = "" if rec["Site"] is None else rec["Site"].replace("?", "")
154            round_ = "" if rec["Round"] is None else rec["Round"].replace("?", "")
155            date = "" if rec["Date"] is None else rec["Date"].replace(".??", "").replace("????.", "")
156
157            try:
158                ply = rec["PlyCount"]
159                length = str(int(ply) // 2) if ply else ""
160            except ValueError:
161                length = ""
162            eco = rec["ECO"]
163            tc = rec["TimeControl"]
164            variant = rec["Variant"]
165            variant = variants[variant].cecp_name.capitalize() if variant else ""
166            fen = rec["FEN"]
167
168            add([game_id, wname, welo, bname, belo, result, date, event, site,
169                 round_, length, eco, tc, variant, fen])
170
171            ply = plys.get(offs) if offs in plys else 0
172            self.records.append((rec, ply))
173
174        self.set_cursor(0)
175
176    def get_record(self, path):
177        if path is None:
178            return None, None
179        else:
180            return self.records[self.modelsort.convert_path_to_child_path(path)[0]]
181
182    def row_activated(self, widget, path, col):
183        rec, ply = self.get_record(path)
184        if rec is None:
185            return
186
187        # Enable unfinished games to continue from newgamedialog
188        if rec["Result"] not in UNDOABLE_STATES:
189            newGameDialog.EnterNotationExtension.run()
190            model = self.persp.chessfile.loadToModel(rec)
191            text = pgn.save(StringIO(), model)
192            newGameDialog.EnterNotationExtension.sourcebuffer.set_text(text)
193            return
194
195        self.gamemodel = GameModel()
196
197        variant = rec["Variant"]
198        if variant:
199            self.gamemodel.tags["Variant"] = variant
200
201        # Lichess exports study .pgn without White and Black tags
202        wp = "" if rec["White"] is None else rec["White"]
203        bp = "" if rec["Black"] is None else rec["Black"]
204        p0 = (LOCAL, Human, (WHITE, wp), wp)
205        p1 = (LOCAL, Human, (BLACK, bp), bp)
206        self.persp.chessfile.loadToModel(rec, -1, self.gamemodel)
207
208        self.gamemodel.endstatus = self.gamemodel.status if self.gamemodel.status in UNDOABLE_STATES else None
209        self.gamemodel.status = WAITING_TO_START
210
211        perspective_manager.activate_perspective("games")
212        perspective = perspective_manager.get_perspective("games")
213        create_task(perspective.generalStart(self.gamemodel, p0, p1))
214