view src/light9/uihelpers.py @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/uihelpers.py@17bee25a20cb
children
line wrap: on
line source

"""all the tiny tk helper functions"""

#from Tkinter import Button
import logging, time
from rdflib import Literal
from tkinter.tix import Button, Toplevel, Tk, IntVar, Entry, DoubleVar
import tkinter
from light9.namespaces import L9
from typing import Dict

log = logging.getLogger("toplevel")

windowlocations = {
    'sub': '425x738+00+00',
    'console': '168x24+848+000',
    'leveldisplay': '144x340+870+400',
    'cuefader': '314x212+546+741',
    'effect': '24x24+0963+338',
    'stage': '823x683+37+030',
    'scenes': '504x198+462+12',
}


def bindkeys(root, key, func):
    root.bind(key, func)
    for w in root.winfo_children():
        w.bind(key, func)


def toplevel_savegeometry(tl, name):
    try:
        geo = tl.geometry()
        if not geo.startswith("1x1"):
            f = open(".light9-window-geometry-%s" % name.replace(' ', '_'), 'w')
            f.write(tl.geometry())
        # else the window never got mapped
    except Exception:
        # it's ok if there's no saved geometry
        pass


def toplevelat(name, existingtoplevel=None, graph=None, session=None):
    tl = existingtoplevel or Toplevel()
    tl.title(name)

    lastSaved = [None]
    setOnce = [False]
    graphSetTime = [0.0]

    def setPosFromGraphOnce():
        """
        the graph is probably initially empty, but as soon as it gives
        us one window position, we stop reading them
        """
        if setOnce[0]:
            return
        geo = graph.value(session, L9['windowGeometry'])
        log.debug("setPosFromGraphOnce %s", geo)

        setOnce[0] = True
        graphSetTime[0] = time.time()
        if geo is not None and geo != lastSaved[0]:
            tl.geometry(geo)
            lastSaved[0] = geo

    def savePos(ev):
        geo = tl.geometry()
        if not isinstance(ev.widget, (Tk, tkinter.Tk)):
            # I think these are due to internal widget size changes,
            # not the toplevel changing
            return
        # this is trying to not save all the startup automatic window
        # sizes. I don't have a better plan for this yet.
        if graphSetTime[0] == 0 or time.time() < graphSetTime[0] + 3:
            return
        if not setOnce[0]:
            return
        lastSaved[0] = geo
        log.debug("saving position %s", geo)
        graph.patchObject(session, session, L9['windowGeometry'], Literal(geo))

    if graph is not None and session is not None:
        graph.addHandler(setPosFromGraphOnce)

    if name in windowlocations:
        tl.geometry(positionOnCurrentDesktop(windowlocations[name]))

    if graph is not None:
        tl._toplevelat_funcid = tl.bind(
            "<Configure>", lambda ev, tl=tl, name=name: savePos(ev))

    return tl


def positionOnCurrentDesktop(xform, screenWidth=1920, screenHeight=1440):
    size, x, y = xform.split('+')
    x = int(x) % screenWidth
    y = int(y) % screenHeight
    return "%s+%s+%s" % (size, x, y)


def toggle_slider(s):
    if s.get() == 0:
        s.set(100)
    else:
        s.set(0)


# for lambda callbacks
def printout(t):
    print('printout', t)


def printevent(ev):
    for k in dir(ev):
        if not k.startswith('__'):
            print('ev', k, getattr(ev, k))


def eventtoparent(ev, sequence):
    "passes an event to the parent, screws up TixComboBoxes"

    wid_class = str(ev.widget.__class__)
    if wid_class == 'Tix.ComboBox' or wid_class == 'Tix.TixSubWidget':
        return

    evdict = {}
    for x in ['state', 'time', 'y', 'x', 'serial']:
        evdict[x] = getattr(ev, x)


#    evdict['button']=ev.num
    par = ev.widget.winfo_parent()
    if par != ".":
        ev.widget.nametowidget(par).event_generate(sequence, **evdict)
    #else the event made it all the way to the top, unhandled


def colorlabel(label):
    """color a label based on its own text"""
    txt = label['text'] or "0"
    lev = float(txt) / 100
    low = (80, 80, 180)
    high = (255, 55, 0o50)
    out = [int(l + lev * (h - l)) for h, l in zip(high, low)]
    col = "#%02X%02X%02X" % tuple(out)  # type: ignore
    label.config(bg=col)


