// SPDX-License-Identifier: GPL-2.0-or-later /* * Inkscape::Widgets::LayerSelector - layer selector widget * * Authors: * MenTaLguY * Abhishek Sharma * * Copyright (C) 2004 MenTaLguY * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include "ui/dialog/layer-properties.h" #include "ui/icon-loader.h" #include #include #include #include "desktop.h" #include "document.h" #include "document-undo.h" #include "layer-manager.h" #include "ui/icon-names.h" #include "ui/util.h" #include "util/reverse-list.h" #include "verbs.h" #include "xml/node-event-vector.h" namespace Inkscape { namespace UI { namespace Widget { namespace { class AlternateIcons : public Gtk::HBox { public: AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b) : _a(nullptr), _b(nullptr) { set_name("AlternateIcons"); if (!a.empty()) { _a = Gtk::manage(sp_get_icon_image(a, size)); _a->set_no_show_all(true); add(*_a); } if (!b.empty()) { _b = Gtk::manage(sp_get_icon_image(b, size)); _b->set_no_show_all(true); add(*_b); } setState(false); } bool state() const { return _state; } void setState(bool state) { _state = state; if (_state) { if (_a) { _a->hide(); } if (_b) { _b->show(); } } else { if (_a) { _a->show(); } if (_b) { _b->hide(); } } } private: Gtk::Image *_a; Gtk::Image *_b; bool _state; }; } /** LayerSelector constructor. Creates lock and hide buttons, * initializes the layer dropdown selector with a label renderer, * and hooks up signal for setting the desktop layer when the * selector is changed. */ LayerSelector::LayerSelector(SPDesktop *desktop) : _desktop(nullptr), _layer(nullptr) { set_name("LayerSelector"); AlternateIcons *label; label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden"))); _visibility_toggle.add(*label); _visibility_toggle.signal_toggled().connect( sigc::compose( sigc::mem_fun(*label, &AlternateIcons::setState), sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active) ) ); _visibility_toggled_connection = _visibility_toggle.signal_toggled().connect( sigc::compose( sigc::mem_fun(*this, &LayerSelector::_hideLayer), sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active) ) ); _visibility_toggle.set_relief(Gtk::RELIEF_NONE); _visibility_toggle.set_tooltip_text(_("Toggle current layer visibility")); pack_start(_visibility_toggle, Gtk::PACK_EXPAND_PADDING); label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked"))); _lock_toggle.add(*label); _lock_toggle.signal_toggled().connect( sigc::compose( sigc::mem_fun(*label, &AlternateIcons::setState), sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active) ) ); _lock_toggled_connection = _lock_toggle.signal_toggled().connect( sigc::compose( sigc::mem_fun(*this, &LayerSelector::_lockLayer), sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active) ) ); _lock_toggle.set_relief(Gtk::RELIEF_NONE); _lock_toggle.set_tooltip_text(_("Lock or unlock current layer")); pack_start(_lock_toggle, Gtk::PACK_EXPAND_PADDING); _selector.set_tooltip_text(_("Current layer")); pack_start(_selector, Gtk::PACK_EXPAND_WIDGET); _layer_model = Gtk::ListStore::create(_model_columns); _selector.set_model(_layer_model); _selector.pack_start(_label_renderer); _selector.set_cell_data_func( _label_renderer, sigc::mem_fun(*this, &LayerSelector::_prepareLabelRenderer) ); _selection_changed_connection = _selector.signal_changed().connect( sigc::mem_fun(*this, &LayerSelector::_setDesktopLayer) ); setDesktop(desktop); } /** Destructor - disconnects signal handler */ LayerSelector::~LayerSelector() { setDesktop(nullptr); _selection_changed_connection.disconnect(); } /** Sets the desktop for the widget. First disconnects signals * for the current desktop, then stores the pointer to the * given \a desktop, and attaches its signals to this one. * Then it selects the current layer for the desktop. */ void LayerSelector::setDesktop(SPDesktop *desktop) { if ( desktop == _desktop ) { return; } if (_desktop) { // _desktop_shutdown_connection.disconnect(); if (_current_layer_changed_connection) _current_layer_changed_connection.disconnect(); if (_layers_changed_connection) _layers_changed_connection.disconnect(); // g_signal_handlers_disconnect_by_func(_desktop, (gpointer)&detach, this); } _desktop = desktop; if (_desktop) { // TODO we need a different signal for this, really..s // _desktop_shutdown_connection = _desktop->connectShutdown( // sigc::bind (sigc::ptr_fun (detach), this)); // g_signal_connect_after(_desktop, "shutdown", GCallback(detach), this); LayerManager *mgr = _desktop->layer_manager; if ( mgr ) { _current_layer_changed_connection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayerSelector::_selectLayer) ); //_layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayerSelector::_updateLayer) ); _layers_changed_connection = mgr->connectChanged( sigc::mem_fun(*this, &LayerSelector::_layersChanged) ); } _selectLayer(_desktop->currentLayer()); } } namespace { class is_layer { public: is_layer(SPDesktop *desktop) : _desktop(desktop) {} bool operator()(SPObject &object) const { return _desktop->isLayer(&object); } private: SPDesktop *_desktop; }; class column_matches_object { public: column_matches_object(Gtk::TreeModelColumn const &column, SPObject &object) : _column(column), _object(object) {} bool operator()(Gtk::TreeModel::const_iterator const &iter) const { SPObject *current=(*iter)[_column]; return current == &_object; } private: Gtk::TreeModelColumn const &_column; SPObject &_object; }; } void LayerSelector::_layersChanged() { if (_desktop) { /* * This code fixes #166691 but causes issues #1066543 and #1080378. * Comment out until solution found. */ //_selectLayer(_desktop->currentLayer()); } } /** Selects the given layer in the dropdown selector. */ void LayerSelector::_selectLayer(SPObject *layer) { using Inkscape::Util::List; using Inkscape::Util::cons; using Inkscape::Util::reverse_list; _selection_changed_connection.block(); _visibility_toggled_connection.block(); _lock_toggled_connection.block(); while (!_layer_model->children().empty()) { Gtk::ListStore::iterator first_row(_layer_model->children().begin()); _destroyEntry(first_row); _layer_model->erase(first_row); } SPObject *root=_desktop->currentRoot(); if (_layer) { sp_object_unref(_layer, nullptr); _layer = nullptr; } if (layer) { List hierarchy=reverse_list(layer, root); if ( layer == root ) { _buildEntries(0, cons(*root, hierarchy)); } else if (hierarchy) { _buildSiblingEntries(0, *root, hierarchy); } Gtk::TreeIter row( std::find_if( _layer_model->children().begin(), _layer_model->children().end(), column_matches_object(_model_columns.object, *layer) ) ); if ( row != _layer_model->children().end() ) { _selector.set_active(row); } _layer = layer; sp_object_ref(_layer, nullptr); } if ( !layer || layer == root ) { _visibility_toggle.set_sensitive(false); _visibility_toggle.set_active(false); _lock_toggle.set_sensitive(false); _lock_toggle.set_active(false); } else { _visibility_toggle.set_sensitive(true); _visibility_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false )); _lock_toggle.set_sensitive(true); _lock_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false )); } _lock_toggled_connection.unblock(); _visibility_toggled_connection.unblock(); _selection_changed_connection.unblock(); } /** Sets the current desktop layer to the actively selected layer. */ void LayerSelector::_setDesktopLayer() { Gtk::ListStore::iterator selected(_selector.get_active()); SPObject *layer=_selector.get_active()->get_value(_model_columns.object); if ( _desktop && layer ) { _current_layer_changed_connection.block(); _layers_changed_connection.block(); _desktop->layer_manager->setCurrentLayer(layer); _current_layer_changed_connection.unblock(); _layers_changed_connection.unblock(); _selectLayer(_desktop->currentLayer()); } if (_desktop && _desktop->canvas) { gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); } } /** Creates rows in the _layer_model data structure for each item * in \a hierarchy, to a given \a depth. */ void LayerSelector::_buildEntries(unsigned depth, Inkscape::Util::List hierarchy) { using Inkscape::Util::List; using Inkscape::Util::rest; _buildEntry(depth, *hierarchy); List remainder=rest(hierarchy); if (remainder) { _buildEntries(depth+1, remainder); } else { _buildSiblingEntries(depth+1, *hierarchy, remainder); } } /** Creates entries in the _layer_model data structure for * all siblings of the first child in \a parent. */ void LayerSelector::_buildSiblingEntries( unsigned depth, SPObject &parent, Inkscape::Util::List hierarchy ) { using Inkscape::Util::rest; auto siblings = parent.children | boost::adaptors::filtered(is_layer(_desktop)) | boost::adaptors::reversed; SPObject *layer( hierarchy ? &*hierarchy : nullptr ); for (auto& sib: siblings) { _buildEntry(depth, sib); if ( &sib == layer ) { _buildSiblingEntries(depth+1, *layer, rest(hierarchy)); } } } namespace { struct Callbacks { sigc::slot update_row; sigc::slot update_list; }; void attribute_changed(Inkscape::XML::Node */*repr*/, gchar const *name, gchar const */*old_value*/, gchar const */*new_value*/, bool /*is_interactive*/, void *data) { if ( !std::strcmp(name, "inkscape:groupmode") ) { reinterpret_cast(data)->update_list(); } else { reinterpret_cast(data)->update_row(); } } void node_added(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) { gchar const *mode=child->attribute("inkscape:groupmode"); if ( mode && !std::strcmp(mode, "layer") ) { reinterpret_cast(data)->update_list(); } } void node_removed(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) { gchar const *mode=child->attribute("inkscape:groupmode"); if ( mode && !std::strcmp(mode, "layer") ) { reinterpret_cast(data)->update_list(); } } void node_reordered(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*old_ref*/, Inkscape::XML::Node */*new_ref*/, void *data) { gchar const *mode=child->attribute("inkscape:groupmode"); if ( mode && !std::strcmp(mode, "layer") ) { reinterpret_cast(data)->update_list(); } } void update_row_for_object(SPObject *object, Gtk::TreeModelColumn const &column, Glib::RefPtr const &model) { Gtk::TreeIter row( std::find_if( model->children().begin(), model->children().end(), column_matches_object(column, *object) ) ); if ( row != model->children().end() ) { model->row_changed(model->get_path(row), row); } } void rebuild_all_rows(sigc::slot rebuild, SPDesktop *desktop) { rebuild(desktop->currentLayer()); } } void LayerSelector::_protectUpdate(sigc::slot slot) { bool visibility_blocked=_visibility_toggled_connection.blocked(); bool lock_blocked=_lock_toggled_connection.blocked(); _visibility_toggled_connection.block(true); _lock_toggled_connection.block(true); slot(); SPObject *layer = _desktop ? _desktop->currentLayer() : nullptr; if ( layer ) { bool wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false ); if ( _lock_toggle.get_active() != wantedValue ) { _lock_toggle.set_active( wantedValue ); } wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false ); if ( _visibility_toggle.get_active() != wantedValue ) { _visibility_toggle.set_active( wantedValue ); } } _visibility_toggled_connection.block(visibility_blocked); _lock_toggled_connection.block(lock_blocked); } /** Builds and appends a row in the layer model object. */ void LayerSelector::_buildEntry(unsigned depth, SPObject &object) { Inkscape::XML::NodeEventVector *vector; Callbacks *callbacks=new Callbacks(); callbacks->update_row = sigc::bind( sigc::mem_fun(*this, &LayerSelector::_protectUpdate), sigc::bind( sigc::ptr_fun(&update_row_for_object), &object, _model_columns.object, _layer_model ) ); SPObject *layer=_desktop->currentLayer(); if ( (&object == layer) || (&object == layer->parent) ) { callbacks->update_list = sigc::bind( sigc::mem_fun(*this, &LayerSelector::_protectUpdate), sigc::bind( sigc::ptr_fun(&rebuild_all_rows), sigc::mem_fun(*this, &LayerSelector::_selectLayer), _desktop ) ); Inkscape::XML::NodeEventVector events = { &node_added, &node_removed, &attribute_changed, nullptr, &node_reordered }; vector = new Inkscape::XML::NodeEventVector(events); } else { Inkscape::XML::NodeEventVector events = { nullptr, nullptr, &attribute_changed, nullptr, nullptr }; vector = new Inkscape::XML::NodeEventVector(events); } Gtk::ListStore::iterator row(_layer_model->append()); row->set_value(_model_columns.depth, depth); sp_object_ref(&object, nullptr); row->set_value(_model_columns.object, &object); Inkscape::GC::anchor(object.getRepr()); row->set_value(_model_columns.repr, object.getRepr()); row->set_value(_model_columns.callbacks, reinterpret_cast(callbacks)); sp_repr_add_listener(object.getRepr(), vector, callbacks); } /** Removes a row from the _model_columns object, disconnecting listeners * on the slot. */ void LayerSelector::_destroyEntry(Gtk::ListStore::iterator const &row) { Callbacks *callbacks=reinterpret_cast(row->get_value(_model_columns.callbacks)); SPObject *object=row->get_value(_model_columns.object); if (object) { sp_object_unref(object, nullptr); } Inkscape::XML::Node *repr=row->get_value(_model_columns.repr); if (repr) { sp_repr_remove_listener_by_data(repr, callbacks); Inkscape::GC::release(repr); } delete callbacks; } /** Formats the label for a given layer row */ void LayerSelector::_prepareLabelRenderer( Gtk::TreeModel::const_iterator const &row ) { unsigned depth=(*row)[_model_columns.depth]; SPObject *object=(*row)[_model_columns.object]; bool label_defaulted(false); // TODO: when the currently selected row is removed, // (or before one has been selected) something appears to // "invent" an iterator with null data and try to render it; // where does it come from, and how can we avoid it? if ( object && object->getRepr() ) { SPObject *layer=( _desktop ? _desktop->currentLayer() : nullptr ); SPObject *root=( _desktop ? _desktop->currentRoot() : nullptr ); bool isancestor = !( (layer && (object->parent == layer->parent)) || ((layer == root) && (object->parent == root))); bool iscurrent = ( (object == layer) && (object != root) ); gchar *format = g_strdup_printf ( "%*s%s%s%s%s%%s%s%s%s", ( _desktop && _desktop->itemIsHidden (SP_ITEM(object)) ? "foreground=\"gray50\"" : "" ), depth, "", ( iscurrent ? "•" : " " ), ( iscurrent ? "" : "" ), ( SP_ITEM(object)->isLocked() ? "[" : "" ), ( isancestor ? "" : "" ), ( isancestor ? "" : "" ), ( SP_ITEM(object)->isLocked() ? "]" : "" ), ( iscurrent ? "" : "" ) ); gchar const *label; if ( object != root ) { label = object->label(); if (!object->label()) { label = object->defaultLabel(); label_defaulted = true; } } else { label = _("(root)"); } gchar *text = g_markup_printf_escaped(format, ink_ellipsize_text (label, 50).c_str()); _label_renderer.property_markup() = text; g_free(text); g_free(format); } else { _label_renderer.property_markup() = " "; } _label_renderer.property_ypad() = 1; _label_renderer.property_style() = ( label_defaulted ? Pango::STYLE_ITALIC : Pango::STYLE_NORMAL ); } void LayerSelector::_lockLayer(bool lock) { if ( _layer && SP_IS_ITEM(_layer) ) { SP_ITEM(_layer)->setLocked(lock); DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE, lock? _("Lock layer") : _("Unlock layer")); } } void LayerSelector::_hideLayer(bool hide) { if ( _layer && SP_IS_ITEM(_layer) ) { SP_ITEM(_layer)->setHidden(hide); DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE, hide? _("Hide layer") : _("Unhide layer")); } } } // namespace Widget } // 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 :