UndoManager mit Tkinter-GUI

Fragen zu Tkinter.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@sedi hier ist doch der Verweis auf das Menü mit dem parent: a = MenuCommand(parent,label="Menutext")

Allerdings wäre das etwas aufwändig, wenn wir auch remove zulassen. Denn dann müssten wir alle config options etwa für checkbuttons merken und die auch im removed zustand ändern lassen.
Man kann es ja am Anfang mal ohne den removed Zustand machen. Dann muss man eben gleich das Menüwidget positionieren und lassen, wo es ist - löschen darf man es natürlich.

Also:

a = MenuCommand(parent,label="Menutext").add()
oder
a = MenuCommand(parent,label="Menutext").insert(x)

Ich werde bei mir aber doch alles implementieren. Dann haben wir eine Gleichbehandlung aller widgets. Und ich brauche das sowieso auch für Stellvertreter Widgets.
Wie es bei Panes für PanedWindows aussieht, muss ich mir auch noch ansehen.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

Ich habe jetzt mal die Tkintererweiterung ohne die Menüfunktionalität der ``_MenuElement`` - Objekte gemacht:

Code: Alles auswählen

# PYTHON 3.X
import tkinter as tk

class _MenuElement(object):
    """abstrakte Klasse"""
    def __init__(self, menuobject, **atts):
        """
        :param menu: {menu object}
        :param atts: alle in Tkinter erlaubten Eigenschaften:
                             label, command, ...
       """
        self.menu = menuobject
        self._atts = atts
        self.label = ""

        for a in atts:
            self.__dict__[a] = atts[a]

    def __str__(self):
        return self.__class__.__name__.lower()

    @property
    def atts(self):
        return self._atts

    @property
    def pos(self):
        return self.menu.entries.index(self)


# 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


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 = []

    def add(self, element):
        """Ueberschreiben der add-Methode, da ja jetzt nur noch
        Instanzen von Klassen ankommen, die von ``_MenuElement``
        abgeleitet wurden.
        """
        self.insert(len(self.entries), element)

    def insert(self, pos, element):
        if not isinstance(element, _MenuElement):
            raise ValueError(...)

        self.entries.insert(pos, element)
        tk.Menu.insert(self, pos, str(element), **element.atts)

    # adds ueberschreiben

    def add_cascade(self, **kw):
        self.add(Cascade(self, **kw))
        
    def add_checkbutton(self, **kw):
        self.add(Checkbutton(self, **kw))
        
    def add_command(self, **kw):
        self.add(Command(self, **kw))
        
    def add_radiobutton(self, **kw):
        self.add(Radiobutton(self, **kw))
        
    def add_separator(self, **kw):
        self.add(Separator(self, **kw))
    
    # inserts ueberschreiben
    
    def insert_cascade(self, index, **kw):
        self.insert(index, Cascade(self, **kw))
    
    def insert_checkbutton(self, index, **kw):
        self.insert(index, Checkbutton(self, **kw))
        
    def insert_command(self, index, **kw):
        self.insert(index, Command(self, **kw))
    
    def insert_radiobutton(self, index, **kw):
        self.insert(index, Radiobutton(self, **kw))
    
    def insert_separator(self, index, **kw):
        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)


class RootMenu(Menu):
    def __init__(self, master, **kw):
        Menu.__init__(self, master, "ROOT", **kw)
Es fehlt jetzt noch Tkintermanipulationen in die ``_MenuElement`` - Objekte zu übernehmen...

@Alfons Mittelmeyer: stimmt
@sedi hier ist doch der Verweis auf das Menü mit dem parent: a = MenuCommand(parent,label="Menutext")
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@sedi Da fehlt noch etwas:

Code: Alles auswählen

   def add_checkbutton(self, **kw):
        self.add(Checkbutton(self, **kw))
Da musst Du noch tk.Menu.add_checkbutton(self,...) aufrufen
Wenn Du nur überschreibst, dann hast Du ja nichts
Zuletzt geändert von Alfons Mittelmeyer am Mittwoch 9. September 2015, 18:26, insgesamt 1-mal geändert.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

So nach ein paar ersten Tests scheint der Tkinter-Wrapper zu funktionieren:

Code: Alles auswählen

# PYTHON 3.X
import tkinter as tk


