// SPDX-License-Identifier: GPL-2.0-or-later #include "dialog-manager.h" #include #include #ifdef G_OS_WIN32 #include namespace filesystem = std::filesystem; #else // Waiting for compiler on MacOS to catch up to C++x17 #include namespace filesystem = boost::filesystem; #endif #include "io/resource.h" #include "inkscape-application.h" #include "dialog-base.h" #include "dialog-container.h" #include "dialog-window.h" #include "enums.h" #include "preferences.h" namespace Inkscape { namespace UI { namespace Dialog { std::optional dm_get_window_position(Gtk::Window &window) { std::optional position = std::nullopt; const int max = std::numeric_limits::max(); int x = max; int y = max; int width = 0; int height = 0; // gravity NW to include window decorations window.property_gravity() = Gdk::GRAVITY_NORTH_WEST; window.get_position(x, y); window.get_size(width, height); if (x != max && y != max && width > 0 && height > 0) { position = window_position_t{x, y, width, height}; } return position; } void dm_restore_window_position(Gtk::Window &window, const window_position_t &position) { // note: Gtk window methods are recommended over low-level Gdk ones to resize and position window window.property_gravity() = Gdk::GRAVITY_NORTH_WEST; window.set_default_size(position.width, position.height); // move & resize positions window on the screen making sure it is not clipped // (meaning it is visible; this works with two monitors too) window.move(position.x, position.y); window.resize(position.width, position.height); } DialogManager &DialogManager::singleton() { static DialogManager dm; return dm; } // store complete dialog window state (including its container state) void DialogManager::store_state(DialogWindow &wnd) { // get window's size and position if (auto pos = dm_get_window_position(wnd)) { if (auto container = wnd.get_container()) { // get container's state auto state = container->get_container_state(&*pos); // find dialogs hosted in this window for (auto dlg : *container->get_dialogs()) { _floating_dialogs[dlg.first] = state; } } } } // bool DialogManager::should_open_floating(const Glib::ustring& dialog_type) { return _floating_dialogs.count(dialog_type) > 0; } void DialogManager::set_floating_dialog_visibility(DialogWindow* wnd, bool show) { if (!wnd) return; if (show) { if (wnd->is_visible()) return; // wnd->present(); - not sure which one is better, show or present... wnd->show(); _hidden_dlg_windows.erase(wnd); // re-add it to application; hiding removed it if (auto app = InkscapeApplication::instance()) { app->gtk_app()->add_window(*wnd); } } else { if (!wnd->is_visible()) return; _hidden_dlg_windows.insert(wnd); wnd->hide(); } } std::vector DialogManager::get_all_floating_dialog_windows() { std::vector windows = InkscapeApplication::instance()->gtk_app()->get_windows(); std::vector result(_hidden_dlg_windows.begin(), _hidden_dlg_windows.end()); for (auto wnd : windows) { if (auto dlg_wnd = dynamic_cast(wnd)) { result.push_back(dlg_wnd); } } return result; } DialogWindow* DialogManager::find_floating_dialog_window(const Glib::ustring& dialog_type) { auto windows = get_all_floating_dialog_windows(); for (auto dlg_wnd : windows) { if (auto container = dlg_wnd->get_container()) { if (container->get_dialog(dialog_type)) { return dlg_wnd; } } } return nullptr; } DialogBase *DialogManager::find_floating_dialog(const Glib::ustring& dialog_type) { auto windows = get_all_floating_dialog_windows(); for (auto dlg_wnd : windows) { if (auto container = dlg_wnd->get_container()) { if (auto dlg = container->get_dialog(dialog_type)) { return dlg; } } } return nullptr; } std::shared_ptr DialogManager::find_dialog_state(const Glib::ustring& dialog_type) { auto it = _floating_dialogs.find(dialog_type); if (it != _floating_dialogs.end()) { return it->second; } return nullptr; } const char dialogs_state[] = "dialogs-state-ex.ini"; const char save_dialog_position[] = "/options/savedialogposition/value"; const char transient_group[] = "transient"; // list of dialogs sharing the same state std::vector DialogManager::count_dialogs(const Glib::KeyFile *state) const { std::vector dialogs; if (!state) return dialogs; for (auto dlg : _floating_dialogs) { if (dlg.second.get() == state) { dialogs.emplace_back(dlg.first); } } return dialogs; } void DialogManager::save_dialogs_state(DialogContainer *docking_container) { if (!docking_container) return; // check if we want to save the state Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE); if (save_state == PREFS_DIALOGS_STATE_NONE) return; // save state of docked dialogs and currently open floating ones auto keyfile = docking_container->save_container_state(); // save transient state of floating dialogs that user might have opened interacting with the app int idx = 1; for (auto dlg : _floating_dialogs) { auto state = dlg.second.get(); auto&& type = dlg.first; auto index = std::to_string(idx++); // state may be empty; all that means it that dialog hasn't been opened yet, // but when it is, then it should be open in a floating state keyfile->set_string(transient_group, "state" + index, state ? state->to_data() : ""); auto dialogs = count_dialogs(state); if (!state) { dialogs.emplace_back(type); } keyfile->set_string_list(transient_group, "dialogs" + index, dialogs); } keyfile->set_integer(transient_group, "count", _floating_dialogs.size()); std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state); try { keyfile->save_to_file(filename); } catch (Glib::FileError &error) { std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; } } // load transient dialog state - it includes state of floating dialogs that may or may not be open void DialogManager::load_transient_state(Glib::KeyFile *file) { int count = file->get_integer(transient_group, "count"); for (int i = 0; i < count; ++i) { auto index = std::to_string(i + 1); auto dialogs = file->get_string_list(transient_group, "dialogs" + index); auto state = file->get_string(transient_group, "state" + index); auto keyfile = std::make_shared(); if (!state.empty()) { keyfile->load_from_data(state); } for (auto type : dialogs) { _floating_dialogs[type] = keyfile; } } } bool file_exists(const std::string& filepath) { #ifdef G_OS_WIN32 bool exists = filesystem::exists(filesystem::u8path(filepath)); #else bool exists = filesystem::exists(filesystem::path(filepath)); #endif return exists; } // restore state of dialogs; populate docking container and open visible floating dialogs void DialogManager::restore_dialogs_state(DialogContainer *docking_container, bool include_floating) { if (!docking_container) return; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE); if (save_state == PREFS_DIALOGS_STATE_NONE) return; try { auto keyfile = std::make_unique(); std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state); bool exists = file_exists(filename); if (exists && keyfile->load_from_file(filename)) { // restore visible dialogs first; that state is up-to-date docking_container->load_container_state(keyfile.get(), include_floating); // then load transient data too; it may be older than above if (include_floating) { try { load_transient_state(keyfile.get()); } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": transient state not loaded - " << error.what().raw() << std::endl; } } } else { // state not available or not valid; prepare defaults dialog_defaults(docking_container); } } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": dialogs state not loaded - " << error.what().raw() << std::endl; } } void DialogManager::remove_dialog_floating_state(const Glib::ustring& dialog_type) { auto it = _floating_dialogs.find(dialog_type); if (it != _floating_dialogs.end()) { _floating_dialogs.erase(it); } } // apply defaults when dialog state cannot be loaded / doesn't exist: // here we load defaults from dedicated ini file void DialogManager::dialog_defaults(DialogContainer* docking_container) { auto keyfile = std::make_unique(); // default/initial state used when running Inkscape for the first time std::string filename = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "default-dialog-state.ini"); bool exists = file_exists(filename); if (exists && keyfile->load_from_file(filename)) { // populate info about floating dialogs, so when users try opening them, // they will pop up in a window, not docked load_transient_state(keyfile.get()); // create docked dialogs only, if any docking_container->load_container_state(keyfile.get(), false); } else { g_warning("Cannot load default dialog state %s", filename.c_str()); } } } // namespace Dialog } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :