Changeset - d7f1f868eb6c
[Not reviewed]
default
0 5 0
drewp@bigasterisk.com - 12 years ago 2012-10-20 21:52:10
drewp@bigasterisk.com
toplevel window pos is saved in the graph. Patch conflicts no longer break as hard, but they don't exactly reset themselves right yet eiher
Ignore-this: 56f96fd0b1a8602abc4e41851685794c
5 files changed with 61 insertions and 22 deletions:
0 comments (0 inline, 0 general)
bin/keyboardcomposer
Show inline comments
 
@@ -456,127 +456,126 @@ class KeyboardComposer(Frame, SubClient)
 
                subbox.scale.fade(value=0.0, length=0)
 

	
 
# move to web lib
 
def postArgGetter(request):
 
    """return a function that takes arg names and returns string
 
    values. Supports args encoded in the url or in postdata. No
 
    support for repeated args."""
 
    # this is something nevow normally does for me
 
    request.content.seek(0)
 
    fields = cgi.FieldStorage(request.content, request.received_headers,
 
                              environ={'REQUEST_METHOD': 'POST'})
 
    def getArg(n):
 
        try:
 
            return request.args[n][0]
 
        except KeyError:
 
            return fields[n].value
 
    return getArg
 

	
 

	
 
class LevelServerHttp(resource.Resource):
 
    isLeaf = True
 
    def __init__(self,name_to_subbox):
 
        self.name_to_subbox = name_to_subbox
 

	
 
    def render_POST(self, request):
 
        arg = postArgGetter(request)
 
        
 
        if request.path == '/fadesub':
 
            # fadesub?subname=scoop&level=0&secs=.2
 
            self.name_to_subbox[arg('subname')].scale.fade(
 
                float(arg('level')),
 
                float(arg('secs')))
 
            return "set %s to %s" % (arg('subname'), arg('level'))
 
        else:
 
            raise NotImplementedError(repr(request))
 

	
 
class Sliders(BCF2000):
 
    def __init__(self, kc):
 
        devices = ['/dev/snd/midiC1D0', '/dev/snd/midiC2D0', '/dev/snd/midiC3D0']
 
        for dev in devices:
 
            try:
 
                BCF2000.__init__(self, dev=dev)
 
            except IOError, e:
 
                if dev is devices[-1]:
 
                    raise
 
            else:
 
                break
 

	
 
        self.kc = kc
 
    def valueIn(self, name, value):
 
        kc = self.kc
 
        if name.startswith("slider"):
 
            kc.hw_slider_moved(int(name[6:]) - 1, value / 127)
 
        elif name.startswith("button-upper"):
 
            kc.change_row(kc.current_row)
 
        elif name.startswith("button-lower"):
 
            col = int(name[12:]) - 1
 
            self.valueOut(name, 0)
 
            try:
 
                tkslider = kc.slider_table[(kc.current_row, col)]
 
            except KeyError:
 
                return
 

	
 
            slider_var = tkslider.slider_var
 
            if slider_var.get() == 1:
 
                slider_var.set(0)
 
            else:
 
                slider_var.set(1)
 
        elif name.startswith("button-corner"):
 
            button_num = int(name[13:]) - 1
 
            if button_num == 1:
 
                diff = -1
 
            elif button_num == 3:
 
                diff = 1
 
            else:
 
                return
 

	
 
            kc.change_row(kc.current_row + diff)
 
            self.valueOut(name, 0)
 

	
 
