EditObj-0.5.7/0000755000076500007650000000000010402350016011440 5ustar jibajibaEditObj-0.5.7/demo/0000755000076500007650000000000010402350016012364 5ustar jibajibaEditObj-0.5.7/demo/demo.py0000755000076500007650000001100210114336756013676 0ustar jibajiba#!/usr/bin/env python # EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import Tkinter, editobj, editobj.editor as editor, editobj.custom as custom # This is a sample EditObj-based editor. # Imagine you have the following object to edit : class MyObj: def __init__(self, name = ""): self.name = name self.filename = "" self.x = 0.0 self.y = 0.0 self.visible = 1 self.power2 = 4 self.powers2 = [1, 2, 4, 8, 16, 32, 64] self.any = None self.content = [] def __repr__(self): return "" % self.name # This object has no real usage has is, but present some typical attributes. # Editobj can already edit this object (as any other one) ! # # But one typically wants that the "name" attribute can be only a string, "x" a # float number, or "filename" a valid file name. EditObj can do that too, if you # take the time to teach that to him. # # EditObj edits each attribute of an object in an editor. The default one is a # one-line Python console, but many other are supported (See editobj.editor for # more info). # If you want so, you can map an attribute name to a specific editor with the # editobj.editor.register_attr(attr, editor_class) function. Notice that only attribute # names are mapped, and the class of the object is NOT involved (contrary to systems # like Java Bean); this is pretty logic because Python is dynamic and often does so. # It can also save you a lot of time, since you have to enter each attribute only # once ! # Say "name" is a string. custom.register_attr("name", editor.StringEditor) # Say that for "name", the values "me" and "you" should be proposed to the user. # Possible values are given as text; as if they were entered by the user in the # editor. custom.register_values("name", ["me", "you"]) # Say "filename" is a file name. custom.register_attr("filename", editor.FilenameEditor) # Say "x" and "y" are float numbers. custom.register_attr("x", editor.FloatEditor) custom.register_attr("y", editor.FloatEditor) # Say "visible" is a boolean. custom.register_attr("visible", editor.BoolEditor) # Say "power2" is a value choosen in a list. # # The editor.ListEditor() function create a new class of editor with the given list # of possible values. custom.register_attr("power2", editor.ListEditor(["1", "2", "4", "8"])) # "any" can be anything, so we do not provide a specific editor. # It will be edited with a one-line Python console. # Say that for "any", the value "None" should be proposed to the user. custom.register_values("any", ["None"]) # Say that "content" contains the items of a MyObj object. # # EditObj automatically display a tree view if the object is a list or # a dictionary, or if it we register a "children" attribute. custom.register_children_attr("content", clazz = MyObj) # Say "content" is hidden (because it is edited as "items", by the tree view). custom.register_attr("content", None) # Say that MyObj object can contains other MyObj object in their "items" attribute # (which is the same that "content") Yes, it is a tree like structure, and EditObj # deals very well with such structures ! # # The editor.register_children function register a (non-exhaustive) list of # possible children for a given class. Children are given as Python strings. custom.register_available_children(["MyObj('new')"], MyObj) # Make MyObj available in the evaluation environment custom.EVAL_ENV["MyObj"] = MyObj # No main Tk window root = Tkinter.Tk(className = 'EditObj') root.withdraw() # Better Tk look root.option_add("*bd", 1) root.option_add("*Entry.background", "#FFFFFF") root.option_add("*List.background", "#FFFFFF") # Create a MyObj object, with another MyObj inside (yes, it is a tree like structure) my_obj = MyObj("root") my_obj.content.append(MyObj("child")) # Edit my_obj ! editobj.edit(my_obj) Tkinter.mainloop() EditObj-0.5.7/demo/test_observe.py0000755000076500007650000001053210114336756015465 0ustar jibajiba# Unit test module for observe.py import unittest import editobj.observe as observe class ObserveTestCase1(unittest.TestCase): def test_diffdict(self): def test(new, old, diff): result = observe.diffdict(new, old) if result != diff: self.fail("observe.diffdict(%s, %s) = %s,\nshould be %s" % (new, old, result, diff)) test({}, {}, []) test({"1" : 1, "2" : 2}, {"1" : 1, "2" : 2}, []) test({"1" : 1, "2" : 2}, {"1" : 1}, [("2", 2, None)]) test({"1" : 1}, {"1" : 1, "2" : 2}, [("2", None, 2)]) test({"1" : 2}, {"1" : 1}, [("1", 2, 1)]) def test_observe_list(self): observed = [] def f(object, type, new, old): observed.append((object, type, new, old)) def check_observed(expected): observe.scan() if observed != expected: self.fail("observed %s\nshould be %s" % (observed, expected)) for i in range(len(observed)): del observed[0] l = [0] observe.observe(l, f) if not observe.isobserved(l, f): self.fail("isobserved() is wrong") ol = l[:]; l.append(1); check_observed([(l, list, l, ol)]) ol = l[:]; del l[1]; check_observed([(l, list, l, ol)]) ol = l[:]; l[0] = 3; check_observed([(l, list, l, ol)]) observe.unobserve(l, f) l[0] = 1 check_observed([]) if observe.isobserved(l, f): self.fail("isobserved() is wrong") def test_observe_dict(self): observed = [] def f(object, type, new, old): observed.append((object, type, new, old)) def check_observed(expected): observe.scan() if observed != expected: self.fail("observed %s\nshould be %s" % (observed, expected)) for i in range(len(observed)): del observed[0] d = {"a" : 1} observe.observe(d, f) if not observe.isobserved(d, f): self.fail("isobserved() is wrong") od = d.copy(); d["b"] = 2; check_observed([(d, dict, d, od)]) od = d.copy(); d["a"] = 3; check_observed([(d, dict, d, od)]) od = d.copy(); del d["b"]; check_observed([(d, dict, d, od)]) observe.unobserve(d, f) d["c"] = 4; check_observed([]) if observe.isobserved(d, f): self.fail("isobserved() is wrong") def test_observe_object(self): observed = [] def f(object, type, new, old): observed.append((object, type, new, old)) def check_observed(expected): observe.scan() if observed != expected: self.fail("observed %s\nshould be %s" % (observed, expected)) for i in range(len(observed)): del observed[0] class O: pass class P: pass o = O() o.x = 1 observe.observe(o, f) if not observe.isobserved(o, f): self.fail("isobserved() is wrong") old = o.__dict__.copy(); o.y = 2; check_observed([(o, object, o.__dict__, old)]) old = o.__dict__.copy(); del o.x; check_observed([(o, object, o.__dict__, old)]) old = o.__dict__.copy(); o.y = 3; check_observed([(o, object, o.__dict__, old)]) o.__class__ = P; check_observed([(o, "__class__", P, O)]) observe.unobserve(o, f) o.z = 4; check_observed([]) if observe.isobserved(o, f): self.fail("isobserved() is wrong") def test_observe_tree(self): observed = [] def f(object, type, new, old): observed.append((object, type, new, old)) def check_observed(expected): observe.scan() if observed != expected: self.fail("observed %s\nshould be %s" % (observed, expected)) for i in range(len(observed)): del observed[0] class O: pass o = O() l = [0] observe.observe_tree(l, f) if not observe.isobserved(l, f): self.fail("isobserved() is wrong") l2 = [] old = l[:]; l.append(l2); check_observed([(l, list, l, old)]) if not observe.isobserved(l2, f): self.fail("items added to the tree should be observed") old = l[:]; del l[0]; check_observed([(l, list, l, old)]) old = l2[:]; l2.append(1); check_observed([(l2, list, l2, old)]) old = l2[:]; l2.append(o); check_observed([(l2, list, l2, old)]) old = o.__dict__.copy(); o.x = 1; check_observed([(o, object, o.__dict__, old)]) old = l[:]; l.remove(l2); check_observed([(l, list, l, old)]) l2.append(2); check_observed([]) observe.unobserve_tree(l, f) l.append(3); check_observed([]) if observe.isobserved(l, f): self.fail("isobserved() is wrong") if __name__ == '__main__': unittest.main() EditObj-0.5.7/demo/tree_demo.py0000755000076500007650000000257410114336756014733 0ustar jibajiba#!/usr/bin/env python # TreeWidget # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from Tkinter import * from editobj.treewidget import * import os, os.path, sys, string, imp class BigNode(Node): """Example of Node subclass -- Each BigNode has 1000 children. Enjoy !""" def __init__(self, parent, i): self.i = i Node.__init__(self, parent) def __str__(self): return str(self.i) def iseditable(self): return 0 def isexpandable(self): return 1 def createchildren(self, oldchildren = None): return map(BigNode, [self] * 1000, range(1000)) root = Tk() tree = Tree(root) tree.frame.pack(expand=1, fill="both") node = BigNode(tree, 0) root.mainloop() EditObj-0.5.7/demo/tree_demo2.py0000755000076500007650000000464310114336756015014 0ustar jibajiba#!/usr/bin/env python # TreeWidget # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from Tkinter import * from editobj.treewidget import * import os, os.path, sys, string, imp import editobj.treewidget # Gets the default icons directory (shipped with TreeWidget). iconsdir = IconsDir(os.path.join(os.path.dirname(editobj.treewidget.__file__), "icons")) class FileNode(Node): """Example Node subclass -- browse the file system.""" def __init__(self, parent, path): self.path = path Node.__init__(self, parent) def __str__(self): return os.path.basename(self.path) or self.path # One may explicitely choose the icon as following (TreeWidget use those as default) : def geticon(self): if self.isexpandable(): if self.expanded: return iconsdir["openfolder.pgm"] else: return iconsdir["folder.pgm"] else: return iconsdir["python.pgm"] def iseditable(self): return os.path.basename(self.path) != "" def isexpandable(self): return os.path.isdir(self.path) def createchildren(self, oldchildren = None): try: files = os.listdir(self.path) except os.error: return [] files.sort(lambda a, b: cmp(os.path.normcase(a), os.path.normcase(b))) children = [] for file in files: children.append(FileNode(self, os.path.join(self.path, file))) return children def settext(self, text): newpath = os.path.dirname(self.path) newpath = os.path.join(newpath, text) if os.path.dirname(newpath) != os.path.dirname(self.path): return try: os.rename(self.path, newpath) self.path = newpath except os.error: print "Cannot rename !" root = Tk() tree = Tree(root) tree.frame.pack(expand=1, fill="both") node = FileNode(tree, os.curdir) root.mainloop() EditObj-0.5.7/icons/0000755000076500007650000000000010402350016012553 5ustar jibajibaEditObj-0.5.7/icons/folder.pgm0000755000076500007650000000120310114336756014551 0ustar jibajibaP6 # CREATOR: The GIMP's PNM Filter Version 1.0 15 13 255 яяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяяџџЯџџЯџџџџяяяяяяяяяяяяяяяяяяЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`яяяџџЯџџЯџџЯџџЯџџЯџџЯџџЯџџЯџџЯџџЯџџЯџџЯЯ`џџЯџџџџџџџџџџџџџџџџџџџџџЯЯЯ`џџЯџџџџџџџџџџџџџџџЯџџџЯџџЯЯ`џџЯџџџџџџџџџџџџџџџџџЯџџџЯЯЯ`џџЯџџџџџџџџџџџЯџџџЯџџџЯџџЯЯ`џџЯџџџџџџџџџџџџџЯџџџЯџџџЯЯЯ`џџЯџџџџџџџЯџџџЯџџџЯџџџЯџЯЯЯ`џџЯџЯџџџЯџџџЯџџџЯџџџЯџЯџЯЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`ЯЯ`яяяEditObj-0.5.7/icons/minus.pgm0000755000076500007650000000147210114336756014441 0ustar jibajibaP6 # CREATOR: The GIMP's PNM Filter Version 1.0 16 16 255 i-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-ži-žмЧюзОьвЖщЭ­чЧЅфТœтН”пИ‹нВ‚кВ‚кВ‚кВ‚кВ‚кВ‚кi-žџџџi-žфдђпЫ№кУэеКыЯВшЪЉцХ уИсКоЕ‡мВ‚кВ‚кi-žџџџџџџџџџi-žьсічиѓтЯёмЧюзОьвЖщЭ­чЧЅфТœтН”пi-žџџџџџџџџџџџџџџџi-žєэњяхїъмѕфдђпЫ№кУэеКыЯВшi-žџџџџџџџџџџџџџџџџџџџџџi-žќњ§їђћёщјьсічиѓтЯёi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџџџџљіќєэњi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџџџџi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-ži-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџEditObj-0.5.7/icons/openfolder.pgm0000755000076500007650000000125210114336756015437 0ustar jibajibaP6 # CREATOR: The GIMP's PNM Filter Version 1.0 16 13 255 №№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№№џџџџџџџџџџ№№№№№№№№№№№№џџџџџџџџџџџџџџ№№№№№№№№№№№№ЯЯ`№№№№№№џџџџџџџџџџџџџџџџџџџџџџЯЯ`ЯЯ`џџЯЯ`№№№џџџџџџџџџџџџџЯџџџЯЯЯ`ЯЯ`ЯЯ`№№№џџџџџџџџџЯџџџџџЯџџЯЯ`ЯЯ`ЯЯ`№№№№№№џџџџџџџџџџџЯџџџЯџЯ№№№№№№џџџџџЯџџџЯџџџЯџЯџЯЯЯ`№№№№№№№№№џЯџџџЯџџџЯџЯџЯџЯЯЯ`№№№№№№№№№№№№№№№EditObj-0.5.7/icons/plus.pgm0000755000076500007650000000147210114336756014271 0ustar jibajibaP6 # CREATOR: The GIMP's PNM Filter Version 1.0 16 16 255 i-ži-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žБйi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žБйБйi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žБйБйБйi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žБйБйБйБйi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žН”пЙŽнЕˆлБйБйi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žЩЇхХЁуСšсН”пЙŽнЕˆлi-žџџџџџџџџџџџџџџџџџџџџџџџџi-žдКъаГшЬ­цЩЇхХЁуСšсН”пi-žџџџџџџџџџџџџџџџџџџџџџi-žрЭ№мЦюиРьдКъаГшЬ­цЩЇхi-žџџџџџџџџџџџџџџџџџџџџџi-žыпѕчйѓфгђрЭ№мЦюиРьi-žџџџџџџџџџџџџџџџџџџџџџџџџi-žїђћѓьљяцїыпѕчйѓi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџџџџћј§їђћi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџџџџџџџi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџџџџi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-žџџџi-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџi-ži-žџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџEditObj-0.5.7/icons/python.pgm0000755000076500007650000000126010114336756014622 0ustar jibajibaP6 # CREATOR: The GIMP's PNM Filter Version 1.0 14 15 255 џџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџџ€€€€€€џџџџџџџџџџџџџџџџџџџџџџџџџџџџџ€€џџџџџџџџџџџџџџџџџџџџџџ€€€€џџџџџџџџџџџџ€€€€€€€€€€€€€€џџџџџџџџџ€€€€€€€€€€€€џ€€€€џџџџџџџџџџџџџџџ€€€€џџџџџџџџџџџџџ€€џџџџџџџџџџџџџџџ€€џџџџџџџџџџџџ€€€€џџџ€€џџџџџџџџ€€€€џџџџџџ€€џџџџџџџџ€€€€€€€€џџџџџџџџ€€€€€€€€€€€€џџџџџџџџџџџ€€€€€€€€€€€€џџџџџџџџџџџџџџџџџџџџџџџџџџEditObj-0.5.7/__init__.py0000755000076500007650000001136210114336755013575 0ustar jibajiba# EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """EditObj -- Display a Tkinter-based dialog box to edit any Python instance, list or dict. This is what Java calls a "Bean Editor", but for Python :-) In terms of MVC (Model View Controler), EditObj is a generic View coupled with a universal Controler. The module only provide the function edit(obj), that returns the Tk top-level window. EditObj will edit all attributes and, for list or dict, all items. The upper part of the window display the list/dict items in a hierarchical tree view, and the lower part lists all the edited instance's attributes. An object can provide an __edit__(window) method, that will be called on edition, with the EditObj's window as argument. If it returns true, it is assumed that the edition is entirely done by the object itself. If an object is edited as a child / item / component of another, __subedit__ will be called, with the EditObj's window as argument. When the children of an object are made visible or invisible in the tree view, the object __editchildren__ method is called (if it has one) with the visibility (1 or 0). An object can also provide a __wrapedit__() method, that returns a wrapper for the object. The wrapper will be edited instead of the object. editobj.TRANSLATOR can be set to a translator function (such as the ones from the gettext module), if you want to translate the names of the properties. Quick example : >>> import editobj >>> class C(list): ... def __init__(self): ... list.__init__(self) ... self.x = 1 ... self.append("A string.") ... self.append([1, 2, 3]) >>> toplevel = editobj.edit(C()) >>> toplevel.mainloop()""" __all__ = ["edit"] import sys, Tkinter from UserList import UserList from UserDict import UserDict def edit(o, **kargs): from editobj.main import EditWindow return EditWindow(o, **kargs) clipboard = None EVAL_ENV = {} _eval = eval def eval(text, globals = EVAL_ENV, locals = EVAL_ENV): try: return _eval(text, globals, locals) except NotImplementedError: raise except: sys.excepthook(*sys.exc_info()) print "Error ?", "-- consider input as a string." return text def is_getset(o): return isinstance(o, property) or (type(o).__name__ == "getset_descriptor") def attrs_of(obj): if hasattr(obj, "__dict__"): attrs = obj.__dict__.keys() else: attrs = [] if hasattr(obj, "__members__"): attrs.extend(obj.__members__) klass = obj.__class__ attrs.extend(filter(lambda attr: is_getset(getattr(klass, attr)), dir(klass))) return attrs def editable(o): """Wrap o if needed (try to call o.__wrapedit__()). Return o itself if no wrapper is provided.""" if hasattr(o, "__wrapedit__"): return o.__wrapedit__() return o def in_hierarchy(item, hierarchy): """A recursive __contains__, for working with hierarchical tree. The caller must assume that, if needed, item and hierarchy are wrapped (by _getEditedObject).""" items = custom._find_children(hierarchy) if items: for i in items: # Wrap i, if needed (already done for item and hierarchy). i = editable(i) if item == i: return 1 if in_hierarchy(item, i): return 1 class MultiEdit(object): def __init__(self, objs): self.__dict__["_objs"] = objs def __observe__(self, func): from editobj.observe import observe for obj in self._objs: observe(obj, func) def __unobserve__(self, func): from editobj.observe import unobserve for obj in self._objs: unobserve(obj, func) def _get_members(self): attrs = [] for obj in self._objs: for attr in attrs_of(obj): if not attr in attrs: attrs.append(attr) return attrs __members__ = property(_get_members) def __getattr__(self, attr): for obj in self._objs: if hasattr(obj, attr): return getattr(obj, attr) raise AttributeError, attr def __setattr__(self, attr, value): for obj in self._objs: if hasattr(obj, "set_" + attr): getattr(obj, "set_" + attr)(value) else: setattr(obj, attr, value) import custom EditObj-0.5.7/cancel.py0000755000076500007650000000545710114336752013270 0ustar jibajiba# Songwrite # Copyright (C) 2001-2002 Jean-Baptiste LAMY # # This program is free software. See README or LICENSE for the license terms. """Cancel -- A framework for multiple undo/redo. """ NUMBER_OF_CANCEL = 20 class CancelmentStack: def __init__(self, previous = None): self.closures = [] self.previous = previous def append(self, closure): self.closures.append(closure) add_cancel = add_redo = append def prepend(self, closure): self.closures.insert(0, closure) def is_noop(self): return not self.closures def __call__(self): self.closures.reverse() # Operation must be undoed in the reverse order !!! for closure in self.closures: closure() class RootCancelmentStack(CancelmentStack): def __init__(self): self.closures = [] def append(self, closure): self.closures.append(closure) if len(self.closures) > NUMBER_OF_CANCEL: del self.closures[0] def prepend(self, closure): self.closures.insert(0, closure) def pop_and_call(self): if not self.closures: return 0 closure = self.closures.pop() if isinstance(closure, CancelmentStack) and closure.is_noop(): return self.pop_and_call() else: closure() return 1 class Context: def __init__(self): self.cancels = self.cur_cancels = RootCancelmentStack() self.redos = self.cur_redos = RootCancelmentStack() self.redoing = 0 def add_cancel(self, closure): self.cur_cancels.append(closure) #if (not self.redoing) and self.redos.closures: # self.redos.closures *= 0 def add_post_cancel(self, closure): self.cur_cancels.prepend(closure) def add_redo(self, closure): self.cur_redos.append(closure) def add_post_redo(self, closure): self.cur_redos.prepend(closure) def get_last_cancelable(self): if self.cancels.closures: return self.cancels.closures[-1] return None def cancel(self): self.push_redo() try: return self.cancels.pop_and_call() finally: self.pop_redo() def redo(self): self.redoing = 1 self.push() try: return self.redos.pop_and_call() finally: self.pop() self.redoing = 0 def push(self): new_stack = CancelmentStack(self.cur_cancels) self.add_cancel(new_stack) self.cur_cancels = new_stack def pop(self): if self.cur_cancels.is_noop(): try: self.cancels.closures.remove(self.cur_cancels) except ValueError: pass self.cur_cancels = self.cur_cancels.previous def push_redo(self): new_stack = CancelmentStack(self.cur_redos) self.add_redo(new_stack) self.cur_redos = new_stack def pop_redo(self): if self.cur_redos.is_noop(): try: self.redos.closures.remove(self.cur_redos) except ValueError: pass self.cur_redos = self.cur_redos.previous EditObj-0.5.7/console.py0000755000076500007650000007263610114336753013511 0ustar jibajiba"""A Tkinter-based console for conversing with the Python interpreter, featuring more tolerant pasting of code from other interactive sessions, better handling of continuations than the standard Python interpreter, highlighting of the most recently-executed code block, the ability to edit and reexecute previously entered code, a history of recently-entered lines, automatic multi-level completion with pop-up menus, and pop-up help. Ka-Ping Yee , 18 April 1999. This software is in the public domain and is provided without express or implied warranty. Permission to use, modify, or distribute the software for any purpose is hereby granted.""" # TODO: autoindent to matching bracket after an unbalanced line (hard) # TODO: outdent after line starting with "break", "raise", "return", etc. # TODO: keep a stack of indent levels for backspace to jump back to # TODO: blink or highlight matching brackets # TODO: delete the prompt when joining lines; allow a way to break lines from Tkinter import * import sys, string, traceback, types, __builtin__ REVISION = "$Revision: 1.1.1.1 $" VERSION = string.split(REVISION)[1] class OutputPipe: """A substitute file object for redirecting output to a function.""" def __init__(self, writer): self.writer = writer self.closed = 0 def __repr__(self): return "" % repr(self.writer) def read(self, length): return "" def write(self, data): if not self.closed: self.writer(data) def close(self): self.closed = 1 class Console(Frame): def __init__(self, parent=None, dict={}, globals={}, **options): """Construct from a parent widget, an optional dictionary to use as the namespace for execution, and any configuration options.""" Frame.__init__(self, parent) # Continuation state. self.continuation = 0 self.error = 0 self.intraceback = 0 self.pasted = 0 # The command history. self.history = [] self.historyindex = None self.current = "" # Completion state. self.compmenus = [] self.compindex = None self.compfinish = "" # Redirection. self.stdout = OutputPipe(lambda data, w=self.write: w(data, "stdout")) self.stderr = OutputPipe(lambda data, w=self.write: w(data, "stderr")) # Interpreter state. if not hasattr(sys, "ps1"): sys.ps1 = ">>> " if not hasattr(sys, "ps2"): sys.ps2 = "... " self.prefixes = [sys.ps1, sys.ps2, ">> ", "> "] self.startup = "Python Console v%s by Ka-Ping Yee \n" % VERSION self.dict = dict self.globals = globals # The text box. self.text = Text(self, insertontime=200, insertofftime=150) self.text.insert("end", self.startup) self.text.insert("end", sys.ps1) self.text.bind("", self.cb_return) self.text.bind("", self.cb_select) self.text.bind("", self.cb_position) self.text.bind("", self.cb_paste) self.text.bind("", self.cb_home) self.text.bind("", self.cb_ctrlhome) self.text.bind("", self.cb_back) self.text.bind("", self.cb_forward) self.text.bind("", self.cb_cleanup) self.text.bind("", self.cb_cleanup) self.text.bind("", self.cb_cleanup) self.text.bind("", self.cb_complete) self.text.bind("", self.cb_position) self.text.bind("", self.cb_space) self.text.bind("", self.cb_backspace) self.text.bind("", self.cb_nothing) self.text.bind("", self.cb_help) self.text.bind("", self.cb_help) self.text.bind("", self.cb_help) # The scroll bar. self.scroll = Scrollbar(self, command=self.text.yview) self.text.config(yscrollcommand=self.scroll.set) self.scroll.pack(side=RIGHT, fill=Y) self.text.pack(fill=BOTH, expand=1) self.text.focus() # Configurable options. self.options = {"stdoutcolour": "#7020c0", "stderrcolour": "#c03020", "morecolour": "#a0d0f0", "badcolour": "#e0b0b0", "runcolour": "#90d090"} apply(self.config, (), self.options) apply(self.config, (), options) def __getitem__(self, key): return self.options[key] def __setitem__(self, key, value): if not self.options.has_key(key): raise KeyError, 'no such configuration option "%s"' % key self.options[key] = value if key == "stdoutcolour": self.text.tag_configure("stdout", foreground=value) if key == "stderrcolour": self.text.tag_configure("stderr", foreground=value) def config(self, *args, **dict): """Get or set configuration options in a Tkinter-like style.""" if args == () and dict == {}: return self.options if len(args) == 1: return self.options[args[0]] for key, value in dict.items(): self[key] = value # Text box routines. def trim(self, command): """Trim any matching prefix from the given command line, returning the amount trimmed and the trimmed result.""" for prefix in self.prefixes: if command[:len(prefix)] == prefix: return len(prefix), command[len(prefix):] return 0, command def getline(self, line=None, trim=0): """Return the command on the current line.""" if line is None: line, pos = self.cursor() command = self.text.get("%d.0" % line, "%d.end" % line) if trim: trimmed, command = self.trim(command) return command def cursor(self): """Get the current line and position of the cursor.""" cursor = self.text.index("insert") [line, pos] = map(string.atoi, string.split(cursor, ".")) return line, pos def write(self, data, tag=None): """Show output from stdout or stderr in the console.""" if self.intraceback and data[-2:] == "\n ": data = data[:-1] start = self.text.index("insert") self.text.insert("insert", data) end = self.text.index("insert") if tag: self.text.tag_add(tag, start, end) # History mechanism. def cb_back(self, event): """Step back in the history.""" if self.history: if self.historyindex == None: self.current = self.getline(trim=1) self.historyindex = len(self.history) - 1 elif self.historyindex > 0: self.historyindex = self.historyindex - 1 self.recall() return "break" def cb_forward(self, event): """Step forward in the history.""" if self.history and self.historyindex is not None: self.historyindex = self.historyindex + 1 if self.historyindex < len(self.history): self.recall() else: self.historyindex = None self.recall(self.current) return "break" def recall(self, command=None): """Show a command from the history on the current line.""" if command is None: command = self.history[self.historyindex] line, pos = self.cursor() current = self.getline(line) trimmed, trimmedline = self.trim(current) cutpos = "%d.%d" % (line, trimmed) self.text.delete(cutpos, "%d.end" % line) self.text.insert(cutpos, command) self.text.mark_set("insert", "%d.end" % line) # Completion mechanism. def precontext(self): # Scan back for the identifier currently being typed. line, pos = self.cursor() command = self.getline() preceding = command[:pos] startchars = string.letters + "_" identchars = string.letters + string.digits + "_" while pos > 0 and preceding[pos-1] in identchars: pos = pos - 1 preceding, ident = preceding[:pos], preceding[pos:] start = "%d.%d" % (line, pos) preceding = string.strip(preceding) context = "" if not ident or ident[0] in startchars: # Look for context before the start of the identifier. while preceding[-1:] == ".": preceding = string.strip(preceding[:-1]) if preceding[-1] in identchars: pos = len(preceding)-1 while pos > 0 and preceding[pos-1] in identchars: pos = pos - 1 if preceding[pos] in startchars: context = preceding[pos:] + "." + context preceding = string.strip(preceding[:pos]) else: break else: break line, pos = self.cursor() endpos = pos while endpos < len(command) and command[endpos] in identchars: endpos = endpos + 1 end = "%d.%d" % (line, endpos) return command, context, ident, start, end def cb_complete(self, event): """Attempt to complete the identifier currently being typed.""" if self.compmenus: if self.cursor() == self.compindex: # Second attempt to complete: add finishing char and continue. self.text.insert("insert", self.compfinish) self.compindex = None self.unpostmenus() return "break" command, context, ident, start, end = self.precontext() # Get the list of possible choices. if context: try: object = eval(context[:-1], self.dict, self.globals) keys = members(object) except: object = None keys = [] else: class Lookup: def __init__(self, dicts): self.dicts = dicts def __getattr__(self, key): for dict in self.dicts: if dict.has_key(key): return dict[key] return None object = Lookup([self.dict, __builtin__.__dict__]) keys = self.dict.keys() + self.globals.keys() + dir(__builtin__) keys = matchingkeys(keys, ident) if not ident: public = [] for key in keys: if key[:1] != "_": public.append(key) keys = public skip = len(ident) # Produce the completion. if len(keys) == 1: # Complete with the single possible choice. if self.cursor() == self.compindex: # Second attempt to complete: add finisher and continue. self.text.insert("insert", self.compfinish) self.compindex = None else: self.text.delete("insert", end) self.text.insert("insert", keys[0][skip:]) try: self.compfinish = finisher(getattr(object, keys[0])) except: self.compfinish = " " if self.compfinish == " ": # Object has no members; stop here. self.text.insert("insert", " ") else: self.compindex = self.cursor() elif len(keys) > 1: # Present a menu. prefix = commonprefix(keys) keys.sort() if len(prefix) > skip: self.text.delete("insert", end) self.text.insert("insert", keys[0][skip:len(prefix)]) skip = len(prefix) if len(keys[0]) == skip: # Common prefix is a valid choice; next try can finish. self.compindex = self.cursor() try: self.compfinish = finisher(getattr(object, keys[0])) except: self.compfinish = " " self.postmenus(keys, skip, end, object) return "break" def postmenus(self, keys, skip, cut, object): """Post a series of menus listing all the given keys, given the length of the existing part so we can position the menus under the cursor, and the index at which to insert the completion.""" width = self.winfo_screenwidth() height = self.winfo_screenheight() bbox = self.text.bbox("insert - %d c" % skip) x = self.text.winfo_rootx() + bbox[0] - 4 y = self.text.winfo_rooty() + bbox[1] + bbox[3] self.compmenus = [] menufont = self.text.cget("font") menu = Menu(font=menufont, bd=1, tearoff=0) self.compmenus.append(menu) while keys: try: finishchar = finisher(getattr(object, keys[0])) except: finishchar = " " def complete(s=self, k=keys[0][skip:], c=cut, f=finishchar): if f == " ": k = k + f s.text.delete("insert", c) s.text.insert("insert", k) s.unpostmenus() if f != " ": s.compfinish = f s.compindex = s.cursor() menu.add_command(label=keys[0], command=complete) menu.update() if y + menu.winfo_reqheight() >= height: menu.delete("end") x = x + menu.winfo_reqwidth() y = 0 menu = Menu(font=menufont, bd=1, tearoff=0) self.compmenus.append(menu) else: keys = keys[1:] if x + menu.winfo_reqwidth() > width: menu.destroy() self.compmenus = self.compmenus[:-1] self.compmenus[-1].delete("end") self.compmenus[-1].add_command(label="...") break x = self.text.winfo_rootx() + bbox[0] - 4 y = self.text.winfo_rooty() + bbox[1] + bbox[3] for menu in self.compmenus: maxtop = height - menu.winfo_reqheight() if y > maxtop: y = maxtop menu.post(x, y) x = x + menu.winfo_reqwidth() self.text.focus() self.text.grab_set() def unpostmenus(self): """Unpost the completion menus.""" for menu in self.compmenus: menu.destroy() self.compmenus = [] self.text.grab_release() def cb_cleanup(self, event=None): if self.compmenus: self.unpostmenus() if self.pasted: self.text.tag_remove("sel", "1.0", "end") self.pasted = 0 def cb_select(self, event): """Handle a menu selection event. We have to check and invoke the completion menus manually because we are grabbing events to give the text box keyboard focus.""" if self.compmenus: for menu in self.compmenus: x, y = menu.winfo_rootx(), menu.winfo_rooty() w, h = menu.winfo_width(), menu.winfo_height() if x < event.x_root < x + w and \ y < event.y_root < y + h: item = menu.index("@%d" % (event.y_root - y)) menu.invoke(item) break else: self.unpostmenus() return "break" # Help mechanism. def cb_help(self, event): command, context, ident, start, end = self.precontext() word = self.text.get(start, end) object = parent = doc = None skip = 0 try: parent = eval(context[:-1], self.dict, self.globals) except: pass # Go merrily searching for the help string. if not object: try: object = getattr(parent, word) skip = len(word) - len(ident) except: pass if not object: try: object = getattr(parent, ident) except: pass if not object: try: object = self.dict[word] skip = len(word) - len(ident) except: pass if not object: try: object = self.dict[ident] except: pass if not object: try: object = __builtin__.__dict__[word] skip = len(word) - len(ident) except: pass if not object: try: object = __builtins__.__dict__[ident] except: pass if not object: if not ident: object = parent try: doc = object.__doc__ except: pass try: if hasattr(object, "__bases__"): doc = object.__init__.__doc__ or doc except: pass if doc: doc = string.rstrip(string.expandtabs(doc)) leftmargin = 99 for line in string.split(doc, "\n")[1:]: spaces = len(line) - len(string.lstrip(line)) if line and spaces < leftmargin: leftmargin = spaces bbox = self.text.bbox("insert + %d c" % skip) width = self.winfo_screenwidth() height = self.winfo_screenheight() menufont = self.text.cget("font") help = Menu(font=menufont, bd=1, tearoff=0) try: classname = object.__class__.__name__ help.add_command(label="" % classname) help.add_command(label="") except: pass for line in string.split(doc, "\n"): if string.strip(line[:leftmargin]) == "": line = line[leftmargin:] help.add_command(label=line) self.compmenus.append(help) x = self.text.winfo_rootx() + bbox[0] - 4 y = self.text.winfo_rooty() + bbox[1] + bbox[3] maxtop = height - help.winfo_reqheight() if y > maxtop: y = maxtop help.post(x, y) self.text.focus() self.text.grab_set() return "break" # Entering commands. def cb_position(self, event): """Avoid moving into the prompt area.""" self.cb_cleanup() line, pos = self.cursor() trimmed, command = self.trim(self.getline()) if pos <= trimmed: self.text.mark_set("insert", "%d.%d" % (line, trimmed)) return "break" def cb_backspace(self, event): self.cb_cleanup() if self.text.tag_ranges("sel"): return # Avoid backspacing over the prompt. line, pos = self.cursor() trimmed, command = self.trim(self.getline()) if pos <= trimmed: return "break" # Extremely basic outdenting. Needs more work here. if not string.strip(command[:pos-trimmed]): step = (pos - trimmed) % 4 cut = pos - (step or 4) if cut < trimmed: cut = trimmed self.text.delete("%d.%d" % (line, cut), "%d.%d" % (line, pos)) return "break" def cb_space(self, event): self.cb_cleanup() line, pos = self.cursor() trimmed, command = self.trim(self.getline()) # Extremely basic indenting. Needs more work here. if not string.strip(command[:pos-trimmed]): start = trimmed + len(command) - len(string.lstrip(command)) self.text.delete("insert", "%d.%d" % (line, start)) step = 4 - (pos - trimmed) % 4 self.text.insert("insert", " " * step) return "break" def cb_home(self, event): """Go to the first non-whitespace character in the line.""" self.cb_cleanup() line, pos = self.cursor() trimmed, command = self.trim(self.getline()) indent = len(command) - len(string.lstrip(command)) self.text.mark_set("insert", "%d.%d" % (line, trimmed + indent)) return "break" def cb_ctrlhome(self, event): """Go to the beginning of the line just after the prompt.""" self.cb_cleanup() line, pos = self.cursor() trimmed, command = self.trim(self.getline()) self.text.mark_set("insert", "%d.%d" % (line, trimmed)) return "break" def cb_nothing(self, event): return "break" def cb_return(self, event, doindent=1): """Handle a keystroke by running from the current line and generating a new prompt.""" self.cb_cleanup() self.text.tag_delete("compiled") self.historyindex = None command = self.getline(trim=1) if string.strip(command): self.history.append(command) line, pos = self.cursor() self.text.mark_set("insert", "%d.end" % line) self.text.insert("insert", "\n") self.runline(line) line, pos = self.cursor() self.text.mark_set("insert", "%d.end" % line) prompt = self.continuation and sys.ps2 or sys.ps1 if pos > 0: self.text.insert("insert", "\n" + prompt) else: self.text.insert("insert", prompt) if doindent and not self.error: self.autoindent(command) self.error = 0 self.text.see("insert") return "break" def autoindent(self, command): # Extremely basic autoindenting. Needs more work here. indent = len(command) - len(string.lstrip(command)) if string.lstrip(command): self.text.insert("insert", command[:indent]) if string.rstrip(command)[-1] == ":": self.text.insert("insert", " ") def cb_paste(self, event): """Handle a paste event (middle-click) in the text box. Pasted text has any leading Python prompts stripped (at last!!).""" self.text.tag_delete("compiled") self.error = 0 self.pasted = 1 try: lines = string.split(self.selection_get(), "\n") except: return for i in range(len(lines)): trimmed, line = self.trim(lines[i]) line = string.rstrip(line) if not line: continue self.text.insert("end", line) self.text.mark_set("insert", "end") if i == len(lines) - 2 and lines[i+1] == "": # Indent the last line if it's blank. self.cb_return(None, doindent=1) elif i < len(lines) - 1: self.cb_return(None, doindent=0) if self.error: break return "break" # Executing commands. def runline(self, line): """Run some source code given the number of the last line in the text box. Scan backwards to get the entire piece of code to run if the line is a continuation of previous lines. Tag the compiled code so that it can be highlighted according to whether it is complete, incomplete, or illegal.""" lastline = line lines = [self.getline(line)] while lines[0][:len(sys.ps2)] == sys.ps2: trimmed, lines[0] = self.trim(lines[0]) self.text.tag_add( "compiled", "%d.%d" % (line, trimmed), "%d.0" % (line+1)) line = line - 1 if line < 0: break lines[:0] = [self.getline(line)] if lines[0][:len(sys.ps1)] == sys.ps1: trimmed, lines[0] = self.trim(lines[0]) self.text.tag_add( "compiled", "%d.%d" % (line, trimmed), "%d.0" % (line+1)) else: self.text.tag_add("compiled", "%d.0" % line, "%d.0" % (line+1)) source = string.join(lines, "\n") if not source: self.continuation = 0 return status, code = self.compile(source) if status == "more": self.text.tag_configure("compiled", background=self["morecolour"]) self.continuation = 1 elif status == "bad": self.text.tag_configure("compiled", background=self["badcolour"]) self.error = 1 self.continuation = 0 self.intraceback = 1 oldout, olderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = self.stdout, self.stderr traceback.print_exception(SyntaxError, code, None) self.stdout, self.stderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = oldout, olderr self.intraceback = 0 elif status == "okay": if self.getline(lastline) == sys.ps2: self.text.tag_remove("compiled", "%d.0" % lastline, "end") self.text.tag_configure("compiled", background=self["runcolour"]) self.continuation = 0 self.run(code) def compile(self, source): """Try to compile a piece of source code, returning a status code and the compiled result. If the status code is "okay" the code is complete and compiled successfully; if it is "more" then the code can be compiled, but an interactive session should wait for more input; if it is "bad" then there is a syntax error in the code and the second returned value is the error message.""" err = err1 = err2 = None code = code1 = code2 = None try: code = compile(source, "", "single") except SyntaxError, err: pass else: return "okay", code try: code1 = compile(source + "\n", "", "single") except SyntaxError, err1: pass else: return "more", code1 try: code2 = compile(source + "\n\n", "", "single") except SyntaxError, err2: pass try: code3 = compile(source + "\n", "", "exec") except SyntaxError, err3: pass else: return "okay", code3 try: code4 = compile(source + "\n\n", "", "exec") except SyntaxError, err4: pass if err3[1][2] != err4[1][2]: return "more", None if err1[1][2] != err2[1][2]: return "more", None return "bad", err1 def run(self, code): """Run a code object within the sandbox for this console. The sandbox redirects stdout and stderr to the console, and executes within the namespace associated with the console.""" oldout, olderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = self.stdout, self.stderr try: # Do NOT change __builtin__._ (already used by gettext's installation) import __builtin__ if hasattr(__builtin__, "_"): old_ = __builtin__._ else: old_ = None exec code in self.dict, self.globals if hasattr(__builtin__, "_"): self.globals["_"] = __builtin__._ else: self.globals["_"] = None __builtin__._ = old_ except: self.error = 1 sys.last_type = sys.exc_type sys.last_value = sys.exc_value sys.last_traceback = sys.exc_traceback.tb_next self.intraceback = 1 traceback.print_exception( sys.last_type, sys.last_value, sys.last_traceback) self.intraceback = 0 self.stdout, self.stderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = oldout, olderr # Helpers for the completion mechanism. def scanclass(klass, result): for key in klass.__dict__.keys(): result[key] = 1 for base in klass.__bases__: scanclass(base, result) # def members(object): # result = {} # try: # for key in object.__members__: result[key] = 1 # result["__members__"] = 1 # except: pass # try: # for key in object.__methods__: result[key] = 1 # result["__methods__"] = 1 # except: pass # try: # for key in object.__dict__.keys(): result[key] = 1 # result["__dict__"] = 1 # except: pass # if type(object) is types.ClassType: # scanclass(object, result) # result["__name__"] = 1 # result["__bases__"] = 1 # if type(object) is types.InstanceType: # scanclass(object.__class__, result) # result["__class__"] = 1 # return result.keys() # Use the more powerfull "attrs_of" function of editobj instead. # This one can figure out attributes defined by properties, or at the C level. from editobj import attrs_of as members def matchingkeys(keys, prefix): prefixmatch = lambda key, l=len(prefix), p=prefix: key[:l] == p return filter(prefixmatch, keys) def commonprefix(keys): if not keys: return '' max = len(keys[0]) prefixes = map(lambda i, key=keys[0]: key[:i], range(max+1)) for key in keys: while key[:max] != prefixes[max]: max = max - 1 if max == 0: return '' return prefixes[max] callabletypes = [types.FunctionType, types.MethodType, types.ClassType, types.BuiltinFunctionType, types.BuiltinMethodType] sequencetypes = [types.TupleType, types.ListType] mappingtypes = [types.DictType] try: import ExtensionClass callabletypes.append(ExtensionClass.ExtensionClassType) except: pass try: import curve c = curve.Curve() callabletypes.append(type(c.read)) except: pass def finisher(object): if type(object) in callabletypes: return "(" elif type(object) in sequencetypes: return "[" elif type(object) in mappingtypes: return "{" elif members(object): return "." return " " # Main program. if __name__ == "__main__": c = Console(dict={}) c.dict["console"] = c c.pack(fill=BOTH, expand=1) c.master.title("Python Console v%s" % VERSION) mainloop() EditObj-0.5.7/custom.py0000755000076500007650000002461210376707107013355 0ustar jibajiba# EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from editobj import MultiEdit, EVAL_ENV # Internationalization -- Set here your translator func # (e.g. the "_" func from the GetText module) TRANSLATOR = lambda key: key # Editors customization _attr_editors = {"children" : {None : None}} def register_attr(attr, editor, clazz = None): """register_attr(attr, editor, clazz = None) Registers EDITOR as the editor for atrribute ATTR of class CLAZZ, or for any class if CLAZZ is None. EDITOR can be either a Tk widget subclass of editobj.editor.Editor, or None to hide the attribute. MRO is used in order to allow subclasses to use the editor registered for their mother.""" for_attr = _attr_editors.get(attr) if for_attr: for_attr[clazz] = editor else: _attr_editors[attr] = { clazz : editor } def _find_editor(obj, attr): if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ for_attr = _attr_editors.get(attr) if for_attr: if (len(for_attr) == 1) and for_attr.has_key(None): return for_attr[None] for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): editor = for_attr.get(clazz, 0) if not editor is 0: return editor if for_attr.has_key(None): return for_attr[None] if attr[0] == "_": return None # Hidden import editobj.editor as editor val = getattr(obj, attr) if isinstance(val, int ): return editor.IntEditor if isinstance(val, float): return editor.FloatEditor if isinstance(val, basestring): if "\n" in val: return editor.TextEditor return editor.StringEditor return editor.EntryEditor # Children customization _children_attrs = {None : [("children", "insert", "__delitem__")]} def register_children_attr(attr, insert = "insert", del_ = "__delitem__", clazz = None): """register_children_attr(attr, insert = "insert", del_ = "__delitem__", clazz = None) Registers ATTR as an attribute that can act as the "content" or the "children" of an object of class CLAZZ (or any class if None). If ATTR is None, the object is used as its own list of children (automatically done for list / dict subclasses). INSERT and DEL_ are the names of the methods called for inserting and deleting items. INSERT can accept 2 arguments (as list.insert) or only one (as list.append), if you don't care the children's order. Default values for INSERT and DEL_ are OK for lists; for dicts, use INSERT = "__setitem__". EditObj will display these items in the tree view. Only one such attribute can be set for a given class (several are accepted for None). MRO is used in order to allow subclasses to use the children attribute registered for their mother. By default, "children" is considered for any class, and instances of classes that inherits from list or dict are their own children.""" if clazz: _children_attrs[clazz] = (attr, insert, del_) else: _children_attrs[None].append((attr, insert, del_)) def _find_children(obj): if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ if issubclass(clazz, list) or issubclass(clazz, dict): return obj for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): children_attrs = _children_attrs.get(clazz, None) if children_attrs: if children_attrs[0]: children = getattr(obj, children_attrs[0]) if callable(children): return children() return children else: return obj for (children_attr, insert, del_) in _children_attrs[None]: children = getattr(obj, children_attr, None) if not children is None: if callable(children): return children() return children def _find_children_insert_remove(obj): if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ if issubclass(clazz, list): return obj, obj.insert, obj.__delitem__ elif issubclass(clazz, dict): return obj, obj.__setitem__, obj.__delitem__ for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): children_attrs = _children_attrs.get(clazz) if children_attrs: children_attr, insert, del_ = children_attrs if children_attr: children = getattr(obj, children_attr) if callable(children): return children() else: children = obj break else: for (children_attr, insert, del_) in _children_attrs[None]: children = getattr(obj, children_attr, None) if not children is None: if callable(children): return children() break else: return None, None, None return children, getattr(obj, insert, None) or getattr(children, insert), getattr(obj, del_, None) or getattr(children, del_) # Methods customization _methods = {} def register_method(method, clazz, *args_editor): """register_method(method, clazz, *args_editor) Registers METHOD as a method that must be displayed in EditObj for instance of CLAZZ. METHOD can be either a method name (a string), or a function (in this case, it is not a method, strictly speaking). *ARGS_EDITOR are the editors used for entering the argument, e.g. use editobj.editor.FloatEditor for a float argument, or editobj.editor.EntryEditor for a Python eval'ed line of code. MRO is used in order to allow subclasses to use the methods registered for their mother. If *ARGS_EDITOR is (None,) the method is hidden. Use this on a subclass to hide a method provided by a mother class.""" methods = _methods.get(clazz) if methods: methods.append((method, args_editor)) else: _methods[clazz] = [(method, args_editor)] def _find_methods(obj): methods = [] if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): methods.extend(_methods.get(clazz, ())) return methods def _find_methods(obj): methods = {} if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ mro = list(getattr(clazz, "__mro__", None) or _mro(clazz)) mro.reverse() for clazz in mro: for method, args_editor in _methods.get(clazz, ()): if args_editor == (None,): if methods.has_key(method): del methods[method] else: methods[method] = method, args_editor return methods.values() # Available and default children class customization (for the "Add..." dialog box) _available_children = {} def register_available_children(children_codes, clazz): """register_available_children(children_codes, clazz) Register the CHILDREN_CODES that are proposed for addition in an instance of CLAZZ. If CHILDREN_CODES is a list of strings (Python code), EditObj will display a dialog box. If CHILDREN_CODES is a single string, no dialog box will be displayed, and this code will automatically be used. If CHILDREN_CODES is "", nothing is done when clicking on the "Add..." button. The codes are just eval'ed to create the children; they can use the "parent" variable, which is set to the list/dict we are adding into.""" if isinstance(children_codes, list): try: _available_children[clazz].extend(children_codes) except: _available_children[clazz] = children_codes else: _available_children[clazz] = children_codes def _find_available_children(obj): available_children = [] if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): a = _available_children.get(clazz) if isinstance(a, list): available_children.extend(a) elif isinstance(a, str): return a return available_children # Proposed values customization _values = {} def register_values(attr, code_expressions): """register_values(attr, code_expressions) Registers CODE_EXPRESSIONS as a proposed value for ATTR.""" code_expressions = map(unicodify, code_expressions) try: _values[attr].extend(code_expressions) except KeyError: _values[attr] = list(code_expressions) def _find_values(attr): return _values.get(attr) or [] # Editing events _on_edit = {} _on_children_visible = {} def register_on_edit(func, clazz): """register_on_edit(func, clazz) Register FUNC as an "on_edit" event for CLAZZ. When an instance of CLAZZ is edited, FUNC is called with the instance and the editor Tkinter window as arguments.""" _on_edit[clazz] = func def _call_on_edit(obj, window): if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): on_edit = _on_edit.get(clazz, None) if on_edit: on_edit(obj, window) def register_on_children_visible(func, clazz): """register_on_children_visible(func, clazz) Register FUNC as an "on_children_visible" event for CLAZZ. When the children of an instance of CLAZZ are shown or hidden, FUNC is called with the instance and the new visibility status (0 or 1) as arguments.""" _on_children_visible[clazz] = func def _call_on_children_visible(obj, visible): if isinstance(obj, MultiEdit): clazz = obj._objs[0].__class__ else: clazz = obj.__class__ for clazz in (getattr(clazz, "__mro__", None) or _mro(clazz)): on_children_visible = _on_children_visible.get(clazz, None) if on_children_visible: on_children_visible(obj, visible) def encode(s): if type(s) is unicode: return s.encode("latin") return s def unicodify(s): if type(s) is str: try: return unicode(s, "utf8") except UnicodeError: return unicode(s, "latin") return s def _mro(clazz): mro = [clazz] for c in clazz.__bases__: mro.extend(_mro(c)) return mro EditObj-0.5.7/editor.py0000755000076500007650000004347710376702474013345 0ustar jibajiba# EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, types, copy, bisect, Tkinter, editobj, editobj.custom as custom from custom import encode, unicodify from Tkinter import * from UserList import UserList from UserDict import UserDict import string, os, os.path import cPickle as pickle class Editor: require_right_menu = 1 expand_right = 0 def __init__(self, master, obj, attr): self.obj = obj self.attr = attr def get_value(self): return getattr(self.obj, self.attr) def set_value(self, value): old_value = getattr(self.obj, self.attr, None) def do_it(): if hasattr(self.obj, "set_" + self.attr): # implicit setter getattr(self.obj, "set_" + self.attr)(value) else: setattr(self.obj, self.attr, value) if self.master.cancel: self.master.cancel.add_cancel(cancel_it) def cancel_it(): if hasattr(self.obj, "set_" + self.attr): # implicit setter getattr(self.obj, "set_" + self.attr)(old_value) else: setattr(self.obj, self.attr, old_value) if self.master.cancel: self.master.cancel.add_redo(do_it) do_it() def update(self): pass class BoolEditor(Editor, Tkinter.Checkbutton): require_right_menu = 0 expand_right = 0 def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) self.value = Tkinter.BooleanVar() self.old_val = self.get_value() self.value.set(self.old_val) Tkinter.Checkbutton.__init__(self, master, variable = self.value, command = self.validate) def validate(self, event = None): self.old_val = value = self.value.get() self.set_value(value) def get_value(self): if getattr(self.obj, self.attr, 0): return 1 return 0 def set_value(self, value): if value: Editor.set_value(self, 1) else: Editor.set_value(self, 0) def update(self): self.old_val = self.get_value() self.value.set(self.old_val) class EntryEditor(Editor, Tkinter.Entry): def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) self.unicode = 0 # Will be overriden by subclasses' get_value() self.value = Tkinter.StringVar() self.old_value = self.get_value() if self.unicode: self.value.set(self.old_value.encode("latin")) # Entry doesn't support Unicode ??? else: self.value.set(self.old_value) Tkinter.Entry.__init__(self, master, width = 25, textvariable = self.value, selectbackground = "#CCCCFF") self.bind("" , self.focus_out) self.bind("", self.validate) def focus_out(self, event = None): value = self.value.get() if self.unicode: value = unicodify(value) else: value = encode (value) if value != self.old_value: self.validate() def validate(self, event = None): value = self.value.get() if self.unicode: value = unicodify(value) else: value = encode (value) self.old_value = value self.set_value(value) def get_value(self): if hasattr(self.obj, self.attr): return repr(getattr(self.obj, self.attr, "")) else: return "" def set_value(self, value): if value and (value[0] != "<"): try: value = editobj.eval(value) except: sys.excepthook(*sys.exc_info()) Editor.set_value(self, value) def update(self): self.old_value = self.get_value() if self.unicode: self.value.set(self.old_value.encode("latin")) # Entry doesn't support Unicode ??? else: self.value.set(self.old_value) class StringEditor(EntryEditor): def get_value(self): value = getattr(self.obj, self.attr, "") self.unicode = isinstance(value, unicode) return value def set_value(self, value): Editor.set_value(self, value) class IntEditor(EntryEditor): require_right_menu = 0 def get_value(self): return str(getattr(self.obj, self.attr, "")) def set_value(self, value): Editor.set_value(self, int(editobj.eval(value))) class FloatEditor(EntryEditor): require_right_menu = 0 def get_value(self): return str(getattr(self.obj, self.attr, "")) def set_value(self, value): Editor.set_value(self, float(editobj.eval(value))) class TextEditor(Editor, Tkinter.Frame): """A muti-line text editor.""" def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) Tkinter.Frame.__init__(self, master) self.columnconfigure(0, weight = 1) value = self.old_value = getattr(obj, attr, "") self.unicode = value.__class__ is unicode self.text = Tkinter.Text(self, width = 25, height = 5, wrap = "word", font = "Helvetica -12", selectbackground = "#CCCCFF") self.text.insert("end", value) self.text.bind("", self.validate) self.text.grid(sticky = "nsew") bar = Tkinter.Scrollbar(self, orient = VERTICAL) bar.grid(row = 0, column = 1, sticky = "nsew") bar['command'] = self.text.yview self.text['yscrollcommand'] = bar.set self.text.focus_out = self.validate def validate(self, event = None): value = self.text.get("0.0", "end") if value[-1] == "\n": value = value[0:-1] if self.unicode: value = unicodify(value) else: value = encode (value) self.set_value(value) def get_value(self): return getattr(self.obj, self.attr, "") def update(self): try: self.old_value = self.get_value() except AttributeError: return 1 # Not readable => do not update ! self.text.delete("0.0", "end") self.text.insert("end", self.old_value) def RangeEditor(min, max): """A slider-based editor.""" class _RangeEditor(Editor, Tkinter.Scale): def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) Tkinter.Scale.__init__(self, master, orient = Tkinter.HORIZONTAL, from_ = min, to = max) self.bind("", self.validate) self.update() def validate(self, event): self.set_value(self.get()) def get_value(self): return int(getattr(self.obj, self.attr, 0)) def update(self): try: self.set(getattr(self.obj, self.attr)) except AttributeError: pass # Not readable => do not update ! return _RangeEditor class _ListEditor(Editor, Tkinter.OptionMenu): require_right_menu = 0 expand_right = 1 _LIST = "(...)" def __init__(self, master, obj, attr, choices): Editor.__init__(self, master, obj, attr) self.choices = choices self.value = Tkinter.StringVar() self.value.set(self.get_value()) Tkinter.OptionMenu.__init__(self, master, self.value, command = self.validate, *choices) def validate(self, value): self.set_value(value) def update(self): self.value.set(self.get_value()) class _LongListEditor(Editor, Tkinter.Frame): require_right_menu = 0 expand_right = 1 def __init__(self, master, obj, attr, choices): Editor.__init__(self, master, obj, attr) Tkinter.Frame.__init__(self, master) self.choices = choices self.columnconfigure(0, weight = 1) self.listbox = Tkinter.Listbox(self, exportselection = 0, selectbackground = "#CCCCFF") i = 0 for choice in choices: self.listbox.insert(i, choice) i = i + 1 self.listbox.grid(sticky = "nsew") bar = Tkinter.Scrollbar(self, orient = VERTICAL) bar.grid(row = 0, column = 1, sticky = "nsew") bar['command'] = self.listbox.yview self.listbox['yscrollcommand'] = bar.set self.listbox.bind("", self.validate) self.update() def validate(self, event = None): self.set_value(self.choices[int(self.listbox.curselection()[0])]) def update(self): i = 0 value = self.get_value() while i < len(self.choices): if self.choices[i] == value: self.listbox.activate(i) self.listbox.selection_set(i) self.listbox.see(i) break i = i + 1 def ListEditor(values): """Create and return a new option-menu like Editor class. The values available are given by VALUES.""" str2values = {} for value in values: try: val = unicode(value) except: val = str(value) str2values[val] = value if len(values) < 30: super = _ListEditor else: super = _LongListEditor class ListEditor(super): def __init__(self, master, obj, attr): values_str = str2values.keys() values_str.sort() super.__init__(self, master, obj, attr, values_str) def get_value(self): try: return unicode(getattr(self.obj, self.attr)) except: return str (getattr(self.obj, self.attr)) def set_value(self, value): try: value = str2values[value] except KeyError: for val in values: text = str(val) if text == value: str2values[text] = val value = val break Editor.set_value(self, value) return ListEditor def EnumEditor(values): """Create and return a new option-menu like Editor class. The values available are given by VALUES, which is intented to be a dictionary mapping values to their string representations (e.g. { 0 : "disabled", 1 : "enabled" }).""" items = values.items() items.sort() values_str = zip(*items)[1] if len(values) < 30: super = _ListEditor else: super = _LongListEditor class ListEditor(super): def __init__(self, master, obj, attr): super.__init__(self, master, obj, attr, values_str) def get_value(self): return values.get(getattr(self.obj, self.attr), "") def set_value(self, value): for key, val in values.items(): if val == value: Editor.set_value(self, key) break return ListEditor def LambdaListEditor(lambd, value_tansformer = None): """Create and return a new option-menu like Editor class. The values available are the return values of LAMBD (which must be callable with one arg, the object edited), eventually transformed by VALUE_TRANSFORMER (which should be a callable with one arg).""" class ListEditor(_ListEditor): _LIST = "(...)" def __init__(self, master, obj, attr): _ListEditor.__init__(self, master, obj, attr, (getattr(obj, attr, ""),)) self.values = None self.bind("", self.on_list) def on_list(self, event = None): if self.values: return self.values = lambd(self.obj) self.str2value = {} menu = self["menu"] variable = self.value callback = self.validate menu.delete(0) if len(self.values) > 16: menu.add_command(label = self._LIST, command = self.on_listall) for value in self.values: text = str(value) self.str2value[text] = value menu.add_command(label = text, command = Tkinter._setit(variable, text, callback)) def on_listall(self, *args): # Usefull if there is too many element in the list. t = Toplevel() l = Listbox(t) l.pack(expand = 1, fill = "both", side = "left") for value in self.values: text = str(value) self.str2value[text] = value l.insert("end", text) b = Scrollbar(t, orient = Tkinter.VERTICAL, command = l.yview) b.pack(side = "right", fill = "y") l["yscrollcommand"] = b.set l.bind("", lambda event: self.set_value(l.get(l.curselection()[0]))) def get_value(self): return str(getattr(self.obj, self.attr)) def set_value(self, value): value = self.str2value[value] if value_tansformer: value = value_tansformer(value) Editor.set_value(self, value) return ListEditor class EditButtonEditor(Editor, Tkinter.Button, object): """Edits an object as a button; when the button is clicked, the object is edited in a new window.""" require_right_menu = 0 expand_right = 0 def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) text = self.get_value() if not isinstance(text, basestring): text = unicode(text) Tkinter.Button.__init__(self, master, text = text, command = self.button_click) def button_click(self): editobj.edit(self.get_value(), dialog = 1) def update(self): self["text"] = str(self.get_value()) class WithButtonEditor(Editor, Tkinter.Frame, object): require_right_menu = 0 expand_right = 1 def __init__(self, master, obj, attr, internal_editor_class, button_text): Editor.__init__(self, master, obj, attr) Tkinter.Frame.__init__(self, master) self.columnconfigure(0, weight = 1) self.internal_editor = internal_editor_class(self, obj, attr) self.internal_editor.grid(row = 0, column = 0, sticky = "NSEW") self.button = Tkinter.Button(self, text = button_text, command = self.button_click) self.button.grid(row = 0, column = 1) def button_click(self): pass def get_value(self): return self.internal_editor.get_value() def set_value(self, value): self.internal_editor.set_value(value) def update(self): self.internal_editor.update() def get_cancel(self): return self.master.cancel cancel = property(get_cancel) class FilenameEditor(WithButtonEditor): def __init__(self, master, obj, attr): WithButtonEditor.__init__(self, master, obj, attr, StringEditor, "...") def button_click(self): import tkFileDialog s = tkFileDialog.askopenfilename() if s: self.set_value(s) class DirnameEditor(WithButtonEditor): def __init__(self, master, obj, attr): WithButtonEditor.__init__(self, master, obj, attr, StringEditor, "...") def button_click(self): import tkFileDialog s = tkFileDialog.askdirectory() if s: self.set_value(s) class MethodEditor(Editor, Tkinter.Frame): require_right_menu = 0 expand_right = 1 def __init__(self, master, obj, attr, args_editor_class): Editor.__init__(self, master, obj, attr) Tkinter.Frame.__init__(self, master) self.args_editor_class = args_editor_class i = 0 for arg_editor_class in args_editor_class: self.columnconfigure(i, weight = 1) if arg_editor_class is FloatEditor: setattr(self, "_arg_%s" % i, 0.0) elif arg_editor_class is IntEditor: setattr(self, "_arg_%s" % i, 0) elif arg_editor_class is BoolEditor: setattr(self, "_arg_%s" % i, 0) elif arg_editor_class is StringEditor: setattr(self, "_arg_%s" % i, "") elif arg_editor_class is TextEditor: setattr(self, "_arg_%s" % i, "") else: setattr(self, "_arg_%s" % i, None) arg_editor = arg_editor_class(self, self, "_arg_%s" % i) arg_editor.grid(row = 0, column = i, sticky = "NSEW") i += 1 if args_editor_class: self.button = Tkinter.Button(self, text = custom.TRANSLATOR("Do"), command = self.button_click) self.button.grid(row = 0, column = i) else: self.columnconfigure(i, weight = 1) self.button = Tkinter.Button(self, text = custom.TRANSLATOR((callable(attr) and attr.__name__) or attr), command = self.button_click) self.button.grid(row = 0, column = i, sticky = "NSEW") def button_click(self): focused = self.focus_lastfor() if hasattr(focused, "focus_out"): focused.focus_out() if callable(self.attr): method = self.attr else: method = getattr(self.obj.__class__, self.attr) method(self.obj, *map(lambda i: getattr(self, "_arg_%s" % i), range(len(self.args_editor_class)))) def get_value(self): return None def set_value(self, value): pass def update(self): pass def get_cancel(self): return self.master.cancel cancel = property(get_cancel) class SubEditor(Editor, Tkinter.Frame, object): """An editor that edits its values in an inner property frame.""" def __init__(self, master, obj, attr): Editor.__init__(self, master, obj, attr) Tkinter.Frame.__init__(self, master) self.columnconfigure(0, weight = 1) import main self.propframe = main.EditPropertyFrame(self) self.propframe.edit(self.get_value()) self.propframe.pack() def get_cancel(self): return self.master.cancel cancel = property(get_cancel) class TypeEditor(Tkinter.OptionMenu): def __init__(self, master, obj, attr): self.obj = obj self.attr = attr self.value = Tkinter.StringVar() choices = custom._find_values(attr) apply(Tkinter.OptionMenu.__init__, [self, master, self.value, ""] + choices + ["(edit...)", "(set in clipboard)", "(paste clipboard)", "(deepcopy clipboard)"], {"command" : self.validate}) def validate(self, event = None): import editobj value = self.value.get() self.value.set("") if value == "(edit...)": editobj.edit(getattr(self.obj, self.attr)) elif value == "(set in clipboard)": editobj.clipboard = getattr(self.obj, self.attr) elif value == "(paste clipboard)": setattr(self.obj, self.attr, editobj.clipboard); self.master.fields[self.attr].update() elif value == "(deepcopy clipboard)": setattr(self.obj, self.attr, copy.deepcopy(editobj.clipboard)); self.master.fields[self.attr].update() else: self.master.fields[self.attr].set_value(value) EditObj-0.5.7/eventobj.py0000755000076500007650000006152210114336754013654 0ustar jibajiba# EventObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # This module is certainly my best hack ;-) # It is nothing else than a sequence of hacks, from the beginning to the end !!!!! """EventObj -- Allow to add attribute-change, content-change or hierarchy-change event to any Python instance. Provides the following functions (view their doc for more info): dumper_event(obj, attr, oldvalue, newvalue) addevent(obj, event) hasevent(obj[, event]) removeevent(obj, event) addevent_rec(obj, event) removeevent_rec(obj, event) And the following constant for addition/removal events: ADDITION REMOVAL Events : An event is any callable object that take 4 parameters (= the listened instance, the changed attribute name, the new value, the old value). After registration with addevent, it will be called just after any attribute is change on the instance. You can add many events on the same instance, and use the same event many times. An instance can implement __addevent__(event, copiable = 0), __hasevent__(event = None) and __removeevent__(event) to allow a custom event system. Notice that the event is weakref'ed, so if it is no longer accessible, the event is silently removed. Caution : As event management is performed by changing the class of the instance, you use eventobj with critical objects at your own risk... ! Quick example : >>> from editobj.eventobj import * >>> class C: pass >>> c = C() >>> def event(obj, attr, value, oldvalue): ... print "c." + attr, "was", `oldvalue` + ", is now", `value` >>> addevent(c, event) >>> c.x = 1 c.x was None, is now 1 Addition/Removal events : if you add them to a list / UserList or a dict / UserDict, or a subclass, events are also called for addition or removal in the list/dict. Addition or removal can be performed by any of the methods of UserList/UserDict (e.g. append, extend, remove, __setitem__,...) In this case, name will be the ADDITION or REMOVAL constant, value the added object for a list and the key-value pair for a dict, and oldvalue None. Quick example : >>> from editobj.eventobj import * >>> c = [] >>> def event(obj, attr, value, oldvalue): ... if attr is ADDITION: # Only for list / dict ... print `value`, "added to c" ... elif attr is REMOVAL: # Only for list / dict ... print `value`, "removed from c" ... else: ... print "c." + attr, "was", `oldvalue` + ", is now", `value` >>> addevent(c, event) >>> c.append(0) 0 added to c Hierachy events : such events are used with UserList or UserDict, and are usefull to listen an entire hierarchy (e.g. a list that contains other lists that can contain other lists...). The event apply to the registred instance, and any other item it contains, and so on if the list/dict is a deeper hierarchy. If you want to automatically add or remove the event when the hierarchy change (addition or removal at any level), use a HierarchyEvent (see example below). Quick example : >>> from editobj.eventobj import * >>> c = [] >>> def event(obj, attr, value, oldvalue): ... if attr is ADDITION: ... print `value`, "added to", `obj` ... elif attr is REMOVAL: ... print `value`, "removed from", `obj` >>> addevent_rec(c, event) >>> c.append([]) # This sub-list was not in c when we add the event... [] added to [[]] # [[]] is c >>> c[0].append(12) # ...but the hierarchical event has been automatically added ! 12 added to [12] # [12] is c[0] """ import warnings warnings.warn("editobj.eventobj is deprecated. Use the new editobj.observe module instead.", DeprecationWarning, stacklevel = 2) __all__ = [ "addevent", "hasevent", "removeevent", "addevent_rec", "removeevent_rec", "dumper_event", "ADDITION", "REMOVAL", ] import sys, new, weakref, types from UserList import UserList from UserDict import UserDict def to_list(obj): if isinstance(obj, list) or isinstance(obj, UserList): return obj if hasattr(obj, "children"): items = obj.children if callable(items): return items() return items if hasattr(obj, "items"): items = obj.items if callable(items): return items() return items if hasattr(obj, "__getitem__"): return obj return None def to_dict(obj): if isinstance(obj, dict) or isinstance(obj, UserDict): return obj return None def to_dict_or_list(obj): content = to_dict(obj) # Try dict... if content is None: content = to_list(obj) # Try list... if content is None: return None, None return list, content else: return dict, content def to_content(obj): content = to_dict(obj) # Try dict... if content is None: return to_list(obj) or () # Try list... return content.values() def dumper_event(obj, attr, value, old): """dumper_event -- an event that dump a stacktrace when called. Usefull for debugging ! dumper_event is the default value for add_event. """ import traceback traceback.print_stack() if attr is ADDITION: print "%s now contains %s." % (obj, value) elif attr is REMOVAL : print "%s no longer contains %s." % (obj, value) else: print "%s.%s was %s, is now %s." % (obj, attr, old, value) def addevent(obj, event = dumper_event): """addevent(obj, event = dumper_event) Add the given attribute-change event to OBJ. OBJ must be a class instance (old or new style) or a list / dict, and EVENT a function that takes 4 args: (obj, attr, value, old). EVENT will be called when any attribute of obj will be changed; OBJ is the object, ATTR the name of the attribute, VALUE the new value of the attribute and OLD the old one. EVENT defaults to the "dumper" event, which print each object modification. Raise eventobj.NonEventableError if OBJ cannot support event. """ event = _wrap_event(obj, event) try: obj.__addevent__(event) except: if hasattr(obj, "__dict__"): # Store the event and the old class of obj in an instance of _EventObj_stuff (a class we use to contain that). # Do that BEFORE we link the event (else we'll get a change event for the "_EventObj_stuff" attrib) ! obj._EventObj_stuff = _EventObj_stuff(obj.__class__) # Change the class of the object. obj.__class__ = _create_class(obj, obj.__class__) else: # Change the class of the object. old_class = obj.__class__ obj.__class__ = _create_class(obj, obj.__class__) stuff_for_non_dyn_objs[id(obj)] = _EventObj_stuff(old_class) obj.__addevent__(event) def hasevent(obj, event = None): """hasevent(obj[, event]) -> Boolean Return wether the obj instance has the given event (or has any event, if event is None).""" return hasattr(obj, "__hasevent__") and obj.__hasevent__(event) def removeevent(obj, event = dumper_event): """removeevent(obj, event = dumper_event) Remove the given event from obj.""" hasattr(obj, "__removeevent__") and obj.__removeevent__(event) ADDITION = "__added__" REMOVAL = "__removed__" NonEventableError = "NonEventableError" # Private stuff : # A dict to store the created classes. #_classes = weakref.WeakKeyDictionary() # old-style class cannot be weakref'ed ! _classes = {} # Create a with-event class for "clazz". the returned class is a "mixin" that will extends clazz and _EventObj (see below). def _create_class(obj, clazz): try: return _classes[clazz] except: if hasattr(obj, "__dict__"): # The name of the new class is the same name of the original class (mimetism !) if issubclass(clazz, list) or issubclass(clazz, UserList): cl = new.classobj(clazz.__name__, (_EventObj_List, _EventObj, clazz), {}) elif issubclass(clazz, dict) or issubclass(clazz, UserDict): cl = new.classobj(clazz.__name__, (_EventObj_Dict, _EventObj, clazz), {}) else: if issubclass(clazz, object): cl = new.classobj(clazz.__name__, (_EventObj, clazz), {}) else: cl = new.classobj(clazz.__name__, (_EventObj_OldStyle, clazz), {}) else: # list and dict were added in _classes at module initialization # Other types are not supported yet ! raise NonEventableError, obj # Change also the module name. cl.__module__ = clazz.__module__ _classes[clazz] = cl return cl # A container for _EventObj attribs. class _EventObj_stuff: def __init__(self, clazz): self.clazz = clazz self.events = [] def __call__(self, obj, attr, value, oldvalue): # Clone the list, since executing an event function may add or remove some events. for event in self.events[:]: event(obj, attr, value, oldvalue) def remove_event(self, event): self.events.remove(event) def has_event(self, event): return event in self.events def _wrap_event(obj, event, hi = 0): if not isinstance(event, WrappedEvent): #dump = repr(event) try: obj = weakref.proxy(obj) # Avoid cyclic ref except TypeError: pass def callback(o): #print "attention !", dump, "est mourant !" # This seems buggy since it is called when some objects are being destructed try: ob = obj if ob: if removeevent and hasevent(ob, event): removeevent(ob, event) except: pass if hi: if type(event) is types.MethodType: event = WeakHiMethod(event, callback) else: event = WeakHiFunc (event, callback) else: if type(event) is types.MethodType: event = WeakMethod(event, callback) else: event = WeakFunc (event, callback) return event class WrappedEvent: pass class WeakFunc(WrappedEvent): def __init__(self, func, callback = None): if callback: self.func = weakref.ref(func, callback) else: self.func = weakref.ref(func) def original(self): return self.func() def __call__(self, *args): self.func()(*args) def __eq__(self, other): return (self.func() == other) or (isinstance(other, WeakFunc) and (self.func() == other.func())) def __repr__(self): return "" % self.func() class WeakMethod(WrappedEvent): def __init__(self, method, callback = None): if callback: self.obj = weakref.ref(method.im_self, callback) else: self.obj = weakref.ref(method.im_self) self.func = method.im_func def original(self): obj = self.obj() return new.instancemethod(self.func, obj, obj.__class__) def __call__(self, *args): self.func(self.obj(), *args) def __eq__(self, other): return ((type(other) is types.MethodType) and (self.obj() is other.im_self) and (self.func is other.im_func)) or (isinstance(other, WeakMethod) and (self.obj() is other.obj()) and (self.func is other.func)) def __repr__(self): return "" % self.original() class HierarchyEvent: def __call__(self, obj, attr, value, oldvalue): if attr is ADDITION: try: if isinstance(obj, _EventObj_List): addevent_rec(value, self.original()) else: addevent_rec(value[1], self.original()) except NonEventableError: pass elif attr is REMOVAL: try: if isinstance(obj, _EventObj_List): removeevent_rec(value, self.original()) else: removeevent_rec(value[1], self.original()) except NonEventableError: pass class WeakHiFunc(HierarchyEvent, WeakFunc): def __call__(self, obj, attr, value, oldvalue): HierarchyEvent.__call__(self, obj, attr, value, oldvalue) WeakFunc.__call__(self, obj, attr, value, oldvalue) class WeakHiMethod(HierarchyEvent, WeakMethod): def __call__(self, obj, attr, value, oldvalue): HierarchyEvent.__call__(self, obj, attr, value, oldvalue) WeakMethod.__call__(self, obj, attr, value, oldvalue) # Mixin class used as base class for any with-event class. class _EventObj: stocks = [] def __setattr__(self, attr, value): # Get the old value of the changing attrib. oldvalue = getattr(self, attr, None) if attr == "__class__": newclass = _create_class(self, value) self._EventObj_stuff.clazz.__setattr__(self, "__class__", newclass) self._EventObj_stuff.clazz = value else: # If a __setattr__ is defined for obj's old class, call it. Else, just set the attrib in obj's __dict__ if hasattr(self._EventObj_stuff.clazz, "__setattr__"): self._EventObj_stuff.clazz.__setattr__(self, attr, value) else: self.__dict__[attr] = value # Comparison may fail try: if value == oldvalue: return except: pass # Call registered events, if needed for event in self._EventObj_stuff.events: event(self, attr, value, oldvalue) def __addevent__(self, event): self._EventObj_stuff.events.append(event) l = to_list(self) if (not l is None) and (not l is self): addevent(l, event) def __hasevent__(self, event = None): return (event is None) or (self._EventObj_stuff.has_event(event)) def __removeevent__(self, event): self._EventObj_stuff.remove_event(event) l = to_list(self) if (not l is None) and (not l is self): removeevent(l, event) if len(self._EventObj_stuff.events) == 0: self.__restore__() def __restore__(self): # If no event left, reset obj to its original class. if hasattr(self._EventObj_stuff.clazz, "__setattr__"): self._EventObj_stuff.clazz.__setattr__(self, "__class__", self._EventObj_stuff.clazz) else: self.__class__ = self._EventObj_stuff.clazz # And delete the _EventObj_stuff. del self._EventObj_stuff # Called at pickling time def __getstate__(self): try: dict = self._EventObj_stuff.clazz.__getstate__(self) if dict is self.__dict__: dict = dict.copy() except: dict = self.__dict__.copy() try: del dict["_EventObj_stuff"] # Remove what we have added. if dict.has_key("children"): dict["children"] = list(dict["children"]) elif dict.has_key("items" ): dict["items" ] = list(dict["items" ]) except: pass # Not a dictionary ?? return dict def __reduce__(self): red = self._EventObj_stuff.clazz.__reduce__(self) if (not isinstance(red[1], list)) and (not isinstance(red[1], tuple)): return red def rec_check(t): if t is self.__class__: return self._EventObj_stuff.clazz if type(t) is tuple: return tuple(map(rec_check, t)) return t if len(red) == 2: return red[0], tuple(map(rec_check, red[1])) else: return red[0], tuple(map(rec_check, red[1])), red[2] class _EventObj_OldStyle(_EventObj): def __deepcopy__(self, memo): if hasattr(self._EventObj_stuff.clazz, "__deepcopy__"): clone = self._EventObj_stuff.clazz.__deepcopy__(self, memo) if clone.__class__ is self.__class__: clone.__class__ = self._EventObj_stuff.clazz if hasattr(clone, "_EventObj_stuff"): del clone._EventObj_stuff return clone else: import copy if hasattr(self, '__getinitargs__'): args = self.__getinitargs__() copy._keep_alive(args, memo) args = copy.deepcopy(args, memo) return apply(self._EventObj_stuff.clazz, args) else: y = copy._EmptyClass() y.__class__ = self._EventObj_stuff.clazz memo[id(self)] = y if hasattr(self, '__getstate__'): state = self.__getstate__() copy._keep_alive(state, memo) else: state = self.__dict__ state = copy.deepcopy(state, memo) if hasattr(y, '__setstate__'): y.__setstate__(state) else: y.__dict__.update(state) return y def __copy__(self): if hasattr(self._EventObj_stuff.clazz, "__copy__"): clone = self._EventObj_stuff.clazz.__copy__(self) if clone.__class__ is self.__class__: clone.__class__ = self._EventObj_stuff.clazz if hasattr(clone, "_EventObj_stuff"): del clone._EventObj_stuff return clone else: import copy if hasattr(self, '__getinitargs__'): args = self.__getinitargs__() return apply(self._EventObj_stuff.clazz, args) else: y = copy._EmptyClass() y.__class__ = self._EventObj_stuff.clazz if hasattr(self, '__getstate__'): state = self.__getstate__() else: state = self.__dict__ if hasattr(y, '__setstate__'): y.__setstate__(state) else: y.__dict__.update(state) return y class _EventObj_List(_EventObj): def __added__ (self, value): self._EventObj_stuff(self, ADDITION, value, None) def __removed__(self, value): self._EventObj_stuff(self, REMOVAL , value, None) def append(self, value): self._EventObj_stuff.clazz.append(self, value) self.__added__(value) def insert(self, before, value): self._EventObj_stuff.clazz.insert(self, before, value) self.__added__(value) def extend(self, list): self._EventObj_stuff.clazz.extend(self, list) for value in list: self.__added__(value) def remove(self, value): self._EventObj_stuff.clazz.remove(self, value) self.__removed__(value) def pop(self, index = -1): value = self._EventObj_stuff.clazz.pop(self, index) self.__removed__(value) return value def __setitem__(self, index, new): old = self[index] self._EventObj_stuff.clazz.__setitem__(self, index, new) self.__removed__(old) self.__added__ (new) def __delitem__(self, index): value = self[index] self._EventObj_stuff.clazz.__delitem__(self, index) self.__removed__(value) def __setslice__(self, i, j, slice): olds = self[i:j] self._EventObj_stuff.clazz.__setslice__(self, i, j, slice) for value in olds : self.__removed__(value) for value in slice: self.__added__ (value) def __delslice__(self, i, j): olds = self[i:j] self._EventObj_stuff.clazz.__delslice__(self, i, j) for value in olds : self.__removed__(value) def __iadd__(self, list): self._EventObj_stuff.clazz.__iadd__(self, list) for value in list: self.__added__(value) return self def __imul__(self, n): olds = self[:] self._EventObj_stuff.clazz.__imul__(self, n) if n == 0: for value in olds: self.__removed__(value) else: for value in olds * (n - 1): self.__added__(value) return self class _EventObj_Dict(_EventObj): def __added__ (self, key, value): self._EventObj_stuff(self, ADDITION, (key, value), None) def __removed__(self, key, value): self._EventObj_stuff(self, REMOVAL , (key, value), None) def update(self, dict): old = {} for key, value in dict.items(): if self.has_key(key): old[key] = value self._EventObj_stuff.clazz.update(self, dict) for key, value in old .items(): self.__removed__(key, value) for key, value in dict.items(): self.__added__ (key, value) def popitem(self): old = self._EventObj_stuff.clazz.popitem(self) self.__removed__(old[0], old[1]) return old def clear(self): old = self.items() self._EventObj_stuff.clazz.clear(self) for key, value in old: self.__removed__(key, value) def __setitem__(self, key, new): if self.has_key(key): old = self[key] self._EventObj_stuff.clazz.__setitem__(self, key, new) self.__removed__(key, old) else: self._EventObj_stuff.clazz.__setitem__(self, key, new) self.__added__(key, new) def __delitem__(self, key): value = self[key] self._EventObj_stuff.clazz.__delitem__(self, key) self.__removed__(key, value) # EventObj class for plain list (e.g. []) and plain dict : # EventObj stuff is not stored in the object's dict (because no such dict...) # but in this dictionary : #stuff_for_non_dyn_objs = weakref.WeakKeyDictionary() stuff_for_non_dyn_objs = {} if sys.version_info[:2] == (2, 2): class _EventObj_PlainList(_EventObj_List, list): __slots__ = [] def _get_EventObj_stuff(self): return stuff_for_non_dyn_objs[id(self)] def _set_EventObj_stuff(self, stuff): stuff_for_non_dyn_objs[id(self)] = stuff _EventObj_stuff = property(_get_EventObj_stuff, _set_EventObj_stuff) def __restore__(self): # If no event left, delete the _EventObj_stuff and reset obj to its original class. # Bypass the _EventObj.__setattr__ (it would crash since _EventObj_stuff is no longer available after the class change) self._EventObj_stuff.clazz.__setattr__(self, "__class__", self._EventObj_stuff.clazz) del stuff_for_non_dyn_objs[id(self)] def __getstate__(self): return None _classes[list] = _EventObj_PlainList class _EventObj_PlainDict(_EventObj_Dict, dict): __slots__ = [] def _get_EventObj_stuff(self): return stuff_for_non_dyn_objs[id(self)] def _set_EventObj_stuff(self, stuff): stuff_for_non_dyn_objs[id(self)] = stuff _EventObj_stuff = property(_get_EventObj_stuff, _set_EventObj_stuff) def __restore__(self): # If no event left, delete the _EventObj_stuff and reset obj to its original class. # Bypass the _EventObj.__setattr__ (it would crash since _EventObj_stuff is no longer available after the class change) self._EventObj_stuff.clazz.__setattr__(self, "__class__", self._EventObj_stuff.clazz) del stuff_for_non_dyn_objs[id(self)] def __getstate__(self): return None _classes[dict] = _EventObj_PlainDict else: class _EventObj_PlainList(list): __slots__ = [] def _get_EventObj_stuff(self): return stuff_for_non_dyn_objs[id(self)] def _set_EventObj_stuff(self, stuff): stuff_for_non_dyn_objs[id(self)] = stuff _EventObj_stuff = property(_get_EventObj_stuff, _set_EventObj_stuff) def __restore__(self): # If no event left, delete the _EventObj_stuff and reset obj to its original class. # Bypass the _EventObj.__setattr__ (it would crash since _EventObj_stuff is no longer available after the class change) self._EventObj_stuff.clazz.__setattr__(self, "__class__", self._EventObj_stuff.clazz) del stuff_for_non_dyn_objs[id(self)] def __getstate__(self): return None # HACK !!! # The _EventObj_List base is not declared in the class, but added later. # This prevent the _EventObj_PlainList class to be based on object, # and e.g. to be allowed a weaklist. _EventObj_PlainList.__bases__ = (_EventObj_List, list) _classes[list] = _EventObj_PlainList class _EventObj_PlainDict(dict): __slots__ = [] def _get_EventObj_stuff(self): return stuff_for_non_dyn_objs[id(self)] def _set_EventObj_stuff(self, stuff): stuff_for_non_dyn_objs[id(self)] = stuff _EventObj_stuff = property(_get_EventObj_stuff, _set_EventObj_stuff) def __restore__(self): # If no event left, delete the _EventObj_stuff and reset obj to its original class. # Bypass the _EventObj.__setattr__ (it would crash since _EventObj_stuff is no longer available after the class change) self._EventObj_stuff.clazz.__setattr__(self, "__class__", self._EventObj_stuff.clazz) del stuff_for_non_dyn_objs[id(self)] def __getstate__(self): return None # HACK strikes again ! _EventObj_PlainDict.__bases__ = (_EventObj_Dict, dict) _classes[dict] = _EventObj_PlainDict # Hierarchy stuff : def addevent_rec(obj, event = dumper_event): """addevent_rec(obj, event = dumper_event) Add event for obj, like addevent, but proceed recursively in all the hierarchy : if obj is a UserList/UserDict, event will be added to each instance obj contains, recursively. If the hierarchy is changed, the newly added items will DO have the event, and the removed ones will no longuer have it.""" if not hasevent(obj, event): # Avoid problem with cyclic list/dict # Wrap event in a hierarchy event if not isinstance(event, HierarchyEvent): wevent = _wrap_event(obj, event, 1) addevent(obj, wevent) for o in to_content(obj): try: addevent_rec(o, event) except NonEventableError: pass def removeevent_rec(obj, event = dumper_event): """removeevent_rec(obj, event = dumper_event) Remove event for obj, like removeevent, but proceed recursively.""" if hasevent(obj, event): # Avoid problem with cyclic list/dict removeevent(obj, event) for o in to_content(obj): if isinstance(o, _EventObj): removeevent_rec(o, event) def change_class(obj, newclass): """Change the class of OBJ to NEWCLASS, but keep the events it may have.""" events = obj._EventObj_stuff.events[:] for event in events: removeevent(obj, event) obj.__class__ = newclass for event in events: addevent(obj, event) EditObj-0.5.7/main.py0000755000076500007650000005432410114336755012767 0ustar jibajiba# EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from Tkinter import * from editobj import * from editobj.observe import * import os, sys, types, editobj, editobj.editor as editor, editobj.custom as custom, editobj.treewidget as treewidget class EditWindow(Toplevel): def __init__(self, o = None, show_right_menu = 1, dialog = 0, command = None, preamble = None, cancel = None, grab = 0): # Get, or create if needed, the root Tk. try: from Tkinter import _default_root tkroot = _default_root except ImportError: pass if not tkroot: tkroot = Tk(className = 'EditObj') tkroot.withdraw() import editobj.observe if not editobj.observe.SCANNING: start_observing_tk() self.history = [] self.dialog = dialog if not cancel: from cancel import Context cancel = Context() if grab and cancel: cancel.push() self.cancel = cancel if not dialog: menubar = Menu(tkroot) self.picklemenu = picklemenu = Menu(menubar) picklemenu.add_command(label = 'Load...' , command = self.pickle_load) picklemenu.add_command(label = 'Save' , command = self.pickle_save) picklemenu.add_command(label = 'Save as...', command = self.pickle_save_as) menubar.add_cascade(label='Pickle', menu = picklemenu) self.copymenu = copymenu = Menu(menubar) copymenu.add_command(label = 'Copy...' , command = self.copy_copy) copymenu.add_command(label = 'Deep copy...', command = self.copy_deepcopy) menubar.add_cascade(label='Copy', menu = copymenu) self.editmenu = editmenu = Menu(menubar) if self.cancel: editmenu.add_command(label = 'Undo' , command = self.edit_undo) editmenu.add_command(label = 'Redo' , command = self.edit_redo) editmenu.add_separator() editmenu.add_command(label = 'New hierarchy view...', command = self.edit_newview) editmenu.add_command(label = 'New property view...' , command = self.edit_newpropview) editmenu.add_command(label = 'Back' , command = self.edit_back) editmenu.add_command(label = 'Console...' , command = self.display_console) menubar.add_cascade(label='Edit', menu = editmenu) else: menubar = None self.filename = None Toplevel.__init__(self, tkroot, menu = menubar) self.bind("", self.__del) self.withdraw() if preamble: t = Text(self, width = 0, height = 10, wrap = "word", highlightthickness = 0, font = "Helvetica -12", selectbackground = "#CCCCFF") t.pack(fill = BOTH, side = TOP) t.insert("end", preamble) if dialog: def _command(event = None): # Focus the OK button so as the currently active editor will be unfocused (as some editors validate themselves when they are unfocused). ok.focus_set() self.update() # Ensure the Tk unfocus event will be sent self.destroy() if command: def do_it(): command() if cancel: cancel.add_post_cancel(cancel_it) # Do it AFTER the other cancellable operation !!! def cancel_it(): command() if cancel: cancel.add_post_redo(do_it) do_it() if grab and cancel: cancel.pop() ok = Button(self, text = "OK", command = _command) ok.pack(expand = 0, fill = X, side = BOTTOM) self.wm_protocol("WM_DELETE_WINDOW", _command) self.scrollpane = treewidget.ScrollPane(self, 0, 1) self.scrollpane.pack(expand = 1, fill = BOTH, side = BOTTOM) self.propframe = EditPropertyFrame(self, show_right_menu, self.cancel, bd = 0) self.propframe.edit = self.edit self.scrollpane.setContent(self.propframe) self.edited = None self.hierarchy = None self.hierarchyedited = None self.buttonsframe = None self.console = None self.edit(o) if dialog: ok.tkraise() self.deiconify() if grab: self.grab_set() def createButtonsFrame(self): self.buttonsframe = Frame(self, borderwidth = 2, relief = "raised") self.buttonsframe.pack(expand = 0, fill = BOTH, side = TOP) self.buttonsframe.columnconfigure(5, weight = 1) self.addbutton = Button(self.buttonsframe, padx = 5, text = custom.TRANSLATOR("Add..."), command = self.button_add, relief = "flat") self.addbutton.grid(row = 0, column = 0, sticky = E + W) self.removebutton = Button(self.buttonsframe, padx = 5, text = custom.TRANSLATOR("Remove"), command = self.button_remove, relief = "flat") self.removebutton.grid(row = 0, column = 1, sticky = E + W) self.upbutton = Button(self.buttonsframe, padx = 5, text = custom.TRANSLATOR("Up"), command = self.button_up, relief = "flat") self.upbutton.grid(row = 0, column = 2, sticky = E + W) self.downbutton = Button(self.buttonsframe, padx = 5, text = custom.TRANSLATOR("Down"), command = self.button_down, relief = "flat") self.downbutton.grid(row = 0, column = 3, sticky = E + W) if self.propframe.show_right_menu: self.clipbutton = Button(self.buttonsframe, padx = 5, text = custom.TRANSLATOR("Copy"), command = self.button_clip, relief = "flat") self.clipbutton.grid(row = 0, column = 4, sticky = E + W) #def __del__(self): # print "__del__ de", self def __del(self, event): if (type(event.widget) is StringType and event.widget[1:] == self.winfo_name()) or event.widget == self: if not self.dialog: self.picklemenu.destroy() self.copymenu .destroy() self.editmenu .destroy() self.unsetevent() def setevent (self): pass def unsetevent(self): pass def edit(self, o, newview = 0, reedition = 0): if newview: edit(o) else: locked = self.winfo_ismapped() if locked: self.pack_propagate(0) o = editobj.editable(o) self.history.append(o) self.edited = o if hasattr(o, "filename"): self.filename = o.filename EditPropertyFrame.edit(self.propframe, o, reedition) self.scrollpane.updateContentSize() if self.console: self.console.dict["obj"] = o # If the new edited object is in the same hierarchy than the former, keep the same hierarchy. Else, create one. if (self.hierarchyedited is None) or not ((self.hierarchyedited is o) or editobj.in_hierarchy(o, self.hierarchyedited)): children = custom._find_children(o) if not children is None: self.unsetevent() self.hierarchyedited = self.edited self.setevent() if self.hierarchy is None: self.createButtonsFrame() self.hierarchy = treewidget.Tree(self) self.hierarchy.pack(expand = 1, fill = BOTH) ItemNode(self.hierarchy, o, editor = self.edit) else: if not self.hierarchyedited is None: self.hierarchy.destroy() self.unsetevent() self.hierarchy = None self.hierarchyedited = None self.buttonsframe.destroy() self.buttonsframe = None if locked: self.pack_propagate(1) def button_add(self): if self.hierarchy.selection: o = self.hierarchy.selection[0] else: o = self.hierarchy.node items, insert, del_ = custom._find_children_insert_remove(o.item) while items is None: o = o.parent items, insert, del_ = custom._find_children_insert_remove(o.item) def add_this(key, item): def do_it(): try: insert(key, item) except TypeError: insert(item) if self.cancel: self.cancel.add_cancel(cancel_it) def cancel_it(): del_(key) if self.cancel: self.cancel.add_redo(do_it) do_it() choices = custom._find_available_children(o.item) if isinstance(choices, list): if isinstance(items, list): AddDialog(o.item, choices, len(items), add_this) else: AddDialog(o.item, choices, "" , add_this) elif choices: try: choice = editobj.eval(choices, locals = {"parent" : o.item}) except NotImplementedError: return add_this(len(items), choice) def button_remove(self): if not self.hierarchy.selection: return o = self.hierarchy.selection[0] items, insert, del_ = custom._find_children_insert_remove(o.itemparent) key = o.key item = o.item def do_it(): del_(key) if self.cancel: self.cancel.add_cancel(cancel_it) def cancel_it(): try: insert(key, item) except TypeError: insert(item) if self.cancel: self.cancel.add_redo(do_it) do_it() def button_up(self): if not self.hierarchy.selection: return o = self.hierarchy.selection[0] items = custom._find_children(o.itemparent) if not isinstance(items, list): return i = o.key def do_it(): items[i - 1], items[i] = items[i], items[i - 1] if self.cancel: self.cancel.add_cancel(cancel_it) def cancel_it(): items[i - 1], items[i] = items[i], items[i - 1] if self.cancel: self.cancel.add_redo(do_it) do_it() def button_down(self): if not self.hierarchy.selection: return o = self.hierarchy.selection[0] items = custom._find_children(o.itemparent) if not isinstance(items, list): return i = o.key def do_it(): if i + 1 == len(items): items[0 ], items[i] = items[i], items[0] else: items[i + 1], items[i] = items[i], items[i + 1] if self.cancel: self.cancel.add_cancel(cancel_it) def cancel_it(): if i + 1 == len(items): items[0 ], items[i] = items[i], items[0] else: items[i + 1], items[i] = items[i], items[i + 1] if self.cancel: self.cancel.add_redo(do_it) do_it() def button_clip(self): editobj.clipboard = self.hierarchy.selection[0].item def pickle_save(self): obj = self.hierarchyedited or self.edited if hasattr(obj, "save"): try: obj.save() return except TypeError: sys.excepthook(*sys.exc_info()) if self.filename: import cPickle as pickle pickle.dump(obj, open(self.filename, "w")) else: self.pickle_save_as() def pickle_save_as(self): obj = self.hierarchyedited or self.edited import cPickle as pickle, tkFileDialog self.filename = tkFileDialog.asksaveasfilename() if hasattr(obj, "save"): try: obj.save(self.filename) return except TypeError: pass pickle.dump(obj, open(self.filename, "w")) def pickle_load(self): import cPickle as pickle, tkFileDialog self.filename = tkFileDialog.askopenfilename() self.edit(pickle.load(open(self.filename, "r"))) def copy_copy(self): import copy edit(copy.copy(self.edited)) def copy_deepcopy(self): import copy edit(copy.deepcopy(self.edited)) def edit_undo(self): self.cancel.cancel() def edit_redo(self): self.cancel.redo() def edit_newview(self): if self.hierarchyedited is None: self.edit_newpropview() else: edit(self.hierarchyedited) def edit_newpropview(self): edit(self.edited) def edit_back(self): if len(self.history) >= 2: self.history.pop() self.edit(self.history.pop()) def display_console(self): if not self.console: import editobj.console dict = { "root" : self.hierarchyedited or self.edited, "obj" : self.edited } self.console = editobj.console.Console(self, dict = dict, globals = editobj.EVAL_ENV) self.console.text.insert("end", """\nYou can access the currently edited obj as "obj", and the root of the hierarchy as "root".\n""") self.console.text.insert("end", sys.ps1) self.console.text.configure(width = 10, height = 10) self.console.pack(fill = BOTH, expand = 1, side = BOTTOM) else: self.console.destroy() self.console = None class EditPropertyFrame(Frame): def __init__(self, master, show_right_menu = 1, cancel = None, **opts): self.edited = None self.show_right_menu = show_right_menu self.cancel = cancel Frame.__init__(self, master, opts) self.columnconfigure(0, weight = 0) self.columnconfigure(1, weight = 1) self.columnconfigure(2, weight = 0) self.bind("", self.__del) def __del(self, event = None): self.unsetevent() def setevent (self): observe (self.edited, self.edited_changed) def unsetevent(self): unobserve(self.edited, self.edited_changed) def edit(self, o, reedition = 0): self.unsetevent() if not reedition: custom._call_on_edit(o, self.master) self.fields = {} # Unfocus the current widget -- avoid change in this widget to be lost ! focused = self.focus_lastfor() if hasattr(focused, "focus_out"): focused.focus_out() # Hack !!! The "else" case should work with ANY editor widget, but it doesn't...?? In particular, it works with TextEditor, but not with StringEditor ! else: try: focused.tk_focusNext().focus_set() focused.update() # Ensure the Tk unfocus event will be sent except: pass for widget in self.children.values(): widget.destroy() self.edited = o if not o is None: self.edited_attrs = editobj.attrs_of(o) attrs = map(lambda attr: (custom.TRANSLATOR(attr), attr), self.edited_attrs) attrs.sort() line = 1 for (translation, attr) in attrs: fieldclass = custom._find_editor(o, attr) if fieldclass: label = Label(self, text = translation) label.grid(row = line, column = 0, sticky = "WNS") field = self.fields[attr] = fieldclass(self, o, attr) if self.show_right_menu and field.require_right_menu: field.grid(row = line, column = 1, sticky = "EWNS") change = editor.TypeEditor(self, o, attr) change.grid(row = line, column = 2, sticky = "EWNS") else: field.grid(row = line, column = 1, columnspan = 1 + field.expand_right, sticky = "EWNS") line += 1 methods = custom._find_methods(o) methods = map(lambda (method, args_editor): (custom.TRANSLATOR((callable(method) and method.__name__) or method), method, args_editor), methods) methods.sort() for translation, method, args_editor in methods: label = Label(self, text = translation) label.grid(row = line, column = 0, sticky = "WNS") field = self.fields[method] = editor.MethodEditor(self, o, method, args_editor) field.grid(row = line, column = 1, columnspan = 1 + field.expand_right, sticky = "EWNS") line += 1 self.setevent() def edited_changed(self, o, type, new_dict, old_dict): if type is object: for attr, new, old in diffdict(new_dict, old_dict): if ((old is None) and (not attr in self.edited_attrs)) or (not hasattr(o, attr)): self.edit(self.edited, reedition = 1) # A new attribute return field = self.fields.get(attr) if field: field.update() def update(self): for field in self.fields.values(): field.update() class ItemNode(treewidget.Node): def __init__(self, parent, item, itemparent = None, editor = edit, key = None, show_key = 0): self.item = item self.itemparent = itemparent self.edit = editor self.key = key self.show_key = show_key treewidget.Node.__init__(self, parent) self.setevent() def setevent (self): items = custom._find_children(self.item) observe(self.item, self.content_changed) if (not items is None) and (not items is self.item): observe(items, self.content_changed) def unsetevent(self): items = custom._find_children(self.item) unobserve(self.item, self.content_changed) if (not items is None) and (not items is self.item): unobserve(items, self.content_changed) def destroy(self): treewidget.Node.destroy(self) self.unsetevent() def unseteventtree(self): self.unsetevent() for child in self.children: child.unseteventtree() def __unicode__(self): if (not self.show_key) or (self.key is None): return unicode(self.item) return unicode(self.key) + ": " + unicode(self.item) def createchildren(self, oldchildren = ()): children = [] items = custom._find_children(self.item) if isinstance(items, list): i = 0 for item in items: for oldchild in oldchildren: # Re-use the child if possible. if oldchild.item is item: oldchild.key = i # Index may have changed... oldchild.update() # ... so we need to update. children.append(oldchild) oldchildren.remove(oldchild) # Do not re-use it twice ! break else: children.append(ItemNode(self, item, self.item, self.edit, i, 0)) i += 1 for child in oldchildren: child._undrawtree() child.unseteventtree() elif isinstance(items, dict): items = items.items() for child in oldchildren: for key, item in items: if item is child.item and key == child.key: children.append(child) # Re-use this one. items.remove((key, item)) # Do not re-use it twice ! break else: # Delete this one. child._undrawtree() child.unseteventtree() for key, item in items: # The new ones are left. children.append(ItemNode(self, item, self.item, self.edit, key, 1)) return children def iseditable(self): return 0 def isexpandable(self): return custom._find_children(self.item) # Avoid calling it too often... def settext(self, text): pass def select(self, event = None): treewidget.Node.select(self) if type(self.item) in _WRAPPED_TYPE: # self.item is not an editable object ! => Wrap it ! self.edit(StringOrNumberWrapper(self.key, self.item, self.itemparent, self)) else: self.edit(self.item) def expand(self, event = None): treewidget.Node.expand(self, event) custom._call_on_children_visible(self.item, 1) def collapse(self, event = None): treewidget.Node.collapse(self, event) custom._call_on_children_visible(self.item, 0) def content_changed(self, o, type, new, old): self.update() self.updatechildren() _WRAPPED_TYPE = (int, long, float, complex, str, unicode) class StringOrNumberWrapper: def __init__(self, key, value, parent, itemnode): self._key = key self.value = value self._parent = parent self._itemnode = itemnode def __setattr__(self, name, value): if name == "value" and hasattr(self, "_parent"): self._itemnode.item = value self._itemnode.update() items, insert, del_ = custom._find_children_insert_remove(self._parent) del_(self._key) try: insert(self._key, value) except TypeError: insert(value) self.__dict__[name] = value def __cmp__(self, other): return cmp(self.value, other) def __eq__ (self, other): return self.value == other def __hash__ (self): return object.__hash__(self) class AddDialog(Toplevel): def __init__(self, add_into, choices, default_key, callback): self.add_into = add_into self.callback = callback choices = choices + [u"(paste clipboard)", u"(deepcopy clipboard)"] Toplevel.__init__(self) self.columnconfigure(0, weight = 1) self.columnconfigure(1, weight = 0) self.entryk = Entry(self) self.entryk.insert(0, repr(default_key)) self.entryk.grid(row = 0, columnspan = 2, sticky = "EW") self.var = StringVar() self.entry = Entry(self, textvariable = self.var) self.entry.grid(row = 1, columnspan = 2, sticky = "EW") self.entry.focus_set() self.entry.bind("", self.validate) self.list = Listbox(self) if choices: apply(self.list.insert, [0] + choices) self.list.bind("", self.list_selected) self.list.bind("", self.validate) self.list.grid(row = 2, column = 0, sticky = "NSEW") self.rowconfigure(2, weight = 1) self.bar = Scrollbar(self, orient = VERTICAL) self.bar.grid(row = 2, column = 1, sticky = "NSEW") self.list['yscrollcommand'] = self.bar.set self.bar['command'] = self.list.yview self.ok = Button(self, text = "OK", command = self.validate) self.ok.grid(row = 3, columnspan = 2, sticky = "EW") def list_selected(self, event = None): self.var.set(self.list.selection_get()) def validate(self, event = None): self.choosen(self.var.get()) self.destroy() def choosen(self, text): if text == u"(paste clipboard)" : added = editobj.clipboard elif text == u"(deepcopy clipboard)": added = copy.deepcopy(editobj.clipboard) else: added = editobj.eval(text, locals = {"parent" : self.add_into}) self.callback(editobj.eval(self.entryk.get(), locals = {"parent" : self.add_into}), added) def list_or_dict_class_of(clazz): "Figure out what is the class that extends UserList or UserDict in the class inheritance tree..." # Guess that the the type of the item is associated with the class that inherit UserList/UserDict if (list in clazz.__bases__) or (dict in clazz.__bases__) or (UserList in clazz.__bases__) or (UserDict in clazz.__bases__): return clazz # Recursive for base in clazz.__bases__: answer = list_or_dict_class_of(base) if answer: return answer EditObj-0.5.7/observe.py0000755000076500007650000002733210376705613013512 0ustar jibajiba# observe.py # Copyright (C) 2003 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """editobj.observe -- Observation framework observe(obj, listener) registers listener as a listener for obj; when obj is modified, listener will be called (asynchronously). obj can be a Python instance (old-style or new-style), a list or a dictionary. The listener is a function of the form: def my_listener(obj, type, new, old): ... where obj is the modified object, type is the type of the modification, and old and new are the old and new values. Type can be: - object (the Python root class): one or more attributes have changed on obj. old and new are the old and new attribute dictionary of obj (this dictionary includes attributes in obj.__dict__, but also Python's property and C-level getset). If you want to know which attribute has changed, use dictdiff on new and old (see the diffdict function docstring). - list : one or more addition / deletion have occured on obj (which is a list). new and old are the new and old list content of obj. - dict : one or more assignment / deletion have occured on obj (which is a mapping). new and old are the new and old dictionary values. - "__class__" : the class of obj has changed. new and old are the new and old classes. Before all, you need to start the observer daemon, either in a new thread by calling start_observing(), or in the Tkinter thread with start_observing_tk(). You can also call scan() yourself when you want to check for change. This module is the successor of the deprecated eventobj; it is much much much cleaner. Quick example : >>> from editobj.observe import * >>> start_observing() >>> class C: pass ... >>> c = C() >>> def listener(obj, type, new, old): ... if type is object: ... for (attr, newvalue, oldvalue) in diffdict(new, old): ... print "c.%s was %s, is now %s" % (attr, oldvalue, newvalue) >>> observe(c, listener) >>> c.x = 1 c.x was None, is now 1 See observe_tree and unobserve_tree for observing nested list and / or dict structures.""" __all__ = [ "observe", "isobserved", "unobserve", "observe_tree", "unobserve_tree", "start_observing", "start_observing_tk", "stop_observing", "scan", "diffdict", ] from weakref import ref observed_lists = {} observed_dicts = {} observed_objects = {} class_to_properties = {} PROPERTY_TYPE_NAMES = ("property", "getset_descriptor") def observe(object, listener): """observe(object, listener) Registers LISTENER as a listener of OBJECT. When OBJECT will be changed, LISTENER will be called (asynchronously). See the module docstrings for more info about what argument receives a listener""" if hasattr(object, "__observe__"): object.__observe__(listener) i = id(object) if isinstance(object, list): observation = observed_lists.get(i) if observation: observation.listeners.append(listener) else: observation = observed_lists[i] = Observation([listener], list(object)) observation.object = object elif isinstance(object, dict): observation = observed_dicts.get(i) if observation: observation.listeners.append(listener) else: observation = observed_dicts[i] = Observation([listener], dict(object)) observation.object = object if hasattr(object, "__dict__"): observation = observed_objects.get(i) if observation: observation.listeners.append(listener) else: observation = observed_objects[i] = Observation([listener], None) try: observation.object = weakref.ref(object) except: observation.object = object observation.old_class = object.__class__ # Checks for property and/or C level getset props = observation.props = class_to_properties.get(object.__class__) if props is None: props = observation.props = class_to_properties[object.__class__] = filter(lambda attr: type(getattr(object.__class__, attr)).__name__ in PROPERTY_TYPE_NAMES, dir(object.__class__)) if props: observation.old = dict(map(lambda prop: (prop, getattr(object, prop, None)), observation.props)) observation.old.update(object.__dict__) else: observation.old = object.__dict__.copy() def isobserved(object, listener = None): """isobserved(object, listener = None) Return true if LISTENER is observing OBJECT. If listener is None, returns the list of observation for OBJECT.""" i = id(object) observation = observed_lists.get(i) or observed_dicts.get(i) or (hasattr(object, "__dict__") and observed_objects.get(i)) if listener: return observation and (listener in observation.listeners) else: return observation and observation.listeners def unobserve(object, listener = None): """unobserve(object, listener = None) Unregisters the listener LISTENER for OBJECT. If LISTENER is not listening OBJECT, nothing is done. If LISTENER is None, unregisters *all* listener on OBJECT.""" if hasattr(object, "__unobserve__"): object.__unobserve__(listener) i = id(object) if listener: if isinstance(object, list): observation = observed_lists.get(i) if observation: try: observation.listeners.remove(listener) except ValueError: pass if not observation.listeners: del observed_lists[i] elif isinstance(object, dict): observation = observed_dicts.get(i) if observation: try: observation.listeners.remove(listener) except ValueError: pass if not observation.listeners: del observed_dicts[i] if hasattr(object, "__dict__"): observation = observed_objects.get(i) if observation: try: observation.listeners.remove(listener) except ValueError: pass if not observation.listeners: del observed_objects[i] if observation.content_attr: unobserve(getattr(object, observation.content_attr), listener) else: if observed_lists .get(object): del observed_lists [i] elif observed_dicts .get(object): del observed_dicts [i] if observed_objects.get(object): del observed_objects[i] class Observation: content_attr = "" def __init__(self, listeners, old): self.listeners = listeners self.old = old def scan(): """scan() Checks for changes in listened objects, and calls the corresponding listeners if needed.""" for i, observation in observed_lists.items(): if observation.old != observation.object: for listener in observation.listeners: listener(observation.object, list, observation.object, observation.old) observation.old = list(observation.object) for i, observation in observed_dicts.items(): if observation.old != observation.object: for listener in observation.listeners: listener(observation.object, dict, observation.object, observation.old) observation.old = dict(observation.object) for i, observation in observed_objects.items(): if type(observation.object) is ref: obj = observation.object() else: obj = observation.object if observation.props: new_dict = dict(map(lambda prop: (prop, getattr(obj, prop, None)), observation.props)) new_dict.update(obj.__dict__) if observation.old != new_dict: for listener in observation.listeners: listener(obj, object, new_dict, observation.old) observation.old = new_dict else: if observation.old != obj.__dict__: for listener in observation.listeners: listener(obj, object, obj.__dict__, observation.old) observation.old = obj.__dict__.copy() if not observation.old_class is obj.__class__: for listener in observation.listeners: listener(obj, "__class__", obj.__class__, observation.old_class) observation.old_class = obj.__class__ SCANNING = 0 def _scan_loop(freq): from time import sleep while SCANNING: scan() sleep(freq) def start_observing(freq = 0.2): """start_observing(freq = 0.2) Starts the observer daemon. This thread calls scan() repetitively, each FREQ seconds.""" global SCANNING import thread SCANNING = 1 thread.start_new_thread(_scan_loop, (freq,)) def start_observing_tk(freq = 0.2): """start_observing_tk(freq = 0.2) Starts the observer daemon in the Tkinter thread. This thread calls scan() repetitively, each FREQ seconds.""" global SCANNING SCANNING = 1 _start_observing_tk(int(freq * 1000.0)) def _start_observing_tk(freq): from Tkinter import _default_root as tk scan() if SCANNING: tk.after(freq, _start_observing_tk2, freq) def _start_observing_tk2(freq): from Tkinter import _default_root as tk tk.after_idle(_start_observing_tk, freq) def stop_observing(): """stop_observing() Stops the observer daemon (started by start_observing() or start_observing_tk()).""" SCANNING = 0 def diffdict(new, old): """diffdict(new, old) -> [(key, new_value, old_value),...] Returns the differences between two dictionaries. In case of addition or deletion, old or new values are None.""" changes = [] for key, val in old.iteritems(): new_val = new.get(key, Ellipsis) if new_val is Ellipsis: changes.append((key, None, val)) elif new_val != val: changes.append((key, new_val, val)) for key, val in new.iteritems(): old_val = old.get(key, Ellipsis) if old_val is Ellipsis: changes.append((key, val, None)) return changes import custom def observe_tree(object, listener): """observe_tree(object, listener) Observes OBJECT with LISTENER, as well as any item in OBJECT (if OBJECT is a list, a dict, or have a "children" or "items" attribute / method). Items added to or removed from OBJECT or one of its items are automatically observed or unobserved. Although called "observe_tree", it works with any nested structure of lists and dicts, including cyclic ones. You must use unobserve_tree to remove the listener.""" _observe_tree(object, _TreeListener(object, listener)) def _observe_tree(object, listener): if not isobserved(object, listener): # Avoid troubles with cyclic list / dict observe(object, listener) children = custom._find_children(object) if not children is None: if not children is object: observe(children, listener) for item in children: _observe_tree(item, listener) def unobserve_tree(object, listener): """unobserve_tree(object, listener) Unregisters the tree listener LISTENER for OBJECT.""" if isobserved(object, listener): # Avoid troubles with cyclic list / dict unobserve(object, listener) children = custom._find_children(object) if not children is None: if not children is object: unobserve(children, listener) for item in children: unobserve_tree(item, listener) class _TreeListener: def __init__(self, object, listener): self.object = object self.listener = listener def __eq__(self, other): return other == self.listener def __call__(self, object, type, new, old): if type is list: for item in old: if not item in new: unobserve_tree(item, self) for item in new: if not item in old: _observe_tree(item, self) self.listener(object, type, new, old) EditObj-0.5.7/setup.cfg0000755000076500007650000000017510114336755013305 0ustar jibajiba[install_lib] compile = 1 optimize = 1 [sdist] force-manifest = 1 dist-dir = /home/jiba/dist EditObj-0.5.7/setup.py0000755000076500007650000000345410402347742013177 0ustar jibajiba# EditObj # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import distutils.core, distutils.sysconfig, os, os.path from distutils.core import setup, Extension install_dir = distutils.sysconfig.get_python_lib() setup(name = "EditObj", version = "0.5.7", description = "EditObj can create a dialog box to edit ANY Python object.", long_description = """EditObj is an automatic GUI generator, similar to Java Bean but designed for Python objects. It can create a dialog box to edit ANY object. It also includes a Tk tree widget, an event and a multiple undo-redo frameworks.""", license = "GPL", author = "Lamy Jean-Baptiste (Jiba)", author_email = "jiba@tuxfamily.org", url = "http://oomadness.tuxfamily.org/en/editobj", package_dir = {"editobj" : ""}, packages = ["editobj"], data_files = [(os.path.join(install_dir, "editobj", "icons"), [os.path.join("icons", file) for file in os.listdir("icons") if (file != "CVS") and (file != ".arch-ids")] )], ) EditObj-0.5.7/treewidget.py0000755000076500007650000005470510136730517014207 0ustar jibajiba# TreeWidget # Copyright (C) 2001-2002 Jean-Baptiste LAMY -- jiba@tuxfamily # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """TreeWidget -- (yet another) Tree widget for Tk. This tree has been greatly inspired from Guido's tree for IDLE, but has been totally rewritten. Main difference are : - Great speed optimizations : only draws visible nodes, delete those who are no longer visible, and redraws only changed nodes ! - Asynchronous drawing. - You directly inherit from the Node class. - Linux frendly icons (but removing lines adds also a good perf bonus !). This tree support very big tree (10000+ nodes); the speed depends on the number of visible nodes, NOT on the total number of nodes ! See the __doc__ of Tree and Node for more info. This module also include: - IconDir, a usefull tool for using image from a directory - ScrolledCanvas, a canvas with scrollbars - ScrollPane, a frame with scrollbars """ # This has been greatly inspired from Guido's tree for IDLE import os, types from Tkinter import * class IconsDir: """IconsDir(path) -- Allows quick access to any image in the given directory. You can get the image called "picture.png" as following : >>>iconsdir["picture"] Image are cached in a dict for speed up.""" def __init__(self, path): self.path = path self.icons = {} def __getitem__(self, name): try: return self.icons[name] except KeyError: name2, ext = os.path.splitext(name) ext = ext or ".pgm" # I don't use gif for patent reasons. file = os.path.join(self.path, name2 + ext) icon = PhotoImage(file = file) self.icons[name] = icon return icon def create_default_iconsdir(): global iconsdir import os.path ICON_DIR = os.path.join(os.path.dirname(__file__), "icons") if not os.path.exists(ICON_DIR): ICON_DIR = os.path.join("/usr", "share", "editobj") if not os.path.exists(ICON_DIR): ICON_DIR = os.path.join("/usr", "share", "python-editobj") iconsdir = IconsDir(ICON_DIR) class Node: """Tree's Node base class. You must inherit this class and override a few method : - __str__() -> string : must return the Node's label. - __unicode__() -> unicode : must return the Node's label. - geticon() -> image : must return the Node's icon (see IconsDir for a handy way to get images). - createchildren(oldchildren = None) -> list : (Optionnal) called the first time we need the children; must return a list of child Node. oldchildren may contains the previous children, in case of an update. - isexpandable() -> boolean : (Optionnal) must return true if this Node has children. - iseditable() -> boolean : (Optionnal) must return true if the Node label is editable. - settext() : (Optionnal) called when the Node's label has been edited. - getheight() -> height : (Optionnal) override this if you use non-regular height. - destroy() : (Optionnal) called when the node is destroyed. In __str__(), __unicode__() or geticon(), you may check the values of self.selected, self.isexpandable() and self.expanded, for example to provide different icons for selected or expanded Nodes. The "icon_width" attribute define the width of the icon. You can override it in geticon(), e.g. just before returning the icon, do 'self.icon_width = icon.width()'. It defaults to 20. If you override __init__(), you must call Node.__init__(parent), AT THE END OF YOUR __init__() ! """ icon_width = 20 def __init__(self, parent): """Create a new Node. Parent can be either the parent's Node, or the tree (for the root Node). If you override __init__(), you must call Node.__init__(parent), AT THE END OF YOUR __init__() ! """ if isinstance(parent, Node): self.parent = parent self.tree = parent.tree self.depth = parent.depth + 1 else: self.parent = None self.tree = parent self.depth = 0 self.children = [] self.childrencreated = 0 self.expanded = 0 self.selected = 0 self.text_id = 0 self.image_id = 0 self.minusplus_id = 0 self.oldy = -1000 self.changed = 0 if not self.parent: self.tree._setnode(self) # Root Node must be passed to the tree. def destroy(self): for child in self.children: child.destroy() def createchildren(self, oldchildren = None): return [] def lastchild(self): if self.childrencreated and self.expanded: return self.children[-1].lastchild() else: return self def previous(self): if isinstance(self.parent, Node): if self.index == 0: return self.parent return self.parent.children[self.index - 1].lastchild() else: return None # Root Node def next(self): if self.expanded and self.children: return self.children[0] else: if isinstance(self.parent, Node): return self.parent._next(self) else: return None # Root Node def _next(self, node): if node.index + 1 >= len(self.children): if isinstance(self.parent, Node): return self.parent._next(self) else: return None # Root Node return self.children[node.index + 1] def update(self): self.changed = 1 try: self.label["text"] = unicode(self) except: pass def updatetree(self): self.update() for child in self.children: child.updatetree() def updatechildren(self): if self.childrencreated: if self.expanded: oldchildren = self.children for child in self.children: child._undraw() self.children = self.createchildren(self.children) for child in oldchildren: if not child in self.children: child.destroy() i = 0 if self.children: for child in self.children: child.index = i i = i + 1 else: self.expanded = 0 # Cannot be expanded if no children self.tree.draw() else: for child in self.children: child.destroy() self.children = [] self.childrencreated = 0 self.redraw() else: self.redraw() def select(self, event = None): if not self.selected: self.tree.deselectall() self.selected = 1 self.changed = 1 self.tree.selection.append(self) self.label.configure(bg = "#CCCCFF")#bg = "#834cb4") self.redraw() def deselect(self, event = None): if self.selected: self.selected = 0 self.changed = 1 self.tree.selection.remove(self) self.label.configure(bg = self.tree["bg"]) self.redraw() def selecttree(self): if not self.selected: self.select() for child in self.children: child.selecttree() def deselecttree(self): self.deselect() for child in self.children: child.deselecttree() def expand(self, event = None): if not self.expanded: if not self.childrencreated: self.children = self.createchildren() i = 0 for child in self.children: child.index = i i = i + 1 self.childrencreated = 1 if len(self.children) > 0: self.changed = 1 self.expanded = 1 self.tree.draw() def collapse(self, event = None): if self.expanded: self.expanded = 0 self.changed = 1 self.tree.draw() def isexpandable(): return len(self.children) def toggle(self, event = None): if self.expanded: self.collapse() else: self.expand() def sizetree(self): if self.expanded: sizetree = 1 for child in self.children: sizetree = sizetree + child.sizetree() return sizetree else: return 1 def redraw(self): if self.oldy >=0: self._draw(self.oldy) def _draw(self, y): x = self.depth * 20 if self.changed or y != self.oldy: #if self.changed: print " draw because changed" #if y != self.oldy: print " draw because move on y", y, self.oldy cx = x + 9 cx = self._drawplusminusicon(cx, y + 7) cx = self._drawicon(cx, y) self._drawtext(cx, y) self.changed = 0 self.oldy = y def _undraw(self): self.oldy = -1000 if self.minusplus_id: self.tree.delete(self.minusplus_id) if self.image_id : self.tree.delete(self.image_id) if self.text_id : self.tree.delete(self.text_id) self.minusplus_id = self.image_id = self.text_id = 0 def _undrawtree(self): self._undraw() for child in self.children: child._undrawtree() def _drawplusminusicon(self, x, y): if self.minusplus_id: self.tree.delete(self.minusplus_id) if self.isexpandable(): if self.expanded: image = self.tree.minusicon else: image = self.tree.plusicon self._plusminusicon = image # prevent image from garbage collection if image: self.minusplus_id = self.tree.create_image(x, y, image = image) self.tree.tag_bind(self.minusplus_id, "<1>", self.toggle) # XXX This leaks bindings until canvas is deleted else: self.minusplus_id = 0 return x + 10 def _drawicon(self, x, y): if self.image_id: self.tree.delete(self.image_id) image = self.geticon() self._icon = image # prevent image from garbage collection if image: self.image_id = self.tree.create_image(x, y, anchor = "nw", image = image) self.tree.tag_bind(self.image_id, "<1>", self.select) self.tree.tag_bind(self.image_id, "", self.toggle) else: self.image_id = 0 return x + self.icon_width def geticon(self): global iconsdir if not iconsdir: create_default_iconsdir() if self.isexpandable(): if self.expanded: return iconsdir["openfolder.pgm"] else: return iconsdir["folder.pgm"] else: return iconsdir["python.pgm"] def _drawtext(self, x, y): if self.text_id: self.tree.delete(self.text_id) if hasattr(self, "entry"): self._edit_finish() try: label = self.label except AttributeError: self.label = Label(self.tree, text = unicode(self).encode("latin"), fg = "black", bg = self.tree["bg"], bd = 0, padx = 2, pady = 2) self.label.bind("<1>", self.select_or_edit) self.label.bind("", self.toggle) self.label.bind("<4>" , self.tree.unit_up) self.label.bind("<5>" , self.tree.unit_down) self.text_id = self.tree.create_window(x, y, anchor = "nw", window = self.label) def select_or_edit(self, event = None): if self.selected and self.iseditable(): self.edit(event) else: self.select(event) def edit(self, event = None): self.entry = Entry(self.label, bd = 0, highlightthickness = 1, width = 0) self.entry.insert(0, self.label['text']) self.entry.selection_range(0, END) self.entry.pack(ipadx = 5) self.entry.focus_set() self.entry.bind("", self._edit_finish) self.entry.bind("", self._edit_cancel) def _edit_finish(self, event = None): try: entry = self.entry del self.entry except AttributeError: return text = entry.get() entry.destroy() if text and text != unicode(self): self.settext(text) self.label['text'] = unicode(self) self.redraw() self.tree.focus_set() def _edit_cancel(self, event = None): self.redraw() self.tree.focus_set() def iseditable(self): return 0 def settext(self, text): pass # ScrolledCanvas and Tree widget (slightly modified) class ScrolledCanvas(Canvas): """ScrolledCanvas -- A Tk canvas with scrollbar. Base class for Tree.""" def __init__(self, master, kw = {}, **opts): if not opts.has_key('yscrollincrement'): opts['yscrollincrement'] = 20 #self.master = master self.frame = Frame(master) self.frame.rowconfigure(0, weight=1) self.frame.columnconfigure(0, weight=1) #apply(Canvas.__init__, (self, self.frame, kw), opts) Canvas.__init__(self, self.frame, kw, **opts) self.grid(row=0, column=0, sticky="nsew") self.vbar = Scrollbar(self.frame, name="vbar") self.vbar.grid(row=0, column=1, sticky="nse") self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal") self.hbar.grid(row=1, column=0, sticky="ews") self['yscrollcommand'] = self.vbar.set self.vbar['command'] = self.yview self['xscrollcommand'] = self.hbar.set self.hbar['command'] = self.xview self.bind("", self.page_up) self.bind("" , self.page_down) self.bind("" , self.unit_up) self.bind("" , self.unit_down) self.bind("<4>" , self.unit_up) self.bind("<5>" , self.unit_down) self.focus_set() self.pack = self.frame.pack self.grid = self.frame.grid self.place = self.frame.place self["width"] = 280 def destroy(self): Canvas.destroy(self) self.vbar.destroy() self.hbar.destroy() self.frame.destroy() def page_up(self, event): self.yview_scroll(-1, "pages") return "break" def page_down(self, event): self.yview_scroll(1, "pages") return "break" def unit_up(self, event): self.yview_scroll(-1, "units") return "break" def unit_down(self, event): self.yview_scroll(1, "units") return "break" class ScrollPane(Canvas): """ScrollPane -- A Tk frame with scrollbar.""" def __init__(self, master, scrollX = 1, scrollY = 1, max_width = 280, max_height = 360, kw = {}, **opts): if not opts.has_key('yscrollincrement' ): opts['yscrollincrement' ] = 20 if not opts.has_key('highlightthickness'): opts["highlightthickness"] = 0 self.frame = Frame(master) self.frame.rowconfigure (0, weight = 1) self.frame.columnconfigure(0, weight = 1) Canvas.__init__(self, self.frame, kw, **opts) self.grid(row = 0, column = 0, sticky = "nsew") self.bind("", self.page_up) self.bind("" , self.page_down) self.bind("" , self.unit_up) self.bind("" , self.unit_down) self.pack = self.frame.pack self.grid = self.frame.grid self.place = self.frame.place self.content = None self["width"] = self["height"] = 0 self.max_width = max_width self.max_height = max_height self.scrollX = scrollX self.scrollY = scrollY if scrollX: self.hbar = Scrollbar(self.frame, name = "hbar", orient = "horizontal") self.hbar.grid(row = 1, column = 0, sticky = "ews") self['xscrollcommand'] = self.hbar.set self.hbar['command'] = self.xview if scrollY: self.vbar = Scrollbar(self.frame, name = "vbar") self.vbar.grid(row = 0, column = 1, sticky = "nse") self['yscrollcommand'] = self.vbar.set self.vbar['command'] = self.yview self.bind("", self._resized) def setContent(self, content): self.content = content self.id = self.create_window(0, 5, window = content, anchor = "nw") self.updateContentSize() def updateContentSize(self): self.update_idletasks() # Required, else dimension of content may not have been computed ? x0, y0, x1, y1 = self.bbox(ALL) self.configure(scrollregion = (0, 0, x1, y1)) if self.scrollX: self["width" ] = min(self.max_width , x1) else: self["width" ] = x1 if self.scrollY: self["height"] = min(self.max_height, y1) else: self["height"] = y1 self.yview_moveto(0) def _resized(self, event = None): width = self.winfo_width() if width <= 1: width = self.winfo_reqwidth() height = self.winfo_height() if height <= 1: height = self.winfo_reqheight() if not self.scrollX: self.itemconfigure(self.id, width = width) if not self.scrollY: self.itemconfigure(self.id, height = height) def destroy(self): Canvas.destroy(self) if self.scrollX: self.hbar.destroy() if self.scrollY: self.vbar.destroy() self.frame.destroy() if self.content: self.content.destroy() def page_up(self, event): self.yview_scroll(-1, "page") return "break" def page_down(self, event): self.yview_scroll(1, "page") return "break" def unit_up(self, event): self.yview_scroll(-1, "unit") return "break" def unit_down(self, event): self.yview_scroll(1, "unit") return "break" iconsdir = None class Tree(ScrolledCanvas): """Tree widget for Tk. First create a Tree widget. Then create a root Node, by passing the tree as parent (= the first arg of the constructor). >>> root = Tk() >>> tree = Tree(root) >>> tree.frame.pack(expand = 1, fill = "both") >>> node = YourNode(tree [, your Node's data]) >>> root.mainloop() You may want to custom the "plus" and "minus" icons, they correspond to the "plusicon" and "minusicon" attributes of the tree.""" def __init__(self, master, async = 1, kw = {}, **opts): """Tree(master, async = 1, kw = {}, **opts) -- Create a new Tree. Set async to 0 to disable asynchronous drawing.""" if not opts.has_key('bg'): opts['bg'] ="white" if not opts.has_key('highlightthickness'): opts['highlightthickness'] = 0 ScrolledCanvas.__init__(self, master, kw, **opts) self.plusicon = self.minusicon = None self.nodeheight = 20 self.sizetree = 0 self.node = None self.first = None self.y = 0 self.selection = [] self.displayed = [] self.async = async self.cancel_draw = None self["width"] = 280 self["height"] = 200 # There must be a better way to register Resize Event... self.bind("", self._resized) def destroy(self): if self.node: self.node.destroy() ScrolledCanvas.destroy(self) def _setnode(self, node): if self.cancel_draw: self.after_cancel(self.cancel_draw) self.cancel_draw = None if self.node: self.node.destroy() for n in self.displayed: n._undraw() node.index = 0 self.node = node self.first = node self.y = 0 self.sizetree = 0 self.selection = [] self.displayed = [] if node: node.expand() def updatescroll(self): """Update the scroll-bar size.""" if self.node: #self.update_idletasks() # Required, else dimension of content may not have been computed ? forgetit, forgetit, x1, forgetit = self.bbox(ALL) self.sizetree = self.node.sizetree() + (self.winfo_height() / self.nodeheight) - 1 self.configure(scrollregion = (0, 0, x1, self.sizetree * self.nodeheight)) def updatetree(self): """Update the full tree. Notice that it is speeder to update only some Nodes (with Node.update() or Node.updatetree() and then tree.draw()), if you know which Nodes have changed.""" if self.node: self.node.update() self.draw() def draw(self): """Draw the tree. Only visible AND modified parts of the tree will be really redrawn. As the drawing is by default asynchronous, you can call draw() a lot of time without perf loss.""" if self.node: if self.async: if self.cancel_draw: self.after_cancel(self.cancel_draw) self.cancel_draw = self.after(3, self._draw) else: self._draw() def _draw(self): self.cancel_draw = None if self.node: if not self.minusicon: global iconsdir if not iconsdir: create_default_iconsdir() self.plusicon = iconsdir["plus"] self.minusicon = iconsdir["minus"] y = self.y height = self.winfo_height() node = self.first todisplay = [] for i in range(height / self.nodeheight + 1): todisplay.append(node) node = node.next() if node is None: break # Undraw nodes that are no longer visible for node in self.displayed: if not node in todisplay: node._undraw() # Draw nodes that are visible -- the _draw method is no-op if the node hasn't moved nor changed. y = 2 + self.y * self.nodeheight for node in todisplay: node._draw(y) y = y + self.nodeheight self.displayed = todisplay self.updatescroll() def yview(self, *args): if args[0] == "scroll": self.yview_scroll(args[1], args[2]) else: self.yview_moveto(args[1]) def yview_scroll(self, *args): height = self.winfo_height() if self.sizetree * self.nodeheight <= height: return if args[1][:1] == "u": self._addy(int(args[0])) ScrolledCanvas.yview_scroll(self, *args) # Must redraw, as some new Node may be visible (But only those Nodes will be redrawn !). self.draw() else: # Convert page-scroll into increment scroll, as i don't know how much is a page-scroll... ;-) yscrollincrement = int(self['yscrollincrement']) nbincrements = int(args[0]) * (height / yscrollincrement - 1) self.yview_scroll(nbincrements, "unit") def yview_moveto(self, arg): newy = int(float(self.sizetree) * float(arg)) if self.y != newy: self._addy(newy - self.y) arg = float(newy) / float(self.sizetree) ScrolledCanvas.yview_moveto(self, arg) # Must redraw, as some new Node may be visible (But only those Nodes will be redrawn !). self.draw() def _addy(self, delta): if delta < 0: for i in range(-delta): node = self.first.previous() if node is None: break self.first = node self.y = self.y - 1 else: max = self.sizetree - self.winfo_height() / self.nodeheight if self.y + delta > max: delta = max - self.y for i in range( delta): node = self.first.next() if node is None: break self.first = node self.y = self.y + 1 def deselectall(self): """Deselect all selected Nodes.""" if self.selection: for node in self.selection[:]: node.deselect() def _resized(self, event = None): if self.node: # self.y may now be invalid (two high). max = self.sizetree - self.winfo_height() / self.nodeheight if self.y > max: self._addy(max - self.y) self.draw() EditObj-0.5.7/PKG-INFO0000644000076500007650000000101010402350016012525 0ustar jibajibaMetadata-Version: 1.0 Name: EditObj Version: 0.5.7 Summary: EditObj can create a dialog box to edit ANY Python object. Home-page: http://oomadness.tuxfamily.org/en/editobj Author: Lamy Jean-Baptiste (Jiba) Author-email: jiba@tuxfamily.org License: GPL Description: EditObj is an automatic GUI generator, similar to Java Bean but designed for Python objects. It can create a dialog box to edit ANY object. It also includes a Tk tree widget, an event and a multiple undo-redo frameworks. Platform: UNKNOWN