wxWidgets / Phoenix

wxPython's Project Phoenix. A new implementation of wxPython, better, stronger, faster than he was before.
http://wxpython.org/
2.23k stars 510 forks source link

How to add persistence support for custom widget? #2419

Open chillb0nes opened 12 months ago

chillb0nes commented 12 months ago

Hello! I want to clarify the question in the issue title. From https://docs.wxpython.org/wx.lib.agw.persist.html#defining-custom-persistent-windows:

User-defined classes can be easily integrated with PersistenceManager. To add support for your custom class MyWidget you just need to:

  • Define a MyWidgetHandler class inheriting from AbstractHandler;

  • Implement its GetKind() method returning a unique string identifying all MyWidget objects, typically something like “widget”;

  • Implement its Save() and Restore() methods to actually save and restore the widget settings using PersistentObject.SaveValue() and PersistentObject.RestoreValue() methods.

If you want to add persistence support for a class not deriving from wx.Window, you need to derive MyPersistentWidget directly from PersistentObject and so implement its PersistentObject.GetName() method too. Additionally, you must ensure that PersistenceManager.SaveAndUnregister() is called when your object is destroyed as this can be only done automatically for windows.

However, just implementing an AbstractHandler subclass is not enough, it is also must be registered in some way. I could not find any automatic handler discovery mechanism in the code, and my custom handler is never called. What is the right way to make custom widget persistent?

nagadomi commented 2 months ago

I have the same question. According to wx.agw.persist.FindHandler, it seems that we can define _persistentHandler. However, unlike HANDLERS, pObject is not passed as an argument of _persistentHandler, so it is unable to create an AbstractHandler subclass instance. https://github.com/wxWidgets/Phoenix/blob/5622abb73deaa26dc2f6dc4cd8b4b2050396b49a/wx/lib/agw/persist/persist_handlers.py#L2573-L2585

How should I implement _persistentHandler?

nagadomi commented 2 months ago

I tried wx.agw.persist.HANDLERS.insert(0, ("myhandler", (mycontrol,))) but cannot create an instance of the handler. That is because it uses eval() and local namespace is different.

Here is a monkeypatch


import wx.lib.agw.persist.persistencemanager                     
OLD_FIND_HANDLER = wx.lib.agw.persist.persistencemanager.FindHandler
USER_HANDLERS = []                                               
def FindHandler(pObject):                                        
    window = pObject.GetWindow()                                 
    klass = window.__class__                                     

    for handler, subclasses in USER_HANDLERS:                    
        for subclass in subclasses:                              
            if issubclass(klass, subclass):                      
                return handler(pObject)                          

    return OLD_FIND_HANDLER(pObject)                             

def register_persistent_handler(handler, subclasses):            
    if not isinstance(subclasses, (list, tuple)):                
        subclasses = [subclasses]                                
    USER_HANDLERS.append((handler, subclasses))                  

wx.lib.agw.persist.persistencemanager.FindHandler = FindHandler  
topic2k commented 2 months ago

I've made a small runnable app that hopefully helps you in making your own persistance handler.

run the app, enter some text in the text control. when you hit enter, the text will be displayed right beside the text control. When you exit the app, the text that is in the text control will be saved and on next start will be restored to the text control (but not to the static text control).

import os

import wx
from wx.lib.agw.persist import AbstractHandler
import wx.lib.agw.persist as persist
from wx.lib.agw import aui

PERSISTANCE_DATA = os.path.join(os.path.split(__file__)[0], f'{os.path.basename(__file__)}.ini')

class MyCustomCtrl(wx.Panel):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.SetName('MyCustomCtrl')
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.txt_ctrl = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
        self.label = wx.StaticText(self, wx.ID_ANY, "Startup Text")
        sizer.Add(self.txt_ctrl, 0, wx.EXPAND | wx.ALL, 10)
        sizer.Add(self.label, 0, wx.EXPAND | wx.ALL, 10)
        self.SetSizer(sizer)
        self.txt_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter)

    def on_text_enter(self, event):
        self.label.SetLabel(event.GetString())

class MyCustomCtrlHandler(AbstractHandler):
    """ Persistance Handler for MyCustomCtrl. """

    def Save(self):
        my_ctrl: MyCustomCtrl = self._window
        txt = my_ctrl.txt_ctrl.GetValue()
        self._pObject.SaveValue('value_of_text_ctrl', txt)
        return True

    def Restore(self):
        my_ctrl: MyCustomCtrl = self._window
        txt = self._pObject.RestoreValue('value_of_text_ctrl')
        if not txt:
            return False
        my_ctrl.txt_ctrl.SetValue(txt)
        return True

    def GetKind(self):
        return 'MyCustomCtrl'

class MyFrame(wx.Frame):
    def __init__(self, parent, title):
        super().__init__(parent, -1, title, name='persist_demo_frame')

        self.aui_mgr = aui_mgr = aui.AuiManager()
        aui_mgr.SetManagedWindow(self)

        self.persist_mgr = persist_mgr = persist.PersistenceManager.Get()
        persist_mgr.SetManagerStyle(persist.PM_DEFAULT_STYLE)
        persist_mgr.SetPersistenceFile(PERSISTANCE_DATA)

        panel = wx.Panel(self)
        self.custom = MyCustomCtrl(panel, wx.ID_ANY)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.custom, 0, wx.ALL, 10)
        panel.SetSizer(sizer)
        panel.Layout()

        sizer = wx.BoxSizer()
        sizer.Add(panel, 1, wx.EXPAND)
        self.SetSizer(sizer)

        self.Fit()
        self.CenterOnScreen(wx.BOTH)

        self.persist_mgr.RegisterAndRestore(self)
        self.persist_mgr.Register(self.custom, MyCustomCtrlHandler)
        self.persist_mgr.Restore(self.custom)

        self.Bind(wx.EVT_CLOSE, self.on_close)

    def on_close(self, event):
        self.persist_mgr.SaveAndUnregister(self.custom)
        self.persist_mgr.SaveAndUnregister(self)
        event.Skip()

class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame(None, "Simple wxPython App")
        self.SetTopWindow(frame)
        frame.Show(True)
        return True

app = MyApp()
app.MainLoop()
nagadomi commented 2 months ago

@topic2k Thank you. It is very helpful. My goal was to override the default handler for ComboBox(it does not save edited value), so some additional code was needed, but it was much nicer.

def persistent_manager_register_all(manager, window):
    # register all child controls without Restore() call
    if window.GetName() not in persist.BAD_DEFAULT_NAMES and persist.HasCtrlHandler(window):
        manager.Register(window)

    for child in window.GetChildren():
        persistent_manager_register_all(manager, child)

def persistent_manager_restore_all(manager):
    # restore all registered controls
    for name, obj in list(manager._persistentObjects.items()):  # NOTE: private attribute
        manager.Restore(obj.GetWindow())

def persistent_manager_register(manager, window, handler):
    # override
    manager.Unregister(window)
    manager.Register(window, handler)

then


editable_comboxes = [
    self.cbo_divergence,
    self.cbo_convergence,
    self.cbo_zoed_resolution,
    self.cbo_edge_dilation,
    self.cbo_fps,
    self.cbo_crf,
]
self.persistence_manager = persist.PersistenceManager.Get()                                           
self.persistence_manager.SetManagerStyle(persist.PM_DEFAULT_STYLE)
self.persistence_manager.SetPersistenceFile(CONFIG_PATH)
persistent_manager_register_all(self.persistence_manager, self)
for control in editable_comboxes:
    persistent_manager_register(self.persistence_manager, control, EditableComboBoxPersistentHandler)
persistent_manager_restore_all(self.persistence_manager)