if __name__ == "__main__":
 
    parser = OptionParser()
 
    parser.add_option('--no-sliders', action='store_true',
 
                      help="don't attach to hardware sliders")
 
    clientsession.add_option(parser)
 
    parser.add_option('-v', action='store_true', help="log info level")
 
    opts, args = parser.parse_args()
 

	
 
    logging.basicConfig(level=logging.INFO if opts.v else logging.WARN)
 
    log = logging.getLogger('keyboardcomposer')
 

	
 
    graph = SyncedGraph("keyboardcomposer")
 

	
 
    root = Tk()
 
    initTkdnd(root.tk, 'tkdnd/trunk/')
 

	
 
    # this has yet to be moved into the session graph
 
    session = clientsession.getUri('keyboardcomposer', opts)
 

	
 
    tl = toplevelat("Keyboard Composer - %s" % opts.session,
 
                    existingtoplevel=root)
 

	
 
    session = clientsession.getUri('keyboardcomposer', opts)
 
                    existingtoplevel=root, graph=graph, session=session)
 

	
 
    kc = KeyboardComposer(tl, graph, session,
 
                          hw_sliders=not opts.no_sliders)
 
    kc.pack(fill=BOTH, expand=1)
 

	
 
    for helpline in ["Bindings: B3 mute; C-l edit levels in subcomposer"]:
 
        tk.Label(root,text=helpline, font="Helvetica -12 italic",
 
                 anchor='w').pack(side='top',fill='x')
 

	
 
    if 0: # needs fixing, or maybe it's obsolete because other progs can just patch the rdf graph
 
        import twisted.internet
 
        try:
 
            reactor.listenTCP(networking.keyboardComposer.port,
 
                              server.Site(LevelServerHttp(kc.name_to_subbox)))
 
        except twisted.internet.error.CannotListenError, e:
 
            log.warn("Can't (and won't!) start level server:")
 
            log.warn(e)
 

	
 
    root.protocol('WM_DELETE_WINDOW', reactor.stop)
 
    
 
    tksupport.install(root,ms=10)
 

	
 

	
 
#    prof.watchPoint("/usr/lib/python2.4/site-packages/rdflib-2.3.3-py2.4-linux-i686.egg/rdflib/Graph.py", 615)
 

	
 
    prof.run(reactor.run, profile=False)
bin/rdfdb
Show inline comments
 
#!bin/python
 
