#!/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 <http://www.gnu.org/licenses/>.
#
# ---------------------------------------------------------------------------

import locale
import os
import time
import tkinter
import tkinter.font
import tkinter.ttk as ttk
import traceback

from pysollib.mfxutil import KwStruct, destruct, kwdefault, openURL
from pysollib.mygettext import _
from pysollib.settings import WIN_SYSTEM
from pysollib.speech import Speech
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

# ************************************************************************
# * abstract base class for the dialogs in this module
# ************************************************************************


class MfxDialog:  # ex. _ToplevelDialog
    img = {}
    button_img = {}

    num_open = 0

    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 = {}
        MfxDialog.num_open += 1
        if MfxDialog.num_open == 1 and self.parent.app is not None:
            self.parent.app.unraiseAll()
        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, "<Escape>", self.mCancel)
        bind(self.top, '<Alt-Key>', 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()
            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
        MfxDialog.num_open -= 1
        if MfxDialog.num_open == 0 and self.parent.app is not None:
            self.parent.app.raiseAll()
        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('<<Invoke>>')

    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=True)
        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 = PysolButton(frame, text=s, state="disabled")
            else:
                widget = PysolButton(
                    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('<<Invoke>>')
            bind(self.top, "<Return>", cb)
            bind(self.top, "<KP_Enter>", 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)
        speech = Speech()
        parent.after(600, lambda: speech.speak(kw.text))
        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 = PysolCheckbutton(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)

        parent.after(500, lambda: app.speech.speak(kw.text + " " + kw.url))
        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
        labeltext = label
        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 = PysolEntry(top_frame, exportselection=1, width=w,
                              fieldname=labeltext)
        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("<Enter>", self._enter))
        self.bindings.append(self.widget.bind("<Leave>", self._leave))
        self.bindings.append(self.widget.bind("<ButtonPress>", 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("<Enter>", self.bindings[0])
            self.widget.unbind("<Leave>", self.bindings[1])
            self.widget.unbind("<ButtonPress>", 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, "<KeyPress-Left>", self.unit_left)
        bind(w, "<KeyPress-Right>", self.unit_right)

    def bindVbar(self, w=None):
        if w is None:
            w = self.canvas
        bind(w, "<KeyPress-Prior>", self.page_up)
        bind(w, "<KeyPress-Next>", self.page_down)
        bind(w, "<KeyPress-Up>", self.unit_up)
        bind(w, "<KeyPress-Down>", self.unit_down)
        bind(w, "<KeyPress-Begin>", self.scroll_top)
        bind(w, "<KeyPress-Home>", self.scroll_top)
        bind(w, "<KeyPress-End>", 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, '<MouseWheel>', 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('<ButtonPress>',
                                            self._buttonPressEvent))
            # self.bindings.append(label.bind('<Enter>', 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('<ButtonPress>', 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
        command = kw.get('command', None)
        self.command = 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
        if 'fieldname' in kw:
            self.field_name = kw['fieldname']
            del kw['fieldname']
        else:
            self.field_name = ''

        self.speech = Speech()

        # 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)
        self.scale.bind("<FocusIn>", self._scale_focus)

        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:
            self.speech.speak(value)
            if self.command:
                self.command(value)
        self.value = value

    def _scale_focus(self, event):
        self.speech.speak(self.field_name + " " + self.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.speech = Speech()
        if 'fieldname' in kw:
            label = kw['fieldname']
            del kw['fieldname']
        else:
            label = None

        self.value = kw.get('textvariable', tkinter.StringVar())
        kw['textvariable'] = self.value

        self._command = None
        if 'selectcommand' in kw:
            self._command = kw['selectcommand']
            del kw['selectcommand']
        ttk.Combobox.__init__(self, master, **kw)

        if label is not None:
            self.field_name = label
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

        self.bind("<Up>", lambda c: self._cycle_items(c, dir=-1))
        self.bind("<Down>", lambda c: self._cycle_items(c, dir=1))

        self.bind('<<ComboboxSelected>>', 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

    def _cycle_items(self, *args, **kw):
        direction = kw.get("dir", 0)
        values = self['values']
        current = self.current()

        new_index = (current + direction) % len(values)
        self.current(new_index)

        selected_label = values[new_index]
        self.speech.speak(selected_label)

        return "break"

    def _focus(self, event):
        self.speech.speak(self.field_name + " " + _("Select box") + " " +
                          str(self.value.get()))


# ************************************************************************
# * Other components (speech support)
# ************************************************************************

class PysolCheckbutton(ttk.Checkbutton):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'prefixtext' in kw:
            prefixtext = kw['prefixtext']
            del kw['prefixtext']
        else:
            prefixtext = None

        self.value = kw.get('variable', tkinter.BooleanVar())
        kw['variable'] = self.value
        ttk.Checkbutton.__init__(self, master, **kw)

        if 'text' in kw:
            if prefixtext is not None:
                self.field_name = prefixtext + " - " + kw['text']
            else:
                self.field_name = kw['text']
        elif prefixtext is not None:
            self.field_name = prefixtext
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        self.speech.speak(self.field_name + " " + _("Check box") + " " +
                          str(self.value.get()))


class PysolRadiobutton(ttk.Radiobutton):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'prefixtext' in kw:
            prefixtext = kw['prefixtext']
            del kw['prefixtext']
        else:
            prefixtext = None

        self.myvalue = kw.get('value', tkinter.StringVar())
        self.value = kw.get('variable', tkinter.StringVar())
        kw['variable'] = self.value
        ttk.Radiobutton.__init__(self, master, **kw)

        if 'text' in kw:
            if prefixtext is not None:
                self.field_name = prefixtext + " - " + kw['text']
            else:
                self.field_name = kw['text']
        elif prefixtext is not None:
            self.field_name = prefixtext
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        self.speech.speak(self.field_name + " " + _("Radio button") + " " +
                          str(self.value.get() == self.myvalue))


class PysolEntry(ttk.Entry):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'fieldname' in kw:
            label = kw['fieldname']
            del kw['fieldname']
        else:
            label = None

        self.value = kw.get('textvariable', tkinter.StringVar())
        kw['textvariable'] = self.value
        ttk.Entry.__init__(self, master, **kw)

        if label is not None:
            self.field_name = label
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        self.speech.speak(self.field_name + " " + _("Text box") + " " +
                          self.value.get())


class PysolText(tkinter.Text):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'fieldname' in kw:
            label = kw['fieldname']
            del kw['fieldname']
        else:
            label = None

        tkinter.Text.__init__(self, master, **kw)

        if label is not None:
            self.field_name = label
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        # Get all text content
        content = self.get("1.0", "end-1c")
        self.speech.speak(self.field_name + " " + _("Text area") + " " +
                          content)


class PysolButton(ttk.Button):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'prefixtext' in kw:
            prefixtext = kw['prefixtext']
            del kw['prefixtext']
        else:
            prefixtext = None

        if 'text' in kw:
            if prefixtext is not None:
                self.field_name = prefixtext + " - " + kw['text']
            else:
                self.field_name = kw['text']
        else:
            self.field_name = ''

        ttk.Button.__init__(self, master, **kw)

        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        self.speech.speak(self.field_name)


class PysolSpinbox(ttk.Spinbox):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        if 'fieldname' in kw:
            label = kw['fieldname']
            del kw['fieldname']
        else:
            label = None

        self.value = kw.get('textvariable', tkinter.StringVar())
        kw['textvariable'] = self.value
        ttk.Spinbox.__init__(self, master, **kw)

        if label is not None:
            self.field_name = label
        else:
            self.field_name = ''
        self.bind('<FocusIn>', self._focus)

    def _focus(self, event):
        self.speech.speak(self.field_name + " " + _("Spin box") + " " +
                          str(self.value.get()))


# Hidden screen reader text - use to store screen
# reader text for other widgets.
class PysolScreenReaderText(ttk.Label):
    def __init__(self, parent, text="", **kw):
        ttk.Label.__init__(self, parent, text=text, **kw)

    def screenreader_text(self):
        return self.cget("text")


class PysolNotebook(ttk.Notebook):
    def __init__(self, master=None, **kw):
        self.speech = Speech()
        ttk.Notebook.__init__(self, master, **kw)
        self.bind('<FocusIn>', self._focus)
        self.bind('<<NotebookTabChanged>>', self._focus)

    def _focus(self, event):
        current_tab_id = self.select()
        tab_text = self.tab(current_tab_id, "text")
        content_text = self._get_screenreader_text(
            self.nametowidget(current_tab_id))
        self.speech.speak(tab_text + " " + _("Tab") + " " + content_text)

    def _get_screenreader_text(self, frame):
        for child in frame.winfo_children():
            if isinstance(child, PysolScreenReaderText):
                return child.screenreader_text()
        return ""
