Changeset - 321fc6150ee3
[Not reviewed]
default
0 8 2
drewp@bigasterisk.com - 12 years ago 2013-03-26 07:04:22
drewp@bigasterisk.com
subcomposer's nice currently-editing DnD box
Ignore-this: f2a8542a4ab38dbe61b26c864da3bace
10 files changed with 494 insertions and 176 deletions:
0 comments (0 inline, 0 general)
bin/keyboardcomposer
Show inline comments
 
@@ -120,26 +120,13 @@ class SubmasterBox(Frame):
 
           ?session :subSetting [a :SubSetting; :sub ?s; :level ?l]
 
        """
 
        # move to syncedgraph patchMapping
 
        with self.sub.graph.currentState() as graph:
 
            adds = set([])
 
            for setting in graph.objects(self.session, L9['subSetting']):
 
                if graph.value(setting, L9['sub']) == uri:
 
                    break
 
            else:
 
                setting = URIRef(self.session + "/setting/%s" %
 
                                 random.randrange(999999999))
 
                adds.update([
 
                    (self.session, L9['subSetting'], setting, self.session),
 
                    (setting, RDF.type, L9['SubSetting'], self.session),
 
                    (setting, L9['sub'], uri, self.session),
 
                    ])
 
            dels = set([])
 
            for prev in graph.objects(setting, L9['level']):
 
                dels.add((setting, L9['level'], prev, self.session))
 
            adds.add((setting, L9['level'], Literal(level), self.session))
 

	
 
            if adds != dels:
 
                self.sub.graph.patch(Patch(delQuads=dels, addQuads=adds))
 
        self.sub.graph.patchMapping(context=self.session,
 
                                    subject=self.session,
 
                                    predicate=L9['subSetting'],
 
                                    nodeClass=L9['SubSetting'],
 
                                    keyPred=L9['sub'], newKey=uri,
 
                                    valuePred=L9['level'], newValue=Literal(level))
 

	
 
    def updateLevelFromGraph(self):
 
        """read rdf level, write it to subbox.slider_var"""
bin/subcomposer
Show inline comments
 
#!bin/python
 
"""
 
subcomposer
 
  session
 
  observable(currentSub), a Submaster which tracks the graph
 

	
 
    EditChoice widget
 
      can change currentSub to another sub
 

	
 
    Levelbox widget
 
      watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets
 

	
 
        OneLevel widget
 
          UI edits are caught here and go all the way back to currentSub
 

	
 

	
 