"""
 
other tools POST themselves to here as subscribers to the graph. They
 
are providing a URL we can PUT to with graph updates.
 

	
 
we immediately PUT them back all the contents of the graph as a bunch
 
of adds.
 

	
 
later we PUT them back with patches (del/add lists) when there are
 
changes.
 

	
 
If we fail to reach a registered caller, we forget about it for future
 
calls. We could PUT empty diffs as a heartbeat to notice disappearing
 
callers faster.
 

	
 
A caller can submit a patch which we'll persist and broadcast to every
 
other client.
 

	
 
Global data undo should probably happen within this service.
 
Global data undo should probably happen within this service. Some
 
operations should not support undo, such as updating the default
 
position of a window. How will we separate those? A blacklist of
 
subj+pred pairs that don't save undo? Or just save the updates like
 
everything else, but when you press undo, there's a way to tell which
 
updates *should* be part of your app's undo system?
 

	
 
Maybe some subgraphs are for transient data (e.g. current timecode,
 
mouse position in curvecalc) that only some listeners want to hear about.
 

	
 
Deletes are graph-specific, so callers may be surprised to delete a
 
stmt from one graph but then find that statement is still true.
 

	
 
Alternate plan: would it help to insist that every patch is within
 
only one subgraph? I think it's ok for them to span multiple ones.
 

	
 
Inserts can be made on any subgraphs, and each subgraph is saved in
 
its own file. The file might not be in a format that can express
 
graphs, so I'm just going to not store the subgraph URI in any file.
 

	
 
I don't support wildcard deletes, and there are race conditions where a
 
s-p could end up with unexpected multiple objects. Every client needs
 
to be ready for this.
 

	
 
We watch the files and push their own changes back to the clients.
 

	
 
Persist our client list, to survive restarts. In another rdf file? A
 
random json one? memcache? Also hold the recent changes. We're not
 
logging everything forever, though, since the output files and a VCS
 
shall be used for that
 

	
 
Bnodes: this rdfdb graph might be able to track bnodes correctly, and
 
they make for more compact n3 files. I'm not sure if it's going to be
 
hard to keep the client bnodes in sync though. File rereads would be
 
hard, if ever a bnode was used across graphs, so that probably should
 
not be allowed.
 

	
 
Our API:
 

	
 
GET /  ui
 
GET /graph    the whole graph, or a query from it (needed? just for ui browsing?)
 
PUT /patches  clients submit changes
 
GET /patches  (recent) patches from clients
 
POST /graphClients clientUpdate={uri} to subscribe
 
GET /graphClients  current clients
 

	
 
format:
 
json {"adds" : [[quads]...],
 
      "deletes": [[quads]],
 
      "senderUpdateUri" : tooluri,
 
      "created":tttt // maybe to help resolve some conflicts
 
     }
 
maybe use some http://json-ld.org/ in there.
 

	
 
proposed rule feature:
 
rdfdb should be able to watch a pair of (sourceFile, rulesFile) and
 
rerun the rules when either one changes. Should the sourceFile be able
 
to specify its own rules file?  That would be easier
 
configuration. How do edits work? Not allowed?  Patch the source only?
 
Also see the source graph loaded into a different ctx, and you can
 
edit that one and see the results in the output context?
 

	
 
Our web ui:
 

	
 
  sections
 

	
 
    registered clients
 

	
 
    recent patches, each one says what client it came from. You can reverse
 
    them here. We should be able to take patches that are close in time
 
    and keep updating the same data (e.g. a stream of changes as the user
 
    drags a slider) and collapse them into a single edit for clarity.
 

	
 
        Ways to display patches, using labels and creator/subj icons
 
        where possible:
 

	
 
          <creator> set <subj>'s <p> to <o>
 
          <creator> changed <subj>'s <pred> from <o1> to <o2>
 
          <creator> added <o> to <s> <p>
 

	
 
    raw messages for debugging this client
 

	
 
    ctx urls take you to->
 
    files, who's dirty, have we seen external changes, notice big
 
    files that are taking a long time to save
 

	
 
    graph contents. plain rdf browser like an outliner or
 
    something. clicking any resource from the other displays takes you
 
    to this, focused on that resource
 

	
 
"""
 
from twisted.internet import reactor
 
import twisted.internet.error
 
import sys, optparse, logging, json, os
 
import cyclone.web, cyclone.httpclient, cyclone.websocket
 
sys.path.append(".")
 
from light9 import networking, showconfig, prof
 
from rdflib import ConjunctiveGraph, URIRef, Graph
 
from light9.rdfdb.graphfile import GraphFile
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 
from light9.rdfdb import syncedgraph
light9/rdfdb/syncedgraph.py
Show inline comments
 
from rdflib import ConjunctiveGraph, RDFS, RDF, Graph
 
import logging, cyclone.httpclient, traceback, urllib
 
from twisted.internet import reactor
 
from twisted.internet import reactor, defer
 
log = logging.getLogger('syncedgraph')
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 

	
 
def sendPatch(putUri, patch, **kw):
 
    """
 
    kwargs will become extra attributes in the toplevel json object
 
    """
 
    body = patch.makeJsonRepr(kw)
 
    log.debug("send body: %r", body)
 
    def ok(done):
 
        if not str(done.code).startswith('2'):
 
            raise ValueError("sendPatch request failed %s: %s" % (done.code, done.body))
 
        log.debug("sendPatch finished, response: %s" % done.body)
 
        return done
 

	
 
    return cyclone.httpclient.fetch(
 
        url=putUri,
 
        method='PUT',
 
        headers={'Content-Type': ['application/json']},
 
        postdata=body,
 
        ).addCallback(ok)
 

	
 
def makePatchEndpointPutMethod(cb):
 
    def put(self):
 
        try:
 
            p = Patch(jsonRepr=self.request.body)
 
            log.info("received patch -%d +%d" % (len(p.delGraph), len(p.addGraph)))
 
            cb(p)
 
        except:
 
            traceback.print_exc()
 
            raise
 
    return put
 

	
 
