import gtk
import groupthink_base as groupthink
import logging
import stringtree
class RecentEntry(groupthink.UnorderedHandlerAcceptor, gtk.Entry):
"""RecentEntry is an extension of gtk.Entry that, when attached to a group,
creates a unified Entry field for all participants"""
def __init__(self, *args, **kargs):
gtk.Entry.__init__(self, *args, **kargs)
self.logger = logging.getLogger('RecentEntry')
self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK)
self._text_changed_handler = self.connect('changed', self._local_change_cb)
self._recent = groupthink.Recentest(self.get_text(), groupthink.string_translator)
self._recent.register_listener(self._remote_change_cb)
def _local_change_cb(self, widget):
self.logger.debug("_local_change_cb()")
self._recent.set_value(self.get_text())
def set_handler(self, handler):
self.logger.debug("set_handler")
self._recent.set_handler(handler)
def _remote_change_cb(self, text):
self.logger.debug("_remote_change_cb(%s)" % text)
if self.get_text() != text:
#The following code will break if running in any thread other than
#the main thread. I do not know how to make code that works with
#both multithreaded gtk _and_ single-threaded gtk.
self.handler_block(self._text_changed_handler)
self.set_text(text)
self.handler_unblock(self._text_changed_handler)
class SharedTreeStore(groupthink.CausalHandlerAcceptor, gtk.GenericTreeModel):
def __init__(self, columntypes=(), translators=()):
self._columntypes = columntypes
self._causaltree = groupthink.CausalTree()
if len(translators) != 0 and len(translators) != len(columntypes):
raise #Error: translators must be empty or match columntypes in length
if len(translators) == len(self._columntypes):
self._columndicts = [groupthink.CausalDict(
key_translator = self._causaltree.node_trans,
value_translator = translators[i])
for i in xrange(len(translators))]
else:
self._columndicts = [groupthink.CausalDict(
key_translator = self._causaltree.node_trans)
for i in xrange(len(translators))]
self._causaltree.register_listener(self._tree_listener)
for i in xrange(len(self._columndicts)):
self._columndicts[i].register_listener(self._generate_dictlistener(i))
def set_handler(self, handler):
self._causaltree.set_handler(handler)
for i in xrange(len(self._columndicts)):
#Make a new handler for each columndict
#Not very future-proof: how do we serialize out and reconstitute
#objects that GroupActivity.cloud is not even aware of?
h = handler.copy(str(i))
self._columndicts[i].set_handler(h)
### Methods necessary to implement gtk.GenericTreeModel ###
def on_get_flags(self):
return gtk.TREE_MODEL_ITERS_PERSIST
def on_get_n_columns(self):
return len(self._columntypes)
def on_get_column_type(self, index):
return self._columntypes[index]
def on_get_iter(self, path):
node = self._causaltree.ROOT
for k in path:
c = list(self._causaltree.get_children(node))
if len(c) <= k:
return None #Invalid path
else:
c.sort()
node = c[k]
return node
def on_get_path(self, rowref):
revpath = []
node = rowref
if rowref in self._causaltree:
while node != self._causaltree.ROOT:
p = self._causaltree.get_parent(node)
c = list(self._causaltree.get_children(p))
c.sort()
revpath.append(c.index(node)) # could be done "faster" using bisect
node = p
return tuple(revpath[::-1])
else:
return None
def on_get_value(self, rowref, column):
return self._columndicts[column][rowref]
def on_iter_next(self, rowref):
p = self._causaltree.get_parent(rowref)
c = list(self._causaltree.get_children(p))
c.sort()
i = c.index(rowref) + 1
if i < len(c):
return c[i]
else:
return None
def on_iter_children(self, parent):
if parent is None:
parent = self._causaltree.ROOT
c = self._causaltree.get_children(parent)
if len(c) > 0:
return min(c)
else:
return None
def on_iter_has_child(self, rowref):
return len(self._causaltree.get_children(rowref)) > 0
def on_iter_n_children(self, rowref):
return len(self._causaltree.get_children(rowref))
def on_iter_nth_child(self, parent, n):
if parent is None:
parent = self._causaltree.ROOT
c = self._causaltree.get_children(parent)
if len(c) > n:
c = list(c)
c.sort()
return c[n]
else:
return None
def on_iter_parent(self, child):
p = self._causaltree.get_parent(child)
if p == self._causaltree.ROOT:
return None
else:
return p
### Methods for passing changes from remote users ###
def _dict_listener(self, i, added, removed):
s = set()
s.update(added.keys())
s.update(removed.keys())
for node in s:
path = self.on_get_path(node)
if path is not None:
it = self.create_tree_iter(node)
self.row_changed(path, it)
self.emit('changed')
def _generate_dict_listener(self, i):
def temp(added,removed):
self._dict_listener(i,added,removed)
return temp
def _tree_listener(self, forward, reverse):
#forward is the list of commands representing the change, and
#reverse is the list representing their inverse. Together, these
#lists represent a total description of the change. However, deriving
#sufficient information to fill in the signals would require replicating
#the entire CausalTree state machine. Therefore, for the moment, we make only a modest
#attempt, and if it fails, throw up an "unknown-change" flag
deleted = set() #unused, since we can only safely handle a single deletion with this method
haschild = set() #All signals may be sent spuriously, but this one especially so
inserted = set()
unknown_change = False
# no reordered, since there is no ordering choice
for cmd in forward:
if cmd[0] == self._causaltree.SET_PARENT:
if cmd[2] in self._causaltree:
haschild.add(cmd[2])
else:
unknown_change = True
if cmd[1] in self._causaltree:
inserted.add(cmd[1])
else:
unknown_change = True
for cmd in reverse:
clean = True
if cmd[0] == self._causaltree.SET_PARENT:
if (clean and
cmd[2] in self._causaltree and
(cmd[1] not in self._causaltree or
cmd[2] != self._causaltree.get_parent(cmd[1]))):
clean = False
haschild.add((cmd[2], cmd[1]))
c = self._causaltree.get_children(cmd[2])
c = list(c)
c.append(cmd[1])
c.sort()
i = c.index(cmd[1])
p = self.on_get_path(cmd[2])
p = list(p)
p.append(i)
p = tuple(p)
self.row_deleted(p)
else:
unknown_change = True
if unknown_change:
self.emit('unknown-change')
for node in inserted:
path = self.on_get_path(node)
if path is not None:
it = self.create_tree_iter(node)
self.row_inserted(path, it)
for node in haschild:
path = self.on_get_path(node)
if path is not None:
it = self.create_tree_iter(node)
self.row_has_child_toggled(path, it)
self.emit('changed')
### Methods for resembling gtk.TreeStore ###
def set_value(self, it, column, value):
node = self.get_user_data(it)
self._columndicts[i][node] = value
def set(self, it, *args):
for i in xrange(0,len(args),2):
self.set_value(it,args[i],args[i+1])
def remove(self, it):
node = self.get_user_data(it)
self._causaltree.delete(node)
for d in self._columndicts:
if node in d:
del d[node]
def append(self, parent, row=None):
if parent is not None:
node = self.get_user_data(it)
else:
node = self._causaltree.ROOT
node = self._causaltree.new_child(node)
if row is not None:
if len(row) != len(columndicts):
raise IndexError("row had the wrong length")
else:
for i in xrange(len(row)):
self._columndicts[i][node] = row[i]
return self.create_tree_iter(node)
def is_ancestor(self, it, descendant):
node = self.get_user_data(it)
d = self.get_user_data(descendant)
d = self._causaltree.get_parent(d)
while d != self._causaltree.ROOT:
if d == node:
return True
else:
d = self._causaltree.get_parent(d)
return False
def iter_depth(self, it):
node = self.get_user_data(it)
i = 0
node = self._causaltree.get_parent(node)
while node != self._causaltree.ROOT:
i = i + 1
node = self._causaltree.get_parent(node)
return i
def clear(self):
self._causaltree.clear()
for d in self._columndicts:
d.clear()
def iter_is_valid(self, it):
node = self.get_user_data(it)
return node in self._causaltree
### Additional Methods ###
def move(self, it, newparent):
node = self.get_user_data(row)
p = self.get_user_data(newparent)
self._causaltree.change_parent(node,p)
class TextBufferUnorderedStringLinker:
def __init__(self,tb,us):
self._tb = tb
self._us = us
self._us.register_listener(self._netupdate_cb)
self._insert_handler = tb.connect('insert-text', self._insert_cb)
self._delete_handler = tb.connect('delete-range', self._delete_cb)
self._logger = logging.getLogger('the Linker')
def _insert_cb(self, tb, itr, text, length):
self._logger.debug('user insert: %s' % text)
pos = itr.get_offset()
self._us.insert(text,pos)
def _delete_cb(self, tb, start_itr, end_itr):
self._logger.debug('user delete')
k = start_itr.get_offset()
n = end_itr.get_offset()-k
self._us.delete(k,n)
def _netupdate_cb(self, edits):
self._logger.debug('update from network: %s' % str(edits))
self._tb.handler_block(self._insert_handler)
self._tb.handler_block(self._delete_handler)
for e in edits:
if isinstance(e, stringtree.Insertion):
itr = self._tb.get_iter_at_offset(e.position)
self._tb.insert(itr, e.text)
elif isinstance(e, stringtree.Deletion):
itr1 = self._tb.get_iter_at_offset(e.position)
itr2 = self._tb.get_iter_at_offset(e.position + e.length)
self._tb.delete(itr1,itr2)
self._tb.handler_unblock(self._insert_handler)
self._tb.handler_unblock(self._delete_handler)
class TextBufferSharePoint(groupthink.UnorderedHandlerAcceptor):
def __init__(self, buff):
self._us = groupthink.UnorderedString(buff.get_text(buff.get_start_iter(), buff.get_end_iter()))
self._linker = TextBufferUnorderedStringLinker(buff, self._us)
def set_handler(self, handler):
self._us.set_handler(handler)
class SharedTextView(groupthink.UnorderedHandlerAcceptor, gtk.TextView):
def __init__(self, *args, **kargs):
gtk.TextView.__init__(self, *args, **kargs)
self._link = TextBufferSharePoint(self.get_buffer())
def set_handler(self, handler):
self._link.set_handler(handler)