"""
 
from __future__ import division, nested_scopes
 
import time, logging
 
from optparse import OptionParser
 
import logging
 
import Tkinter as tk
 
@@ -10,14 +26,12 @@ from rdflib import URIRef
 

	
 
from run_local import log
 
from light9.dmxchanedit import Levelbox
 
from light9 import dmxclient, Patch, Submaster, showconfig, prof
 
from light9 import dmxclient, Patch, Submaster, prof
 
from light9.uihelpers import toplevelat
 
from light9.rdfdb.syncedgraph import SyncedGraph
 
from light9.rdfdb import clientsession
 
from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister
 

	
 
log.setLevel(logging.DEBUG)
 

	
 
class _NoNewVal(object):
 
    pass
 

	
 
@@ -94,6 +108,7 @@ class EditChoice(tk.Frame):
 

	
 

	
 
    def uriChanged(self, newUri):
 
        print "chg", newUri
 
        # i guess i show the label and icon for this
 
        if newUri is Local:
 
            self.subIcon.config(text="(local)")
 
@@ -110,70 +125,98 @@ class Subcomposer(tk.Frame):
 
    editing. If we don't have a currentSub, then we're actually
 
    editing a session-local sub called <session> l9:currentSub <sessionLocalSub>
 

	
 
    I'm not sure that Locals should even be PersistentSubmaster with
 
    uri and graph storage, but I think that way is making fewer
 
    special cases.
 

	
 
    Contains an EditChoice widget
 

	
 
    UI actions:
 
    - drag a sub uri on here to make it the one we're editing
 

	
 
    - button to clear the currentSub (putting it back to
 
      sessionLocalSub, and also resetting sessionLocalSub to be empty
 
      again)
 

	
 
    - drag the sub uri off of here to send it to another receiver, but
 
      session local sub is not easily addressable elsewhere
 
    Dependencies:
 

	
 
    - make a new sub: transfers the current data (from a shared sub or
 
      from the local one) to the new sub. If you're on a local sub,
 
      the new sub is named automatically, ideally something brief,
 
      pretty distinct, readable, and based on the lights that are
 
      on. If you're on a named sub, the new one starts with a
 
      'namedsub 2' style name. The uri can also be with a '2' suffix,
 
      although maybe that will be stupid. If you change the name
 
      before anyone knows about this uri, we could update the current
 
      sub's uri to a slug of the new label.
 
      graph (?session :currentSub ?s) -> self.currentSub
 
      self.currentSub -> graph
 
      self.currentSub -> self._currentChoice (which might be Local)
 
      self._currentChoice (which might be Local) -> self.currentSub
 

	
 
    - rename this sub: not available if you're on a local sub. Sets
 
      the label of a named sub. Might update the uri of the named sub
 
      if it's new enough that no one else would have that uri. Not
 
      sure where we measure that 'new enough' value. Maybe track if
 
      the sub has 'never been dragged out of this subcomposer
 
      session'? But subs will also show up in other viewers and
 
      finders.
 
      inside the current sub:
 
      graph -> Submaster levels (handled in Submaster)
 
      Submaster levels -> OneLevel widget
 
      OneLevel widget -> Submaster.editLevel
 
      Submaster.editLevel -> graph (handled in Submaster)
 

	
 
    """
 
    def __init__(self, master, graph, session):
 
        tk.Frame.__init__(self, master, bg='black')
 
        self.graph = graph
 
        self.session = session
 
        self.currentSub = Submaster.PersistentSubmaster(graph, URIRef('http://hello'))
 

	
 
        self.levelbox = Levelbox(self, graph)
 
        self.levelbox.pack(side='top')
 
        # this is a PersistentSubmaster (even for local) or None if we're not initialized
 
        self.currentSub = Observable(None)
 

	
 
        currentUri = Observable(Local)
 
        currentUri = Observable("http://curr")
 

	
 
        def pc(val):
 
            log.info("change viewed sub to %s", val)
 
            print "change viewed sub to", val
 
        currentUri.subscribe(pc)
 

	
 
        EditChoice(self, self.graph, currentUri).frame.pack(side='top')
 
        EditChoice(self, self.graph, self._currentChoice).frame.pack(side='top')
 

	
 
    def setupSubChoiceLinks(self):
 

	
 
        def ann():
 
            print "currently: session=%s currentSub=%r _currentChoice=%r" % (
 
                self.session, self.currentSub(), self._currentChoice())
 

	
 
        def alltozero():
 
            for lev in self.levelbox.levels:
 
                lev.setlevel(0)
 
        @graph.addHandler
 
        def graphChanged():
 
            s = graph.value(self.session, L9['currentSub'])
 
            if s is None:
 
                s = self.makeLocal()
 
            self.currentSub(Submaster.PersistentSubmaster(graph, s))
 

	
 
        tk.Button(self, text="all to zero", command=alltozero).pack(side='top')
 
        @self.currentSub.subscribe
 
        def subChanged(newSub):
 
            if newSub is None:
 
                graph.patchObject(self.session,
 
                                  self.session, L9['currentSub'], None)
 
                return
 
            self.sendupdate()
 
            graph.patchObject(self.session,
 
                              self.session, L9['currentSub'], newSub.uri)
 

	
 
        dispatcher.connect(self.sendupdate, "levelchanged")
 
            if newSub and 'local' in newSub.uri: # wrong- use rdf:type or something?
 
                self._currentChoice(Local)
 
            else:
 
                # i think right here is the point that the last local
 
                # becomes garbage, and we could clean it up. 
 
                self._currentChoice(newSub.uri)
 

	
 
        dispatcher.connect(self.levelsChanged, "sub levels changed")
 
            
 
        @self._currentChoice.subscribe
 
        def choiceChanged(newChoice):
 
            if newChoice is Local:
 
                newChoice = self.makeLocal()
 
            if newChoice is not None:
 
                self.currentSub(Submaster.PersistentSubmaster(
 
                    graph, newChoice))
 

	
 
    def switchToLocalSub(self, *args):
 
        """
 
        stop editing a shared sub and go back to our local sub
 
        """
 
    def levelsChanged(self, sub):
 
        if sub == self.currentSub():
 
            self.sendupdate()
 
        
 
    def makeLocal(self):
 
        # todo: put a type on this, so subChanged can identify it right
 
        # todo: where will these get stored, or are they local to this
 
        # subcomposer process and don't use PersistentSubmaster at all?
 
        return URIRef("http://local/%s" % time.time())
 
        
 
    def setupLevelboxUi(self):
 
        self.levelbox = Levelbox(self, graph, self.currentSub)
 
        self.levelbox.pack(side='top')
 

	
 
    def fill_both_boxes(self, subname):
 
        for box in [self.savebox, self.loadbox]:
 
            box.set(subname)
 
        tk.Button(self, text="All to zero",
 
             command=lambda *args: self.currentSub().clear()).pack(side='top')
 

	
 
    def savenewsub(self, subname):
 
        leveldict={}
 
@@ -184,58 +227,49 @@ class Subcomposer(tk.Frame):
 
        s=Submaster.Submaster(subname,leveldict=leveldict)
 
        s.save()
 

	
 
    # this is going to be more like 'tie to sub' and 'untied'
 
    def loadsub(self, subname):
 
        """puts a sub into the levels, replacing old level values"""
 
        s=Submaster.Submasters(showconfig.getGraph()).get_sub_by_name(subname)
 
        self.set_levels(s.get_dmx_list())
 
        dispatcher.send("levelchanged")
 

	
 
    def toDmxLevels(self):
 
        # the dmx levels we edit and output, range is 0..1 (dmx chan 1 is
 
        # the 0 element)
 
        out = {}
 
        for lev in self.levelbox.levels:
 
            out[lev.channelnum] = lev.currentlevel
 
        if not out:
 
            return []
 

	
 
        return [out.get(i, 0) for i in range(max(out.keys()) + 1)]
 

	
 
    def sendupdate(self):
 
        dmxclient.outputlevels(self.toDmxLevels(), twisted=True)
 
        d = self.currentSub().get_dmx_list()
 
        dmxclient.outputlevels(d, twisted=True)
 

	
 

	
 
class EntryCommand(tk.Frame):
 
    def __init__(self, master, verb="Save", cmd=None):
 
        tk.Frame.__init__(self, master, bd=2, relief='raised')
 
        tk.Label(self, text="Sub name:").pack(side='left')
 
        self.cmd = cmd
 
        self.entry = tk.Entry(self)
 
        self.entry.pack(side='left', expand=True, fill='x')
 
def launch(opts, args, root, graph, session):
 
    if not opts.no_geometry:
 
        toplevelat("subcomposer - %s" % opts.session, root, graph, session)
 

	
 
    sc = Subcomposer(root, graph, session)
 
    sc.pack()
 

	
 
        self.entry.bind("<Return>", self.action)
 
        tk.Button(self, text=verb, command=self.action).pack(side='left')
 
    tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump",
 
             font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x')
 

	
 
    if len(args) == 1:
 
        # it might be a little too weird that cmdline arg to this
 
        # process changes anything else in the same session. But also
 
        # I'm not sure who would ever make 2 subcomposers of the same
 
        # session (except when quitting and restarting, to get the
 
        # same window pos), so maybe it doesn't matter. But still,
 
        # this tool should probably default to making new sessions
 
        # usually instead of loading the same one
 

	
 
    def action(self, *args):
 
        subname = self.entry.get()
 
        self.cmd(subname)
 
        log.info("command %s %s", self.cmd, subname)
 
        print "sub", self.cmd, subname
 

	
 
    def set(self, text):
 
        self.entry.delete(0, 'end')
 
        self.entry.insert(0, text)
 
    task.LoopingCall(sc.sendupdate).start(10)
 

	
 

	
 
#############################
 

	
 
if __name__ == "__main__":
 
    parser = OptionParser(usage="%prog [subname]")
 
    parser = OptionParser(usage="%prog [suburi]")
 
    parser.add_option('--no-geometry', action='store_true',
 
                      help="don't save/restore window geometry")
 
    clientsession.add_option(parser)
 
    opts, args = parser.parse_args()
 

	
 
    logging.basicConfig(level=logging.DEBUG)
 

	
 
    root=tk.Tk()
 
    root.config(bg='black')
 
    root.tk_setPalette("#004633")
 
@@ -245,21 +279,7 @@ if __name__ == "__main__":
 
    graph = SyncedGraph("subcomposer")
 
    session = clientsession.getUri('subcomposer', opts)
 

	
 
    if not opts.no_geometry:
 
        toplevelat("subcomposer - %s" % opts.session, root, graph, session)
 

	
 
    sc = Subcomposer(root, graph, session)
 
    sc.pack()
 

	
 
    tk.Label(root,text="Bindings: B1 adjust level; B2 set full; B3 instant bump",
 
             font="Helvetica -12 italic",anchor='w').pack(side='top',fill='x')
 

	
 
    if len(args) == 1:
 
        root.config(bg='green') # trying to make these look distinctive
 
        sc.loadsub(args[0])
 
        sc.fill_both_boxes(args[0])
 

	
 
    task.LoopingCall(sc.sendupdate).start(1)
 
    graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, session))
 

	
 
    root.protocol('WM_DELETE_WINDOW', reactor.stop)
 
    tksupport.install(root,ms=10)
light9/Submaster.py
Show inline comments
 
@@ -9,7 +9,7 @@ from louie import dispatcher
 
log = logging.getLogger('submaster')
 

	
 
