diff options
Diffstat (limited to 'groupthink/gtk_tools.py')
-rw-r--r-- | groupthink/gtk_tools.py | 338 |
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) |