// SPDX-License-Identifier: GPL-2.0-or-later /** @file * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned. * * Authors: see git history * Tavmjong Bah * * Copyright (c) 2018 Tavmjong Bah, Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "dialog-container.h" #include #include #include #include #include #include #include "enums.h" #include "inkscape-application.h" #include "inkscape-window.h" // #include "ui/dialog/align-and-distribute.h" #include "ui/dialog/clonetiler.h" #include "ui/dialog/dialog-data.h" #include "ui/dialog/dialog-multipaned.h" #include "ui/dialog/dialog-notebook.h" #include "ui/dialog/dialog-window.h" #include "ui/dialog/document-properties.h" #include "ui/dialog/export.h" #include "ui/dialog/fill-and-stroke.h" #include "ui/dialog/filter-effects-dialog.h" #include "ui/dialog/find.h" #include "ui/dialog/glyphs.h" #include "ui/dialog/icon-preview.h" #include "ui/dialog/inkscape-preferences.h" #include "ui/dialog/input.h" #include "ui/dialog/livepatheffect-editor.h" #include "ui/dialog/memory.h" #include "ui/dialog/messages.h" #include "ui/dialog/object-attributes.h" #include "ui/dialog/object-properties.h" #include "ui/dialog/objects.h" #include "ui/dialog/paint-servers.h" #include "ui/dialog/prototype.h" #include "ui/dialog/selectorsdialog.h" #include "ui/shortcuts.h" #if WITH_GSPELL #include "ui/dialog/spellcheck.h" #endif #include "ui/dialog/styledialog.h" #include "ui/dialog/svg-fonts-dialog.h" #include "ui/dialog/swatches.h" #include "ui/dialog/symbols.h" #include "ui/dialog/text-edit.h" #include "ui/dialog/tile.h" #include "ui/dialog/tracedialog.h" #include "ui/dialog/transformation.h" #include "ui/dialog/undo-history.h" #include "ui/dialog/xml-tree.h" #include "ui/icon-names.h" #include "ui/widget/canvas-grid.h" namespace Inkscape { namespace UI { namespace Dialog { DialogContainer::~DialogContainer() { // delete columns; desktop widget deletes dialog container before it get "unrealized", // so it doesn't get a chance to remove them delete columns; } DialogContainer::DialogContainer(InkscapeWindow* inkscape_window) : _inkscape_window(inkscape_window) { g_assert(_inkscape_window != nullptr); get_style_context()->add_class("DialogContainer"); // Setup main column columns = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_HORIZONTAL)); connections.emplace_back(columns->signal_prepend_drag_data().connect( sigc::bind(sigc::mem_fun(*this, &DialogContainer::prepend_drop), columns))); connections.emplace_back(columns->signal_append_drag_data().connect( sigc::bind(sigc::mem_fun(*this, &DialogContainer::append_drop), columns))); // Setup drop targets. target_entries.emplace_back(Gtk::TargetEntry("GTK_NOTEBOOK_TAB")); columns->set_target_entries(target_entries); add(*columns); // Should probably be moved to window. // connections.emplace_back(signal_unmap().connect(sigc::mem_fun(*this, &DialogContainer::cb_on_unmap))); show_all_children(); } DialogMultipaned *DialogContainer::create_column() { DialogMultipaned *column = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_VERTICAL)); connections.emplace_back(column->signal_prepend_drag_data().connect( sigc::bind(sigc::mem_fun(*this, &DialogContainer::prepend_drop), column))); connections.emplace_back(column->signal_append_drag_data().connect( sigc::bind(sigc::mem_fun(*this, &DialogContainer::append_drop), column))); connections.emplace_back(column->signal_now_empty().connect( sigc::bind(sigc::mem_fun(*this, &DialogContainer::column_empty), column))); column->set_target_entries(target_entries); return column; } /** * Get an instance of a DialogBase dialog using the associated dialog name. */ DialogBase *DialogContainer::dialog_factory(const Glib::ustring& dialog_type) { // clang-format off if( dialog_type == "AlignDistribute") return &Inkscape::UI::Dialog::ArrangeDialog::getInstance(); else if(dialog_type == "CloneTiler") return &Inkscape::UI::Dialog::CloneTiler::getInstance(); else if(dialog_type == "DocumentProperties") return &Inkscape::UI::Dialog::DocumentProperties::getInstance(); else if(dialog_type == "Export") return &Inkscape::UI::Dialog::Export::getInstance(); else if(dialog_type == "FillStroke") return &Inkscape::UI::Dialog::FillAndStroke::getInstance(); else if(dialog_type == "FilterEffects") return &Inkscape::UI::Dialog::FilterEffectsDialog::getInstance(); else if(dialog_type == "Find") return &Inkscape::UI::Dialog::Find::getInstance(); else if(dialog_type == "Glyphs") return &Inkscape::UI::Dialog::GlyphsPanel::getInstance(); else if(dialog_type == "IconPreview") return &Inkscape::UI::Dialog::IconPreviewPanel::getInstance(); else if(dialog_type == "Input") return &Inkscape::UI::Dialog::InputDialog::getInstance(); else if(dialog_type == "LivePathEffect") return &Inkscape::UI::Dialog::LivePathEffectEditor::getInstance(); else if(dialog_type == "Memory") return &Inkscape::UI::Dialog::Memory::getInstance(); else if(dialog_type == "Messages") return &Inkscape::UI::Dialog::Messages::getInstance(); else if(dialog_type == "ObjectAttributes") return &Inkscape::UI::Dialog::ObjectAttributes::getInstance(); else if(dialog_type == "ObjectProperties") return &Inkscape::UI::Dialog::ObjectProperties::getInstance(); else if(dialog_type == "Objects") return &Inkscape::UI::Dialog::ObjectsPanel::getInstance(); else if(dialog_type == "PaintServers") return &Inkscape::UI::Dialog::PaintServersDialog::getInstance(); else if(dialog_type == "Preferences") return &Inkscape::UI::Dialog::InkscapePreferences::getInstance(); else if(dialog_type == "Selectors") return &Inkscape::UI::Dialog::SelectorsDialog::getInstance(); else if(dialog_type == "SVGFonts") return &Inkscape::UI::Dialog::SvgFontsDialog::getInstance(); else if(dialog_type == "Swatches") return &Inkscape::UI::Dialog::SwatchesPanel::getInstance(); else if(dialog_type == "Symbols") return &Inkscape::UI::Dialog::SymbolsDialog::getInstance(); else if(dialog_type == "Text") return &Inkscape::UI::Dialog::TextEdit::getInstance(); else if(dialog_type == "Trace") return &Inkscape::UI::Dialog::TraceDialog::getInstance(); else if(dialog_type == "Transform") return &Inkscape::UI::Dialog::Transformation::getInstance(); else if(dialog_type == "UndoHistory") return &Inkscape::UI::Dialog::UndoHistory::getInstance(); else if(dialog_type == "XMLEditor") return &Inkscape::UI::Dialog::XmlTree::getInstance(); #if WITH_GSPELL else if(dialog_type == "Spellcheck") return &Inkscape::UI::Dialog::SpellCheck::getInstance(); #endif #ifdef DEBUG else if(dialog_type == "Prototype") return &Inkscape::UI::Dialog::Prototype::getInstance(); #endif else { std::cerr << "DialogContainer::dialog_factory: Unhandled dialog: " << dialog_type << std::endl; return nullptr; } // clang-format on } // Create the notebook tab Gtk::Widget *DialogContainer::create_notebook_tab(Glib::ustring label_str, Glib::ustring image_str, const Glib::ustring shortcut) { Gtk::Label *label = Gtk::manage(new Gtk::Label(label_str)); Gtk::Image *image = Gtk::manage(new Gtk::Image()); Gtk::Button *close = Gtk::manage(new Gtk::Button()); image->set_from_icon_name(image_str, Gtk::ICON_SIZE_MENU); Gtk::Box *tab = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2)); close->set_image_from_icon_name("window-close"); close->set_halign(Gtk::ALIGN_END); close->set_tooltip_text(_("Close Tab")); close->get_style_context()->add_class("close-button"); Glib::ustring label_str_fix = label_str; label_str_fix = Glib::Regex::create("\\W")->replace_literal(label_str_fix, 0, "-", (Glib::RegexMatchFlags)0); tab->get_style_context()->add_class(label_str_fix); tab->pack_start(*image); tab->pack_end(*close); tab->pack_end(*label); tab->show_all(); // Workaround to the fact that Gtk::Box doesn't receive on_button_press event Gtk::EventBox *cover = Gtk::manage(new Gtk::EventBox()); cover->add(*tab); // Add shortcut tooltip if (shortcut.size() > 0) { auto tlabel = shortcut; int pos = tlabel.find("&", 0); if (pos >= 0 && pos < tlabel.length()) { tlabel.replace(pos, 1, "&"); } tab->set_tooltip_markup(label_str + " (" + tlabel + ")"); } else { tab->set_tooltip_text(label_str); } return cover; } // find dialog's multipaned parent; is there a better way? DialogMultipaned* get_dialog_parent(DialogBase* dialog) { if (!dialog) return nullptr; // dialogs are nested inside Gtk::Notebook if (auto notebook = dynamic_cast(dialog->get_parent())) { // notebooks are inside viewport, inside scrolled window if (auto viewport = dynamic_cast(notebook->get_parent())) { if (auto scroll = dynamic_cast(viewport->get_parent())) { // finally get the panel if (auto panel = dynamic_cast(scroll->get_parent())) { return panel; } } } } return nullptr; } /** * Add new dialog to the current container or in a floating window, based on preferences. */ void DialogContainer::new_dialog(const Glib::ustring& dialog_type ) { // Open all dialogs as floating, if set in preferences Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (prefs == nullptr) { return; } int dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE); bool floating = DialogManager::singleton().should_open_floating(dialog_type); if (dockable == PREFS_DIALOGS_BEHAVIOR_FLOATING || floating) { new_floating_dialog(dialog_type); } else { new_dialog(dialog_type, nullptr); } if (DialogBase* dialog = find_existing_dialog(dialog_type)) { dialog->focus_dialog(); } } DialogBase* DialogContainer::find_existing_dialog(const Glib::ustring& dialog_type) { DialogBase *existing_dialog = get_dialog(dialog_type); if (!existing_dialog) { existing_dialog = DialogManager::singleton().find_floating_dialog(dialog_type); } return existing_dialog; } /** * Overloaded new_dialog */ void DialogContainer::new_dialog(const Glib::ustring& dialog_type, DialogNotebook *notebook) { columns->ensure_multipaned_children(); // Limit each container to containing one of any type of dialog. if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) { // make sure parent window is not hidden/collapsed if (auto panel = get_dialog_parent(existing_dialog)) { panel->show(); } // found existing dialog; blink & exit existing_dialog->blink(); return; } // Create the dialog widget DialogBase *dialog = dialog_factory(dialog_type); if (!dialog) { std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << std::endl; return; } // manage the dialog instance dialog = Gtk::manage(dialog); // Create the notebook tab auto const &dialog_data = get_dialog_data(); Glib::ustring image("inkscape-logo"); auto it = dialog_data.find(dialog_type); if (it != dialog_data.end()) { image = it->second.icon_name; } Glib::ustring label; Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')"; auto app = InkscapeApplication::instance(); std::vector accels = app->gtk_app()->get_accels_for_action(action_name); if (accels.size() > 0) { guint key = 0; Gdk::ModifierType mods; Gtk::AccelGroup::parse(accels[0], key, mods); label = Gtk::AccelGroup::get_label(key, mods); } Gtk::Widget *tab = create_notebook_tab(dialog->get_name(), image, label); // If not from notebook menu add at top of last column. if (!notebook) { // Look to see if last column contains a multipane. If not, add one. DialogMultipaned *last_column = dynamic_cast(columns->get_last_widget()); if (!last_column) { last_column = create_column(); columns->append(last_column); } // Look to see if first widget in column is notebook, if not add one. notebook = dynamic_cast(last_column->get_first_widget()); if (!notebook) { notebook = Gtk::manage(new DialogNotebook(this)); last_column->prepend(notebook); } } // Add dialog notebook->add_page(*dialog, *tab, dialog->get_name()); if (auto panel = dynamic_cast(notebook->get_parent())) { // if panel is collapsed, show it now, or else new dialog will be mysteriously missing panel->show(); } } // recreate dialogs hosted (docked) in a floating DialogWindow; window will be created bool DialogContainer::recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile) { g_assert(inkscape_window != nullptr); bool restored = false; // Step 1: check if we want to load the state Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int save_state = prefs->getInt("/options/savedialogposition/value", PREFS_DIALOGS_STATE_SAVE); if (save_state == PREFS_DIALOGS_STATE_NONE) { return restored; // User has turned off this feature in Preferences } // if it isn't dockable, all saved docked dialogs are made floating bool is_dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING; if (!is_dockable) return false; // not applicable if docking is off // Step 2: get the number of windows; should be 1 int windows_count = 0; try { windows_count = keyfile->get_integer("Windows", "Count"); } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; } // Step 3: for each window, load its state. for (int window_idx = 0; window_idx < windows_count; ++window_idx) { Glib::ustring group_name = "Window" + std::to_string(window_idx); bool has_position = keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position"); window_position_t pos; if (has_position) { // floating window position recorded? pos.x = keyfile->get_integer(group_name, "x"); pos.y = keyfile->get_integer(group_name, "y"); pos.width = keyfile->get_integer(group_name, "width"); pos.height = keyfile->get_integer(group_name, "height"); } // Step 3.0: read the window parameters int column_count = 0; try { column_count = keyfile->get_integer(group_name, "ColumnCount"); } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; } // Step 3.1: get the window's container columns where we want to create the dialogs DialogWindow *dialog_window = new DialogWindow(inkscape_window, nullptr); DialogContainer *active_container = dialog_window->get_container(); DialogMultipaned *active_columns = active_container ? active_container->get_columns() : nullptr; if (!active_container || !active_columns) { continue; } // Step 3.2: for each column, load its state for (int column_idx = 0; column_idx < column_count; ++column_idx) { Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx); // Step 3.2.0: read the column parameters int notebook_count = 0; bool before_canvas = false; try { notebook_count = keyfile->get_integer(column_group_name, "NotebookCount"); if (keyfile->has_key(column_group_name, "BeforeCanvas")) { before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas"); } } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; } // Step 3.2.1: create the column DialogMultipaned *column = active_container->create_column(); before_canvas ? active_columns->prepend(column) : active_columns->append(column); // Step 3.2.2: for each noteboook, load its dialogs for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) { Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs"; // Step 3.2.2.0 read the list of dialogs in the current notebook std::vector dialogs; try { dialogs = keyfile->get_string_list(column_group_name, key); } catch (Glib::Error &error) { std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl; } if (!dialogs.size()) { continue; } DialogNotebook *notebook = nullptr; auto const &dialog_data = get_dialog_data(); // Step 3.2.2.1 create each dialog in the current notebook for (auto type : dialogs) { if (DialogManager::singleton().find_floating_dialog(type)) { // avoid duplicates continue; } if (dialog_data.find(type) != dialog_data.end()) { if (!notebook) { notebook = Gtk::manage(new DialogNotebook(active_container)); column->append(notebook); } active_container->new_dialog(type, notebook); } else { std::cerr << "recreate_dialogs_from_state: invalid dialog type: " << type << std::endl; } } } } if (has_position) { dm_restore_window_position(*dialog_window, pos); } else { dialog_window->update_window_size_to_fit_children(); } dialog_window->show_all(); restored = true; } return restored; } /** * Add a new floating dialog (or reuse existing one if it's already up) */ DialogWindow *DialogContainer::new_floating_dialog(const Glib::ustring& dialog_type) { return create_new_floating_dialog(dialog_type, true); } DialogWindow *DialogContainer::create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink) { // check if this dialog is already open if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) { // found existing dialog; blink & exit if (blink) { existing_dialog->blink(); // show its window if it is hidden if (auto window = DialogManager::singleton().find_floating_dialog_window(dialog_type)) { DialogManager::singleton().set_floating_dialog_visibility(window, true); } } return nullptr; } // check if this dialog *was* open and floating; if so recreate its window if (auto state = DialogManager::singleton().find_dialog_state(dialog_type)) { if (recreate_dialogs_from_state(_inkscape_window, state.get())) { return nullptr; } } // Create the dialog widget DialogBase *dialog = dialog_factory(dialog_type); if (!dialog) { std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type << std::endl; return nullptr; } // manage the dialog instance dialog = Gtk::manage(dialog); // Create the notebook tab gchar* image = nullptr; Glib::ustring label; Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')"; auto app = InkscapeApplication::instance(); std::vector accels = app->gtk_app()->get_accels_for_action(action_name); if (accels.size() > 0) { guint key = 0; Gdk::ModifierType mods; Gtk::AccelGroup::parse(accels[0], key, mods); label = Gtk::AccelGroup::get_label(key, mods); } Gtk::Widget *tab = create_notebook_tab(dialog->get_name(), image ? Glib::ustring(image) : INKSCAPE_ICON("inkscape-logo"), label); // New temporary noteboook DialogNotebook *notebook = Gtk::manage(new DialogNotebook(this)); notebook->add_page(*dialog, *tab, dialog->get_name()); return notebook->pop_tab_callback(); } // toggle dialogs (visibility) is invoked on a top container embedded in Inkscape window void DialogContainer::toggle_dialogs() { // check how many dialog panels are visible and how many are hidden // we use this info to decide what it means to toggle visibility int visible = 0; int hidden = 0; for (auto child : columns->get_children()) { // only examine panels, skip drop zones and handles if (auto panel = dynamic_cast(child)) { if (panel->is_visible()) { ++visible; } else { ++hidden; } } } // next examine floating dialogs auto windows = DialogManager::singleton().get_all_floating_dialog_windows(); for (auto wnd : windows) { if (wnd->is_visible()) { ++visible; } else { ++hidden; } } bool show_dialogs = true; // if some dialogs are hidden, toggle will first show them; // another option could be to hide all if some dialogs are visible if (hidden > 0) { show_dialogs = true; } else { // if everything's visible, hide them show_dialogs = false; } // set visibility of floating dialogs for (auto wnd : windows) { DialogManager::singleton().set_floating_dialog_visibility(wnd, show_dialogs); } // set visibility of docked dialogs columns->toggle_multipaned_children(show_dialogs); } // Update dialogs void DialogContainer::update_dialogs() { for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->update(); }); } void DialogContainer::set_inkscape_window(InkscapeWindow* inkscape_window) { g_assert(inkscape_window != nullptr); _inkscape_window = inkscape_window; auto desktop = _inkscape_window->get_desktop(); for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->setDesktop(desktop); }); } bool DialogContainer::has_dialog_of_type(DialogBase *dialog) { return (dialogs.find(dialog->get_type()) != dialogs.end()); } DialogBase *DialogContainer::get_dialog(const Glib::ustring& dialog_type) { auto found = dialogs.find(dialog_type); if (found != dialogs.end()) { return found->second; } return nullptr; } // Add dialog to list. void DialogContainer::link_dialog(DialogBase *dialog) { dialogs.insert(std::pair(dialog->get_type(), dialog)); DialogWindow *window = dynamic_cast(get_toplevel()); if (window) { window->update_dialogs(); } else { // dialog without DialogWindow has been docked; remove it's floating state // so if user closes and reopens it, it shows up docked again, not floating DialogManager::singleton().remove_dialog_floating_state(dialog->get_type()); } } // Remove dialog from list. void DialogContainer::unlink_dialog(DialogBase *dialog) { if (!dialog) { return; } auto found = dialogs.find(dialog->get_type()); if (found != dialogs.end()) { dialogs.erase(found); } DialogWindow *window = dynamic_cast(get_toplevel()); if (window) { window->update_dialogs(); } } /** * Load last open window's dialog configuration state. * * For the keyfile format, check `save_container_state()`. */ void DialogContainer::load_container_state(Glib::KeyFile *keyfile, bool include_floating) { // Step 1: check if we want to load the state Inkscape::Preferences *prefs = Inkscape::Preferences::get(); // if it isn't dockable, all saved docked dialogs are made floating bool is_dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING; // Step 2: get the number of windows int windows_count = keyfile->get_integer("Windows", "Count"); // Step 3: for each window, load its state. Only the first window is not floating (the others are DialogWindow) for (int window_idx = 0; window_idx < windows_count; ++window_idx) { if (window_idx > 0 && !include_floating) break; Glib::ustring group_name = "Window" + std::to_string(window_idx); // Step 3.0: read the window parameters int column_count = 0; bool floating = window_idx != 0; window_position_t pos; bool has_position = false; try { column_count = keyfile->get_integer(group_name, "ColumnCount"); floating = keyfile->get_boolean(group_name, "Floating"); if (keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position")) { pos.x = keyfile->get_integer(group_name, "x"); pos.y = keyfile->get_integer(group_name, "y"); pos.width = keyfile->get_integer(group_name, "width"); pos.height = keyfile->get_integer(group_name, "height"); has_position = true; } } catch (Glib::Error &error) { std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; } // Step 3.1: get the window's container columns where we want to create the dialogs DialogContainer *active_container = nullptr; DialogMultipaned *active_columns = nullptr; DialogWindow *dialog_window = nullptr; if (is_dockable) { if (floating) { dialog_window = new DialogWindow(_inkscape_window, nullptr); if (dialog_window) { active_container = dialog_window->get_container(); active_columns = dialog_window->get_container()->get_columns(); } } else { active_container = this; active_columns = columns; } if (!active_container || !active_columns) { continue; } } // Step 3.2: for each column, load its state for (int column_idx = 0; column_idx < column_count; ++column_idx) { Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx); // Step 3.2.0: read the column parameters int notebook_count = 0; bool before_canvas = false; try { notebook_count = keyfile->get_integer(column_group_name, "NotebookCount"); before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas"); } catch (Glib::Error &error) { std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; } // Step 3.2.1: create the column DialogMultipaned *column = nullptr; if (is_dockable) { column = active_container->create_column(); if (!column) { continue; } before_canvas ? active_columns->prepend(column) : active_columns->append(column); } // Step 3.2.2: for each noteboook, load its dialogs for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) { Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs"; // Step 3.2.2.0 read the list of dialogs in the current notebook std::vector dialogs; try { dialogs = keyfile->get_string_list(column_group_name, key); } catch (Glib::Error &error) { std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl; } if (!dialogs.size()) { continue; } DialogNotebook *notebook = nullptr; if (is_dockable) { notebook = Gtk::manage(new DialogNotebook(active_container)); column->append(notebook); } auto const &dialog_data = get_dialog_data(); // Step 3.2.2.1 create each dialog in the current notebook for (auto type : dialogs) { if (dialog_data.find(type) != dialog_data.end()) { if (is_dockable) { active_container->new_dialog(type, notebook); } else { dialog_window = create_new_floating_dialog(type, false); } } else { std::cerr << "load_container_state: invalid dialog type: " << type << std::endl; } } } } if (dialog_window) { if (has_position) { dm_restore_window_position(*dialog_window, pos); } else { dialog_window->update_window_size_to_fit_children(); } dialog_window->show_all(); } } } void save_wnd_position(Glib::KeyFile *keyfile, const Glib::ustring &group_name, const window_position_t *position) { keyfile->set_boolean(group_name, "Position", position != nullptr); if (position) { // floating window position? keyfile->set_integer(group_name, "x", position->x); keyfile->set_integer(group_name, "y", position->y); keyfile->set_integer(group_name, "width", position->width); keyfile->set_integer(group_name, "height", position->height); } } // get *this* container's state only; store window 'position' in the state if given std::shared_ptr DialogContainer::get_container_state(const window_position_t *position) const { std::shared_ptr keyfile = std::make_shared(); DialogMultipaned *window = columns; const int window_idx = 0; // Step 2: save the number of windows keyfile->set_integer("Windows", "Count", 1); // Step 3.0: get all the multipanes of the window std::vector multipanes; for (auto const &column : window->get_children()) { if (auto paned = dynamic_cast(column)) { multipanes.push_back(paned); } } // Step 3.1: for each non-empty column, save its data. int column_count = 0; // non-empty columns count for (size_t column_idx = 0; column_idx < multipanes.size(); ++column_idx) { Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx); int notebook_count = 0; // non-empty notebooks count // Step 3.1.0: for each notebook, get its dialogs for (auto const &columns_widget : multipanes[column_idx]->get_children()) { if (auto dialog_notebook = dynamic_cast(columns_widget)) { std::vector dialogs; for (auto const &widget : dialog_notebook->get_notebook()->get_children()) { if (DialogBase *dialog = dynamic_cast(widget)) { dialogs.push_back(dialog->get_type()); } } // save the dialogs type Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs"; keyfile->set_string_list(group_name, key, dialogs); // increase the notebook count notebook_count++; } } // Step 3.1.1: increase the column count if (notebook_count != 0) { column_count++; } // Step 3.1.2: Save the column's data keyfile->set_integer(group_name, "NotebookCount", notebook_count); } // Step 3.2: save the window group Glib::ustring group_name = "Window" + std::to_string(window_idx); keyfile->set_integer(group_name, "ColumnCount", column_count); save_wnd_position(keyfile.get(), group_name, position); return keyfile; } /** * Save container state. The configuration of open dialogs and the relative positions of the notebooks are saved. * * The structure of such a KeyFile is: * * There is a "Windows" group that records the number of the windows: * [Windows] * Count=1 * * A "WindowX" group saves the number of columns the window's container has and whether the window is floating: * * [Window0] * ColumnCount=1 * Floating=false * * For each column, we have a "WindowWColumnX" group, where X is the index of the column. "BeforeCanvas" checks * if the column is before the canvas or not. "NotebookCount" records how many notebooks are in each column and * "NotebookXDialogs" records a list of the types for the dialogs in notebook X. * * [Window0Column0] * Notebook0Dialogs=Text; * NotebookCount=2 * BeforeCanvas=false * */ std::unique_ptr DialogContainer::save_container_state() { std::unique_ptr keyfile = std::make_unique(); auto app = InkscapeApplication::instance(); // Step 1: get all the container columns (in order, from the current container and all DialogWindow containers) std::vector windows(1, columns); std::vector dialog_windows(1, nullptr); for (auto const &window : app->gtk_app()->get_windows()) { DialogWindow *dialog_window = dynamic_cast(window); if (dialog_window) { windows.push_back(dialog_window->get_container()->get_columns()); dialog_windows.push_back(dialog_window); } } // Step 2: save the number of windows keyfile->set_integer("Windows", "Count", windows.size()); // Step 3: for each window, save its data. Only the first window is not floating (the others are DialogWindow) for (int window_idx = 0; window_idx < (int)windows.size(); ++window_idx) { // Step 3.0: get all the multipanes of the window std::vector multipanes; // used to check if the column is before or after canvas std::vector::iterator multipanes_it = multipanes.begin(); bool canvas_seen = window_idx != 0; // no floating windows (window_idx > 0) have a canvas int before_canvas_columns_count = 0; for (auto const &column : windows[window_idx]->get_children()) { if (!canvas_seen) { UI::Widget::CanvasGrid *canvas = dynamic_cast(column); if (canvas) { canvas_seen = true; } else { DialogMultipaned *paned = dynamic_cast(column); if (paned) { multipanes_it = multipanes.insert(multipanes_it, paned); before_canvas_columns_count++; } } } else { DialogMultipaned *paned = dynamic_cast(column); if (paned) { multipanes.push_back(paned); } } } // Step 3.1: for each non-empty column, save its data. int column_count = 0; // non-empty columns count for (int column_idx = 0; column_idx < (int)multipanes.size(); ++column_idx) { Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx); int notebook_count = 0; // non-empty notebooks count // Step 3.1.0: for each notebook, get its dialogs' types for (auto const &columns_widget : multipanes[column_idx]->get_children()) { DialogNotebook *dialog_notebook = dynamic_cast(columns_widget); if (dialog_notebook) { std::vector dialogs; for (auto const &widget : dialog_notebook->get_notebook()->get_children()) { DialogBase *dialog = dynamic_cast(widget); if (dialog) { dialogs.push_back(dialog->get_type()); } } // save the dialogs type Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs"; keyfile->set_string_list(group_name, key, dialogs); // increase the notebook count notebook_count++; } } // Step 3.1.1: increase the column count if (notebook_count != 0) { column_count++; } // Step 3.1.2: Save the column's data keyfile->set_integer(group_name, "NotebookCount", notebook_count); keyfile->set_boolean(group_name, "BeforeCanvas", (column_idx < before_canvas_columns_count)); } // Step 3.2: save the window group Glib::ustring group_name = "Window" + std::to_string(window_idx); keyfile->set_integer(group_name, "ColumnCount", column_count); keyfile->set_boolean(group_name, "Floating", window_idx != 0); if (window_idx != 0) { // floating? if (auto wnd = dynamic_cast(dialog_windows.at(window_idx))) { // store window position auto pos = dm_get_window_position(*wnd); save_wnd_position(keyfile.get(), group_name, pos ? &*pos : nullptr); } } } return keyfile; } // Signals ----------------------------------------------------- /** * No zombie windows. TODO: Need to work on this as it still leaves Gtk::Window! (?) */ void DialogContainer::on_unrealize() { // Disconnect all signals for_each(connections.begin(), connections.end(), [&](auto c) { c.disconnect(); }); delete columns; columns = nullptr; parent_type::on_unrealize(); } // Create a new notebook and move page. DialogNotebook *DialogContainer::prepare_drop(const Glib::RefPtr context) { Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context); // Find source notebook and page Gtk::Notebook *old_notebook = dynamic_cast(source); if (!old_notebook) { std::cerr << "DialogContainer::prepare_drop: notebook not found!" << std::endl; return nullptr; } // Find page Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page()); if (!page) { std::cerr << "DialogContainer::prepare_drop: page not found!" << std::endl; return nullptr; } // Create new notebook and move page. DialogNotebook *new_notebook = Gtk::manage(new DialogNotebook(this)); new_notebook->move_page(*page); // move_page() takes care of updating dialog lists. return new_notebook; } // Notebook page dropped on prepend target. Call function to create new notebook and then insert. void DialogContainer::prepend_drop(const Glib::RefPtr context, DialogMultipaned *multipane) { DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page. if (!new_notebook) { std::cerr << "DialogContainer::prepend_drop: no new notebook!" << std::endl; return; } if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { // Columns // Create column DialogMultipaned *column = create_column(); column->prepend(new_notebook); columns->prepend(column); } else { // Column multipane->prepend(new_notebook); } update_dialogs(); // Always update dialogs on Notebook change } // Notebook page dropped on append target. Call function to create new notebook and then insert. void DialogContainer::append_drop(const Glib::RefPtr context, DialogMultipaned *multipane) { DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page. if (!new_notebook) { std::cerr << "DialogContainer::append_drop: no new notebook!" << std::endl; return; } if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) { // Columns // Create column DialogMultipaned *column = create_column(); column->append(new_notebook); columns->append(column); } else { // Column multipane->append(new_notebook); } update_dialogs(); // Always update dialogs on Notebook change } /** * If a DialogMultipaned column is empty and it can be removed, remove it */ void DialogContainer::column_empty(DialogMultipaned *column) { DialogMultipaned *parent = dynamic_cast(column->get_parent()); if (parent) { parent->remove(*column); } DialogWindow *window = dynamic_cast(get_toplevel()); if (window && parent) { auto children = parent->get_children(); // Close the DialogWindow if you're in an empty one if (children.size() == 3 && parent->has_empty_widget()) { window->close(); } } } } // 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 :