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 os
25import sys
26import traceback
27
28import configobj
29
30import pysollib.settings
31from pysollib.mfxutil import print_err
32from pysollib.mygettext import _
33from pysollib.mygettext import myGettext
34from pysollib.pysoltk import TOOLBAR_BUTTONS, TOOLKIT
35from pysollib.resource import CSI
36
37
38import six
39
40import validate
41
42# ************************************************************************
43# * Options
44# ************************************************************************
45
46_global_settings = {
47    'mouse_button1': 1,
48    'mouse_button2': 2,
49    'mouse_button3': 3,
50}
51
52
53def calcCustomMouseButtonsBinding(binding_format):
54    assert _global_settings['mouse_button1']
55    return binding_format.format(
56        mouse_button1=_global_settings['mouse_button1'],
57        mouse_button2=_global_settings['mouse_button2'],
58        mouse_button3=_global_settings['mouse_button3'],
59    )
60
61
62configspec = '''
63[general]
64player = string
65confirm = boolean
66update_player_stats = boolean
67autofaceup = boolean
68autodrop = boolean
69autodeal = boolean
70quickplay = boolean
71shuffle = boolean
72undo = boolean
73bookmarks = boolean
74hint = boolean
75highlight_piles = boolean
76highlight_cards = boolean
77highlight_samerank = boolean
78highlight_not_matching = boolean
79mahjongg_show_removed = boolean
80mahjongg_create_solvable = integer(0, 2)
81shisen_show_hint = boolean
82shisen_show_matching = boolean
83animations = integer(0, 5)
84redeal_animation = boolean
85win_animation = boolean
86flip_animation = boolean
87compact_stacks = boolean
88shadow = boolean
89shade = boolean
90shrink_face_down = boolean
91shade_filled_stacks = boolean
92demo_logo = boolean
93tile_theme = string
94default_tile_theme = string
95toolbar = integer(0, 4)
96toolbar_style = string
97toolbar_relief = string
98toolbar_compound = string
99toolbar_size = integer(0, 1)
100statusbar = boolean
101statusbar_game_number = boolean
102statusbar_stuck = boolean
103num_cards = boolean
104helpbar = boolean
105num_recent_games = integer(10, 100)
106last_gameid = integer
107game_holded = integer
108wm_maximized = boolean
109splashscreen = boolean
110mouse_type = string
111mouse_undo = boolean
112negative_bottom = boolean
113randomize_place = boolean
114dragcursor = boolean
115save_games_geometry = boolean
116game_geometry = int_list(min=2, max=2)
117sound = boolean
118sound_mode = integer(0, 1)
119sound_sample_volume = integer(0, 128)
120sound_sample_buffer_size = integer(1, 4)
121music = boolean
122tabletile_name = string
123center_layout = boolean
124recent_gameid = int_list
125favorite_gameid = int_list
126visible_buttons = string_list
127translate_game_names = boolean
128solver_presets = string_list
129solver_show_progress = boolean
130solver_max_iterations = integer
131solver_iterations_output_step = integer
132solver_preset = string
133display_win_message = boolean
134language = string
135
136[sound_samples]
137move = boolean
138autodrop = boolean
139drop = boolean
140nomove = boolean
141gameperfect = boolean
142deal = boolean
143gamelost = boolean
144autopilotwon = boolean
145flip = boolean
146undo = boolean
147gamefinished = boolean
148areyousure = boolean
149startdrag = boolean
150autoflip = boolean
151autopilotlost = boolean
152turnwaste = boolean
153gamewon = boolean
154droppair = boolean
155redo = boolean
156dealwaste = boolean
157extra = boolean
158
159[fonts]
160sans = list
161small = list
162fixed = list
163canvas_default = list
164canvas_small = list
165canvas_fixed = list
166canvas_large = list
167
168[colors]
169piles = string
170text = string
171table = string
172hintarrow = string
173cards_1 = string
174cards_2 = string
175samerank_1 = string
176samerank_2 = string
177not_matching = string
178
179[timeouts]
180highlight_samerank = float(0.2, 9.9)
181raise_card = float(0.2, 9.9)
182demo = float(0.2, 9.9)
183highlight_cards = float(0.2, 9.9)
184hint = float(0.2, 9.9)
185highlight_piles = float(0.2, 9.9)
186
187[cardsets]
1880 = string_list(min=2, max=2)
1891 = string_list(min=2, max=2)
1902 = string_list(min=2, max=2)
1913 = string_list(min=2, max=2)
1924 = string_list(min=2, max=2)
1935 = string_list(min=2, max=2)
1946 = string_list(min=2, max=2)
1957 = string_list(min=2, max=2)
1968 = string_list(min=2, max=2)
1979 = string_list(min=2, max=2)
198scale_cards = boolean
199scale_x = float
200scale_y = float
201auto_scale = boolean
202spread_stacks = boolean
203preserve_aspect_ratio = boolean
204'''.splitlines()
205
206
207class Options:
208    GENERAL_OPTIONS = [
209        ('player', 'str'),
210        ('confirm', 'bool'),
211        ('update_player_stats', 'bool'),
212        ('autofaceup', 'bool'),
213        ('autodrop', 'bool'),
214        ('autodeal', 'bool'),
215        ('quickplay', 'bool'),
216        ('shuffle', 'bool'),
217        ('undo', 'bool'),
218        ('bookmarks', 'bool'),
219        ('hint', 'bool'),
220        ('highlight_piles', 'bool'),
221        ('highlight_cards', 'bool'),
222        ('highlight_samerank', 'bool'),
223        ('highlight_not_matching', 'bool'),
224        ('mahjongg_show_removed', 'bool'),
225        ('mahjongg_create_solvable', 'int'),
226        ('shisen_show_hint', 'bool'),
227        ('shisen_show_matching', 'bool'),
228        ('accordion_deal_all', 'bool'),
229        ('animations', 'int'),
230        ('redeal_animation', 'bool'),
231        ('win_animation', 'bool'),
232        ('flip_animation', 'bool'),
233        ('compact_stacks', 'bool'),
234        ('shadow', 'bool'),
235        ('shade', 'bool'),
236        ('shrink_face_down', 'bool'),
237        ('shade_filled_stacks', 'bool'),
238        ('demo_logo', 'bool'),
239        ('tile_theme', 'str'),
240        ('default_tile_theme', 'str'),
241        ('toolbar', 'int'),
242        ('toolbar_style', 'str'),
243        ('toolbar_relief', 'str'),
244        ('toolbar_compound', 'str'),
245        ('toolbar_size', 'int'),
246        ('statusbar', 'bool'),
247        ('statusbar_game_number', 'bool'),
248        ('statusbar_stuck', 'bool'),
249        ('num_cards', 'bool'),
250        ('helpbar', 'bool'),
251        ('num_recent_games', 'int'),
252        ('last_gameid', 'int'),
253        ('game_holded', 'int'),
254        ('wm_maximized', 'bool'),
255        ('splashscreen', 'bool'),
256        ('mouse_type', 'str'),
257        ('mouse_undo', 'bool'),
258        ('negative_bottom', 'bool'),
259        ('randomize_place', 'bool'),
260        # ('save_cardsets', 'bool'),
261        ('dragcursor', 'bool'),
262        ('save_games_geometry', 'bool'),
263        ('sound', 'bool'),
264        ('sound_mode', 'int'),
265        ('sound_sample_volume', 'int'),
266        ('sound_music_volume', 'int'),
267        ('sound_sample_buffer_size', 'int'),
268        ('music', 'bool'),
269        ('tabletile_name', 'str'),
270        ('center_layout', 'bool'),
271        ('translate_game_names', 'bool'),
272        ('solver_presets', 'list'),
273        ('solver_show_progress', 'bool'),
274        ('solver_max_iterations', 'int'),
275        ('solver_iterations_output_step', 'int'),
276        ('solver_preset', 'string'),
277        ('mouse_button1', 'int'),
278        ('mouse_button2', 'int'),
279        ('mouse_button3', 'int'),
280        # ('toolbar_vars', 'list'),
281        # ('recent_gameid', 'list'),
282        # ('favorite_gameid', 'list'),
283        ('display_win_message', 'bool'),
284        ('language', 'str'),
285        ]
286
287    def __init__(self):
288        self._config = None             # configobj.ConfigObj instance
289        self._config_encoding = 'utf-8'
290
291        self.version_tuple = pysollib.settings.VERSION_TUPLE  # XXX
292        self.saved = 0                  # XXX
293        # options menu:
294        self.player = _("Unknown")
295        self.confirm = True
296        self.update_player_stats = True
297        self.autofaceup = True
298        self.autodrop = False
299        self.autodeal = True
300        self.quickplay = True
301        self.shuffle = True
302        self.undo = True
303        self.bookmarks = True
304        self.hint = True
305        self.highlight_piles = True
306        self.highlight_cards = True
307        self.highlight_samerank = True
308        self.highlight_not_matching = True
309        self.mahjongg_show_removed = False
310        self.mahjongg_create_solvable = 2  # 0 - none, 1 - easy, 2 - hard
311        self.accordion_deal_all = True
312        if TOOLKIT == 'kivy':
313            self.mahjongg_create_solvable = 1  # 0 - none, 1 - easy, 2 - hard
314        self.shisen_show_hint = True
315        self.shisen_show_matching = False
316        self.animations = 3             # default to Medium
317        self.redeal_animation = True
318        self.win_animation = True
319        if TOOLKIT == 'kivy':
320            self.redeal_animation = False
321            self.win_animation = False
322        self.flip_animation = True
323        self.compact_stacks = True
324        self.shadow = True
325        self.shade = True
326        self.shrink_face_down = True
327        self.shade_filled_stacks = True
328        self.demo_logo = True
329        self.tile_theme = 'default'
330        self.default_tile_theme = 'default'
331        self.toolbar = 1       # 0 == hide, 1,2,3,4 == top, bottom, lef, right
332        # self.toolbar_style = 'default'
333        if TOOLKIT == 'kivy':
334            self.toolbar = 4  # 0 == hide, 1,2,3,4 == top, bottom, lef, right
335        self.toolbar_style = 'bluecurve'
336        self.toolbar_relief = 'flat'
337        self.toolbar_compound = 'none'  # icons only
338        self.toolbar_size = 0
339        self.toolbar_vars = {}
340        for w in TOOLBAR_BUTTONS:
341            self.toolbar_vars[w] = True  # show all buttons
342        self.statusbar = True
343        self.statusbar_game_number = False  # show game number in statusbar
344        self.statusbar_stuck = False        # show stuck indicator
345        self.num_cards = False
346        self.helpbar = False
347        self.splashscreen = True
348        self.mouse_button1 = 1
349        self.mouse_button2 = 2
350        self.mouse_button3 = 3
351        self.mouse_type = 'drag-n-drop'  # or 'sticky-mouse' or 'point-n-click'
352        self.mouse_undo = False         # use mouse for undo/redo
353        self.negative_bottom = True
354        self.translate_game_names = True
355        self.display_win_message = True
356        self.language = ''
357        # sound
358        self.sound = True
359        self.sound_mode = 1
360        self.sound_sample_volume = 75
361        self.sound_music_volume = 100
362        self.sound_sample_buffer_size = 1  # 1 - 4 (1024 - 4096 bytes)
363        self.music = True
364        self.sound_samples = {
365            'areyousure': True,
366            'autodrop': True,
367            'autoflip': True,
368            'autopilotlost': True,
369            'autopilotwon': True,
370            'deal': True,
371            'dealwaste': True,
372            'droppair': True,
373            'drop': True,
374            'extra': True,
375            'flip': True,
376            'move': True,
377            'nomove': True,
378            'redo': True,
379            'startdrag': True,
380            'turnwaste': True,
381            'undo': True,
382            'gamefinished': False,
383            'gamelost': False,
384            'gameperfect': False,
385            'gamewon': False,
386            }
387        # fonts
388        self.fonts = {
389            "default": None,
390            # "default": ("helvetica", 12),
391            "sans": ("times",     12),  # for html
392            "fixed": ("courier",   12),  # for html & log
393            "small": ("helvetica", 12),
394            "canvas_default": ("helvetica", 12),
395            # "canvas_card": ("helvetica", 12),
396            "canvas_fixed": ("courier",   12),
397            "canvas_large": ("helvetica", 16),
398            "canvas_small": ("helvetica", 10),
399            }
400        # colors
401        self.colors = {
402            'table':        '#008200',
403            'text':         '#ffffff',
404            'piles':        '#ffc000',
405            'cards_1':      '#ffc000',
406            'cards_2':      '#0000ff',
407            'samerank_1':   '#ffc000',
408            'samerank_2':   '#0000ff',
409            'hintarrow':    '#303030',
410            'not_matching': '#ff0000',
411            }
412        # delays
413        self.timeouts = {
414            'hint':               1.0,
415            'demo':               1.0,
416            'raise_card':         1.0,
417            'highlight_piles':    1.0,
418            'highlight_cards':    1.0,
419            'highlight_samerank': 1.0,
420            }
421        # additional startup information
422        self.num_recent_games = 15
423        self.recent_gameid = []
424        self.favorite_gameid = []
425        if TOOLKIT == 'kivy':
426            self.favorite_gameid = [2, 7, 8, 19, 140, 116, 152, 176, 181,
427                                    194, 207, 706, 721, 756, 903, 5034,
428                                    11004, 14405, 14410, 15411, 22225]
429        self.last_gameid = 0            # last game played
430        self.game_holded = 0            # gameid or 0
431        self.wm_maximized = 0
432        self.save_games_geometry = False
433        # saved games geometry (gameid: (width, height))
434        self.games_geometry = {}
435        self.game_geometry = (0, 0)  # game geometry before exit
436        self.offsets = {}           # cards offsets
437        #
438        self.randomize_place = False
439        # self.save_cardsets = True
440        self.dragcursor = True
441        #
442        self.scale_cards = False
443        self.scale_x = 1.0
444        self.scale_y = 1.0
445        self.auto_scale = False
446        self.spread_stacks = False
447        self.center_layout = True
448        self.preserve_aspect_ratio = True
449        # solver
450        self.solver_presets = [
451            'none',
452            'abra-kadabra',
453            'blue-yonder',
454            'conspiracy-theory',
455            'cookie-monster',
456            'cool-jives',
457            'crooked-nose',
458            'fools-gold',
459            'good-intentions',
460            'hello-world',
461            'john-galt-line',
462            'looking-glass',
463            'one-big-family',
464            'rin-tin-tin',
465            'slick-rock',
466            'the-last-mohican',
467            'video-editing',
468            'yellow-brick-road',
469            ]
470        self.solver_show_progress = True
471        self.solver_max_iterations = 100000
472        self.solver_iterations_output_step = 100
473        self.solver_preset = 'video-editing'
474
475    def setDefaults(self, top=None):
476        WIN_SYSTEM = pysollib.settings.WIN_SYSTEM
477        # toolbar
478        # if WIN_SYSTEM == 'win32':
479        #    self.toolbar_style = 'crystal'
480        # fonts
481        if WIN_SYSTEM == 'win32':
482            self.fonts["sans"] = ("times new roman", 12)
483            self.fonts["fixed"] = ("courier new", 10)
484        elif WIN_SYSTEM == 'x11':
485            self.fonts["sans"] = ("helvetica", -12)
486        # tile theme
487        if WIN_SYSTEM == 'win32':
488            self.tile_theme = self.default_tile_theme = 'winnative'
489            if sys.getwindowsversion() >= (5, 1):  # xp
490                self.tile_theme = 'xpnative'
491        elif WIN_SYSTEM == 'x11':
492            self.tile_theme = 'clam'
493            self.default_tile_theme = 'default'
494        elif WIN_SYSTEM == 'aqua':
495            self.tile_theme = self.default_tile_theme = 'aqua'
496        #
497        sw, sh, sd = 0, 0, 8
498        if top:
499            sw, sh, sd = (top.winfo_screenwidth(),
500                          top.winfo_screenheight(),
501                          top.winfo_screendepth())
502        # bg
503        if sd > 8:
504            self.tabletile_name = "Nostalgy.gif"  # basename
505        else:
506            self.tabletile_name = None
507        # cardsets
508        c = "Standard"
509        if sw < 800 or sh < 600:
510            c = "2000"
511        if TOOLKIT == 'kivy':
512            c = "Standard"
513
514        # if sw > 1024 and sh > 768:
515        #    c = 'Dondorf'
516        self.cardset = {
517            # game_type:        (cardset_name, back_file)
518            0:                  (c, ""),
519            CSI.TYPE_FRENCH:    (c, ""),
520            CSI.TYPE_HANAFUDA:  ("Kintengu", ""),
521            CSI.TYPE_MAHJONGG:  ("Crystal Mahjongg", ""),
522            CSI.TYPE_TAROCK:    ("Vienna 2K", ""),
523            CSI.TYPE_HEXADECK:  ("Hex A Deck", ""),
524            CSI.TYPE_MUGHAL_GANJIFA: ("Mughal Ganjifa", ""),
525            # CSI.TYPE_NAVAGRAHA_GANJIFA: ("Navagraha Ganjifa", ""),
526            CSI.TYPE_NAVAGRAHA_GANJIFA: ("Dashavatara Ganjifa", ""),
527            CSI.TYPE_DASHAVATARA_GANJIFA: ("Dashavatara Ganjifa", ""),
528            CSI.TYPE_TRUMP_ONLY: ("Matrix", ""),
529        }
530
531    # not changeable options
532    def setConstants(self):
533        if 'shuffle' not in self.toolbar_vars:
534            # new in v.1.1
535            self.toolbar_vars['shuffle'] = True
536        if isinstance(self.mahjongg_create_solvable, bool):
537            # changed in v.1.1
538            self.mahjongg_create_solvable = 2
539        pass
540
541    def copy(self):
542        opt = Options()
543        opt.__dict__.update(self.__dict__)
544        opt.setConstants()
545        return opt
546
547    def save(self, filename):
548        config = self._config
549
550        # general
551        for key, t in self.GENERAL_OPTIONS:
552            val = getattr(self, key)
553            if isinstance(val, str):
554                if sys.version_info < (3,):
555                    val = six.text_type(val, 'utf-8')
556            config['general'][key] = val
557
558        config['general']['recent_gameid'] = self.recent_gameid
559        config['general']['favorite_gameid'] = self.favorite_gameid
560        visible_buttons = [b for b in self.toolbar_vars
561                           if self.toolbar_vars[b]]
562        config['general']['visible_buttons'] = visible_buttons
563        if 'none' in config['general']['solver_presets']:
564            config['general']['solver_presets'].remove('none')
565
566        # sound_samples
567        config['sound_samples'] = self.sound_samples
568
569        # fonts
570        for key, val in self.fonts.items():
571            if key == 'default':
572                continue
573            if val is None:
574                continue
575            config['fonts'][key] = val
576
577        # colors
578        config['colors'] = self.colors
579
580        # timeouts
581        config['timeouts'] = self.timeouts
582
583        # cardsets
584        for key, val in self.cardset.items():
585            config['cardsets'][str(key)] = val
586        for key in ('scale_cards', 'scale_x', 'scale_y',
587                    'auto_scale', 'spread_stacks',
588                    'preserve_aspect_ratio'):
589            config['cardsets'][key] = getattr(self, key)
590
591        # games_geometry
592        config['games_geometry'].clear()
593        for key, val in self.games_geometry.items():
594            config['games_geometry'][str(key)] = val
595        config['general']['game_geometry'] = self.game_geometry
596
597        # offsets
598        for key, val in self.offsets.items():
599            config['offsets'][key] = val
600
601        config.write()
602        # config.write(sys.stdout); print
603
604    def _getOption(self, section, key, t):
605        config = self._config
606        try:
607            if config[section][key] is None:
608                # invalid value
609                return None
610            if t == 'bool':
611                val = config[section].as_bool(key)
612            elif t == 'int':
613                val = config[section].as_int(key)
614            elif t == 'float':
615                val = config[section].as_float(key)
616            elif t == 'list':
617                val = config[section][key]
618                assert isinstance(val, (list, tuple))
619            else:  # str
620                val = config[section][key]
621        except KeyError:
622            val = None
623        except Exception:
624            print_err('load option error: %s: %s' % (section, key))
625            traceback.print_exc()
626            val = None
627        return val
628
629    def load(self, filename):
630
631        # create ConfigObj instance
632        try:
633            config = configobj.ConfigObj(filename,
634                                         configspec=configspec,
635                                         encoding=self._config_encoding)
636        except configobj.ParseError:
637            traceback.print_exc()
638            config = configobj.ConfigObj(configspec=configspec,
639                                         encoding=self._config_encoding)
640        self._config = config
641
642        # create sections
643        for section in (
644            'general',
645            'sound_samples',
646            'fonts',
647            'colors',
648            'timeouts',
649            'cardsets',
650            'games_geometry',
651            'offsets',
652                ):
653            if section not in config:
654                config[section] = {}
655
656        # add initial comment
657        if not os.path.exists(filename):
658            config.initial_comment = ['-*- coding: %s -*-' %
659                                      self._config_encoding]
660            return
661
662        # validation
663        vdt = validate.Validator()
664        res = config.validate(vdt)
665        # from pprint import pprint; pprint(res)
666        if isinstance(res, dict):
667            for section, data in res.items():
668                if data is True:
669                    continue
670                for key, value in data.items():
671                    if value is False:
672                        print_err('config file: validation error: '
673                                  'section: "%s", key: "%s"' % (section, key))
674                        config[section][key] = None
675
676        # general
677        for key, t in self.GENERAL_OPTIONS:
678            val = self._getOption('general', key, t)
679            if val == 'None':
680                setattr(self, key, None)
681            elif val is not None:
682                setattr(self, key, val)
683
684        pysollib.settings.TRANSLATE_GAME_NAMES = self.translate_game_names
685
686        recent_gameid = self._getOption('general', 'recent_gameid', 'list')
687        if recent_gameid is not None:
688            try:
689                self.recent_gameid = [int(i) for i in recent_gameid]
690            except Exception:
691                traceback.print_exc()
692
693        favorite_gameid = self._getOption('general', 'favorite_gameid', 'list')
694        if favorite_gameid is not None:
695            try:
696                self.favorite_gameid = [int(i) for i in favorite_gameid]
697            except Exception:
698                traceback.print_exc()
699
700        visible_buttons = self._getOption('general', 'visible_buttons', 'list')
701        if visible_buttons is not None:
702            for key in TOOLBAR_BUTTONS:
703                self.toolbar_vars[key] = (key in visible_buttons)
704
705        myGettext.language = self.language
706
707        # solver
708        solver_presets = self._getOption('general', 'solver_presets', 'list')
709        if solver_presets is not None:
710            if 'none' not in solver_presets:
711                solver_presets.insert(0, 'none')
712            self.solver_presets = solver_presets
713
714        # sound_samples
715        for key in self.sound_samples:
716            val = self._getOption('sound_samples', key, 'bool')
717            if val is not None:
718                self.sound_samples[key] = val
719
720        # fonts
721        for key in self.fonts:
722            if key == 'default':
723                continue
724            val = self._getOption('fonts', key, 'str')
725            if val is not None:
726                try:
727                    val[1] = int(val[1])
728                except Exception:
729                    traceback.print_exc()
730                else:
731                    val = tuple(val)
732                    self.fonts[key] = val
733
734        # colors
735        for key in self.colors:
736            val = self._getOption('colors', key, 'str')
737            if val is not None:
738                self.colors[key] = val
739
740        # timeouts
741        for key in self.timeouts:
742            val = self._getOption('timeouts', key, 'float')
743            if val is not None:
744                self.timeouts[key] = val
745
746        # cardsets
747        for key in self.cardset:
748            val = self._getOption('cardsets', str(key), 'list')
749            if val is not None:
750                try:
751                    self.cardset[int(key)] = val
752                except Exception:
753                    traceback.print_exc()
754        for key, t in (('scale_cards', 'bool'),
755                       ('scale_x', 'float'),
756                       ('scale_y', 'float'),
757                       ('auto_scale', 'bool'),
758                       ('spread_stacks', 'bool'),
759                       ('preserve_aspect_ratio', 'bool')):
760            val = self._getOption('cardsets', key, t)
761            if val is not None:
762                setattr(self, key, val)
763
764        # games_geometry
765        for key, val in config['games_geometry'].items():
766            try:
767                val = [int(i) for i in val]
768                assert len(val) == 2
769                self.games_geometry[int(key)] = val
770            except Exception:
771                traceback.print_exc()
772        game_geometry = self._getOption('general', 'game_geometry', 'list')
773        if game_geometry is not None:
774            try:
775                self.game_geometry = tuple(int(i) for i in game_geometry)
776            except Exception:
777                traceback.print_exc()
778
779        # cards offsets
780        for key, val in config['offsets'].items():
781            try:
782                val = [int(i) for i in val]
783                assert len(val) == 2
784                self.offsets[key] = val
785            except Exception:
786                traceback.print_exc()
787
788        # mouse buttons swap
789        def _positive(button):
790            return max([button, 1])
791        _global_settings['mouse_button1'] = _positive(self.mouse_button1)
792        _global_settings['mouse_button2'] = _positive(self.mouse_button2)
793        _global_settings['mouse_button3'] = _positive(self.mouse_button3)
794
795    def calcCustomMouseButtonsBinding(self, binding_format):
796        """docstring for calcCustomMouseButtonsBinding"""
797        def _positive(button):
798            return max([button, 1])
799        return binding_format.format(
800            mouse_button1=_positive(self.mouse_button1),
801            mouse_button2=_positive(self.mouse_button2),
802            mouse_button3=_positive(self.mouse_button3),
803        )
804