class Submaster(object):
 
    "Contain a dictionary of levels, but you didn't need to know that"
 
    """mapping of channels to levels"""
 
    def __init__(self, name, levels):
 
        """this sub has a name just for debugging. It doesn't get persisted.
 
        See PersistentSubmaster.
 
@@ -67,6 +67,8 @@ class Submaster(object):
 

	
 
    def __cmp__(self, other):
 
        # not sure how useful this is
 
        if not isinstance(other, Submaster):
 
            return -1
 
        return cmp(self.ident(), other.ident())
 

	
 
    def __hash__(self):
 
@@ -82,7 +84,8 @@ class Submaster(object):
 
            try:
 
                dmxchan = Patch.get_dmx_channel(k) - 1
 
            except ValueError:
 
                log.error("error trying to compute dmx levels for submaster %s" % self.name)
 
                log.error("error trying to compute dmx levels for submaster %s"
 
                          % self.name)
 
                raise
 
            if dmxchan >= len(levels):
 
                levels.extend([0] * (dmxchan - len(levels) + 1))
 
@@ -124,6 +127,8 @@ class Submaster(object):
 

	
 
class PersistentSubmaster(Submaster):
 
    def __init__(self, graph, uri):
 
        if uri is None:
 
            raise TypeError("uri must be URIRef")
 
        self.graph, self.uri = graph, uri
 
        self.graph.addHandler(self.setName)
 
        self.graph.addHandler(self.setLevels)
 
@@ -148,6 +153,8 @@ class PersistentSubmaster(Submaster):
 
        self.setLevelsFromGraph()
 
        if oldLevels != self.levels:
 
            log.info("sub %s changed" % self.name)
 
            # dispatcher too? this would help subcomposer
 
            dispatcher.send("sub levels changed", sub=self)
 

	
 
    def setLevelsFromGraph(self):
 
        if hasattr(self, 'levels'):
 
@@ -155,16 +162,55 @@ class PersistentSubmaster(Submaster):
 
        else:
 
            self.levels = {}
 
        for lev in self.graph.objects(self.uri, L9['lightLevel']):
 
            log.debug(" lightLevel %s %s", self.uri, lev)
 
            chan = self.graph.value(lev, L9['channel'])
 

	
 
            name = self.graph.label(chan)
 
            if not name:
 
                log.error("sub %r has channel %r with no name- leaving out that channel" % (self.name, chan))
 
                log.error("sub %r has channel %r with no name- "
 
                          "leaving out that channel" % (self.name, chan))
 
                continue
 
            val = self.graph.value(lev, L9['level'])
 
            self.levels[name] = float(val)
 
            if val is None:
 
                raise ValueError("sub %r has lightLevel %r with channel %r "
 
                                 "and level %r" % (self.uri, lev, chan, val))
 
            log.debug("   new val %r", val)
 
            try:
 
                self.levels[name] = float(val)
 
            except:
 
                log.error("name %r val %r" % (name, val))
 
                raise
 

	
 
    def saveContext(self):
 
        """the context in which we should save all the lightLevel triples for
 
        this sub"""
 
        with self.graph.currentState() as g:
 
            try:
 
                ctx = g.contextsForStatement(
 
                    (self.uri, RDF.type, L9['Submaster']))[0]
 
            except IndexError:
 
                raise ValueError(
 
                    "no context has %s rdf:type :Submaster, "
 
                    "so I don't know where to save this sub's data" % self.uri)
 
        return ctx
 

	
 
    def editLevel(self, chan, newLevel):
 
        self.graph.patchMapping(self.saveContext(),
 
                                subject=self.uri, predicate=L9['lightLevel'],
 
                                nodeClass=L9['ChannelSetting'],
 
                                keyPred=L9['channel'], newKey=chan,
 
                                valuePred=L9['level'],
 
                                newValue=Literal(newLevel))
 

	
 
    def clear(self):
 
        """set all levels to 0"""
 
        with self.graph.currentState() as g:
 
            levs = list(g.objects(self.uri, L9['lightLevel']))
 
        for lev in levs:
 
            self.graph.removeMappingNode(self.saveContext(), lev)
 

	
 
    def save(self):
 
        raise NotImplementedError("obsolete?")
 
        if self.temporary:
 
            log.info("not saving temporary sub named %s",self.name)
 
            return
 
@@ -176,7 +222,10 @@ class PersistentSubmaster(Submaster):
 
            try:
 
                chanUri = Patch.get_channel_uri(chan)
 
            except KeyError:
 
                log.error("saving dmx channels with no :Channel node is not supported yet. Give channel %s a URI for it to be saved. Omitting this channel from the sub." % chan)
 
                log.error("saving dmx channels with no :Channel node "
 
                          "is not supported yet. Give channel %s a URI "
 
                          "for it to be saved. Omitting this channel "
 
                          "from the sub." % chan)
 
                continue
 
            lev = BNode()
 
            graph.add((subUri, L9['lightLevel'], lev))
light9/dmxchanedit.py
Show inline comments
 
@@ -18,9 +18,8 @@ proposal for new attribute system:
 
"""
 
from __future__ import nested_scopes,division
 
import Tkinter as tk
 
from rdflib import RDF
 
from rdflib import RDF, Literal
 
from light9.namespaces import L9
 
import louie as dispatcher
 

	
 
stdfont = ('Arial', 9)
 

	
 
