Habe nun einen ersten Entwurf des UndoManagers fertig, musste aber die Tkintermenüerweiterung etwas anpassen!
Die Tkintermenüerweiterung:
Code: Alles auswählen
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from fnmatch import fnmatch
# PYTHON 3.X
import tkinter as tk
Logger = logging.getLogger("tkme")
class _MenuElement(object):
"""abstrakte Klasse"""
def __init__(self, menuobject, **atts):
self._atts = {}
# Label kann immer gesetzt werden, aber nicht immer ist
# es ein erlaubte Tkintereigenschaft -> Property
self._label = ""
self.menu = menuobject
self.atts = atts
def __str__(self):
return self.__class__.__name__.lower()
@property
def pos(self):
return self.menu.entries.index(self)
@property
def atts(self):
return self._atts
@atts.setter
def atts(self, atts):
self._atts.update(atts)
if "label" in atts:
self._label = atts.pop("label")
self.__dict__.update(atts)
@property
def label(self):
return self._label
@label.setter
def label(self, value):
self._label = value
if "label" in self._atts:
self._atts["label"] = value
def update(self, **atts):
self.atts = atts
self.menu.entryconfigure(self.pos, **self._atts)
# Typen: 'cascade', 'checkbutton', 'command', 'radiobutton', or 'separator'
# (infohost.nmt.edu/tcc/help/pubs/tkinter/web/menu.html)
class Command(_MenuElement):
pass
class Checkbutton(_MenuElement):
pass
class Radiobutton(_MenuElement):
pass
class Separator(_MenuElement):
pass
class Cascade(_MenuElement):
pass
Entries = dict([(str(e).capitalize(), e) for e in
[Command, Checkbutton, Radiobutton, Separator, Cascade]])
class _Menu(tk.Menu):
def __init__(self, master, label, **kw):
if "tearoff" not in kw: kw["tearoff"] = 0
tk.Menu.__init__(self, master, **kw)
self.label = label
self.entries = []
# Die Tkintermethoden ``add`` und ``insert`` ganz
# bewusst mit verschiedener Signatur ueberschreiben
def add(self, element):
return self.insert(len(self.entries), element)
def insert(self, pos, element):
if not isinstance(element, _MenuElement):
err = "one of '{}' expected! ".format(list(Entries.keys()))
err += "got {} instead".format(str(element).capitalize())
raise ValueError(err)
self.entries.insert(pos, element)
tk.Menu.insert(self, pos, str(element), **element.atts)
return element
# adds ueberschreiben
def add_cascade(self, **kw):
return self.add(Cascade(self, **kw))
def add_checkbutton(self, **kw):
return self.add(Checkbutton(self, **kw))
def add_command(self, **kw):
return self.add(Command(self, **kw))
def add_radiobutton(self, **kw):
return self.add(Radiobutton(self, **kw))
def add_separator(self, **kw):
return self.add(Separator(self, **kw))
# inserts ueberschreiben
def insert_cascade(self, index, **kw):
return self.insert(index, Cascade(self, **kw))
def insert_checkbutton(self, index, **kw):
return self.insert(index, Checkbutton(self, **kw))
def insert_command(self, index, **kw):
return self.insert(index, Command(self, **kw))
def insert_radiobutton(self, index, **kw):
return self.insert(index, Radiobutton(self, **kw))
def insert_separator(self, index, **kw):
return self.insert(index, Separator(self, **kw))
# delete ueberschreiben
def delete(self, index1, index2=None):
last = index1
if index2 is not None:
last = index2
for i in range(index1, last+1):
del self.entries[i]
tk.Menu.delete(self, index1, last)
# Eintraege aendern
def entryconfigure(self, index, **kw):
entry = self.entries[index]
entry.atts = kw
tk.Menu.entryconfigure(self, index, **entry.atts)
return entry
change = entryconfigure
class Menu(_Menu):
def __init__(self, master, label, **kw):
_Menu.__init__(self, master, label, **kw)
def install(self, **kw):
kw["menu"] = self
kw["label"] = self.label
self.master.add_cascade(**kw)
###
# Auslagern der **Komfortzone** in eine eigene Klasse
class FnmatchMenu(Menu):
def __init__(self, master, label, **kw):
Menu.__init__(self, master, label, **kw)
def get_entries_by_fnmatch(self, entryname):
return [e for e in self.entries
if fnmatch(e.label, entryname)]
class RootMenu(_Menu):
def __init__(self, master, **kw):
_Menu.__init__(self, master, "ROOT", **kw)
def install(self):
tl = self.master.winfo_toplevel()
tl.config(menu=self)
Der UndoManager
Code: Alles auswählen
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import enhancetkmenu as mnu
Logger = logging.getLogger("undomanager")
class AppCommand(object):
"""Jedes Kommando, dass den Undomanager benuetzt muss ein
``AppCommand`` sein. Von diesem muessen minimal die beiden
Methoden ``do`` und ``undo`` implementiert werden!
"""
def __init__(self, undomanager, info):
self.mngr = undomanager
self.info = str(info)
Logger.debug("AppCommand %s init done", self.info)
def do(self):
raise NotImplementedError("to implement in subclass")
def undo(self):
raise NotImplementedError("to implement in subclass")
def redo(self):
self.do()
Logger.debug("AppCommand %s redo done", self.info)
def __call__(self):
self.mngr.do(self)
class UndoManager(object):
def __init__(self, menu_undo=None, menu_redo=None):
"""
:param menu_undo: {mnu.Command object}
:param menu_redo: {mnu.Command object}
"""
self.menu_undo = None
self.menu_redo = None
self.undos = []
self.redos = []
self.setup(menu_undo, menu_redo)
Logger.debug("UndoManager init done")
def setup(self, menu_undo, menu_redo):
"""
:param menu_undo: {mnu.Command object}
:param menu_redo: {mnu.Command object}
"""
if menu_undo is not None and isinstance(menu_undo, mnu.Command):
self.menu_undo = menu_undo
if menu_redo is not None and isinstance(menu_redo, mnu.Command):
self.menu_redo = menu_redo
Logger.debug("UndoManager setup done")
def check_menu(self):
btn = self.menu_undo
if len(self.undos) > 0:
cmd = self.undos[-1]
btn.menu.change(btn.pos,
state="normal",
label="Rückgängig ({})".format(cmd.info),
command=self.undo)
else:
btn.menu.change(btn.pos,
state="disabled",
label="Rückgängig",
command=None)
btn = self.menu_redo
if len(self.redos) > 0:
cmd = self.redos[-1]
btn.menu.change(btn.pos,
state="normal",
label="Wiederholen ({})".format(cmd.info),
command=self.redo)
else:
btn.menu.change(btn.pos,
state="disabled",
label="Wiederholen",
command=None)
Logger.debug("UndoManager check_menu done")
def undo(self):
command = self.undos.pop()
command.undo()
self.redos.append(command)
self.check_menu()
Logger.debug("UndoManager undo of %s done", command.info)
def redo(self):
command = self.redos.pop()
command.redo()
self.undos.append(command)
self.check_menu()
Logger.debug("UndoManager redo of %s done", command.info)
def do(self, command):
Logger.debug("=== UndoManager: start command %s ===", command.info)
command.do()
self.undos.append(command)
self.redos = []
self.check_menu()
Logger.debug("UndoManager do of %s done", command.info)
Und hier ein kleiner Test zu den beiden
Code: Alles auswählen
import logging
import enhancetkmenu as mnu
from undomanager import AppCommand, UndoManager
Logger = logging.getLogger("root")
class TestApp(mnu.tk.Frame):
def __init__(self, parent):
"""
:param args: {list}
:param kwargs: {dict}
"""
mnu.tk.Frame.__init__(self, parent, height=100, width=500, bg="red")
self.menu = None
self.view = None
self.undomanager = UndoManager()
self.setup()
Logger.debug("TestApp init done")
def setup(self):
self.setup_view()
self.setup_menu()
self.undomanager.setup(
self.menu.bearbeiten.undo,
self.menu.bearbeiten.redo)
Logger.debug("TestApp setup done")
def setup_view(self):
self.view = mnu.tk.StringVar()
mnu.tk.Label(
self,
bg="red",
height=5,
width=100,
textvariable=self.view).pack(expand=1, fill="x")
self.view.set(value="Aktionsbeschreibung")
Logger.debug("TestApp setup_view done")
def setup_menu(self):
self._setup_normal_menu()
self._setup_undo_menu()
Logger.debug("TestApp setup_menu done")
# === Protected ===
def _setup_normal_menu(self):
toplevel = self.winfo_toplevel()
class Callback(object):
"""
einfacher, aufrufbarer Callback - um Informationen
ueber das gesamte Menue ausgeben zu koennen.
"""
def __init__(self, info, mnu):
self.mnu = mnu
self.info = info
def __call__(self, *args, **kwargs):
fmt = " - {} [{}]"
print("=== Menü: {} ===".format(self.mnu.label))
for entry in self.mnu.entries:
print(str(entry).capitalize() + ":")
for att in entry.atts:
print(fmt.format(entry.atts[att], att))
###
# Menu Root
mn = self.menu = mnu.RootMenu(toplevel)
self.menu.install()
mn = mn.datei = mnu.FnmatchMenu(self.menu, "Datei")
mn.install()
mn.add_command(label="Schließen", command=toplevel.quit)
###
# Menu Bearbeiten
mn = self.menu.bearbeiten = mnu.FnmatchMenu(self.menu, "Bearbeiten")
mn.install()
for nr in range(5):
txt = "Bearbeiten " + str(nr)
mn.add_command(label=txt, command=Callback(txt, mn))
# Insert testen
self.menu.bearbeiten.undo = mn.insert_command(0,
label="Rückgängig",
state="disabled")
self.menu.bearbeiten.redo = mn.insert_command(1,
label="Wiederholen",
state="disabled")
# Separator
mn.add_separator()
# Untermenue klassisch erstellt ueber cascade
umn = mn.untermenue = mnu.FnmatchMenu(self.menu, "Untermenü")
mn.add_cascade(label="Untermenü", menu=umn)
for nr in range(5):
txt = "Untermenü " + str(nr)
umn.add_command(label=txt, command=Callback(txt, umn))
###
# Menue checkbuttons
mn = self.menu.check = mnu.FnmatchMenu(self.menu, "Checkbuttons")
mn.install()
opts = {"label": "",
"onvalue": 1,
"offvalue": 0,
"variable": None,}
for nr in range(5):
info = "chkbtn " + str(nr)
opts["label"] = info
opts["variable"] = mnu.tk.BooleanVar()
opts["command"] = Callback(info, mn)
mn.add_checkbutton(**opts)
###
# Menue radiobuttons
mn = self.menu.radio = mnu.FnmatchMenu(self.menu, "Radiobuttons")
mn.install()
# Radiobuttons benoetigen genau **eine** Variable,
# ueber die der Status der Radiobuttens abgefragt
# werden kann. Diese muss deshalb vorher erzeugt
# werden
opts = {"variable": mnu.tk.StringVar()}
for nr in range(5):
info = "Radio " + str(nr)
opts["label"] = info
opts["value"] = str(nr)
opts["command"] = Callback(info, mn)
mn.add_radiobutton(**opts)
Logger.debug("TestApp _setup_normal_menu done")
def _setup_undo_menu(self):
mnuundo = mnu.FnmatchMenu(self.menu, "UndoManager")
mnuundo.install()
###
# Aktionen
class _MenAction(AppCommand):
def __init__(self, app, info):
AppCommand.__init__(self,
undomanager=app.undomanager,
info=info)
self.view = app.view
def do(self):
self.view.set(value="do: {}".format(self.info))
def undo(self):
self.view.set(value="undo: {}".format(self.info))
def redo(self):
self.view.set(value="redo: {}".format(self.info))
def create_action(nr):
txt = "Aktion " + str(nr)
class Action(_MenAction):
def __init__(self, app=self, info=txt):
_MenAction.__init__(self, app, info=info)
mnuundo.add_command(label=txt, command=Action())
for i in range(5):
create_action(i)
Logger.debug("TestApp _setup_undo_menu done")
def test():
logging.basicConfig(level=logging.DEBUG,
name="root",
propagate=1)
# GUI fuer den Test erstellen
root = mnu.tk.Tk()
root.title("neues Menü")
app = TestApp(root)
app.pack()
app.mainloop()
test()