def makePatchEndpoint(cb):
 
    class Update(cyclone.web.RequestHandler):
 
        put = makePatchEndpointPutMethod(cb)
 
    return Update
 

	
 
class GraphWatchers(object):
 
    """
 
    store the current handlers that care about graph changes
 
    """
 
    def __init__(self):
 
        self._handlersSp = {} # (s,p): set(handlers)
 
        self._handlersPo = {} # (p,o): set(handlers)
 

	
 
    def addSubjPredWatcher(self, func, s, p):
 
        if func is None:
 
            return
 
        key = s, p
 
        try:
 
            self._handlersSp.setdefault(key, set()).add(func)
 
        except Exception:
 
            log.error("with key %r and func %r" % (key, func))
 
            raise
 

	
 
    def addPredObjWatcher(self, func, p, o):
 
        self._handlersPo.setdefault((p, o), set()).add(func)
 

	
 
    def whoCares(self, patch):
 
        """what handler functions would care about the changes in this patch?
 

	
 
        this removes the handlers that it gives you
 
        """
 
        #self.dependencies()
 
        affectedSubjPreds = set([(s, p) for s, p, o, c in patch.addQuads]+
 
                                [(s, p) for s, p, o, c in patch.delQuads])
 
        affectedPredObjs = set([(p, o) for s, p, o, c in patch.addQuads]+
 
                                [(p, o) for s, p, o, c in patch.delQuads])
 
        
 
        ret = set()
 
        for (s, p), funcs in self._handlersSp.iteritems():
 
            if (s, p) in affectedSubjPreds:
 
                ret.update(funcs)
 
                funcs.clear()
 
                
 
        for (p, o), funcs in self._handlersPo.iteritems():
 
            if (p, o) in affectedPredObjs:
 
                ret.update(funcs)
 
                funcs.clear()
 

	
 
        return ret
 

	
 
    def dependencies(self):
 
        """
 
        for debugging, make a list of all the active handlers and what
 
        data they depend on. This is meant for showing on the web ui
 
        for browsing.
 
        """
 
        log.info("whocares:")
 
        from pprint import pprint
 
        pprint(self._handlersSp)
 
        
 

	
 
class PatchSender(object):
 
    """
 
    SyncedGraph may generate patches faster than we can send
 
    them. This object buffers and may even collapse patches before
 
    they go the server
 
    """
 
    def __init__(self, target, myUpdateResource):
 
        self.target = target
 
        self.myUpdateResource = myUpdateResource
 
        self._patchesToSend = []
 
        self._currentSendPatchRequest = None
 

	
 
    def sendPatch(self, p):
 
        self._patchesToSend.append(p)
 
        sendResult = defer.Deferred()
 
        self._patchesToSend.append((p, sendResult))
 
        self._continueSending()
 
        return sendResult
 

	
 
    def _continueSending(self):
 
        if not self._patchesToSend or self._currentSendPatchRequest:
 
            return
 
        if len(self._patchesToSend) > 1:
 
            log.info("%s patches left to send", len(self._patchesToSend))
 
            # this is where we could concatenate little patches into a
 
            # bigger one. Often, many statements will cancel each
 
            # other out. not working yet:
 
            if 0:
 
                p = self._patchesToSend[0].concat(self._patchesToSend[1:])
 
                print "concat down to"
 
                print 'dels'
 
                for q in p.delQuads: print q
 
                print 'adds'
 
                for q in p.addQuads: print q
 
                print "----"
 
            else:
 
                p = self._patchesToSend.pop(0)
 
                p, sendResult = self._patchesToSend.pop(0)
 
        else:
 
            p = self._patchesToSend.pop(0)
 
            p, sendResult = self._patchesToSend.pop(0)
 
            
 
        self._currentSendPatchRequest = sendPatch(
 
            self.target, p, senderUpdateUri=self.myUpdateResource)
 
        self._currentSendPatchRequest.addCallbacks(self._sendPatchDone,
 
                                                   self._sendPatchErr)
 
        self._currentSendPatchRequest.chainDeferred(sendResult)
 

	
 
    def _sendPatchDone(self, result):
 
        self._currentSendPatchRequest = None
 
        self._continueSending()
 

	
 
    def _sendPatchErr(self, e):
 
        self._currentSendPatchRequest = None
 
        # we're probably out of sync with the master now, since
 
        # SyncedGraph.patch optimistically applied the patch to our
 
        # local graph already. What happens to this patch? What
 
        # happens to further pending patches? Some of the further
 
        # patches, especially, may be commutable with the bad one and
 
        # might still make sense to apply to the master graph.
 

	
 
        # if someday we are folding pending patches together, this
 
        # would be the time to UNDO that and attempt the original
 
        # separate patches again
 

	
 
        # this should screen for 409 conflict responses and raise a
 
        # special exception for that, so SyncedGraph.sendFailed can
 
        # screen for only that type
 

	
 
        # this code is going away; we're going to raise an exception that contains all the pending patches
 
        log.error("_sendPatchErr")
 
        log.error(e)
 
        self._continueSending()
 
        
 

	
 
