DinkydauSet / ExploreFractals

A tool for testing the effect of Mandelbrot set Julia morphings
GNU General Public License v3.0
5 stars 1 forks source link

Managing multiple windows #37

Closed DinkydauSet closed 2 years ago

DinkydauSet commented 2 years ago

How to manage multiple windows? They should all be closed when the main window is closed. The program should not crash while closing.

Relevant facts:

  1. API::close_window does not call the destructor of the window to close, so it doesn't free memory for the window. It just stops the process.
  2. calling API::close_window from a different thread than the one that created the window works, but it returns before the nana instance of the window has ended.
  3. calling API::close_window on a panel (which is also a window in nana) works. It doesn't crash the program. The panel just doesn't appear anymore. I only see the background.
DinkydauSet commented 2 years ago

What I find difficult is where to keep the data for the opened windows. I want to create and open a window from a function like this:

void functionname()
{
    json_form fm;
    fm.show();
}

This creates the window and then immediately closes it, which makes sense because at the end of the function the local variable fm is destroyed.

My current solution to the problem is to create the window in a different thread and blocking the thread until the window is closed. A different thread automatically means a different instance of nana (I read somewhere), so the normal thing to do is to create a form and then call exec, which blocks the thread until all windows are closed:

thread([&, this]()
{
    lock_guard<mutex> guard(helpwindow_mutex);
    {
        assert(exists == false);
        exists = true;

        helpform fm;
        fm.caption("Help");
        helpwindow = (window)fm; //window is a pointer type

        fm.show();
        exec();

        exists = false;
        helpwindow = nullptr;
    }
}).detach();

This approach creates a new problem: when the main window is closed, nana's exec function returns, because the main window is the only window in its instance. Meanwhile there may still be a helpwindow open, but that window is from a different instance, so before main returns I have to make sure that all windows created like this are closed. I do that by storing the window pointers as global variables and calling API::close_window on those. That's still not enough, because that API function returns immediately after closing the window. It doesn't wait for the nana instance to end. That's why I use a mutex,. When the mutex lock is released, that means exec has returned and so the nana instances has ended. This works, so that's not a problem, but it becomes more complicated if I want to allow any number of windows to be open. Every window would need its own mutex, and where do I store those mutexes (as data) so that main can wait for them?

DinkydauSet commented 2 years ago

The solution: I put all this in the main_form class.

// This base class makes it possible to store pointers to all child windows in 1 vector, even if the windows are of different types.
class child_window_base {
public:
    virtual ~child_window_base() {}
    virtual void focus() = 0;
};

// The instances of this template are the actual child window classes. formtype must be a subclass of form
template <typename formtype>
class child_window : public child_window_base {
public:
    formtype fm;
    void focus() { fm.focus(); }
};

vector<shared_ptr<child_window_base>> childWindows;

/*
    creates a childwindow of the specified type

    The reason for PostMessage is this: I want the window to be removed from the vector of childWindows when it's destroyed. Doing so during the handling of the destroyed event causes a deadlock. I don't know why; nana must use some mutex during the event that is required by one of the destructors. The solution is to postpone cleanup of the resources until after the event has been handled, by sending a new message.
*/
template <typename formtype>
void spawn_child()
{
    shared_ptr<child_window<formtype>> child = make_shared<child_window<formtype>>();
    childWindows.push_back(child);

    child_window_base* childpointer = child.get();

    child->fm.events().destroy([childpointer]()
    {
        if(debug) cout << "child window is being closed" << endl;

        PostMessage(main_form_hwnd, Message::CLEANUP_CHILD_WINDOW, reinterpret_cast<WPARAM>(childpointer), 0);

        if(debug) cout << "child window closed" << endl;
    });

    //type specific things
    if constexpr( is_same<formtype, json_form>::value )
    {
        json_form& json = child->fm;

        json.capture.events().click([this, &json](const arg_click& arg)
        {
            FractalCanvas* canvas = activeCanvas();
            if (canvas != nullptr) {
                json.text.caption( canvas->P().toJson() );
            }
            else {
                msgbox mb(json, "Error", msgbox::ok);
                mb.icon(mb.icon_error);
                mb << "No fractal tab open";
                mb.show();
            }
        });

        json.apply.events().click([this, &json](const arg_click& arg)
        {
            FractalCanvas* canvas = activeCanvas();
            if (canvas != nullptr) {
                //try to apply the changes to a copy first
                FractalParameters P = canvas->P();
                string textContent = json.text.caption();
                bool success = P.fromJson(textContent);

                if (success) {
                    canvas->changeParameters(P, EventSource::JSON);
                }
                else {
                    msgbox mb(json, "Error", msgbox::ok);
                    mb.icon(mb.icon_error);
                    mb << "Invalid JSON";
                    mb.show();
                }
            }
            else {
                msgbox mb(json, "Error", msgbox::ok);
                mb.icon(mb.icon_error);
                mb << "No fractal tab open";
                mb.show();
            }
        });
    }

    child->fm.show();
}

