1#!/usr/bin/env python
2# -*- mode: python; coding: utf-8; -*-
3# ---------------------------------------------------------------------------##
4#
5#  Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer
6#  Copyright (C) 2003 Mt. Hood Playing Card Co.
7#  Copyright (C) 2005-2009 Skomoroh
8#
9#  This program is free software: you can redistribute it and/or modify
10#  it under the terms of the GNU General Public License as published by
11#  the Free Software Foundation, either version 3 of the License, or
12#  (at your option) any later version.
13#
14#  This program is distributed in the hope that it will be useful,
15#  but WITHOUT ANY WARRANTY; without even the implied warranty of
16#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17#  GNU General Public License for more details.
18#
19#  You should have received a copy of the GNU General Public License
20#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21#
22# ---------------------------------------------------------------------------##
23
24import locale
25import os
26
27from pysollib.gamedb import GI
28from pysollib.help import help_about, help_html
29from pysollib.mfxutil import Struct, openURL
30from pysollib.mfxutil import USE_PIL, print_err
31from pysollib.mygettext import _
32from pysollib.pysolrandom import construct_random
33from pysollib.pysoltk import AllGames_StatsDialog, SingleGame_StatsDialog
34from pysollib.pysoltk import ColorsDialog
35from pysollib.pysoltk import EditTextDialog
36from pysollib.pysoltk import FontsDialog
37from pysollib.pysoltk import FullLog_StatsDialog, SessionLog_StatsDialog
38from pysollib.pysoltk import GameInfoDialog
39from pysollib.pysoltk import MfxExceptionDialog
40from pysollib.pysoltk import MfxMessageDialog, MfxSimpleEntry
41from pysollib.pysoltk import PlayerOptionsDialog
42from pysollib.pysoltk import ProgressionDialog
43from pysollib.pysoltk import PysolMenubarTk, PysolToolbarTk
44from pysollib.pysoltk import Status_StatsDialog, Top_StatsDialog
45from pysollib.pysoltk import TimeoutsDialog
46from pysollib.pysoltk import create_find_card_dialog
47from pysollib.pysoltk import create_solver_dialog
48from pysollib.settings import DEBUG
49from pysollib.settings import PACKAGE_URL, TITLE
50from pysollib.settings import TOP_SIZE
51from pysollib.stats import FileStatsFormatter
52
53
54# ************************************************************************
55# * menubar
56# ************************************************************************
57
58class PysolMenubar(PysolMenubarTk):
59    def __init__(self, app, top, progress=None):
60        self.app = app
61        self.top = top
62        self.game = None
63        # enabled/disabled - this is set by updateMenuState()
64        self.menustate = Struct(
65            save=0,
66            save_as=0,
67            hold_and_quit=0,
68            undo=0,
69            redo=0,
70            restart=0,
71            deal=0,
72            hint=0,
73            autofaceup=0,
74            autodrop=0,
75            shuffle=0,
76            autodeal=0,
77            quickplay=0,
78            demo=0,
79            highlight_piles=0,
80            autoscale=0,
81            find_card=0,
82            rules=0,
83            pause=0,
84            custom_game=0,
85        )
86        PysolMenubarTk.__init__(self, app, top, progress)
87
88    #
89    # delegation to Game
90    #
91
92    def _finishDrag(self):
93        return self.game is None or self.game._finishDrag()
94
95    def _cancelDrag(self, break_pause=True):
96        if self.game is None:
97            return True
98        ret = self.game._cancelDrag(break_pause=break_pause)
99        self._setPauseMenu(self.game.pause)
100        return ret
101
102    def changed(self, *args, **kw):
103        assert self.game is not None
104        return self.game.changed(*args, **kw)
105
106    #
107    # menu updates
108    #
109
110    def _clearMenuState(self):
111        ms = self.menustate
112        for k, v in ms.__dict__.items():
113            if isinstance(v, list):
114                ms.__dict__[k] = [0] * len(v)
115            else:
116                ms.__dict__[k] = 0
117
118    # update self.menustate for menu items and toolbar
119    def _updateMenuState(self):
120        self._clearMenuState()
121        game = self.game
122        assert game is not None
123        opt = self.app.opt
124        ms = self.menustate
125        # 0 = DISABLED, 1 = ENABLED
126        ms.save_as = game.canSaveGame()
127        ms.hold_and_quit = ms.save_as
128        if game.filename and ms.save_as:
129            ms.save = 1
130        if opt.undo:
131            if game.canUndo() and game.moves.index > 0:
132                ms.undo = 1
133            if game.canRedo() and game.moves.index < len(game.moves.history):
134                ms.redo = 1
135        if game.moves.index > 0:
136            ms.restart = 1
137        if game.canDealCards():
138            ms.deal = 1
139        if game.getHintClass() is not None:
140            if opt.hint:
141                ms.hint = 1
142            # if not game.demo:       # if not already running
143            ms.demo = 1
144        autostacks = game.getAutoStacks()
145        if autostacks[0]:
146            ms.autofaceup = 1
147        if autostacks[1] and game.s.foundations:
148            ms.autodrop = 1
149        if game.s.waste:
150            ms.autodeal = 1
151        if autostacks[2]:
152            ms.quickplay = 1
153        if opt.highlight_piles and game.getHighlightPilesStacks():
154            ms.highlight_piles = 1
155        if opt.auto_scale:
156            ms.autoscale = 1
157        if game.canFindCard():
158            ms.find_card = 1
159        if game.app.getGameRulesFilename(game.id):  # note: this may return ""
160            ms.rules = 1
161        if not game.finished:
162            ms.pause = 1
163        if game.gameinfo.si.game_type == GI.GT_CUSTOM:
164            ms.custom_game = 1
165        if game.canShuffle():
166            if opt.shuffle:
167                ms.shuffle = 1
168
169    # update menu items and toolbar
170    def _updateMenus(self):
171        if self.game is None:
172            return
173        ms = self.menustate
174        # File menu
175        self.setMenuState(ms.save, "file.save")
176        self.setMenuState(ms.save_as, "file.saveas")
177        self.setMenuState(ms.hold_and_quit, "file.holdandquit")
178        # Edit menu
179        self.setMenuState(ms.undo, "edit.undo")
180        self.setMenuState(ms.redo, "edit.redo")
181        self.setMenuState(ms.redo, "edit.redoall")
182        self.updateBookmarkMenuState()
183        self.setMenuState(ms.restart, "edit.restart")
184        self.setMenuState(ms.custom_game, "edit.editcurrentgame")
185        self.setMenuState(ms.custom_game, "edit.deletecurrentgame")
186        # Game menu
187        self.setMenuState(ms.deal, "game.dealcards")
188        self.setMenuState(ms.autodrop, "game.autodrop")
189        self.setMenuState(ms.shuffle, "game.shuffletiles")
190        self.setMenuState(ms.pause, "game.pause")
191        # Assist menu
192        self.setMenuState(ms.hint, "assist.hint")
193        self.setMenuState(ms.highlight_piles, "assist.highlightpiles")
194        self.setMenuState(ms.find_card, "assist.findcard")
195        self.setMenuState(ms.demo, "assist.demo")
196        self.setMenuState(ms.demo, "assist.demoallgames")
197        # Options menu
198        self.setMenuState(ms.autofaceup, "options.automaticplay.autofaceup")
199        self.setMenuState(ms.autodrop, "options.automaticplay.autodrop")
200        self.setMenuState(ms.autodeal, "options.automaticplay.autodeal")
201        self.setMenuState(ms.quickplay, "options.automaticplay.quickplay")
202        if USE_PIL:
203            self.setMenuState(ms.autoscale,
204                              "options.cardsize.preserveaspectratio")
205            self.setMenuState(not ms.autoscale,
206                              "options.cardsize.increasethecardsize")
207            self.setMenuState(not ms.autoscale,
208                              "options.cardsize.decreasethecardsize")
209            self.setMenuState(not ms.autoscale,
210                              "options.cardsize.resetthecardsize")
211        # Help menu
212        self.setMenuState(ms.rules, "help.rulesforthisgame")
213        # Toolbar
214        self.setToolbarState(ms.restart, "restart")
215        self.setToolbarState(ms.save_as, "save")
216        self.setToolbarState(ms.undo, "undo")
217        self.setToolbarState(ms.redo, "redo")
218        self.setToolbarState(ms.autodrop, "autodrop")
219        self.setToolbarState(ms.shuffle, "shuffle")
220        self.setToolbarState(ms.pause, "pause")
221        self.setToolbarState(ms.rules, "rules")
222
223    # update menu items and toolbar
224    def updateMenus(self):
225        if self.game is None:
226            return
227        self._updateMenuState()
228        self._updateMenus()
229
230    # disable menu items and toolbar
231    def disableMenus(self):
232        if self.game is None:
233            return
234        self._clearMenuState()
235        self._updateMenus()
236
237    #
238    # File menu
239    #
240
241    def mNewGame(self, *args):
242        if self._cancelDrag():
243            return
244        if self.changed():
245            if not self.game.areYouSure(_("New game")):
246                return
247        if self.game.nextGameFlags(self.game.id) == 0:
248            self.game.endGame()
249            self.game.newGame()
250        else:
251            self.game.endGame()
252            self.game.quitGame(self.game.id)
253
254    def _mSelectGame(self, id, random=None, force=False):
255        if self._cancelDrag():
256            return
257        if not force and self.game.id == id:
258            return
259        if self.changed():
260            if not self.game.areYouSure(_("Select game")):
261                return
262        self.game.endGame()
263        self.game.quitGame(id, random=random)
264
265    def _mNewGameBySeed(self, seed, origin):
266        try:
267            random = construct_random(seed)
268            if random is None:
269                return
270            id = self.game.id
271            if not self.app.getGameInfo(id):
272                raise ValueError
273        except (ValueError, TypeError):
274            MfxMessageDialog(self.top, title=_("Invalid game number"),
275                             text=_("Invalid game number\n") + str(seed),
276                             bitmap="error")
277            return
278        f = self.game.nextGameFlags(id, random)
279        if f & 17 == 0:
280            return
281        random.origin = origin
282        if f & 15 == 0:
283            self.game.endGame()
284            self.game.newGame(random=random)
285        else:
286            self.game.endGame()
287            self.game.quitGame(id, random=random)
288
289    def mNewGameWithNextId(self, *args):
290        if self._cancelDrag():
291            return
292        if self.changed():
293            if not self.game.areYouSure(_("Select next game number")):
294                return
295        r = self.game.random
296        seed = r.increaseSeed(r.initial_seed)
297        seed = r.str(seed)
298        self._mNewGameBySeed(seed, self.game.random.ORIGIN_NEXT_GAME)
299
300    def mSelectGameById(self, *args):
301        if self._cancelDrag(break_pause=False):
302            return
303        f = self.game.getGameNumber(format=0)
304        d = MfxSimpleEntry(self.top, _("Select new game number"),
305                           _("\n\nEnter new game number"), f,
306                           strings=(_("&OK"), _("&Next number"), _("&Cancel")),
307                           default=0, e_width=25)
308        if d.status != 0:
309            return
310        if d.button == 2:
311            return
312        if d.button == 1:
313            self.mNewGameWithNextId()
314            return
315        if self.changed():
316            if not self.game.areYouSure(_("Select new game number")):
317                return
318        self._mNewGameBySeed(d.value, self.game.random.ORIGIN_SELECTED)
319
320    def mSelectRandomGame(self, type='all'):
321        if self._cancelDrag():
322            return
323        if self.changed():
324            if not self.game.areYouSure(_("Select random game")):
325                return
326        game_id = None
327        games = []
328        for g in self.app.gdb.getGamesIdSortedById():
329            gi = self.app.getGameInfo(g)
330            if 1 and gi.id == self.game.id:
331                # force change of game
332                continue
333            if 1 and gi.category != self.game.gameinfo.category:
334                # don't change game category
335                continue
336            won, lost = self.app.stats.getStats(self.app.opt.player, gi.id)
337            if type == 'all':
338                games.append(gi.id)
339            elif type == 'won' and won > 0:
340                games.append(gi.id)
341            elif type == 'not won' and won == 0 and lost > 0:
342                games.append(gi.id)
343            elif type == 'not played' and won+lost == 0:
344                games.append(gi.id)
345        if games:
346            game_id = self.app.chooseRandomOutOfGames(games)
347        if game_id and game_id != self.game.id:
348            self.game.endGame()
349            self.game.quitGame(game_id)
350
351    def _mSelectNextGameFromList(self, gl, step):
352        if self._cancelDrag():
353            return
354        id = self.game.id
355        gl = list(gl)
356        if len(gl) < 2 or (id not in gl):
357            return
358        if self.changed():
359            if not self.game.areYouSure(_("Select next game")):
360                return
361        index = (gl.index(id) + step) % len(gl)
362        self.game.endGame()
363        self.game.quitGame(gl[index])
364
365    def mSelectNextGameById(self, *args):
366        self._mSelectNextGameFromList(self.app.gdb.getGamesIdSortedById(), 1)
367
368    def mSelectPrevGameById(self, *args):
369        self._mSelectNextGameFromList(self.app.gdb.getGamesIdSortedById(), -1)
370
371    def mSelectNextGameByName(self, *args):
372        self._mSelectNextGameFromList(self.app.gdb.getGamesIdSortedByName(), 1)
373
374    def mSelectPrevGameByName(self, *args):
375        self._mSelectNextGameFromList(
376            self.app.gdb.getGamesIdSortedByName(), -1)
377
378    def mSave(self, *args):
379        if self._cancelDrag(break_pause=False):
380            return
381        if self.menustate.save_as:
382            if self.game.filename:
383                self.game.saveGame(self.game.filename)
384            else:
385                self.mSaveAs()
386
387    def mHoldAndQuit(self, *args):
388        if self._cancelDrag():
389            return
390        self.game.endGame(holdgame=1)
391        self.game.quitGame(holdgame=1)
392
393    def mQuit(self, *args):
394        if self._cancelDrag():
395            return
396        if self.changed():
397            if not self.game.areYouSure(_("Quit %s") % TITLE):
398                return
399        self.game.endGame()
400        self.game.quitGame()
401
402    #
403    # Edit menu
404    #
405
406    def mUndo(self, *args):
407        if self._cancelDrag():
408            return
409        if self.menustate.undo:
410            self.game.playSample("undo")
411            self.game.undo()
412
413    def mRedo(self, *args):
414        if self._cancelDrag():
415            return
416        if self.menustate.redo:
417            self.game.playSample("redo")
418            self.game.redo()
419            self.game.checkForWin()
420
421    def mRedoAll(self, *args):
422        if self._cancelDrag():
423            return
424        if self.menustate.redo:
425            self.app.top.busyUpdate()
426            self.game.playSample("redo", loop=1)
427            while self.game.moves.index < len(self.game.moves.history):
428                self.game.redo()
429                if self.game.checkForWin():
430                    break
431            self.game.stopSamples()
432
433    def mSetBookmark(self, n, confirm=1):
434        if self._cancelDrag():
435            return
436        if not self.app.opt.bookmarks:
437            return
438        if not (0 <= n <= 8):
439            return
440        self.game.setBookmark(n, confirm=confirm)
441        self.game.updateMenus()
442
443    def mGotoBookmark(self, n, confirm=-1):
444        if self._cancelDrag():
445            return
446        if not self.app.opt.bookmarks:
447            return
448        if not (0 <= n <= 8):
449            return
450        self.game.gotoBookmark(n, confirm=confirm)
451        self.game.updateMenus()
452
453    def mClearBookmarks(self, *args):
454        if self._cancelDrag():
455            return
456        if not self.app.opt.bookmarks:
457            return
458        if not self.game.gsaveinfo.bookmarks:
459            return
460        if not self.game.areYouSure(_("Clear bookmarks"),
461                                    _("Clear all bookmarks?")):
462            return
463        self.game.gsaveinfo.bookmarks = {}
464        self.game.updateMenus()
465
466    def mRestart(self, *args):
467        if self._cancelDrag():
468            return
469        if self.game.moves.index == 0:
470            return
471        if self.changed(restart=1):
472            if not self.game.areYouSure(_("Restart game"),
473                                        _("Restart this game?")):
474                return
475        self.game.restartGame()
476
477    #
478    # Game menu
479    #
480
481    def mDeal(self, *args):
482        if self._cancelDrag():
483            return
484        self.game.dealCards()
485
486    def mDrop(self, *args):
487        if self._cancelDrag():
488            return
489        # self.game.autoPlay(autofaceup=-1, autodrop=1)
490        self.game.autoDrop(autofaceup=-1)
491
492    def mDrop1(self, *args):
493        if self._cancelDrag():
494            return
495        # self.game.autoPlay(autofaceup=1, autodrop=1)
496        self.game.autoDrop(autofaceup=1)
497
498    def mShuffle(self, *args):
499        if self._cancelDrag():
500            return
501        if self.menustate.shuffle:
502            if self.game.canShuffle():
503                self.game._mahjonggShuffle()
504
505    def mFindCard(self, *args):
506        if self.game.canFindCard():
507            create_find_card_dialog(self.game.top, self.game,
508                                    self.app.getFindCardImagesDir())
509
510    def mSolver(self, *args):
511        create_solver_dialog(self.game.top, self.app)
512
513    def mEditGameComment(self, *args):
514        if self._cancelDrag(break_pause=False):
515            return
516        game, gi = self.game, self.game.gameinfo
517        kw = {'game': gi.name,
518              'id': game.getGameNumber(format=1)}
519        cc = _("Comments for %(game)s %(id)s:\n\n") % kw
520        c = game.gsaveinfo.comment or cc
521        d = EditTextDialog(game.top, _("Comments for %(id)s") % kw, text=c)
522        if d.status == 0 and d.button == 0:
523            text = d.text
524            if text.strip() == cc.strip():
525                game.gsaveinfo.comment = ""
526            else:
527                game.gsaveinfo.comment = d.text
528                # save to file
529                fn = os.path.join(self.app.dn.config, "comments.txt")
530                fn = os.path.normpath(fn)
531                if not text.endswith(os.linesep):
532                    text += os.linesep
533                enc = locale.getpreferredencoding()
534                try:
535                    with open(fn, 'at') as fh:
536                        fh.write(text.encode(enc, 'replace'))
537                except Exception as err:
538                    d = MfxExceptionDialog(
539                        self.top, err,
540                        text=_("Error while writing to file"))
541                else:
542                    d = MfxMessageDialog(
543                        self.top, title=_("%s Info") % TITLE, bitmap="info",
544                        text=_("Comments were appended to\n\n%(filename)s")
545                        % {'filename': fn})
546        self._setCommentMenu(bool(game.gsaveinfo.comment))
547
548    #
549    # Game menu - statistics
550    #
551
552    def _mStatsSave(self, player, filename, write_method):
553        if player is None:
554            text = _("Demo statistics were appended to\n\n%(filename)s")
555            filename = filename + "_demo"
556        else:
557            text = _("Your statistics were appended to\n\n%(filename)s")
558        filename = os.path.join(self.app.dn.config, filename + ".txt")
559        filename = os.path.normpath(filename)
560        try:
561            a = FileStatsFormatter(self.app, open(filename, "a"))
562            write_method(a, player)
563        except EnvironmentError as ex:
564            MfxExceptionDialog(self.top, ex,
565                               text=_("Error while writing to file"))
566        else:
567            MfxMessageDialog(
568                self.top, title=_("%s Info") % TITLE, bitmap="info",
569                text=text % {'filename': filename})
570
571    def mPlayerStats(self, *args, **kw):
572        wasPaused = False
573        if not self.game.pause:
574            self.game.doPause()
575            wasPaused = True
576        mode = kw.get("mode", 101)
577        demo = 0
578        gameid = None
579        while mode > 0:
580            if mode > 1000:
581                demo = not demo
582                mode = mode % 1000
583            #
584            d = Struct(status=-1, button=-1)
585            if demo:
586                player = None
587            else:
588                player = self.app.opt.player
589            n = self.game.gameinfo.name
590            # translation keywords
591            transkw = {'app': TITLE,
592                       'player': player,
593                       'game': n,
594                       'tops': TOP_SIZE}
595            #
596            if mode == 100:
597                d = Status_StatsDialog(self.top, game=self.game)
598            elif mode == 101:
599                header = (_("%(app)s Demo Statistics for %(game)s") if demo
600                          else _("Statistics for %(game)s")) % transkw
601                d = SingleGame_StatsDialog(
602                   self.top, header, self.app, player, gameid=self.game.id)
603                gameid = d.selected_game
604            elif mode == 102:
605                header = (_("%(app)s Demo Statistics") if demo
606                          else _("Statistics for %(player)s")) % transkw
607                d = AllGames_StatsDialog(self.top, header, self.app, player)
608                gameid = d.selected_game
609            elif mode == 103:
610                header = (_("%(app)s Demo Full log") if demo
611                          else _("Full log for %(player)s")) % transkw
612                d = FullLog_StatsDialog(self.top, header, self.app, player)
613            elif mode == 104:
614                header = (_("%(app)s Demo Session log") if demo
615                          else _("Session log for %(player)s")) % transkw
616                d = SessionLog_StatsDialog(self.top, header, self.app, player)
617            elif mode == 105:
618                # TRANSLATORS: eg. top 10 or top 5 results for a certain game
619                header = (_("%(app)s Demo Top %(tops)d for %(game)s") if demo
620                          else _("Top %(tops)d for %(game)s")) % transkw
621                d = Top_StatsDialog(
622                    self.top, header, self.app, player, gameid=self.game.id)
623            elif mode == 106:
624                header = _("Game Info")
625                d = GameInfoDialog(self.top, header, self.app)
626            elif mode == 107:
627                header = _("Statistics progression")
628                d = ProgressionDialog(
629                    self.top, header, self.app, player, gameid=self.game.id)
630            elif mode == 202:
631                # print stats to file
632                write_method = FileStatsFormatter.writeStats
633                self._mStatsSave(player, "stats", write_method)
634            elif mode == 203:
635                # print full log to file
636                write_method = FileStatsFormatter.writeFullLog
637                self._mStatsSave(player, "log", write_method)
638            elif mode == 204:
639                # print session log to file
640                write_method = FileStatsFormatter.writeSessionLog
641                self._mStatsSave(player, "log", write_method)
642            elif mode == 301:
643                # reset all player stats
644                if self.game.areYouSure(
645                    _("Reset all statistics"),
646                    _("Reset ALL statistics and logs for player\n" +
647                      "%(player)s?") % transkw,
648                    confirm=1, default=1
649                ):
650                    self.app.stats.resetStats(player, 0)
651                    self.game.updateStatus(stats=self.app.stats.getStats(
652                        self.app.opt.player, self.game.id))
653            elif mode == 302:
654                # reset player stats for current game
655                if self.game.areYouSure(
656                    _("Reset game statistics"),
657                    _('Reset statistics and logs for player\n%(player)s\n'
658                      'and game\n%(game)s?') % transkw,
659                    confirm=1, default=1
660                ):
661                    self.app.stats.resetStats(player, self.game.id)
662                    self.game.updateStatus(stats=self.app.stats.getStats(
663                        self.app.opt.player, self.game.id))
664            elif mode == 401:
665                # start a new game with a gameid
666                if gameid and gameid != self.game.id:
667                    self.game.endGame()
668                    self.game.quitGame(gameid)
669            elif mode == 402:
670                # start a new game with a gameid / gamenumber
671                # TODO
672                pass
673            else:
674                print_err("stats problem: %s %s %s" % (mode, demo, player))
675                pass
676            if d.status != 0:
677                break
678            mode = d.button
679        if self.game.pause:
680            if wasPaused:
681                self.game.doPause()
682
683    #
684    # Assist menu
685    #
686
687    def mHint(self, *args):
688        if self._cancelDrag():
689            return
690        if self.app.opt.hint:
691            if self.game.showHint(0, self.app.opt.timeouts['hint']):
692                self.game.stats.hints += 1
693
694    def mHint1(self, *args):
695        if self._cancelDrag():
696            return
697        if self.app.opt.hint:
698            if self.game.showHint(1, self.app.opt.timeouts['hint']):
699                self.game.stats.hints += 1
700
701    def mHighlightPiles(self, *args):
702        if self._cancelDrag():
703            return
704        if self.app.opt.highlight_piles:
705            if self.game.highlightPiles(
706                self.app.opt.timeouts['highlight_piles']
707            ):
708                self.game.stats.highlight_piles += 1
709
710    def mDemo(self, *args):
711        if self._cancelDrag():
712            return
713        if self.game.getHintClass() is not None:
714            self._mDemo(mixed=0)
715
716    def mMixedDemo(self, *args):
717        if self._cancelDrag():
718            return
719        self._mDemo(mixed=1)
720
721    def _mDemo(self, mixed):
722        if self.changed():
723            # only ask if there have been no demo moves or hints yet
724            if self.game.stats.demo_moves == 0 and self.game.stats.hints == 0:
725                if not self.game.areYouSure(_("Play demo")):
726                    return
727        # self.app.demo_counter = 0
728        self.game.startDemo(mixed=mixed)
729
730    #
731    # Options menu
732    #
733
734    def mOptPlayerOptions(self, *args):
735        if self._cancelDrag(break_pause=False):
736            return
737        d = PlayerOptionsDialog(self.top, _("Set player options"), self.app)
738        if d.status == 0 and d.button == 0:
739            self.app.opt.confirm = bool(d.confirm)
740            self.app.opt.update_player_stats = bool(d.update_stats)
741            self.app.opt.win_animation = bool(d.win_animation)
742            # n = string.strip(d.player)
743            n = d.player[:30].strip()
744            if 0 < len(n) <= 30:
745                self.app.opt.player = n
746                self.game.updateStatus(player=self.app.opt.player)
747                self.game.updateStatus(stats=self.app.stats.getStats(
748                    self.app.opt.player, self.game.id))
749
750    def mOptColors(self, *args):
751        if self._cancelDrag(break_pause=False):
752            return
753        d = ColorsDialog(self.top, _("Set colors"), self.app)
754        text_color = self.app.opt.colors['text']
755        if d.status == 0 and d.button == 0:
756            self.app.opt.colors['text'] = d.text_color
757            self.app.opt.colors['piles'] = d.piles_color
758            self.app.opt.colors['cards_1'] = d.cards_1_color
759            self.app.opt.colors['cards_2'] = d.cards_2_color
760            self.app.opt.colors['samerank_1'] = d.samerank_1_color
761            self.app.opt.colors['samerank_2'] = d.samerank_2_color
762            self.app.opt.colors['hintarrow'] = d.hintarrow_color
763            self.app.opt.colors['not_matching'] = d.not_matching_color
764            #
765            if text_color != self.app.opt.colors['text']:
766                self.app.setTile(self.app.tabletile_index, force=True)
767
768    def mOptFonts(self, *args):
769        if self._cancelDrag(break_pause=False):
770            return
771        d = FontsDialog(self.top, _("Set fonts"), self.app)
772        if d.status == 0 and d.button == 0:
773            self.app.opt.fonts.update(d.fonts)
774            self._cancelDrag()
775            self.game.endGame(bookmark=1)
776            self.game.quitGame(bookmark=1)
777
778    def mOptTimeouts(self, *args):
779        if self._cancelDrag(break_pause=False):
780            return
781        d = TimeoutsDialog(self.top, _("Set timeouts"), self.app)
782        if d.status == 0 and d.button == 0:
783            self.app.opt.timeouts['demo'] = d.demo_timeout
784            self.app.opt.timeouts['hint'] = d.hint_timeout
785            self.app.opt.timeouts['raise_card'] = d.raise_card_timeout
786            self.app.opt.timeouts['highlight_piles'] = \
787                d.highlight_piles_timeout
788            self.app.opt.timeouts['highlight_cards'] = \
789                d.highlight_cards_timeout
790            self.app.opt.timeouts['highlight_samerank'] = \
791                d.highlight_samerank_timeout
792
793    #
794    # Help menu
795    #
796
797    def mHelpHtml(self, *args):
798        print('mHelpHtml: %s' % str(args))
799        if self._cancelDrag(break_pause=False):
800            return
801        help_html(self.app, args[0], "html")
802
803    def mHelp(self, *args):
804        if self._cancelDrag(break_pause=False):
805            return
806        help_html(self.app, "index.html", "html")
807
808    def mHelpHowToPlay(self, *args):
809        if self._cancelDrag(break_pause=False):
810            return
811        help_html(self.app, "howtoplay.html", "html")
812
813    def mHelpRules(self, *args):
814        if self._cancelDrag(break_pause=False):
815            return
816        if not self.menustate.rules:
817            return
818        dir = os.path.join("html", "rules")
819        # FIXME: plugins
820        help_html(self.app, self.app.getGameRulesFilename(self.game.id), dir)
821
822    def mHelpLicense(self, *args):
823        if self._cancelDrag(break_pause=False):
824            return
825        help_html(self.app, "license.html", "html")
826
827    def mHelpNews(self, *args):
828        if self._cancelDrag(break_pause=False):
829            return
830        help_html(self.app, "news.html", "html")
831
832    def mHelpWebSite(self, *args):
833        openURL(PACKAGE_URL)
834
835    def mHelpAbout(self, *args):
836        if self._cancelDrag(break_pause=False):
837            return
838        help_about(self.app)
839
840    #
841    # misc
842    #
843
844    def mScreenshot(self, *args):
845        if self._cancelDrag():
846            return
847        f = os.path.join(self.app.dn.config, "screenshots")
848        if not os.path.isdir(f):
849            return
850        f = os.path.join(f, self.app.getGameSaveName(self.game.id))
851        i = 1
852        while 1:
853            fn = "%s-%d.ppm" % (f, i)
854            if not os.path.exists(fn):
855                break
856            i = i + 1
857            if i >= 10000:      # give up
858                return
859        self.top.screenshot(fn)
860
861    def mPlayNextMusic(self, *args):
862        if self._cancelDrag(break_pause=False):
863            return
864        if (self.app.audio and self.app.music and
865                self.app.opt.sound_music_volume > 0):
866            self.app.audio.playNextMusic()
867            if 1 and DEBUG:
868                index = self.app.audio.getMusicInfo()
869                music = self.app.music_manager.get(index)
870                if music:
871                    print("playing music:", music.filename)
872
873    def mIconify(self, *args):
874        if self._cancelDrag(break_pause=False):
875            return
876        self.top.wm_iconify()
877
878
879# ************************************************************************
880# * toolbar
881# ************************************************************************
882
883class PysolToolbar(PysolToolbarTk):
884    def __init__(self, *args, **kwargs):
885        self.game = None
886        PysolToolbarTk.__init__(self, *args, **kwargs)
887
888    #
889    # public methods
890    #
891
892    def connectGame(self, game):
893        self.game = game
894
895    #
896    # button event handlers - delegate to menubar
897    #
898
899    def mNewGame(self, *args):
900        if not self._busy():
901            self.menubar.mNewGame()
902        return 1
903
904    def mOpen(self, *args):
905        if not self._busy():
906            self.menubar.mOpen()
907        return 1
908
909    def mRestart(self, *args):
910        if not self._busy():
911            self.menubar.mRestart()
912        return 1
913
914    def mSave(self, *args):
915        if not self._busy():
916            self.menubar.mSaveAs()
917        return 1
918
919    def mUndo(self, *args):
920        if not self._busy():
921            self.menubar.mUndo()
922        return 1
923
924    def mRedo(self, *args):
925        if not self._busy():
926            self.menubar.mRedo()
927        return 1
928
929    def mDrop(self, *args):
930        if not self._busy():
931            self.menubar.mDrop()
932        return 1
933
934    def mShuffle(self, *args):
935        if not self._busy():
936            self.menubar.mShuffle()
937        return 1
938
939    def mPause(self, *args):
940        if not self._busy():
941            self.menubar.mPause()
942        return 1
943
944    def mPlayerStats(self, *args):
945        if not self._busy():
946            self.menubar.mPlayerStats()
947        return 1
948
949    def mHelpRules(self, *args):
950        if not self._busy():
951            self.menubar.mHelpRules()
952        return 1
953
954    def mQuit(self, *args):
955        if not self._busy():
956            self.menubar.mQuit()
957        return 1
958
959    def mOptPlayerOptions(self, *args):
960        if not self._busy():
961            self.menubar.mOptPlayerOptions()
962        return 1
963