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
 
@@ -504,79 +504,78 @@ class Sliders(BCF2000):
 
        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.
 

	
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):
 
@@ -64,124 +64,127 @@ class GraphWatchers(object):
 
    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()
 
        
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)
0 comments (0 inline, 0 general)