Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/groupthink/gtk_tools.py
diff options
context:
space:
mode:
Diffstat (limited to 'groupthink/gtk_tools.py')
-rw-r--r--groupthink/gtk_tools.py338
1 files changed, 338 insertions, 0 deletions
diff --git a/groupthink/gtk_tools.py b/groupthink/gtk_tools.py
new file mode 100644
index 0000000..0cd4029
--- /dev/null
+++ b/groupthink/gtk_tools.py
@@ -0,0 +1,338 @@
+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)