class SyncedGraph(object):
 
    """
 
    graph for clients to use. Changes are synced with the master graph
 
    in the rdfdb process.
 

	
 
    This api is like rdflib.Graph but it can also call you back when
 
    there are graph changes to the parts you previously read.
 

	
 
    If we get out of sync, we abandon our local graph (even any
 
    pending local changes) and get the data again from the
 
    server.
 
    """
 
    def __init__(self, label):
 
        """
 
        label is a string that the server will display in association
 
        with your connection
 
        """
 
        _graph = self._graph = ConjunctiveGraph()
 
        self._watchers = GraphWatchers()
 
        
 
        def onPatch(p):
 
            """
 
            central server has sent us a patch
 
            """
 
            patchQuads(_graph, p.delQuads, p.addQuads, perfect=True)
 
            log.info("graph now has %s statements" % len(_graph))
 
            try:
 
                self.updateOnPatch(p)
 
            except Exception:
 
                # don't reflect this back to the server; we did
 
                # receive its patch correctly.
 
                traceback.print_exc()
 

	
 
        listen = reactor.listenTCP(0, cyclone.web.Application(handlers=[
 
            (r'/update', makePatchEndpoint(onPatch)),
 
        ]))
 
        port = listen._realPortNumber  # what's the right call for this?
 
        self.updateResource = 'http://localhost:%s/update' % port
 
        log.info("listening on %s" % port)
 
        self.register(label)
 
        self.currentFuncs = [] # stack of addHandler callers
 
        self._sender = PatchSender('http://localhost:8051/patches',
 
                                   self.updateResource)
 

	
 
    def resync(self):
 
        """
 
        get the whole graph again from the server (e.g. we had a
 
        conflict while applying a patch and want to return to the
 
        truth).
 

	
 
        To avoid too much churn, we remember our old graph and diff it
 
        against the replacement. This way, our callers only see the
 
        corrections.
 

	
 
        Edits you make during a resync will surely be lost, so I
 
        should just fail them. There should be a notification back to
 
        UIs who want to show that we're doing a resync.
 
        """
 
        return cyclone.httpclient.fetch(
 
            url="http://localhost:8051/graph",
 
            method="GET",
 
            headers={'Accept':'x-trig'},
 
            ).addCallback(self._resyncGraph)
 

	
 
    def _resyncGraph(self, response):
 
        pass
 
        #diff against old entire graph
 
        #broadcast that change
light9/rdfdb/web/index.xhtml
Show inline comments
 