// Creates a childwindow of the specified type if none exist yet, otherwise focuses the existing one
template <typename formtype>
void spawn_unique_child()
{
    for (int i=0; i<childWindows.size(); i++)
    {
        child_window<formtype>* child = dynamic_cast<child_window<formtype>*>(childWindows[i].get());
        if (child != nullptr)
        {
            // This type of window already exists. Focus it:    
            child->focus();
            return;
        }
    }

    //This type of windows does not exist yet. Create one:
    spawn_child<formtype>();
}

It's tricks with templates and polymorphism to achieve something that I want to do which is not normally possible:

  1. I want to store the resources for windows somewhere. The problem is that they all have different datatypes so I can't just make a vector for them. What I can do is store all the pointers in one vector, but even then the program must remember what datatypes those pointers point to, to destruct the windows properly. This is what polymorphism allows. Pointers to a common base class can be stored in the same vector, and the actual datatype is determined at runtime.

I can't just make every window type a subclass of a common base class because I don't have control over nanas classes, but what I can do is create wrapper classes for each window type which ARE subclasses of a common base class. (I saw some people on the internet recommend against this approach because it's an anti-pattern or something but it works so what's the problem??) The polymorphism of c++ automatically does what I could program myself but fortunately I don't have to: store an indicator of the actual type for each base class pointer, and when destructing the resource, calling the right destructor based on the type indicator. For this, it's important that the destructor of the base class is virtual.

  1. Ok, so with polymorphism I can store all window pointers in one vector. The next thing is I want to be able to spawn a certain kind of window, like this:
spawn_child(json_form);

Here I pass a type as a function parameter, but that's not something that exists in c++ so I solved the problem by making the function a template, so that I could instead do:

spawn_child<json_form>();

The cleanup goes like this:

sc.make_before(Message::CLEANUP_CHILD_WINDOW, [&fm](UINT, WPARAM wParam, LPARAM, LRESULT*)
{
    if(debug) cout << "Message CLEANUP_CHILD_WINDOW" << endl;
    main_form::child_window_base* child = reinterpret_cast<main_form::child_window_base*>(wParam);

    int indexof = -1;
    for (int i=0; i<fm.childWindows.size(); i++) {
        if (fm.childWindows[i].get() == child) {
            indexof = i;
            break;
        }
    }

    // There is a valid situation in which indexof remains -1. This happens when a window is closed BY removing its resource from childWindows (which happens in the destroy event handler below). That also triggers the cleanup, but there is nothing to clean up.
    if (indexof >= 0)
        fm.childWindows.erase(fm.childWindows.begin() + indexof);

    return false;
});

A window is identified by its memory address. When the window is in the vector (note that this may not always be the case - in other words, the cleanup is not always needed) it's removed from the vector. This frees all resources.

Actually the only thing that has to be done to close a window is to destruct the resources. That's how nana works, apparently, which is nice. To close all child windows when the program has to close, I just remove every element from the vector of child windows:

fm.events().destroy([&fm]()
{
    if(debug) cout << "main form is being destroyed" << endl;
    fm.childWindows.clear(); //this closes all child windows
    if(debug) cout << "all child windows closed" << endl;
});