class _MenuElement(object):
    """abstrakte Klasse"""
    def __init__(self, menuobject, **atts):
        """
        :param menu: {menu object}
        :param atts: alle in Tkinter erlaubten Eigenschaften:
                             label, command, ...
       """
        self._atts = {}
        self.menu = menuobject
        self.label = ""
        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)
        self.__dict__.update(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 = []

    def add(self, element):
        """Ueberschreiben der add-Methode, da ja jetzt nur noch
        Instanzen von Klassen ankommen, die von ``_MenuElement``
        abgeleitet wurden.
        """
        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)

    # adds ueberschreiben

    def add_cascade(self, **kw):
        self.add(Cascade(self, **kw))
        
    def add_checkbutton(self, **kw):
        self.add(Checkbutton(self, **kw))
        
    def add_command(self, **kw):
        self.add(Command(self, **kw))
        
    def add_radiobutton(self, **kw):
        self.add(Radiobutton(self, **kw))
        
    def add_separator(self, **kw):
        self.add(Separator(self, **kw))
    
    # inserts ueberschreiben
    
    def insert_cascade(self, index, **kw):
        self.insert(index, Cascade(self, **kw))
    
    def insert_checkbutton(self, index, **kw):
        self.insert(index, Checkbutton(self, **kw))
        
    def insert_command(self, index, **kw):
        self.insert(index, Command(self, **kw))
    
    def insert_radiobutton(self, index, **kw):
        self.insert(index, Radiobutton(self, **kw))
    
    def insert_separator(self, index, **kw):
        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)

    def entryconfigure(self, index, **kw):
        entry = self.entries[index]
        entry.atts = kw
        tk.Menu.entryconfigure(self, index, **kw)


class RootMenu(Menu):
    def __init__(self, master, **kw):
        Menu.__init__(self, master, "ROOT", **kw)
Was sagt ihr dazu?
und vor allen Dingen: Wie verbindet man das jetzt mit dem UndoManager?
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@sedi bitte noch meinen vorigen post beachten und dann solltest Du erläutern, welche Aktionen rückgängig gemacht und wiederholt werden sollen. Wohl nicht nur Klicks im Menü?

Und undo und redo haben fast gar nichts mit Deinem Menü zu tun, nur dass das vom Menü mit den Menüpunkten 'undo' und 'redo' aufgerufen wird.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

Alfons Mittelmeyer hat geschrieben:@sedi bitte noch meinen vorigen post beachten und dann solltest Du erläutern, welche Aktionen rückgängig gemacht und wiederholt werden sollen. Wohl nicht nur Klicks im Menü?
@Alfons Mittelmeyer:

1) zum vorigen Post: bei mir funktioniert das so wie es ist - in ``add_XY`` wird das Objekt erzeugt, dann an die ``add`` weitergereicht und letztlich in der ``insert`` erzeugt. Tkinterspezifische Funktion wird ja in der ``insert`` aufgerufen - das genügt.

2) Das mit dem UndoManager habe ich noch gar nicht in angriff genommen - mein Gedanke ist so etwas in der Art wie ein ``AppCommand``, der drei Methoden ``do``, ``undo`` und ``redo`` implementieren muss, so wie ich es bereits weiter oben angedeutet habe (siehe auch UndoManager Thread http://www.python-forum.de/viewtopic.ph ... 37#p283437)

Will man nun einen Befehl ausführen, der den Undo-Mechanismus benützen soll, dann muss halt ein Objekt vom ``AppCommand`` erstellt werden.
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
BlackJack

@sedi: Was ist denn der Unterschied zwischen `do()` und `redo()`? Ein Beispiel wo die nicht beide das gleiche machen?

Qt erlaubt auch so einem Kommando andere Kommandos als Kinder mit zu geben, so dass man aus einfachen Kommandos komplexere zusammenbauen kann die man dann gemeinsam anwenden oder rückgängig machen kann. Und es gibt einen Mechanismus bei dem man Kommandos ”komprimieren” kann in dem mehrere vom gleichen ”Typ” zu einem verschmolzen werden können.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@BlackJack do() und redo() müssen ausführen. Redo entnimmt, was zu tun ist aus der redo Liste und muss nicht unbedingt wieder in diese Liste schreiben, weil es ja schon drin ist.
Außerdem ist auch nicht nötig, dass dafür eine undo Funktion nochmals erfasst wird, die ist ja auch schon in der undo Liste drin. Bei redo() muss der index beider Listen erhöht werden.

do() bekommt übergeben, was zu tun ist, schreibt es in die redo liste und muss mit diesem Eintrag auch das Ende der redo und undo liste kennzeichnen. Und natürlich wird was zu tun ist ausgeführt.
Außerdem muß bei do() auch die redo Aktion erzeugt - aber nicht ausgeführt - und in die redo Liste geschrieben werden. Normalerweise übernimmt die auszuführende Aktion das Schreiben in die redo liste, denn nur die kann wissen, was tun ist, oder es muss ein tuple aus Aktion und Undo Aktion existieren.

Bei undo() wird das undo Kommando aus der undo liste ausgeführt und der Index beider Listen um eins erniedrigt.
BlackJack

@Alfons Mittelmeyer: Du verwechselst da den `UndoManager` mit dem `AppCommand`. Auf letzteres bezog sich meine Frage weil sedi schrieb diese Objekte hätten diese drei Methoden.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

BlackJack hat geschrieben:@sedi: Was ist denn der Unterschied zwischen `do()` und `redo()`? Ein Beispiel wo die nicht beide das gleiche machen
Szenario: Bei einem Dokument muss ein Inhaltselement neu erstellt werden, dies geschieht im ``do``. Im ``redo`` wird es nicht neu erstellt, sondern man nimmt das bereits Erstellte! ;)

