// SPDX-License-Identifier: GPL-2.0-or-later /* Authors: * Jon A. Cruz * John Bintz * Abhishek Sharma * * Copyright (C) 2005 Jon A. Cruz * Copyright (C) 2008 John Bintz * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include #include "swatches.h" #include #include #include #include #include #include #include #include #include #include #include #include "color-item.h" #include "desktop.h" #include "desktop-style.h" #include "document.h" #include "document-undo.h" #include "extension/db.h" #include "inkscape.h" #include "io/sys.h" #include "io/resource.h" #include "message-context.h" #include "path-prefix.h" #include "ui/previewholder.h" #include "widgets/desktop-widget.h" #include "widgets/gradient-vector.h" #include "display/cairo-utils.h" #include "object/sp-defs.h" #include "object/sp-gradient-reference.h" #include "dialog-manager.h" #include "verbs.h" #include "gradient-chemistry.h" #include "helper/action.h" namespace Inkscape { namespace UI { namespace Dialog { enum { SWATCHES_SETTINGS_SIZE = 0, SWATCHES_SETTINGS_MODE = 1, SWATCHES_SETTINGS_SHAPE = 2, SWATCHES_SETTINGS_WRAP = 3, SWATCHES_SETTINGS_BORDER = 4, SWATCHES_SETTINGS_PALETTE = 5 }; #define VBLOCK 16 #define PREVIEW_PIXBUF_WIDTH 128 void _loadPaletteFile( gchar const *filename, gboolean user=FALSE ); std::list userSwatchPages; std::list systemSwatchPages; static std::map docPalettes; static std::vector docTrackings; static std::map docPerPanel; class SwatchesPanelHook : public SwatchesPanel { public: static void convertGradient( GtkMenuItem *menuitem, gpointer userData ); static void deleteGradient( GtkMenuItem *menuitem, gpointer userData ); }; static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) { ColorItem* item = reinterpret_cast(callback_data); if ( item ) { item->buttonClicked(false); } } static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) { ColorItem* item = reinterpret_cast(callback_data); if ( item ) { item->buttonClicked(true); } } static GtkWidget* popupMenu = nullptr; static GtkWidget *popupSubHolder = nullptr; static GtkWidget *popupSub = nullptr; static std::vector popupItems; static std::vector popupExtras; static ColorItem* bounceTarget = nullptr; static SwatchesPanel* bouncePanel = nullptr; static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) { if ( bounceTarget ) { handleClick( GTK_WIDGET(menuitem), bounceTarget ); } } static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) { if ( bounceTarget ) { handleSecondaryClick( GTK_WIDGET(menuitem), 0, bounceTarget ); } } static void editGradientImpl( SPDesktop* desktop, SPGradient* gr ) { if ( gr ) { bool shown = false; if ( desktop && desktop->doc() ) { Inkscape::Selection *selection = desktop->getSelection(); std::vector const items(selection->items().begin(), selection->items().end()); if (!items.empty()) { SPStyle query( desktop->doc() ); int result = objects_query_fillstroke((items), &query, true); if ( (result == QUERY_STYLE_MULTIPLE_SAME) || (result == QUERY_STYLE_SINGLE) ) { // could be pertinent if (query.fill.isPaintserver()) { SPPaintServer* server = query.getFillPaintServer(); if ( SP_IS_GRADIENT(server) ) { SPGradient* grad = SP_GRADIENT(server); if ( grad->isSwatch() && grad->getId() == gr->getId()) { desktop->_dlg_mgr->showDialog("FillAndStroke"); shown = true; } } } } } } if (!shown) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) { // Legacy gradient dialog GtkWidget *dialog = sp_gradient_vector_editor_new( gr ); gtk_widget_show( dialog ); } else { // Invoke the gradient tool Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT ); if ( verb ) { SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) ); if ( action ) { sp_action_perform( action, nullptr ); } } } } } } static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) { if ( bounceTarget ) { SwatchesPanel* swp = bouncePanel; SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; SPDocument *doc = desktop ? desktop->doc() : nullptr; if (doc) { std::string targetName(bounceTarget->def.descr); std::vector gradients = doc->getResourceList("gradient"); for (auto gradient : gradients) { SPGradient* grad = SP_GRADIENT(gradient); if ( targetName == grad->getId() ) { editGradientImpl( desktop, grad ); break; } } } } } void SwatchesPanelHook::convertGradient( GtkMenuItem * /*menuitem*/, gpointer userData ) { if ( bounceTarget ) { SwatchesPanel* swp = bouncePanel; SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; SPDocument *doc = desktop ? desktop->doc() : nullptr; gint index = GPOINTER_TO_INT(userData); if ( doc && (index >= 0) && (static_cast(index) < popupItems.size()) ) { Glib::ustring targetName = popupItems[index]; std::vector gradients = doc->getResourceList("gradient"); for (auto gradient : gradients) { SPGradient* grad = SP_GRADIENT(gradient); if ( targetName == grad->getId() ) { grad->setSwatch(); DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Add gradient stop")); break; } } } } } void SwatchesPanelHook::deleteGradient( GtkMenuItem */*menuitem*/, gpointer /*userData*/ ) { if ( bounceTarget ) { SwatchesPanel* swp = bouncePanel; SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; sp_gradient_unset_swatch(desktop, bounceTarget->def.descr); } } static SwatchesPanel* findContainingPanel( GtkWidget *widget ) { SwatchesPanel *swp = nullptr; std::map rawObjects; for (std::map::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { rawObjects[GTK_WIDGET(it->first->gobj())] = it->first; } for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) { if (rawObjects.find(curr) != rawObjects.end()) { swp = rawObjects[curr]; } } return swp; } static void removeit( GtkWidget *widget, gpointer data ) { gtk_container_remove( GTK_CONTAINER(data), widget ); } /* extern'ed from color-item.cpp */ bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data) { gboolean handled = FALSE; if ( event && (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) { SwatchesPanel* swp = findContainingPanel( GTK_WIDGET(preview->gobj()) ); if ( !popupMenu ) { popupMenu = gtk_menu_new(); GtkWidget* child = nullptr; //TRANSLATORS: An item in context menu on a colour in the swatches child = gtk_menu_item_new_with_label(_("Set fill")); g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(redirClick), user_data); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); //TRANSLATORS: An item in context menu on a colour in the swatches child = gtk_menu_item_new_with_label(_("Set stroke")); g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(redirSecondaryClick), user_data); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); child = gtk_separator_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); popupExtras.push_back(child); child = gtk_menu_item_new_with_label(_("Delete")); g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(SwatchesPanelHook::deleteGradient), user_data ); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); popupExtras.push_back(child); gtk_widget_set_sensitive( child, FALSE ); child = gtk_menu_item_new_with_label(_("Edit...")); g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(editGradient), user_data ); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); popupExtras.push_back(child); child = gtk_separator_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); popupExtras.push_back(child); child = gtk_menu_item_new_with_label(_("Convert")); gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); //popupExtras.push_back(child); //gtk_widget_set_sensitive( child, FALSE ); { popupSubHolder = child; popupSub = gtk_menu_new(); gtk_menu_item_set_submenu( GTK_MENU_ITEM(child), popupSub ); } gtk_widget_show_all(popupMenu); } if ( user_data ) { ColorItem* item = reinterpret_cast(user_data); bool show = swp && (swp->getSelectedIndex() == 0); for (auto & popupExtra : popupExtras) { gtk_widget_set_sensitive(popupExtra, show); } bounceTarget = item; bouncePanel = swp; popupItems.clear(); if ( popupMenu ) { gtk_container_foreach(GTK_CONTAINER(popupSub), removeit, popupSub); bool processed = false; GtkWidget *wdgt = gtk_widget_get_ancestor(GTK_WIDGET(preview->gobj()), SP_TYPE_DESKTOP_WIDGET); if ( wdgt ) { SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); if ( dtw && dtw->desktop ) { // Pick up all gradients with vectors std::vector gradients = (dtw->desktop->doc())->getResourceList("gradient"); gint index = 0; for (auto gradient : gradients) { SPGradient* grad = SP_GRADIENT(gradient); if ( grad->hasStops() && !grad->isSwatch() ) { //gl = g_slist_prepend(gl, curr->data); processed = true; GtkWidget *child = gtk_menu_item_new_with_label(grad->getId()); gtk_menu_shell_append(GTK_MENU_SHELL(popupSub), child); popupItems.emplace_back(grad->getId()); g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(SwatchesPanelHook::convertGradient), GINT_TO_POINTER(index) ); index++; } } gtk_widget_show_all(popupSub); } } gtk_widget_set_sensitive( popupSubHolder, processed ); gtk_menu_popup_at_pointer(GTK_MENU(popupMenu), reinterpret_cast(event)); handled = TRUE; } } } return handled; } static char* trim( char* str ) { char* ret = str; while ( *str && (*str == ' ' || *str == '\t') ) { str++; } ret = str; while ( *str ) { str++; } str--; while ( str >= ret && (( *str == ' ' || *str == '\t' ) || *str == '\r' || *str == '\n') ) { *str-- = 0; } return ret; } static void skipWhitespace( char*& str ) { while ( *str == ' ' || *str == '\t' ) { str++; } } static bool parseNum( char*& str, int& val ) { val = 0; while ( '0' <= *str && *str <= '9' ) { val = val * 10 + (*str - '0'); str++; } bool retval = !(*str == 0 || *str == ' ' || *str == '\t' || *str == '\r' || *str == '\n'); return retval; } void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/) { Glib::ustring filename = Glib::path_get_basename(path); char block[1024]; FILE *f = Inkscape::IO::fopen_utf8name(path.c_str(), "r"); if ( f ) { char* result = fgets( block, sizeof(block), f ); if ( result ) { if ( strncmp( "GIMP Palette", block, 12 ) == 0 ) { bool inHeader = true; bool hasErr = false; SwatchPage *onceMore = new SwatchPage(); onceMore->_name = filename.c_str(); do { result = fgets( block, sizeof(block), f ); block[sizeof(block) - 1] = 0; if ( result ) { if ( block[0] == '#' ) { // ignore comment } else { char *ptr = block; // very simple check for header versus entry while ( *ptr == ' ' || *ptr == '\t' ) { ptr++; } if ( (*ptr == 0) || (*ptr == '\r') || (*ptr == '\n') ) { // blank line. skip it. } else if ( '0' <= *ptr && *ptr <= '9' ) { // should be an entry link inHeader = false; ptr = block; Glib::ustring name(""); skipWhitespace(ptr); if ( *ptr ) { int r = 0; int g = 0; int b = 0; hasErr = parseNum(ptr, r); if ( !hasErr ) { skipWhitespace(ptr); hasErr = parseNum(ptr, g); } if ( !hasErr ) { skipWhitespace(ptr); hasErr = parseNum(ptr, b); } if ( !hasErr && *ptr ) { char* n = trim(ptr); if (n != nullptr && *n) { name = g_dpgettext2(nullptr, "Palette", n); } if (name == "") { name = Glib::ustring::compose("#%1%2%3", Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), r), Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), g), Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), b) ).uppercase(); } } if ( !hasErr ) { // Add the entry now Glib::ustring nameStr(name); ColorItem* item = new ColorItem( r, g, b, nameStr ); onceMore->_colors.push_back(item); } } else { hasErr = true; } } else { if ( !inHeader ) { // Hmmm... probably bad. Not quite the format we want? hasErr = true; } else { char* sep = strchr(result, ':'); if ( sep ) { *sep = 0; char* val = trim(sep + 1); char* name = trim(result); if ( *name ) { if ( strcmp( "Name", name ) == 0 ) { onceMore->_name = val; } else if ( strcmp( "Columns", name ) == 0 ) { gchar* endPtr = nullptr; guint64 numVal = g_ascii_strtoull( val, &endPtr, 10 ); if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { // overflow } else if ( (numVal == 0) && (endPtr == val) ) { // failed conversion } else { onceMore->_prefWidth = numVal; } } } else { // error hasErr = true; } } else { // error hasErr = true; } } } } } } while ( result && !hasErr ); if ( !hasErr ) { if (user) userSwatchPages.push_back(onceMore); else systemSwatchPages.push_back(onceMore); #if ENABLE_MAGIC_COLORS ColorItem::_wireMagicColors( onceMore ); #endif // ENABLE_MAGIC_COLORS } else { delete onceMore; } } } fclose(f); } } static bool compare_swatch_names(SwatchPage const *a, SwatchPage const *b) { return g_utf8_collate(a->_name.c_str(), b->_name.c_str()) < 0; } static void load_palettes() { static bool init_done = false; if (init_done) { return; } init_done = true; for (auto &filename: Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) { bool userPalette = Inkscape::IO::file_is_writable(filename.c_str()); _loadPaletteFile(filename, userPalette); } // Sort the list of swatches by name, grouped by user/system userSwatchPages.sort(compare_swatch_names); systemSwatchPages.sort(compare_swatch_names); } SwatchesPanel& SwatchesPanel::getInstance() { return *new SwatchesPanel(); } /** * Constructor */ SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SWATCHES), _menu(nullptr), _holder(nullptr), _clear(nullptr), _remove(nullptr), _currentIndex(0), _currentDesktop(nullptr), _currentDocument(nullptr) { _holder = new PreviewHolder(); _build_menu(); auto menu_button = Gtk::manage(new Gtk::MenuButton()); menu_button->set_halign(Gtk::ALIGN_END); menu_button->set_relief(Gtk::RELIEF_NONE); menu_button->set_image_from_icon_name("pan-start-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR); menu_button->set_popup(*_menu); auto box = Gtk::manage(new Gtk::Box()); if (_prefs_path == "/dialogs/swatches") { box->set_orientation(Gtk::ORIENTATION_VERTICAL); box->pack_start(*menu_button, Gtk::PACK_SHRINK); } else { box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); box->pack_end(*menu_button, Gtk::PACK_SHRINK); _updateSettings(SWATCHES_SETTINGS_MODE, 1); _holder->setOrientation(SP_ANCHOR_SOUTH); } box->pack_start(*_holder, Gtk::PACK_EXPAND_WIDGET); _getContents()->pack_start(*box); load_palettes(); Gtk::RadioMenuItem* hotItem = nullptr; _clear = new ColorItem( ege::PaintDef::CLEAR ); _remove = new ColorItem( ege::PaintDef::NONE ); if (docPalettes.empty()) { SwatchPage *docPalette = new SwatchPage(); docPalette->_name = "Auto"; docPalettes[nullptr] = docPalette; } if ( !systemSwatchPages.empty() || !userSwatchPages.empty()) { SwatchPage* first = nullptr; int index = 0; Glib::ustring targetName; if ( !_prefs_path.empty() ) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); targetName = prefs->getString(_prefs_path + "/palette"); if (!targetName.empty()) { if (targetName == "Auto") { first = docPalettes[nullptr]; } else { std::vector pages = _getSwatchSets(); for (auto & page : pages) { if ( page->_name == targetName ) { first = page; break; } index++; } } } } if ( !first ) { first = docPalettes[nullptr]; _currentIndex = 0; } else { _currentIndex = index; } _rebuild(); Gtk::RadioMenuItem::Group groupOne; int i = 0; std::vector swatchSets = _getSwatchSets(); for (auto curr : swatchSets) { Gtk::RadioMenuItem* single = Gtk::manage(new Gtk::RadioMenuItem(groupOne, curr->_name)); if ( curr == first ) { hotItem = single; } _regItem(single, i); i++; } } if ( hotItem ) { hotItem->set_active(); } show_all_children(); } SwatchesPanel::~SwatchesPanel() { _trackDocument( this, nullptr ); _documentConnection.disconnect(); _selChanged.disconnect(); if ( _clear ) { delete _clear; } if ( _remove ) { delete _remove; } if ( _holder ) { delete _holder; } delete _menu; } void SwatchesPanel::_build_menu() { guint panel_size = 0, panel_mode = 0, panel_ratio = 100, panel_border = 0; bool panel_wrap = false; if (!_prefs_path.empty()) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); panel_wrap = prefs->getBool(_prefs_path + "/panel_wrap"); panel_size = prefs->getIntLimited(_prefs_path + "/panel_size", 1, 0, UI::Widget::PREVIEW_SIZE_HUGE); panel_mode = prefs->getIntLimited(_prefs_path + "/panel_mode", 1, 0, 10); panel_ratio = prefs->getIntLimited(_prefs_path + "/panel_ratio", 100, 0, 500 ); panel_border = prefs->getIntLimited(_prefs_path + "/panel_border", UI::Widget::BORDER_NONE, 0, 2 ); } _menu = new Gtk::Menu(); if (_prefs_path == "/dialogs/swatches") { Gtk::RadioMenuItem::Group group; Glib::ustring list_label(_("List")); Glib::ustring grid_label(_("Grid")); Gtk::RadioMenuItem *list_item = Gtk::manage(new Gtk::RadioMenuItem(group, list_label)); Gtk::RadioMenuItem *grid_item = Gtk::manage(new Gtk::RadioMenuItem(group, grid_label)); if (panel_mode == 0) { list_item->set_active(true); } else if (panel_mode == 1) { grid_item->set_active(true); } _menu->append(*list_item); _menu->append(*grid_item); _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); list_item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 0)); grid_item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 1)); } { Glib::ustring heightItemLabel(C_("Swatches", "Size")); //TRANSLATORS: Indicates size of colour swatches const gchar *heightLabels[] = { NC_("Swatches height", "Tiny"), NC_("Swatches height", "Small"), NC_("Swatches height", "Medium"), NC_("Swatches height", "Large"), NC_("Swatches height", "Huge") }; Gtk::MenuItem *sizeItem = Gtk::manage(new Gtk::MenuItem(heightItemLabel)); Gtk::Menu *sizeMenu = Gtk::manage(new Gtk::Menu()); sizeItem->set_submenu(*sizeMenu); Gtk::RadioMenuItem::Group heightGroup; for (unsigned int i = 0; i < G_N_ELEMENTS(heightLabels); i++) { Glib::ustring _label(g_dpgettext2(nullptr, "Swatches height", heightLabels[i])); Gtk::RadioMenuItem* _item = Gtk::manage(new Gtk::RadioMenuItem(heightGroup, _label)); sizeMenu->append(*_item); if (i == panel_size) { _item->set_active(true); } _item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SIZE, i)); } _menu->append(*sizeItem); } { Glib::ustring widthItemLabel(C_("Swatches", "Width")); //TRANSLATORS: Indicates width of colour swatches const gchar *widthLabels[] = { NC_("Swatches width", "Narrower"), NC_("Swatches width", "Narrow"), NC_("Swatches width", "Medium"), NC_("Swatches width", "Wide"), NC_("Swatches width", "Wider") }; Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); item->set_submenu(*type_menu); _menu->append(*item); Gtk::RadioMenuItem::Group widthGroup; guint values[] = {0, 25, 50, 100, 200, 400}; guint hot_index = 3; for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { // Assume all values are in increasing order if ( values[i] <= panel_ratio ) { hot_index = i; } } for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { Glib::ustring _label(g_dpgettext2(nullptr, "Swatches width", widthLabels[i])); Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); type_menu->append(*_item); if ( i <= hot_index ) { _item->set_active(true); } _item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SHAPE, values[i])); } } { Glib::ustring widthItemLabel(C_("Swatches", "Border")); //TRANSLATORS: Indicates border of colour swatches const gchar *widthLabels[] = { NC_("Swatches border", "None"), NC_("Swatches border", "Solid"), NC_("Swatches border", "Wide"), }; Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); item->set_submenu(*type_menu); _menu->append(*item); Gtk::RadioMenuItem::Group widthGroup; guint values[] = {0, 1, 2}; guint hot_index = 0; for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { // Assume all values are in increasing order if ( values[i] <= panel_border ) { hot_index = i; } } for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { Glib::ustring _label(g_dpgettext2(nullptr, "Swatches border", widthLabels[i])); Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); type_menu->append(*_item); if ( i <= hot_index ) { _item->set_active(true); } _item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_BORDER, values[i])); } } if (_prefs_path == "/embedded/swatches") { //TRANSLATORS: "Wrap" indicates how colour swatches are displayed Glib::ustring wrap_label(C_("Swatches","Wrap")); Gtk::CheckMenuItem *check = Gtk::manage(new Gtk::CheckMenuItem(wrap_label)); check->set_active(panel_wrap); _menu->append(*check); check->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_wrapToggled), check)); } _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); _menu->show_all(); _updateSettings(SWATCHES_SETTINGS_SIZE, panel_size); _updateSettings(SWATCHES_SETTINGS_MODE, panel_mode); _updateSettings(SWATCHES_SETTINGS_SHAPE, panel_ratio); _updateSettings(SWATCHES_SETTINGS_WRAP, panel_wrap); _updateSettings(SWATCHES_SETTINGS_BORDER, panel_border); } void SwatchesPanel::setDesktop( SPDesktop* desktop ) { if ( desktop != _currentDesktop ) { if ( _currentDesktop ) { _documentConnection.disconnect(); _selChanged.disconnect(); } _currentDesktop = desktop; if ( desktop ) { _currentDesktop->selection->connectChanged( sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); _currentDesktop->selection->connectModified( sigc::hide(sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection)))); _currentDesktop->connectToolSubselectionChanged( sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); sigc::bound_mem_functor1 first = sigc::mem_fun(*this, &SwatchesPanel::_setDocument); sigc::slot base2 = first; sigc::slot slot2 = sigc::hide<0>( base2 ); _documentConnection = desktop->connectDocumentReplaced( slot2 ); _setDocument( desktop->doc() ); } else { _setDocument(nullptr); } } } void SwatchesPanel::_updateSettings(int settings, int value) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); switch (settings) { case SWATCHES_SETTINGS_SIZE: { prefs->setInt(_prefs_path + "/panel_size", value); auto curr_type = _holder->getPreviewType(); guint curr_ratio = _holder->getPreviewRatio(); auto curr_border = _holder->getPreviewBorder(); switch (value) { case 0: _holder->setStyle(UI::Widget::PREVIEW_SIZE_TINY, curr_type, curr_ratio, curr_border); break; case 1: _holder->setStyle(UI::Widget::PREVIEW_SIZE_SMALL, curr_type, curr_ratio, curr_border); break; case 2: _holder->setStyle(UI::Widget::PREVIEW_SIZE_MEDIUM, curr_type, curr_ratio, curr_border); break; case 3: _holder->setStyle(UI::Widget::PREVIEW_SIZE_BIG, curr_type, curr_ratio, curr_border); break; case 4: _holder->setStyle(UI::Widget::PREVIEW_SIZE_HUGE, curr_type, curr_ratio, curr_border); break; default: break; } break; } case SWATCHES_SETTINGS_MODE: { prefs->setInt(_prefs_path + "/panel_mode", value); auto curr_size = _holder->getPreviewSize(); guint curr_ratio = _holder->getPreviewRatio(); auto curr_border = _holder->getPreviewBorder(); switch (value) { case 0: _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_LIST, curr_ratio, curr_border); break; case 1: _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_GRID, curr_ratio, curr_border); break; default: break; } break; } case SWATCHES_SETTINGS_SHAPE: { prefs->setInt(_prefs_path + "/panel_ratio", value); auto curr_type = _holder->getPreviewType(); auto curr_size = _holder->getPreviewSize(); auto curr_border = _holder->getPreviewBorder(); _holder->setStyle(curr_size, curr_type, value, curr_border); break; } case SWATCHES_SETTINGS_BORDER: { prefs->setInt(_prefs_path + "/panel_border", value); auto curr_size = _holder->getPreviewSize(); auto curr_type = _holder->getPreviewType(); guint curr_ratio = _holder->getPreviewRatio(); switch (value) { case 0: _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_NONE); break; case 1: _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_SOLID); break; case 2: _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_WIDE); break; default: break; } break; } case SWATCHES_SETTINGS_WRAP: { prefs->setBool(_prefs_path + "/panel_wrap", value); _holder->setWrap(value); break; } case SWATCHES_SETTINGS_PALETTE: { std::vector pages = _getSwatchSets(); if (value >= 0 && value < static_cast(pages.size()) ) { _currentIndex = value; prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name); _rebuild(); } } default: break; } } void SwatchesPanel::_wrapToggled(Gtk::CheckMenuItem* toggler) { if (toggler) { _updateSettings(SWATCHES_SETTINGS_WRAP, toggler->get_active() ? 1 : 0); } } void SwatchesPanel::_regItem(Gtk::MenuItem* item, int id) { _menu->append(*item); item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_PALETTE, id)); item->show(); } class DocTrack { public: DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) : doc(doc->doRef()), updatePending(false), lastGradientUpdate(0.0), gradientRsrcChanged(gradientRsrcChanged), defsChanged(defsChanged), defsModified(defsModified) { if ( !timer ) { timer = new Glib::Timer(); refreshTimer = Glib::signal_timeout().connect( sigc::ptr_fun(handleTimerCB), 33 ); } timerRefCount++; } ~DocTrack() { timerRefCount--; if ( timerRefCount <= 0 ) { refreshTimer.disconnect(); timerRefCount = 0; if ( timer ) { timer->stop(); delete timer; timer = nullptr; } } if (doc) { gradientRsrcChanged.disconnect(); defsChanged.disconnect(); defsModified.disconnect(); doc->doUnref(); doc = nullptr; } } static bool handleTimerCB(); /** * Checks if update should be queued or executed immediately. * * @return true if the update was queued and should not be immediately executed. */ static bool queueUpdateIfNeeded(SPDocument *doc); static Glib::Timer *timer; static int timerRefCount; static sigc::connection refreshTimer; SPDocument *doc; bool updatePending; double lastGradientUpdate; sigc::connection gradientRsrcChanged; sigc::connection defsChanged; sigc::connection defsModified; private: DocTrack(DocTrack const &) = delete; // no copy DocTrack &operator=(DocTrack const &) = delete; // no assign }; Glib::Timer *DocTrack::timer = nullptr; int DocTrack::timerRefCount = 0; sigc::connection DocTrack::refreshTimer; static const double DOC_UPDATE_THREASHOLD = 0.090; bool DocTrack::handleTimerCB() { double now = timer->elapsed(); std::vector needCallback; for (auto track : docTrackings) { if ( track->updatePending && ( (now - track->lastGradientUpdate) >= DOC_UPDATE_THREASHOLD) ) { needCallback.push_back(track); } } for (auto track : needCallback) { if ( std::find(docTrackings.begin(), docTrackings.end(), track) != docTrackings.end() ) { // Just in case one gets deleted while we are looping // Note: calling handleDefsModified will call queueUpdateIfNeeded and thus update the time and flag. SwatchesPanel::handleDefsModified(track->doc); } } return true; } bool DocTrack::queueUpdateIfNeeded( SPDocument *doc ) { bool deferProcessing = false; for (auto track : docTrackings) { if ( track->doc == doc ) { double now = timer->elapsed(); double elapsed = now - track->lastGradientUpdate; if ( elapsed < DOC_UPDATE_THREASHOLD ) { deferProcessing = true; track->updatePending = true; } else { track->lastGradientUpdate = now; track->updatePending = false; } break; } } return deferProcessing; } void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document ) { SPDocument *oldDoc = nullptr; if (docPerPanel.find(panel) != docPerPanel.end()) { oldDoc = docPerPanel[panel]; if (!oldDoc) { docPerPanel.erase(panel); // Should not be needed, but clean up just in case. } } if (oldDoc != document) { if (oldDoc) { docPerPanel[panel] = nullptr; bool found = false; for (std::map::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { found = (it->second == document); } if (!found) { for (std::vector::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){ if ((*it)->doc == oldDoc) { delete *it; docTrackings.erase(it); break; } } } } if (document) { bool found = false; for (std::map::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { found = (it->second == document); } docPerPanel[panel] = document; if (!found) { sigc::connection conn1 = document->connectResourcesChanged( "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) ); sigc::connection conn2 = document->getDefs()->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) ); sigc::connection conn3 = document->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) ); DocTrack *dt = new DocTrack(document, conn1, conn2, conn3); docTrackings.push_back(dt); if (docPalettes.find(document) == docPalettes.end()) { SwatchPage *docPalette = new SwatchPage(); docPalette->_name = "Auto"; docPalettes[document] = docPalette; } } } } } void SwatchesPanel::_setDocument( SPDocument *document ) { if ( document != _currentDocument ) { _trackDocument(this, document); _currentDocument = document; handleGradientsChange( document ); } } static void recalcSwatchContents(SPDocument* doc, boost::ptr_vector &tmpColors, std::map &previewMappings, std::map &gradMappings) { std::vector newList; std::vector gradients = doc->getResourceList("gradient"); for (auto gradient : gradients) { SPGradient* grad = SP_GRADIENT(gradient); if ( grad->isSwatch() ) { newList.push_back(SP_GRADIENT(gradient)); } } if ( !newList.empty() ) { std::reverse(newList.begin(), newList.end()); for (auto grad : newList) { cairo_surface_t *preview = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, PREVIEW_PIXBUF_WIDTH, VBLOCK); cairo_t *ct = cairo_create(preview); Glib::ustring name( grad->getId() ); ColorItem* item = new ColorItem( 0, 0, 0, name ); cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); cairo_pattern_t *gradient = grad->create_preview_pattern(PREVIEW_PIXBUF_WIDTH); cairo_set_source(ct, check); cairo_paint(ct); cairo_set_source(ct, gradient); cairo_paint(ct); cairo_destroy(ct); cairo_pattern_destroy(gradient); cairo_pattern_destroy(check); cairo_pattern_t *prevpat = cairo_pattern_create_for_surface(preview); cairo_surface_destroy(preview); previewMappings[item] = prevpat; tmpColors.push_back(item); gradMappings[item] = grad; } } } void SwatchesPanel::handleGradientsChange(SPDocument *document) { SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; if (docPalette) { boost::ptr_vector tmpColors; std::map tmpPrevs; std::map tmpGrads; recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); for (auto & tmpPrev : tmpPrevs) { tmpPrev.first->setPattern(tmpPrev.second); cairo_pattern_destroy(tmpPrev.second); } for (auto & tmpGrad : tmpGrads) { tmpGrad.first->setGradient(tmpGrad.second); } docPalette->_colors.swap(tmpColors); // Figure out which SwatchesPanel instances are affected and update them. for (auto & it : docPerPanel) { if (it.second == document) { SwatchesPanel* swp = it.first; std::vector pages = swp->_getSwatchSets(); SwatchPage* curr = pages[swp->_currentIndex]; if (curr == docPalette) { swp->_rebuild(); } } } } } void SwatchesPanel::handleDefsModified(SPDocument *document) { SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; if (docPalette && !DocTrack::queueUpdateIfNeeded(document) ) { boost::ptr_vector tmpColors; std::map tmpPrevs; std::map tmpGrads; recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); if ( tmpColors.size() != docPalette->_colors.size() ) { handleGradientsChange(document); } else { int cap = std::min(docPalette->_colors.size(), tmpColors.size()); for (int i = 0; i < cap; i++) { ColorItem *newColor = &tmpColors[i]; ColorItem *oldColor = &docPalette->_colors[i]; if ( (newColor->def.getType() != oldColor->def.getType()) || (newColor->def.getR() != oldColor->def.getR()) || (newColor->def.getG() != oldColor->def.getG()) || (newColor->def.getB() != oldColor->def.getB()) ) { oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB()); } if (tmpGrads.find(newColor) != tmpGrads.end()) { oldColor->setGradient(tmpGrads[newColor]); } if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) { oldColor->setPattern(tmpPrevs[newColor]); } } } for (auto & tmpPrev : tmpPrevs) { cairo_pattern_destroy(tmpPrev.second); } } } std::vector SwatchesPanel::_getSwatchSets() const { std::vector tmp; if (docPalettes.find(_currentDocument) != docPalettes.end()) { tmp.push_back(docPalettes[_currentDocument]); } tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end()); tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end()); return tmp; } void SwatchesPanel::_updateFromSelection() { SwatchPage *docPalette = (docPalettes.find(_currentDocument) != docPalettes.end()) ? docPalettes[_currentDocument] : nullptr; if ( docPalette ) { Glib::ustring fillId; Glib::ustring strokeId; SPStyle tmpStyle(_currentDesktop->getDocument()); int result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_FILL ); switch (result) { case QUERY_STYLE_SINGLE: case QUERY_STYLE_MULTIPLE_AVERAGED: case QUERY_STYLE_MULTIPLE_SAME: { if (tmpStyle.fill.set && tmpStyle.fill.isPaintserver()) { SPPaintServer* server = tmpStyle.getFillPaintServer(); if ( SP_IS_GRADIENT(server) ) { SPGradient* target = nullptr; SPGradient* grad = SP_GRADIENT(server); if ( grad->isSwatch() ) { target = grad; } else if ( grad->ref ) { SPGradient *tmp = grad->ref->getObject(); if ( tmp && tmp->isSwatch() ) { target = tmp; } } if ( target ) { //XML Tree being used directly here while it shouldn't be gchar const* id = target->getRepr()->attribute("id"); if ( id ) { fillId = id; } } } } break; } } result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_STROKE ); switch (result) { case QUERY_STYLE_SINGLE: case QUERY_STYLE_MULTIPLE_AVERAGED: case QUERY_STYLE_MULTIPLE_SAME: { if (tmpStyle.stroke.set && tmpStyle.stroke.isPaintserver()) { SPPaintServer* server = tmpStyle.getStrokePaintServer(); if ( SP_IS_GRADIENT(server) ) { SPGradient* target = nullptr; SPGradient* grad = SP_GRADIENT(server); if ( grad->isSwatch() ) { target = grad; } else if ( grad->ref ) { SPGradient *tmp = grad->ref->getObject(); if ( tmp && tmp->isSwatch() ) { target = tmp; } } if ( target ) { //XML Tree being used directly here while it shouldn't be gchar const* id = target->getRepr()->attribute("id"); if ( id ) { strokeId = id; } } } } break; } } for (auto & _color : docPalette->_colors) { ColorItem* item = &_color; bool isFill = (fillId == item->def.descr); bool isStroke = (strokeId == item->def.descr); item->setState( isFill, isStroke ); } } } void SwatchesPanel::_rebuild() { std::vector pages = _getSwatchSets(); SwatchPage* curr = pages[_currentIndex]; _holder->clear(); if ( curr->_prefWidth > 0 ) { _holder->setColumnPref( curr->_prefWidth ); } _holder->freezeUpdates(); // TODO restore once 'clear' works _holder->addPreview(_clear); _holder->addPreview(_remove); for (auto & _color : curr->_colors) { _holder->addPreview(&_color); } _holder->thawUpdates(); } } //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 :