@@ -36,12 +35,23 @@ class Onelevel(tk.Frame):
 
        ch:b11-c     a :Channel;
 
         :output dmx:c54;
 
         rdfs:label "b11-c" .
 

	
 
    and the level is like this:
 

	
 
       ?editor :currentSub ?sub .
 
       ?sub :lightLevel [:channel ?ch; :level ?level] .
 

	
 
    levels come in with self.setTo and go out by the onLevelChange
 
    callback. This object does not use the graph for level values,
 
    which I'm doing for what I think is efficiency. Unclear why I
 
    didn't use Observable for that API.
 
    """
 
    def __init__(self, parent, graph, channelUri):
 
    def __init__(self, parent, graph, channelUri, onLevelChange):
 
        tk.Frame.__init__(self,parent, height=20)
 
        self.graph = graph
 
        self.onLevelChange = onLevelChange
 
        self.uri = channelUri
 
        self.currentlevel = 0 # the level we're displaying, 0..1
 
        self.currentLevel = 0 # the level we're displaying, 0..1
 

	
 
        # no statement yet
 
        self.channelnum = int(
 
@@ -73,7 +83,6 @@ class Onelevel(tk.Frame):
 
                                  padx=1, pady=0, bd=0, height=1)
 
        self.level_lab.pack(side='left')
 

	
 
        self.setlevel(0)
 
        self.setupmousebindings()
 

	
 
    def updateLabel(self):
 
@@ -83,10 +92,10 @@ class Onelevel(tk.Frame):
 
        def b1down(ev):
 
            self.desc_lab.config(bg='cyan')
 
            self._start_y=ev.y
 
            self._start_lev=self.currentlevel
 
            self._start_lev=self.currentLevel
 
        def b1motion(ev):
 
            delta=self._start_y-ev.y
 
            self.setlevel(self._start_lev+delta*.005)
 
            self.setlevel(max(0, min(1, self._start_lev+delta*.005)))
 
        def b1up(ev):
 
            self.desc_lab.config(bg='black')
 
        def b3up(ev):
 
@@ -114,28 +123,40 @@ class Onelevel(tk.Frame):
 
        self.level_lab.config(bg=gradient(lev))
 

	
 
    def setlevel(self, newlev):
 
        """the main program is telling us to change our
 
        display. newlev is 0..1"""
 
        self.currentlevel = min(1, max(0, newlev))
 
        newlev = "%d" % (self.currentlevel * 100)
 
        """UI received a level change, which we put in the graph"""
 
        self.onLevelChange(self.uri, newlev)
 

	
 
    def setTo(self, newLevel):
 
        """levelbox saw a change in the graph"""
 
        self.currentLevel = min(1, max(0, newLevel))
 
        newLevel = "%d" % (self.currentLevel * 100)
 
        olddisplay=self.level_lab.cget('text')
 
        if newlev!=olddisplay:
 
            self.level_lab.config(text=newlev)
 
        if newLevel != olddisplay:
 
            self.level_lab.config(text=newLevel)
 
            self.colorlabel()
 
        dispatcher.send("levelchanged", channel=self.uri, newlevel=newlev)
 

	
 

	
 
class Levelbox(tk.Frame):
 
    def __init__(self, parent, graph):
 
    """
 
    this also watches all the levels in the sub and sets the boxes when they change
 
    """
 
    def __init__(self, parent, graph, currentSub):
 
        """
 
        currentSub is an Observable(PersistentSubmaster)
 
        """
 
        tk.Frame.__init__(self,parent)
 

	
 
        self.currentSub = currentSub
 
        self.graph = graph
 
        graph.addHandler(self.updateChannels)
 

	
 
        self.currentSub.subscribe(lambda _: graph.addHandler(self.updateLevelValues))
 

	
 
    def updateChannels(self):
 
        """(re)make Onelevel boxes for the defined channels"""
 

	
 
        [ch.destroy() for ch in self.winfo_children()]
 
        self.levels = [] # Onelevel objects
 
        self.levelFromUri = {} # channel : OneLevel
 

	
 
        chans = list(self.graph.subjects(RDF.type, L9.Channel))
 
        chans.sort(key=lambda c: int(self.graph.value(c, L9.output).rsplit('/c')[-1]))
 
@@ -151,14 +172,33 @@ class Levelbox(tk.Frame):
 

	
 
        for i, channel in enumerate(chans): # sort?
 
            # frame for this channel
 
            f = Onelevel(columnFrames[i // rows], self.graph, channel)
 
            f = Onelevel(columnFrames[i // rows], self.graph, channel,
 
                         self.onLevelChange)
 

	
 
            self.levelFromUri[channel] = f
 
            f.pack(side='top')
 

	
 
            self.levels.append(f)
 
            f.pack(side='top')
 
        #dispatcher.connect(setalevel,"setlevel")
 
    def updateLevelValues(self):
 
        """set UI level from graph"""
 
        submaster = self.currentSub()
 
        if submaster is None:
 
            return
 
        sub = submaster.uri
 
        if sub is None:
 
            raise ValueError("currentSub is %r" % submaster)
 

	
 
    def setlevels(self,newlevels):
 
        """sets levels to the new list of dmx levels (0..1). list can
 
        be any length"""
 
        for l,newlev in zip(self.levels,newlevels):
 
            l.setlevel(newlev)
 
        remaining = set(self.levelFromUri.keys())
 
        for ll in self.graph.objects(sub, L9['lightLevel']):
 
            chan = self.graph.value(ll, L9['channel'])
 
            lev = self.graph.value(ll, L9['level']).toPython()
 
            self.levelFromUri[chan].setTo(lev)
 
            remaining.remove(chan)
 
        for channel in remaining:
 
            self.levelFromUri[channel].setTo(0)
 

	
 
    def onLevelChange(self, chan, newLevel):
 
        """UI received a change which we put in the graph"""
 
        if self.currentSub() is None:
 
            raise ValueError("no currentSub in Levelbox")
 
        self.currentSub().editLevel(chan, newLevel)
 

	
light9/editchoice.py
Show inline comments
 
new file 100644
 
import Tkinter as tk
 
from rdflib import URIRef
 
from light9.tkdnd import dragSourceRegister, dropTargetRegister
 

	
 
class Local(object):
 
    """placeholder for the local uri that EditChoice does not
 
    manage. Set resourceObservable to Local to indicate that you're
 
    unlinked"""
 

	
 
class EditChoice(tk.Frame):
 
    """
 
    Observable <-> linker UI
 

	
 
    widget for tying some UI to a shared resource for editing, or
 
    unlinking it (which means associating it with a local resource
 
    that's not named or shared). This object does not own the choice
 
    of resource; the caller does.
 

	
 
    UI actions:
 
    - drag a uri on here to make it the one we're editing
 

	
 
    - button to clear the currentSub (putting it back to
 
      sessionLocalSub, and also resetting sessionLocalSub to be empty
 
      again)
 

	
 
    - drag the sub uri off of here to send it to another receiver,
 
      but, if we're in local mode, the local sub should not be so
 
      easily addressable. Maybe you just can't drag it off.
 

	
 

	
 
    Todo:
 

	
 
    - filter by type so you can't drag a curve onto a subcomposer
 

	
 
    - 'save new' : make a new sub: transfers the current data (from a shared sub or
 
      from the local one) to the new sub. If you're on a local sub,
 
      the new sub is named automatically, ideally something brief,
 
      pretty distinct, readable, and based on the lights that are
 
      on. If you're on a named sub, the new one starts with a
 
      'namedsub 2' style name. The uri can also be with a '2' suffix,
 
      although maybe that will be stupid. If you change the name
 
      before anyone knows about this uri, we could update the current
 
      sub's uri to a slug of the new label.
 

	
 
    - rename this sub: not available if you're on a local sub. Sets
 
      the label of a named sub. Might update the uri of the named sub
 
      if it's new enough that no one else would have that uri. Not
 
      sure where we measure that 'new enough' value. Maybe track if
 
      the sub has 'never been dragged out of this subcomposer
 
      session'? But subs will also show up in other viewers and
 
      finders.
 

	
 
    - list of recent resources that this choice was set to
 
    """
 
    def __init__(self, parent, graph, resourceObservable):
 
        """
 
        getResource is called to get the URI of the currently
 
        """
 
        self.graph = graph
 
        self.frame = tk.Frame(parent, relief='raised', border=2)
 
        self.frame.pack(side='top')
 
        tk.Label(self.frame, text="Editing:").pack(side='left')
 
        self.currentLinkFrame = tk.Frame(self.frame)
 
        self.currentLinkFrame.pack(side='left')
 

	
 
        self.subIcon = tk.Label(self.currentLinkFrame, text="...",
 
                                borderwidth=2, relief='raised',
 
                                padx=10, pady=10)
 
        self.subIcon.pack()
 

	
 
        self.resourceObservable = resourceObservable
 
        resourceObservable.subscribe(self.uriChanged)
 

	
 
        # when the value is local, this should stop being a drag source
 
        dragSourceRegister(self.subIcon, 'copy', 'text/uri-list',
 
                           self.resourceObservable)
 
        def onEv(ev):
 
            self.resourceObservable(URIRef(ev.data))
 
            return "link"
 
        self.onEv = onEv
 

	
 
        b=tk.Button(self.frame, text="Unlink", command=self.switchToLocalSub)
 
        b.pack(side='left')
 

	
 
        # it would be nice if I didn't receive my own drags here, and
 
        # if the hover display wasn't per widget
 
        for target in ([self.frame, self.currentLinkFrame] +
 
                       self.frame.winfo_children() +
 
                       self.currentLinkFrame.winfo_children()):
 
            dropTargetRegister(target, typeList=["*"], onDrop=onEv,
 
                               hoverStyle=dict(background="#555500"))
 

	
 
    def uriChanged(self, newUri):
 
        # if this resource had a type icon or a thumbnail, those would be
 
        # cool to show in here too
 
        if newUri is Local:
 
            self.subIcon.config(text="(local)")
 
        else:
 
            self.graph.addHandler(self.updateLabel)
 

	
 
    def updateLabel(self):
 
        uri = self.resourceObservable()
 
        print "get label", repr(uri)
 
        label = self.graph.label(uri)
 
        self.subIcon.config(text=label or uri)
 

	
 
    def switchToLocalSub(self):
 
        self.resourceObservable(Local)
 

	
light9/observable.py
Show inline comments
 
new file 100644
 
class _NoNewVal(object):
 
    pass
 

	
 
class Observable(object):
 
    """
 
    like knockout's observable. Hopefully this can be replaced by a
 
    better python one
 

	
 
    compare with:
 
    http://knockoutjs.com/documentation/observables.html
 
    https://github.com/drpancake/python-observable/blob/master/observable/observable.py
 
    """
 
    def __init__(self, val):
 
        self.val = val
 
        self.subscribers = set()
 

	
 
    def __call__(self, newVal=_NoNewVal):
 
        if newVal is _NoNewVal:
 
            return self.val
 
        if newVal == self.val:
 
            print "%r unchanged from %r" % (newVal, self.val)
 
            return
 
        self.val = newVal
 
        for s in self.subscribers:
 
            s(newVal)
 

	
 
    def subscribe(self, cb, callNow=True):
 
        """cb is called with new values, and also right now with the
 
        current value unless you opt out"""
 
        self.subscribers.add(cb)
 
        if callNow:
 
            cb(self.val)
light9/rdfdb/patch.py
Show inline comments
 
@@ -62,19 +62,19 @@ class Patch(object):
 
    @property
 
    def addQuads(self):
 
        if self._addQuads is None:
 
            if self._addGraph is None:
 
                return []
 
            self._addQuads = list(quadsWithContextUris(
 
                self._addGraph.quads(ALLSTMTS)))
 
            if self._addGraph is not None:
 
                self._addQuads = list(self._addGraph.quads(ALLSTMTS))
 
            else:
 
                raise
 
        return self._addQuads
 

	
 
    @property
 
    def delQuads(self):
 
        if self._delQuads is None:
 
            if self._delGraph is None:
 
                return []
 
            self._delQuads = list(quadsWithContextUris(
 
                self._delGraph.quads(ALLSTMTS)))
 
            if self._delGraph is not None:
 
                self._delQuads = list(self._delGraph.quads(ALLSTMTS))
 
            else:
 
                raise
 
        return self._delQuads
 

	
 
    @property
light9/rdfdb/syncedgraph.py
Show inline comments
 
from rdflib import ConjunctiveGraph, RDFS, RDF, Graph
 
import logging, cyclone.httpclient, traceback, urllib
 
from rdflib import ConjunctiveGraph, RDFS, RDF, URIRef
 
import logging, cyclone.httpclient, traceback, urllib, random
 
from itertools import chain
 
from twisted.internet import reactor, defer
 
log = logging.getLogger('syncedgraph')
 
from light9.rdfdb.patch import Patch, ALLSTMTS
 
from light9.rdfdb.rdflibpatch import patchQuads
 
from light9.rdfdb.patch import Patch
 
from light9.rdfdb.rdflibpatch import patchQuads, contextsForStatement as rp_contextsForStatement
 

	
 
# everybody who writes literals needs to get this
 
from rdflibpatch_literal import patch
 
patch()
 

	
 

	
 
def sendPatch(putUri, patch, **kw):
 
    """
 
@@ -176,6 +182,12 @@ class SyncedGraph(object):
 
    This api is like rdflib.Graph but it can also call you back when
 
    there are graph changes to the parts you previously read.
 

	
 
    You may want to attach to self.initiallySynced deferred so you
 
    don't attempt patches before we've heard the initial contents of
 
    the graph. It would be ok to accumulate some patches of new
 
    material, but usually you won't correctly remove the existing
 
    statements unless we have the correct graph.
 

	
 
    If we get out of sync, we abandon our local graph (even any
 
    pending local changes) and get the data again from the
 
    server.
 
@@ -185,6 +197,7 @@ class SyncedGraph(object):
 
        label is a string that the server will display in association
 
        with your connection
 
        """
 
        self.initiallySynced = defer.Deferred()
 
        _graph = self._graph = ConjunctiveGraph()
 
        self._watchers = GraphWatchers()
 

	
 
@@ -201,6 +214,11 @@ class SyncedGraph(object):
 
                # receive its patch correctly.
 
                traceback.print_exc()
 

	
 
            if self.initiallySynced:
 
                self.initiallySynced.callback(None)
 
                self.initiallySynced = None
 

	
 

	
 
        listen = reactor.listenTCP(0, cyclone.web.Application(handlers=[
 
            (r'/update', makePatchEndpoint(onPatch)),
 
        ]))
 
@@ -258,7 +276,7 @@ class SyncedGraph(object):
 
        # these could fail if we're out of sync. One approach:
 
        # Rerequest the full state from the server, try the patch
 
        # again after that, then give up.
 
        
 
        log.info("%s add %s", [q[2] for q in p.delQuads], [q[2] for q in  p.addQuads])
 
        patchQuads(self._graph, p.delQuads, p.addQuads, perfect=True)
 
        self.updateOnPatch(p)
 
        self._sender.sendPatch(p).addErrback(self.sendFailed)
 
@@ -268,6 +286,7 @@ class SyncedGraph(object):
 
        we asked for a patch to be queued and sent to the master, and
 
        that ultimately failed because of a conflict
 
        """
 
        print "sendFailed"
 
        #i think we should receive back all the pending patches,
 
        #do a resysnc here,
 
        #then requeue all the pending patches (minus the failing one?) after that's done.
 
@@ -275,7 +294,10 @@ class SyncedGraph(object):
 

	
 
    def patchObject(self, context, subject, predicate, newObject):
 
        """send a patch which removes existing values for (s,p,*,c)
 
        and adds (s,p,newObject,c). Values in other graphs are not affected"""
 
        and adds (s,p,newObject,c). Values in other graphs are not affected.
 

	
 
        newObject can be None, which will remove all (subj,pred,*) statements.
 
        """
 

	
 
        existing = []
 
        for spo in self._graph.triples((subject, predicate, None),
 
@@ -284,16 +306,56 @@ class SyncedGraph(object):
 
        # what layer is supposed to cull out no-op changes?
 
        self.patch(Patch(
 
            delQuads=existing,
 
            addQuads=[(subject, predicate, newObject, context)]))
 
            addQuads=([(subject, predicate, newObject, context)]
 
                      if newObject is not None else [])))
 

	
 
    def patchMapping(self, context, subject, predicate, keyPred, valuePred, newKey, newValue):
 
    def patchMapping(self, context, subject, predicate, nodeClass, keyPred, valuePred, newKey, newValue):
 
        """
 
        proposed api for updating things like ?session :subSetting [
 
        :sub ?s; :level ?v ]. Keyboardcomposer has an implementation
 
        already. There should be a complementary readMapping that gets
 
        you a value since that's tricky too
 
        creates/updates a structure like this:
 

	
 
           ?subject ?predicate [
 
             a ?nodeClass;
 
             ?keyPred ?newKey;
 
             ?valuePred ?newValue ] .
 

	
 
        There should be a complementary readMapping that gets you a
 
        value since that's tricky too
 
        """
 

	
 
        with self.currentState() as graph:
 
            adds = set([])
 
            for setting in graph.objects(subject, predicate):
 
                if graph.value(setting, keyPred) == newKey:
 
                    break
 
            else:
 
                setting = URIRef(subject + "/map/%s" %
 
                                 random.randrange(999999999))
 
                adds.update([
 
                    (subject, predicate, setting, context),
 
                    (setting, RDF.type, nodeClass, context),
 
                    (setting, keyPred, newKey, context),
 
                    ])
 
            dels = set([])
 
            for prev in graph.objects(setting, valuePred):
 
                dels.add((setting, valuePred, prev, context))
 
            adds.add((setting, valuePred, newValue, context))
 

	
 
            if adds != dels:
 
                self.patch(Patch(delQuads=dels, addQuads=adds))
 

	
 
    def removeMappingNode(self, context, node):
 
        """
 
        removes the statements with this node as subject or object, which
 
        is the right amount of statements to remove a node that
 
        patchMapping made.
 
        """
 
        p = Patch(delQuads=[spo+(context,) for spo in
 
                            chain(self._graph.triples((None, None, node),
 
                                                      context=context),
 
                                  self._graph.triples((node, None, None),
 
                                                      context=context))])
 
        self.patch(p)
 
                
 
    def addHandler(self, func):
 
        """
 
        run this (idempotent) func, noting what graph values it
 
@@ -333,12 +395,21 @@ class SyncedGraph(object):
 
        """
 
        a graph you can read without being in an addHandler
 
        """
 
        if context is not None:
 
            raise NotImplementedError("currentState with context arg")
 

	
 
        class Mgr(object):
 
            def __enter__(self2):
 
                # this should be a readonly view of the existing graph
 
                g = Graph()
 
                for s in self._graph.triples((None, None, None), context):
 
                    g.add(s)
 
                # this should be a readonly view of the existing
 
                # graph, maybe with something to guard against
 
                # writes/patches happening while reads are being
 
                # done. Typical usage will do some reads on this graph
 
                # before moving on to writes.
 
                
 
                g = ConjunctiveGraph()
 
                for s,p,o,c in self._graph.quads((None,None,None)):
 
                    g.store.add((s,p,o), c)
 
                g.contextsForStatement = lambda t: contextsForStatementNoWildcards(g, t)
 
                return g
 

	
 
            def __exit__(self, type, val, tb):
 
@@ -388,7 +459,19 @@ class SyncedGraph(object):
 
        self._watchers.addPredObjWatcher(func, predicate, object)
 
        return self._graph.subjects(predicate, object)
 

	
 
    # i find myself wanting 'patch' versions of these calls that tell
 
    def contextsForStatement(self, triple):
 
        """currently this needs to be in an addHandler section, but it
 
        sets no watchers so it won't actually update if the statement
 
        was added or dropped from contexts"""
 
        func = self._getCurrentFunc()
 
        return contextsForStatementNoWildcards(self._graph, triple)
 

	
 
    # i find myself wanting 'patch' (aka enter/leave) versions of these calls that tell
 
    # you only what results have just appeared or disappeared. I think
 
    # I'm going to be repeating that logic a lot. Maybe just for the
 
    # subjects(RDF.type, t) call
 

	
 
def contextsForStatementNoWildcards(g, triple):
 
    if None in triple:
 
        raise NotImplementedError("no wildcards")
 
    return rp_contextsForStatement(g, triple)
light9/rdfdb/web/index.xhtml
Show inline comments
 
@@ -29,6 +29,8 @@
 
      $(function(){
 

	
 
          function collapseCuries(s) {
 
            // this is temporary. The correct thing is to parse the quad (or receive it as a tree) and then make links to the full URIs and display curies of them
 

	
 
              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; })
light9/uihelpers.py
Show inline comments
 
@@ -62,15 +62,11 @@ def toplevelat(name, existingtoplevel=No
 

	
 
    def savePos(ev):
 
        geo = tl.geometry()
 
        if not isinstance(ev.widget, Tkinter.Tk):
 
            # I think these are due to internal widget size changes,
 
            # not the toplevel changing
 

	
 
        # 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
 
        # 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 geo == lastSaved[0]:
 
            return
 
        if not setOnce[0]:
0 comments (0 inline, 0 general)