Zur Klärung: Sowohl der ``AppCommand`` als auch der ``UndoManager`` sollten alle drei Methoden ``do``, ``undo`` und ``redo`` implementieren, da es Fälle geben kann, wo sich ``do`` und ``redo`` unterscheiden:

Code: Alles auswählen

class AppCommand:
    def do(self): pass
    def undo(self): pass
    def redo(self): pass

class UndoManager:
    def __init__(self):
        self.undos, self.redos = [], []
    def undo(self):
        command = self.undos.pop()
        self.redos.append(command)
        command.undo()
    def redo(self):
        command = self.redos.pop()
        self.undos.append(command)
        command.redo()
    def execute(self, command):
        self.undos.append(command)
        self.redos = []
        # hier nicht ``redo`` sondern ``do``!
        command.do()
Falls es noch von Interesse ist - habe mein Tkintermenu-Wrapper etwas geändert - habe die zu Anfang angeregte Komfortzone eingebaut:

Code: Alles auswählen

class _MenuElement(object):
    """abstrakte Klasse"""
    def __init__(self, menuobject, **atts):
        """Die Klassen sollten nicht direkt initialisiert
        werden, sondern ueber deren Menuemethoden ``add_XY``!

        Den ``_MenuElement`` - Objekten kann jederzeit ein
        Wert fuer den ``label`` zugewiesen werden, aber den
        Tkinterattributen wird der Wert nur dann zugewiesen,
        wenn bei der Initialisierung ein 'label'-Argument
        uebergeben wurde!

        :param menu: {menu object}
        :param atts: alle in Tkinter erlaubten Eigenschaften:
                     label, command, ...
        """
        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 = []

    def add(self, element):
        """Ueberschreiben der add-Methode, da ja jetzt nur noch
        Instanzen von Klassen ankommen, die von ``_MenuElement``
        abgeleitet wurden.
        """
        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)

    # adds ueberschreiben

    def add_cascade(self, **kw):
        self.add(Cascade(self, **kw))
        
    def add_checkbutton(self, **kw):
        self.add(Checkbutton(self, **kw))
        
    def add_command(self, **kw):
        self.add(Command(self, **kw))
        
    def add_radiobutton(self, **kw):
        self.add(Radiobutton(self, **kw))
        
    def add_separator(self, **kw):
        self.add(Separator(self, **kw))
    
    # inserts ueberschreiben
    
    def insert_cascade(self, index, **kw):
        self.insert(index, Cascade(self, **kw))
    
    def insert_checkbutton(self, index, **kw):
        self.insert(index, Checkbutton(self, **kw))
        
    def insert_command(self, index, **kw):
        self.insert(index, Command(self, **kw))
    
    def insert_radiobutton(self, index, **kw):
        self.insert(index, Radiobutton(self, **kw))
    
    def insert_separator(self, index, **kw):
        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)

    def entryconfigure(self, index, **kw):
        entry = self.entries[index]
        entry.atts = kw
        tk.Menu.entryconfigure(self, index, **entry.atts)
    
    # und hier die Komfortzone ;)
    def get_entry_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)
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
BlackJack

@sedi: Das geht auch ohne die Methode in dem man das Element einfach cached und beim `redo()` (als einzige Methode) prüft ob bereits ein gecachetes Element existiert oder man ein neues erstellen muss. Also ungefähr so:

Code: Alles auswählen

class CreateElementCommand(Command):

    def __init__(self, description, document):
        self.description = description
        self.document = document
        self.element = None
        self.position = None

    def redo(self):
        if self.element is None:
            self.position = self.document.get_current_position()
            self.element = self.document.create_element()
        else:
            self.document.insert_element(self.position, self.element)

    def undo(self):
        self.document.delete_element(self.position)
Falls es beides, `do()` und `redo()` gibt, würde ich in der Basisklasse aber zumindest `redo()` so implementieren das es `do()` aufruft, damit man `redo()` nur implementieren muss wenn man das wirklich braucht.
sedi
User
Beiträge: 104
Registriert: Sonntag 9. Dezember 2007, 19:22

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()
CU sedi
----------------------------------------------------------
Python 3.5; Python 3.6
LinuxMint18
Antworten