# TODO: get everyone to use this
def colorfade(low, high, percent):
    '''not foolproof.  make sure 0 < percent < 1'''
    out = [int(l + percent * (h - l)) for h, l in zip(high, low)]
    col = "#%02X%02X%02X" % tuple(out)  # type: ignore
    return col


def colortotuple(anytkobj, colorname):
    'pass any tk object and a color name, like "yellow"'
    rgb = anytkobj.winfo_rgb(colorname)
    return [v / 256 for v in rgb]


class Togglebutton(Button):
    """works like a single radiobutton, but it's a button so the
    label's on the button face, not to the side. the optional command
    callback is called on button set, not on unset. takes a variable
    just like a checkbutton"""

    def __init__(self,
                 parent,
                 variable=None,
                 command=None,
                 downcolor='red',
                 **kw):

        self.oldcommand = command
        Button.__init__(self, parent, command=self.invoke, **kw)

        self._origbkg = self.cget('bg')
        self.downcolor = downcolor

        self._variable = variable
        if self._variable:
            self._variable.trace('w', self._varchanged)
            self._setstate(self._variable.get())
        else:
            self._setstate(0)

        self.bind("<Return>", self.invoke)
        self.bind("<1>", self.invoke)
        self.bind("<space>", self.invoke)

    def _varchanged(self, *args):
        self._setstate(self._variable.get())

    def invoke(self, *ev):
        if self._variable:
            self._variable.set(not self.state)
        else:
            self._setstate(not self.state)

        if self.oldcommand and self.state:  # call command only when state goes to 1
            self.oldcommand()
        return "break"

    def _setstate(self, newstate):
        self.state = newstate
        if newstate:  # set
            self.config(bg=self.downcolor, relief='sunken')
        else:  # unset
            self.config(bg=self._origbkg, relief='raised')
        return "break"


class FancyDoubleVar(DoubleVar):

    def __init__(self, master=None):
        DoubleVar.__init__(self, master)
        self.callbacklist: Dict[str, str] = {}  # cbname : mode
        self.namedtraces: Dict[str, str] = {}  # name : cbname

    def trace_variable(self, mode, callback):
        """Define a trace callback for the variable.

        MODE is one of "r", "w", "u" for read, write, undefine.
        CALLBACK must be a function which is called when
        the variable is read, written or undefined.

        Return the name of the callback.
        """
        cbname = self._master._register(callback)
        self._tk.call("trace", "variable", self._name, mode, cbname)

        # we build a list of the trace callbacks (the py functrions and the tcl functionnames)
        self.callbacklist[cbname] = mode
        #        print "added trace:",callback,cbname

        return cbname

    trace = trace_variable

    def disable_traces(self):
        for cb, mode in list(self.callbacklist.items()):
            #            DoubleVar.trace_vdelete(self,v[0],k)
            self._tk.call("trace", "vdelete", self._name, mode, cb)
            # but no master delete!

    def recreate_traces(self):
        for cb, mode in list(self.callbacklist.items()):
            #            self.trace_variable(v[0],v[1])
            self._tk.call("trace", "variable", self._name, mode, cb)

    def trace_named(self, name, callback):
        if name in self.namedtraces:
            print(
                "FancyDoubleVar: already had a trace named %s - replacing it" %
                name)
            self.delete_named(name)

        cbname = self.trace_variable(
            'w', callback)  # this will register in self.callbacklist too

        self.namedtraces[name] = cbname
        return cbname

    def delete_named(self, name):
        if name in self.namedtraces:

            cbname = self.namedtraces[name]

            self.trace_vdelete('w', cbname)
            #self._tk.call("trace","vdelete",self._name,'w',cbname)
            print("FancyDoubleVar: successfully deleted trace named %s" % name)
        else:
            print(
                "FancyDoubleVar: attempted to delete named %s which wasn't set to any function"
                % name)


def get_selection(listbox):
    'Given a listbox, returns first selection as integer'
    selection = int(listbox.curselection()[0])  # blech
    return selection


if __name__ == '__main__':
    root = Tk()
    root.tk_focusFollowsMouse()
    iv = IntVar()

    def cb():
        print("cb!")

    t = Togglebutton(root, text="testbutton", command=cb, variable=iv)
    t.pack()
    Entry(root, textvariable=iv).pack()
    root.mainloop()