#!/usr/bin/env python # -*- mode: python; coding: utf-8; -*- # --------------------------------------------------------------------------- # # Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer # Copyright (C) 2003 Mt. Hood Playing Card Co. # Copyright (C) 2005-2009 Skomoroh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # --------------------------------------------------------------------------- import locale import os import time import tkinter import traceback from pysollib.mfxutil import KwStruct, destruct, kwdefault, openURL from pysollib.mygettext import _ from pysollib.settings import WIN_SYSTEM from pysollib.ui.tktile.tkcanvas import MfxCanvas from pysollib.ui.tktile.tkutil import after, after_cancel from pysollib.ui.tktile.tkutil import bind, unbind_destroy from pysollib.ui.tktile.tkutil import makeToplevel, setTransient from six.moves import tkinter_font from six.moves import tkinter_ttk as ttk # ************************************************************************ # * abstract base class for the dialogs in this module # ************************************************************************ class MfxDialog: # ex. _ToplevelDialog img = {} button_img = {} def __init__(self, parent, title="", resizable=False, default=-1): self.parent = parent self.status = 0 self.button = default self.timer = None self.buttons = [] self.accel_keys = {} self.top = makeToplevel(parent, title=title) # self._frame = ttk.Frame(self.top) # self._frame.pack(expand=True, fill='both') self._frame = self.top self.top.wm_resizable(resizable, resizable) # w, h = self.top.winfo_screenwidth(), self.top.winfo_screenheight() # self.top.wm_maxsize(w-4, h-32) bind(self.top, "WM_DELETE_WINDOW", self.wmDeleteWindow) def mainloop(self, focus=None, timeout=0, transient=True, geometry=""): bind(self.top, "", self.mCancel) bind(self.top, '', self.altKeyEvent) # for accelerators if focus is not None: focus.focus() if transient: setTransient(self.top, self.parent) try: self.top.grab_set() except tkinter.TclError: if traceback: traceback.print_exc() pass if geometry != "": self.top.geometry(geometry) if timeout > 0: self.timer = after(self.top, timeout, self.mTimeout) try: self.top.mainloop() except SystemExit: pass self.destroy() def destroy(self): after_cancel(self.timer) unbind_destroy(self.top) self.top.destroy() self.top.update_idletasks() self.top = None self.parent = None def wmDeleteWindow(self, *event): self.status = 1 raise SystemExit # return EVENT_HANDLED def mCancel(self, *event): self.status = 1 raise SystemExit def mTimeout(self, *event): self.status = 2 raise SystemExit def mDone(self, button): self.button = button raise SystemExit def altKeyEvent(self, event): widget = None if event.keysym in self.accel_keys: widget = self.accel_keys[event.keysym] else: key = event.char try: if os.name == 'nt': key = str(key, locale.getpreferredencoding()) else: key = str(key, 'utf-8') except Exception: pass else: key = key.lower() widget = self.accel_keys.get(key) if widget is not None: widget.event_generate('<>') def initKw(self, kw): kw = KwStruct(kw, timeout=0, resizable=False, text="", justify="center", strings=(_("&OK"),), default=0, width=0, padx=20, pady=20, bitmap=None, bitmap_side="left", bitmap_padx=10, bitmap_pady=20, image=None, image_side="left", image_padx=10, image_pady=20, ) # default to separator if more than one button sep = len(kw.strings) > 1 kwdefault(kw.__dict__, separator=sep) return kw def createFrames(self, kw): bottom_frame = ttk.Frame(self._frame, relief='flat', borderwidth=4) bottom_frame.pack(side='bottom', fill='both', expand=False) if kw.separator: separator = ttk.Separator(self._frame) separator.pack(side='bottom', fill='x') top_frame = ttk.Frame(self._frame) top_frame.pack(side='top', fill='both', expand=1) return top_frame, bottom_frame def createBitmaps(self, frame, kw): if kw.bitmap: # in ("error", "info", "question", "warning") img = self.img.get(kw.bitmap) b = ttk.Label(frame, image=img) b.pack(side=kw.bitmap_side, padx=kw.bitmap_padx, pady=kw.bitmap_pady) elif kw.image: b = ttk.Label(frame, image=kw.image) b.pack(side=kw.image_side, padx=kw.image_padx, pady=kw.image_pady) def createButtons(self, frame, kw): padx, pady = 4, 4 xbutton = column = -1 focus = None max_len = 0 if 'sep' in kw.strings: sep_column = list(kw.strings).index('sep') strings = kw.strings[sep_column+1:] else: sep_column = 0 strings = kw.strings for s in strings: if isinstance(s, tuple): s = s[0] if s: s = s.replace('&', '') max_len = max(max_len, len(s)) # print s, len(s) if max_len > 12 and WIN_SYSTEM == 'x11': button_width = max_len elif max_len > 9: button_width = max_len+1 elif max_len > 6: button_width = max_len+2 else: button_width = 8 # for s in kw.strings: if s is None: xbutton += 1 continue if s == 'sep': column += 1 continue if isinstance(s, tuple): assert len(s) == 2 button = int(s[1]) s = s[0] else: xbutton += 1 button = xbutton accel_indx = s.find('&') button_img = None if MfxDialog.button_img: button_img = MfxDialog.button_img.get(s) s = s.replace('&', '') if button < 0: widget = ttk.Button(frame, text=s, state="disabled") else: widget = ttk.Button( frame, text=s, default="normal", command=lambda self=self, button=button: self.mDone(button)) if button == kw.default: focus = widget focus.config(default="active") self.buttons.append(widget) # column += 1 if column >= sep_column: widget.config(width=button_width) else: widget.config(width=max(button_width, len(s))) if accel_indx >= 0: # key accelerator widget.config(underline=accel_indx) key = s[accel_indx] self.accel_keys[key.lower()] = widget # if button_img: widget.config(compound='left', image=button_img) widget.grid( column=column, row=0, sticky="nse", padx=padx, pady=pady) if focus is not None: def cb(event=None, w=focus): return w.event_generate('<>') bind(self.top, "", cb) bind(self.top, "", cb) # right justify frame.columnconfigure(sep_column, weight=1) return focus # ************************************************************************ # * replacement for the tk_dialog script # ************************************************************************ class MfxMessageDialog(MfxDialog): def __init__(self, parent, title, **kw): kw = self.initKw(kw) MfxDialog.__init__(self, parent, title, kw.resizable, kw.default) top_frame, bottom_frame = self.createFrames(kw) self.createBitmaps(top_frame, kw) # self.button = kw.default msg = ttk.Label(top_frame, text=kw.text, justify=kw.justify, width=kw.width) msg.pack(fill='both', expand=True, padx=kw.padx, pady=kw.pady) # focus = self.createButtons(bottom_frame, kw) self.mainloop(focus, kw.timeout) # ************************************************************************ # * # ************************************************************************ class MfxExceptionDialog(MfxMessageDialog): def __init__(self, parent, ex, title="Error", **kw): kw = KwStruct(kw, bitmap="error") text = kw.get("text", "") if not text.endswith("\n"): text = text + "\n" text = text + "\n" if isinstance(ex, EnvironmentError) and ex.filename is not None: t = "[Errno %s] %s:\n%s" % \ (ex.errno, ex.strerror, repr(ex.filename)) else: t = str(ex) kw.text = text + t MfxMessageDialog.__init__(self, parent, title, **kw.getKw()) # ************************************************************************ # * # ************************************************************************ class PysolAboutDialog(MfxMessageDialog): def __init__(self, app, parent, title, **kw): self._url = kw['url'] self.app = app kw = self.initKw(kw) MfxDialog.__init__(self, parent, title, kw.resizable, kw.default) top_frame, bottom_frame = self.createFrames(kw) self.createBitmaps(top_frame, kw) # self.button = kw.default frame = ttk.Frame(top_frame) frame.pack(fill='both', expand=True, padx=kw.padx, pady=kw.pady) msg = ttk.Label(frame, text=kw.text, justify=kw.justify, width=kw.width) msg.pack(fill='both', expand=True) # font_name = msg.lookup('TLabel', 'font') font_name = 'TkDefaultFont' font = tkinter_font.Font(parent, name=font_name, exists=True) font = font.copy() font.configure(underline=True) url_label = ttk.Label(frame, text=kw.url, font=font, foreground='blue', cursor='hand2') url_label.pack() from pysollib.options import calcCustomMouseButtonsBinding url_label.bind( calcCustomMouseButtonsBinding('<{mouse_button1}>'), self._urlClicked ) # focus = self.createButtons(bottom_frame, kw) self.splashscreen = tkinter.BooleanVar() self.splashscreen.set(app.opt.splashscreen) show_on_start = ttk.Checkbutton(bottom_frame, variable=self.splashscreen, command=self._splashUpdate, text=_("Show this on startup")) show_on_start.grid(row=0, column=0, sticky='w', padx=1, pady=1) self.mainloop(focus, kw.timeout) def _urlClicked(self, event): openURL(self._url) def _splashUpdate(self): self.app.opt.splashscreen = self.splashscreen.get() # ************************************************************************ # * # ************************************************************************ class MfxSimpleEntry(MfxDialog): def __init__(self, parent, title, label, value, **kw): kw = self.initKw(kw) MfxDialog.__init__(self, parent, title, kw.resizable, kw.default) top_frame, bottom_frame = self.createFrames(kw) self.createBitmaps(top_frame, kw) # self.value = value if label: label = ttk.Label(top_frame, text=label, takefocus=0) label.pack(pady=5) w = kw.get("e_width", 0) # width in characters self.var = ttk.Entry(top_frame, exportselection=1, width=w) self.var.insert(0, value) self.var.pack(side='top', padx=kw.padx, pady=kw.pady) # focus = self.createButtons(bottom_frame, kw) focus = self.var self.mainloop(focus, kw.timeout) def initKw(self, kw): kw = KwStruct(kw, strings=(_("&OK"), _("&Cancel")), default=0, separator=False, ) return MfxDialog.initKw(self, kw) def mDone(self, button): self.button = button self.value = self.var.get() raise SystemExit # ************************************************************************ # * a simple tooltip # ************************************************************************ class MfxTooltip: last_leave_time = 0 def __init__(self, widget): # private vars self.widget = widget self.text = None self.timer = None self.cancel_timer = None self.tooltip = None self.label = None self.bindings = [] self.bindings.append(self.widget.bind("", self._enter)) self.bindings.append(self.widget.bind("", self._leave)) self.bindings.append(self.widget.bind("", self._leave)) # user overrideable settings self.timeout = 800 # milliseconds self.cancel_timeout = 5000 self.leave_timeout = 400 self.relief = 'solid' self.justify = 'left' self.fg = "#000000" self.bg = "#ffffe0" self.xoffset = 0 self.yoffset = 4 def setText(self, text): self.text = text def _unbind(self): if self.bindings and self.widget: self.widget.unbind("", self.bindings[0]) self.widget.unbind("", self.bindings[1]) self.widget.unbind("", self.bindings[2]) self.bindings = [] def destroy(self): self._unbind() self._leave() def _enter(self, *event): after_cancel(self.timer) after_cancel(self.cancel_timer) self.cancel_timer = None if time.time() - MfxTooltip.last_leave_time < self.leave_timeout/1000.: self._showTip() else: self.timer = after(self.widget, self.timeout, self._showTip) def _leave(self, *event): after_cancel(self.timer) after_cancel(self.cancel_timer) self.timer = self.cancel_timer = None if self.tooltip: self.label.destroy() destruct(self.label) self.label = None self.tooltip.destroy() destruct(self.tooltip) self.tooltip = None MfxTooltip.last_leave_time = time.time() def _showTip(self): self.timer = None if self.tooltip or not self.text: return # if isinstance(self.widget, (ttk.Button, ttk.Checkbutton)): # if self.widget["state"] == 'disabled': # return # x = self.widget.winfo_rootx() x = self.widget.winfo_pointerx() y = self.widget.winfo_rooty() + self.widget.winfo_height() x += self.xoffset y += self.yoffset self.tooltip = tkinter.Toplevel() self.tooltip.wm_iconify() self.tooltip.wm_overrideredirect(1) self.tooltip.wm_protocol("WM_DELETE_WINDOW", self.destroy) self.label = tkinter.Label(self.tooltip, text=self.text, relief=self.relief, justify=self.justify, fg=self.fg, bg=self.bg, bd=1, takefocus=0) self.label.pack(ipadx=1, ipady=1) self.tooltip.wm_geometry("%+d%+d" % (x, y)) self.tooltip.wm_deiconify() self.cancel_timer = after( self.widget, self.cancel_timeout, self._leave) # self.tooltip.tkraise() # ************************************************************************ # * A canvas widget with scrollbars and some useful bindings. # ************************************************************************ class MfxScrolledCanvas: def __init__(self, parent, hbar=True, vbar=True, propagate=0, **kw): kwdefault(kw, highlightthickness=0, bd=1, relief='sunken') self.parent = parent self.createFrame(kw) self.canvas = None self.hbar = None self.vbar = None self.hbar_show = False self.vbar_show = False self.createCanvas(kw) self.frame.grid_rowconfigure(0, weight=1) self.frame.grid_columnconfigure(0, weight=1) self.frame.grid_propagate(propagate) if hbar: self.createHbar() self.bindHbar() if vbar: self.createVbar() self.bindVbar() # self.canvas.focus_set() # # # def destroy(self): self.unbind_all() self.canvas.destroy() self.frame.destroy() def pack(self, **kw): self.frame.pack(**kw) def grid(self, **kw): self.frame.grid(**kw) # # # def setTile(self, app, i, scale_method, force=False): tile = app.tabletile_manager.get(i) if tile is None or tile.error: return False # print i, tile if i == 0: assert tile.color assert tile.filename is None else: assert tile.color is None assert tile.filename assert tile.basename if not force: if (i == app.tabletile_index and tile.color == app.opt.colors['table']): return False # stretch = tile.stretch save_aspect = tile.save_aspect if scale_method < 0 and stretch: scale_method = app.opt.tabletile_scale_method elif not stretch: scale_method = 0 if scale_method > 0: stretch = scale_method > 1 save_aspect = scale_method > 2 if not self.canvas.setTile(tile.filename, stretch, save_aspect): tile.error = True return False if i == 0: self.canvas.config(bg=tile.color) # app.top.config(bg=tile.color) else: self.canvas.config(bg=app.top_bg) # app.top.config(bg=app.top_bg) self.canvas.setTextColor(app.opt.colors['text']) return True # # # def unbind_all(self): unbind_destroy(self.hbar) unbind_destroy(self.vbar) unbind_destroy(self.canvas) unbind_destroy(self.frame) def createFrame(self, kw): width = kw.get("width") height = kw.get("height") self.frame = ttk.Frame(self.parent, width=width, height=height) def createCanvas(self, kw): bd = kw['bd'] kw['bd'] = 0 relief = kw['relief'] del kw['relief'] frame = tkinter.Frame(self.frame, bd=bd, relief=relief) frame.grid(row=0, column=0, sticky="news") self.canvas = MfxCanvas(frame, **kw) self.canvas.pack(expand=True, fill='both') def createHbar(self): self.hbar = ttk.Scrollbar(self.frame, takefocus=0, orient="horizontal") self.canvas["xscrollcommand"] = self._setHbar self.hbar["command"] = self.canvas.xview self.hbar.grid(row=1, column=0, sticky="we") self.hbar.grid_remove() def createVbar(self): self.vbar = ttk.Scrollbar(self.frame, takefocus=0) self.canvas["yscrollcommand"] = self._setVbar self.vbar["command"] = self.canvas.yview self.vbar.grid(row=0, column=1, sticky="ns") self.vbar.grid_remove() def bindHbar(self, w=None): if w is None: w = self.canvas bind(w, "", self.unit_left) bind(w, "", self.unit_right) def bindVbar(self, w=None): if w is None: w = self.canvas bind(w, "", self.page_up) bind(w, "", self.page_down) bind(w, "", self.unit_up) bind(w, "", self.unit_down) bind(w, "", self.scroll_top) bind(w, "", self.scroll_top) bind(w, "", self.scroll_bottom) # mousewheel support if WIN_SYSTEM == 'x11': bind(w, '<4>', self.mouse_wheel_up) bind(w, '<5>', self.mouse_wheel_down) # don't work on Linux # bind(w, '', self.mouse_wheel) def mouse_wheel(self, *args): print('MfxScrolledCanvas.mouse_wheel', args) def _setHbar(self, first, last): if self.canvas.busy: return sb = self.hbar # TODO - Setting this to .99 takes the scrollbar size into account. # But there is probably a better way to do it. if float(first) <= 0 and float(last) >= .99: sb.grid_remove() self.hbar_show = False else: if self.canvas.winfo_ismapped(): sb.grid() self.hbar_show = True sb.set(first, last) def _setVbar(self, first, last): if self.canvas.busy: return sb = self.vbar # TODO - See _setHbar above. if float(first) <= 0 and float(last) >= .99: sb.grid_remove() self.vbar_show = False else: if self.canvas.winfo_ismapped(): sb.grid() self.vbar_show = True sb.set(first, last) def _xview(self, *args): if self.hbar_show: self.canvas.xview(*args) return 'break' def _yview(self, *args): if self.vbar_show: self.canvas.yview(*args) return 'break' def page_up(self, *event): return self._yview('scroll', -1, 'page') def page_down(self, *event): return self._yview('scroll', 1, 'page') def unit_up(self, *event): return self._yview('scroll', -1, 'unit') def unit_down(self, *event): return self._yview('scroll', 1, 'unit') def mouse_wheel_up(self, *event): return self._yview('scroll', -5, 'unit') def mouse_wheel_down(self, *event): return self._yview('scroll', 5, 'unit') def page_left(self, *event): return self._xview('scroll', -1, 'page') def page_right(self, *event): return self._xview('scroll', 1, 'page') def unit_left(self, *event): return self._xview('scroll', -1, 'unit') def unit_right(self, *event): return self._xview('scroll', 1, 'unit') def scroll_top(self, *event): return self._yview('moveto', 0) def scroll_bottom(self, *event): return self._yview('moveto', 1) # ************************************************************************ # * # ************************************************************************ class StackDesc: def __init__(self, game, stack): self.game = game self.stack = stack self.canvas = game.canvas self.bindings = [] font = game.app.getFont('canvas_small') # print self.app.cardset.CARDW, self.app.images.CARDW cardw = game.app.images.getSize()[0] x, y = stack.x+cardw//2, stack.y text = stack.getHelp()+'\n'+stack.getBaseCard() text = text.strip() if text: frame = tkinter.Frame(self.canvas) self.frame = frame label = tkinter.Message(frame, font=font, text=text, width=cardw-8, relief='solid', fg='#000000', bg='#ffffe0', bd=1) label.pack() self.label = label self.id = self.canvas.create_window(x, y, window=frame, anchor='n') self.bindings.append(label.bind('', self._buttonPressEvent)) # self.bindings.append(label.bind('', self._enterEvent)) else: self.id = None def _buttonPressEvent(self, *event): # self.game.deleteStackDesc() self.frame.tkraise() def _enterEvent(self, *event): self.frame.tkraise() def delete(self): if self.id: self.canvas.delete(self.id) for b in self.bindings: self.label.unbind('', b) # ************************************************************************ # * ttk.Scale workaround (label and resolution) # ************************************************************************ class MyPysolScale: def __init__(self, parent, **kw): if 'resolution' in kw: self.resolution = kw['resolution'] del kw['resolution'] else: self.resolution = 1 if 'from_' in kw: kw['from_'] //= self.resolution if 'to' in kw: kw['to'] //= self.resolution if 'variable' in kw: self.variable = kw['variable'] del kw['variable'] else: self.variable = None value = None if 'value' in kw: value = kw['value'] del kw['value'] elif self.variable: value = self.variable.get() self.value = value self.command = command = None if 'command' in kw: command = kw['command'] kw['command'] = self._scale_command if 'label' in kw: self.label_text = kw['label'] width = len(self.label_text)+4 # width = None del kw['label'] else: self.label_text = None width = 3 # create widgets side = 'left' # 'top' self.frame = ttk.Frame(parent) self.label = ttk.Label(self.frame, anchor='w', width=width, padding=(5, 0)) self.label.pack(side=side, expand=False, fill='x') self.scale = ttk.Scale(self.frame, **kw) self.scale.pack(side=side, expand=True, fill='both', pady=4) if self.variable: self.variable.trace('w', self._trace_var) if value is not None: self._set_text(self._round(value)) if self.variable: self.variable.set(value) self.command = command def _round(self, value): return int(round(float(value)/self.resolution))*self.resolution def _trace_var(self, *args): self.scale.set(float(self.variable.get())/self.resolution) def _set_text(self, v): if self.label_text: t = self.label_text+' '+str(v) else: t = str(v) self.label.configure(text=t) def _scale_command(self, value): v = self._round(float(value)*self.resolution) self._set_text(v) self.variable.set(v) if value != self.value and self.command: self.command(value) self.value = value def pack(self, **kw): self.frame.pack(**kw) def grid(self, **kw): self.frame.grid(**kw) def configure(self, **kw): self.scale.configure(**kw) config = configure def state(self, v): self.scale.state(statespec=(v,)) self.label.state(statespec=(v,)) def get(self): return self.variable.get() def set(self, v): self.variable.set(v) class TkinterScale(tkinter.Scale): def __init__(self, parent, **kw): if 'value' in kw: del kw['value'] tkinter.Scale.__init__(self, parent, **kw) PysolScale = MyPysolScale # PysolScale = TkinterScale # ************************************************************************ # * ttk.Combobox workaround (clear selection) # ************************************************************************ class PysolCombo(ttk.Combobox): def __init__(self, master=None, **kw): self._command = None if 'selectcommand' in kw: self._command = kw['selectcommand'] del kw['selectcommand'] ttk.Combobox.__init__(self, master, **kw) self.bind('<>', self._callback) def _callback(self, *args): # self.selection_clear() self.selection_range(0, 0) if self._command is not None: return self._command(*args) return None