<?xml version="1.0" encoding="utf8"?>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
 
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
 
  <head>
 
    <title>rdfdb</title>
 
    <link rel="stylesheet" type="text/css" href="style.css"/>
 
  </head>
 
  <body>
 
    <h1>rdfdb</h1>
 
    <p>status: <span id="status">starting...</span></p>
 
    
 
    <section>
 
      <h2>Edits</h2>
 
      <div id="patches"></div>
 
    </section>
 

	
 
    <p>Clients: <span id="clients"/></p>
 

	
 
    <fieldset>
 
      <legend>Messages</legend>
 
      <div id="out"></div>
 
    </fieldset>
 

	
 
    <script type="text/javascript" src="lib/jquery-1.7.2.min.js"></script>
 
    <script type="text/javascript" src="websocket.js"></script>
 
    <script type="text/javascript">
 
      // <![CDATA[
 
      $(function(){
 

	
 
          function collapseCuries(s) {
 
              return s
 
                  .replace(/<http:\/\/www.w3.org\/2001\/XMLSchema#/g, function (match, short) { return "xsd:"+short; })
 
                  .replace(/<http:\/\/light9.bigasterisk.com\/(.*?)>/g, function (match, short) { return "light9:"+short; })
 
                  .replace(/<http:\/\/light9.bigasterisk.com\/show\/dance2012\/sessions\/(.*?)>/g, function (match, short) { return "kcsession:"+short });
 
          }
 

	
 
          function onMessage(d) {
 
              if (d.clients !== undefined) {
 
                  $("#clients").empty().text(JSON.stringify(d.clients));
 
              }
 
              if (d.patch !== undefined) {
 
                  $("#patches").prepend(
 
                      $("<fieldset>").addClass("patch")
 
                          .append($("<legend>").text("Patch"))
 
                          .append($("<div>").addClass("deletes").text(d.patch.deletes))
 
                          .append($("<div>").addClass("adds").text(d.patch.adds))
 
                          .append($("<div>").addClass("deletes").text(collapseCuries(d.patch.deletes)))
 
                          .append($("<div>").addClass("adds").text(collapseCuries(d.patch.adds)))
 
                  );
 
              }
 

	
 
              $('#out').append($('<div>').text(JSON.stringify(d)));
 
          }
 
          reconnectingWebSocket("ws://localhost:8051/live", onMessage);
 
      });
 
      // ]]>
 
    </script>
 

	
 
  </body>
 
</html>
 
\ No newline at end of file
light9/uihelpers.py
Show inline comments
 
"""all the tiny tk helper functions"""
 

	
 
from __future__ import nested_scopes
 
from Tkinter import *
 
from Tix import *
 
from types import StringType
 
#from Tkinter import Button
 
from rdflib import Literal
 
from Tix import Button, Toplevel, Tk, IntVar, Entry, DoubleVar
 
from light9.namespaces import L9
 

	
 
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, e:
 
        # it's ok if there's no saved geometry
 
        pass
 

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

	
 
    try:
 
        f=open(".light9-window-geometry-%s" % name.replace(' ','_'))
 
        windowlocations[name]=f.read() # file has no newline
 
    except:
 
        # it's ok if there's no saved geometry
 
        pass
 
    lastSaved = [None]
 
    setOnce = [False]
 
    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)
 

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

	
 
    def savePos():
 
        geo = tl.geometry()
 
        # todo: need a way to filter out the startup window sizes that
 
        # weren't set by the user
 
        if geo.startswith("1x1") or geo.startswith(("378x85", "378x86")):
 
            return
 
        if geo == lastSaved[0]:
 
            return
 
        lastSaved[0] = 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]))
 

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

	
 
    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 t
 

	
 
def printevent(ev):
 
    for k in dir(ev):
 
        if not k.startswith('__'):
 
            print k,getattr(ev,k)
 
    print ""
 
    
 
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,050)
 
    out = [int(l+lev*(h-l)) for h,l in zip(high,low)]
 
    col="#%02X%02X%02X" % tuple(out)
 
    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)
 
    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)
0 comments (0 inline, 0 general)