Code

preferences.cpp: Remove an assert that causes problems with some
[inkscape.git] / src / preferences.cpp
1 /** @file
2  * @brief  Singleton class to access the preferences file - implementation
3  */
4 /* Authors:
5  *   Krzysztof KosiƄski <tweenk.pl@gmail.com>
6  *
7  * Copyright (C) 2008 Authors
8  *
9  * Released under GNU GPL.  Read the file 'COPYING' for more information.
10  */
12 #include "preferences.h"
13 #include "preferences-skeleton.h"
14 #include "inkscape.h"
15 #include "xml/repr.h"
16 #include "xml/node-observer.h"
17 #include "xml/node-iterators.h"
18 #include "xml/attribute-record.h"
19 #include <cstring>
20 #include <vector>
21 #include <glibmm/fileutils.h>
22 #include <glibmm/i18n.h>
23 #include <glib.h>
24 #include <glib/gstdio.h>
25 #include <gtkmm/messagedialog.h>
27 #define PREFERENCES_FILE_NAME "preferences.xml"
29 namespace Inkscape {
31 // private inner class definition
33 /**
34  * @brief XML - prefs observer bridge
35  *
36  * This is an XML node observer that watches for changes in the XML document storing the preferences.
37  * It is used to implement preference observers.
38  */
39 class Preferences::PrefNodeObserver : public XML::NodeObserver {
40 public:
41     PrefNodeObserver(Observer &o, Glib::ustring const &filter) :
42         _observer(o),
43         _filter(filter)
44     {}
45     virtual ~PrefNodeObserver() {}
46     virtual void notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared<char>, Util::ptr_shared<char>);
47 private:
48     Observer &_observer;
49     Glib::ustring const _filter;
50 };
53 Preferences::Preferences() :
54     _prefs_basename(PREFERENCES_FILE_NAME),
55     _prefs_dir(""),
56     _prefs_filename(""),
57     _prefs_doc(NULL),
58     _use_gui(true),
59     _quiet(false),
60     _loaded(false),
61     _writable(false)
62 {
63     // profile_path essentailly returns the argument prefixed by the profile directory.
64     gchar *path = profile_path(NULL);
65     _prefs_dir = path;
66     g_free(path);
67     
68     path = profile_path(_prefs_basename.data());
69     _prefs_filename = path;
70     g_free(path);
71     
72     _loadDefaults();
73 }
75 Preferences::~Preferences()
76 {
77     // when the preferences are unloaded, save them
78     save();
79     
80     // delete all PrefNodeObservers
81     for (_ObsMap::iterator i = _observer_map.begin(); i != _observer_map.end(); ) {
82         delete (*i++).second; // avoids reference to a deleted key
83     }
84     // unref XML document
85     Inkscape::GC::release(_prefs_doc);
86 }
88 /**
89  * @brief Load internal defaults
90  *
91  * In the future this will try to load the system-wide file before falling
92  * back to the internal defaults.
93  */
94 void Preferences::_loadDefaults()
95 {
96     _prefs_doc = sp_repr_read_mem(preferences_skeleton, PREFERENCES_SKELETON_SIZE, NULL);
97 }
99 /**
100  * @brief Load the user's customized preferences
101  *
102  * Tries to load the user's preferences.xml file. If there is none, creates it.
103  * Displays dialog boxes on any errors.
104  */
105 void Preferences::load(bool use_gui, bool quiet)
106 {   
107     Glib::ustring const not_saved = _("Inkscape will run with default settings, "
108                                 "and new settings will not be saved. ");
109     _use_gui = use_gui;
110     _quiet = quiet;
111     _loaded = true;
112     
113     // NOTE: After we upgrade to Glib 2.16, use Glib::ustring::compose
114     
115     // 1. Does the file exist?
116     if (!g_file_test(_prefs_filename.data(), G_FILE_TEST_EXISTS)) {
117         // No - we need to create one.
118         // Does the profile directory exist?
119         if (!g_file_test(_prefs_dir.data(), G_FILE_TEST_EXISTS)) {
120             // No - create the profile directory
121             if (g_mkdir(_prefs_dir.data(), 0755)) {
122                 // the creation failed
123                 //_errorDialog(Glib::ustring::compose(_("Cannot create profile directory %1."),
124                 //    Glib::filename_to_utf8(_prefs_dir)), not_saved);
125                 gchar *msg = g_strdup_printf(_("Cannot create profile directory %s."),
126                     Glib::filename_to_utf8(_prefs_dir).data());
127                 _errorDialog(msg, not_saved);
128                 g_free(msg);
129                 return;
130             }
131             // create some subdirectories for user stuff
132             char const *user_dirs[] = {"keys", "templates", "icons", "extensions", "palettes", NULL};
133             for(int i=0; user_dirs[i]; ++i) {
134                 char *dir = profile_path(user_dirs[i]);
135                 g_mkdir(dir, 0755);
136                 g_free(dir);
137             }
138             
139         } else if (!g_file_test(_prefs_dir.data(), G_FILE_TEST_IS_DIR)) {
140             // The profile dir is not actually a directory
141             //_errorDialog(Glib::ustring::compose(_("%1 is not a valid directory."),
142             //    Glib::filename_to_utf8(_prefs_dir)), not_saved);
143             gchar *msg = g_strdup_printf(_("%s is not a valid directory."),
144                 Glib::filename_to_utf8(_prefs_dir).data());
145             _errorDialog(msg, not_saved);
146             g_free(msg);
147             return;
148         }
149         // The profile dir exists and is valid.
150         if (!g_file_set_contents(_prefs_filename.data(), preferences_skeleton, PREFERENCES_SKELETON_SIZE, NULL)) {
151             // The write failed.
152             //_errorDialog(Glib::ustring::compose(_("Failed to create the preferences file %1."),
153             //    Glib::filename_to_utf8(_prefs_filename)), not_saved);
154             gchar *msg = g_strdup_printf(_("Failed to create the preferences file %s."),
155                 Glib::filename_to_utf8(_prefs_filename).data());
156             _errorDialog(msg, not_saved);
157             g_free(msg);
158             return;
159         }
160         
161         // The prefs file was just created.
162         // We can return now and skip the rest of the load process.
163         _writable = true;
164         return;
165     }
166     
167     // Yes, the pref file exists.
168     // 2. Is it a regular file?
169     if (!g_file_test(_prefs_filename.data(), G_FILE_TEST_IS_REGULAR)) {
170         //_errorDialog(Glib::ustring::compose(_("The preferences file %1 is not a regular file."),
171         //    Glib::filename_to_utf8(_prefs_filename)), not_saved);
172         gchar *msg = g_strdup_printf(_("The preferences file %s is not a regular file."),
173             Glib::filename_to_utf8(_prefs_filename).data());
174         _errorDialog(msg, not_saved);
175         g_free(msg);
176         return;
177     }
178     
179     // 3. Is the file readable?
180     gchar *prefs_xml = NULL; gsize len = 0;
181     if (!g_file_get_contents(_prefs_filename.data(), &prefs_xml, &len, NULL)) {
182         //_errorDialog(Glib::ustring::compose(_("The preferences file %1 could not be read."),
183         //    Glib::filename_to_utf8(_prefs_filename)), not_saved);
184         gchar *msg = g_strdup_printf(_("The preferences file %s could not be read."),
185             Glib::filename_to_utf8(_prefs_filename).data());
186         _errorDialog(msg, not_saved);
187         g_free(msg);
188         return;
189     }
190     // 4. Is it valid XML?
191     Inkscape::XML::Document *prefs_read = sp_repr_read_mem(prefs_xml, len, NULL);
192     g_free(prefs_xml);
193     if (!prefs_read) {
194         //_errorDialog(Glib::ustring::compose(_("The preferences file %1 is not a valid XML document."),
195         //    Glib::filename_to_utf8(_prefs_filename)), not_saved);
196         gchar *msg = g_strdup_printf(_("The preferences file %s is not a valid XML document."),
197             Glib::filename_to_utf8(_prefs_filename).data());
198         _errorDialog(msg, not_saved);
199         g_free(msg);
200         return;
201     }
202     // 5. Basic sanity check: does the root element have a correct name?
203     if (strcmp(prefs_read->root()->name(), "inkscape")) {
204         //_errorDialog(Glib::ustring::compose(_("The file %1 is not a valid Inkscape preferences file."),
205         //    Glib::filename_to_utf8(_prefs_filename)), not_saved);
206         gchar *msg = g_strdup_printf(_("The file %s is not a valid Inkscape preferences file."),
207             Glib::filename_to_utf8(_prefs_filename).data());
208         _errorDialog(msg, not_saved);
209         g_free(msg);
210         Inkscape::GC::release(prefs_read);
211         return;
212     }
213     
214     // Merge the loaded prefs with defaults.
215     _prefs_doc->root()->mergeFrom(prefs_read->root(), "id");
216     Inkscape::GC::release(prefs_read);
217     _writable = true;
220 /**
221  * @brief Flush all pref changes to the XML file
222  */
223 void Preferences::save()
225     if (!_writable) return; // no-op if the prefs file is not writable
226     
227     // sp_repr_save_file uses utf-8 instead of the glib filename encoding.
228     // I don't know why filenames are kept in utf-8 in Inkscape and then
229     // converted to filename encoding when necessary through special functions
230     // - wouldn't it be easier to keep things in the encoding they are supposed
231     // to be in?
232     Glib::ustring utf8name = Glib::filename_to_utf8(_prefs_filename);
233     if (utf8name.empty()) return;
234     sp_repr_save_file(_prefs_doc, utf8name.data());
238 // Now for the meat.
240 /**
241  * @brief Get names of all entries in the specified path
242  * @param path Preference path to query
243  * @return A vector containing all entries in the given directory
244  */
245 std::vector<Preferences::Entry> Preferences::getAllEntries(Glib::ustring const &path)
247     std::vector<Entry> temp;
248     Inkscape::XML::Node *node = _getNode(path, false);
249     if (!node) return temp;
250     
251     // argh - purge this Util::List nonsense from XML classes fast
252     Inkscape::Util::List<Inkscape::XML::AttributeRecord const> alist = node->attributeList();
253     for (; alist; ++alist)
254         temp.push_back( Entry(path + '/' + g_quark_to_string(alist->key), static_cast<void const*>(alist->value.pointer())) );
255     return temp;
258 /**
259  * @brief Get the paths to all subdirectories of the specified path
260  * @param path Preference path to query
261  * @return A vector containing absolute paths to all subdirectories in the given path
262  */
263 std::vector<Glib::ustring> Preferences::getAllDirs(Glib::ustring const &path)
265     std::vector<Glib::ustring> temp;
266     Inkscape::XML::Node *node = _getNode(path, false);
267     if (!node) return temp;
268     
269     for (Inkscape::XML::NodeSiblingIterator i = node->firstChild(); i; ++i) {
270         temp.push_back(path + '/' + i->attribute("id"));
271     }
272     return temp;
275 // getter methods
277 Preferences::Entry const Preferences::getEntry(Glib::ustring const &pref_path)
279     gchar const *v;
280     _getRawValue(pref_path, v);
281     return Entry(pref_path, v);
284 // setter methods
286 /**
287  * @brief Set a boolean attribute of a preference
288  * @param pref_path Path of the preference to modify
289  * @param value The new value of the pref attribute
290  */
291 void Preferences::setBool(Glib::ustring const &pref_path, bool value)
293     /// @todo Boolean values should be stored as "true" and "false",
294     /// but this is not possible due to an interaction with event contexts.
295     /// Investigate this in depth.
296     _setRawValue(pref_path, ( value ? "1" : "0" ));
299 /**
300  * @brief Set an integer attribute of a preference
301  * @param pref_path Path of the preference to modify
302  * @param value The new value of the pref attribute
303  */
304 void Preferences::setInt(Glib::ustring const &pref_path, int value)
306     gchar intstr[32];
307     g_snprintf(intstr, 32, "%d", value);
308     _setRawValue(pref_path, intstr);
311 /**
312  * @brief Set a floating point attribute of a preference
313  * @param pref_path Path of the preference to modify
314  * @param value The new value of the pref attribute
315  */
316 void Preferences::setDouble(Glib::ustring const &pref_path, double value)
318     gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
319     g_ascii_dtostr(buf, G_ASCII_DTOSTR_BUF_SIZE, value);
320     _setRawValue(pref_path, buf);
323 /**
324  * @brief Set a string attribute of a preference
325  * @param pref_path Path of the preference to modify
326  * @param value The new value of the pref attribute
327  */
328 void Preferences::setString(Glib::ustring const &pref_path, Glib::ustring const &value)
330     _setRawValue(pref_path, value.data());
333 void Preferences::setStyle(Glib::ustring const &pref_path, SPCSSAttr *style)
335     gchar *css_str = sp_repr_css_write_string(style);
336     _setRawValue(pref_path, css_str);
337     g_free(css_str);
340 void Preferences::mergeStyle(Glib::ustring const &pref_path, SPCSSAttr *style)
342     SPCSSAttr *current = getStyle(pref_path);
343     sp_repr_css_merge(current, style);
344     gchar *css_str = sp_repr_css_write_string(current);
345     _setRawValue(pref_path, css_str);
346     g_free(css_str);
347     sp_repr_css_attr_unref(current);
351 // Observer stuff
352 namespace {
354 /**
355  * @brief Structure that holds additional information for registered Observers
356  */
357 struct _ObserverData {
358     Inkscape::XML::Node *_node; ///< Node at which the wrapping PrefNodeObserver is registered
359     bool _is_attr; ///< Whether this Observer watches a single attribute
360 };
362 } // anonymous namespace
364 Preferences::Observer::Observer(Glib::ustring const &path) :
365     observed_path(path)
369 Preferences::Observer::~Observer()
371     // on destruction remove observer to prevent invalid references
372     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
373     prefs->removeObserver(*this);
376 void Preferences::PrefNodeObserver::notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared<char>, Util::ptr_shared<char> new_value)
378     // filter out attributes we don't watch
379     gchar const *attr_name = g_quark_to_string(name);
380     if ( !_filter.empty() && _filter != attr_name ) return;
381     
382     _ObserverData *d = static_cast<_ObserverData*>(Preferences::_get_pref_observer_data(_observer));
383     Glib::ustring notify_path = _observer.observed_path;
384     
385     if (!d->_is_attr) {
386         std::vector<gchar const *> path_fragments;
387         notify_path.reserve(256); // this will make appending operations faster
388         
389         // walk the XML tree, saving each of the id attributes in a vector
390         // we terminate when we hit the observer's attachment node, because the path to this node
391         // is already stored in notify_path
392         for (XML::NodeParentIterator n = &node; static_cast<XML::Node*>(n) != d->_node; ++n)
393             path_fragments.push_back(n->attribute("id"));
394         // assemble the elements into a path
395         for (std::vector<gchar const *>::reverse_iterator i = path_fragments.rbegin(); i != path_fragments.rend(); ++i) {
396             notify_path.push_back('/');
397             notify_path.append(*i);
398         }
400         // append attribute name
401         notify_path.push_back('/');
402         notify_path.append(attr_name);
403     }
404     
405     Entry const val = Preferences::_create_pref_value(notify_path, static_cast<void const*>(new_value.pointer()));
406     _observer.notify(val);
409 /**
410  * @brief Find the XML node to observe
411  */
412 XML::Node *Preferences::_findObserverNode(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key, bool create)
414     // first assume that the last path element is an entry.
415     _keySplit(pref_path, node_key, attr_key);
416     
417     // find the node corresponding to the "directory".
418     Inkscape::XML::Node *node = _getNode(node_key, create), *child;
419     for (child = node->firstChild(); child; child = child->next()) {
420         // If there is a node with id corresponding to the attr key,
421         // this means that the last part of the path is actually a key (folder).
422         // Change values accordingly.
423         if (attr_key == child->attribute("id")) {
424             node = child;
425             attr_key = "";
426             node_key = pref_path;
427             break;
428         }
429     }
430     return node;
433 void Preferences::addObserver(Observer &o)
435     // prevent adding the same observer twice
436     if ( _observer_map.find(&o) != _observer_map.end() ) return;
437     
438     Glib::ustring node_key, attr_key;
439     Inkscape::XML::Node *node;
440     node = _findObserverNode(o.observed_path, node_key, attr_key, false);
441     if (!node) return;
442     
443     // set additional data
444     _ObserverData *priv_data = new _ObserverData;
445     priv_data->_node = node;
446     priv_data->_is_attr = !attr_key.empty();
447     o._data = static_cast<void*>(priv_data);
448     
449     _observer_map[&o] = new PrefNodeObserver(o, attr_key);
450     
451     // if we watch a single pref, we want to receive notifications only for a single node
452     if (priv_data->_is_attr) {
453         node->addObserver( *(_observer_map[&o]) );
454     } else {
455         node->addSubtreeObserver( *(_observer_map[&o]) );
456     }
459 void Preferences::removeObserver(Observer &o)
461     // prevent removing an observer which was not added
462     if ( _observer_map.find(&o) == _observer_map.end() ) return;
463     Inkscape::XML::Node *node = static_cast<_ObserverData*>(o._data)->_node;
464     _ObserverData *priv_data = static_cast<_ObserverData*>(o._data);
465     o._data = NULL;
466     
467     if (priv_data->_is_attr)
468         node->removeObserver( *(_observer_map[&o]) );
469     else
470         node->removeSubtreeObserver( *(_observer_map[&o]) );
472     delete priv_data;
473     delete _observer_map[&o];
474     _observer_map.erase(&o);
478 /**
479  * @brief Get the XML node corresponding to the given pref key
480  * @param pref_key Preference key (path) to get
481  * @param create Whether to create the corresponding node if it doesn't exist
482  * @param separator The character used to separate parts of the pref key
483  * @return XML node corresponding to the specified key
484  *
485  * Derived from former inkscape_get_repr(). Private because it assumes that the backend is
486  * a flat XML file, which may not be the case e.g. if we are using GConf (in future).
487  */
488 Inkscape::XML::Node *Preferences::_getNode(Glib::ustring const &pref_key, bool create)
490     // verify path
491     g_assert( pref_key.at(0) == '/' );
492     // No longer necessary, can cause problems with input devices which have a dot in the name
493     // g_assert( pref_key.find('.') == Glib::ustring::npos );
495     Inkscape::XML::Node *node = _prefs_doc->root(), *child = NULL;
496     gchar **splits = g_strsplit(pref_key.data(), "/", 0);
497     
498     if ( splits == NULL ) return node;
499     
500     for (int part_i = 0; splits[part_i]; ++part_i) {
501         // skip empty path segments
502         if (!splits[part_i][0]) continue;
503         
504         for (child = node->firstChild(); child; child = child->next())
505             if (!strcmp(splits[part_i], child->attribute("id"))) break;
506         
507         // If the previous loop found a matching key, child now contains the node
508         // matching the processed key part. If no node was found then it is NULL.
509         if (!child) {
510             if (create) {
511                 // create the rest of the key
512                 while(splits[part_i]) {
513                     child = node->document()->createElement("group");
514                     child->setAttribute("id", splits[part_i]);
515                     node->appendChild(child);
516                     
517                     ++part_i;
518                     node = child;
519                 }
520                 g_strfreev(splits);
521                 return node;
522             } else {
523                 return NULL;
524             }
525         }
527         node = child;
528     }
529     g_strfreev(splits);
530     return node;
533 void Preferences::_getRawValue(Glib::ustring const &path, gchar const *&result)
535     // create node and attribute keys
536     Glib::ustring node_key, attr_key;
537     _keySplit(path, node_key, attr_key);
538     
539     // retrieve the attribute
540     Inkscape::XML::Node *node = _getNode(node_key, false);
541     if ( node == NULL ) {
542         result = NULL;
543     } else {
544         gchar const *attr = node->attribute(attr_key.data());
545         if ( attr == NULL ) {
546             result = NULL;
547         } else {
548             result = attr;
549         }
550     }
553 void Preferences::_setRawValue(Glib::ustring const &path, gchar const *value)
555     // create node and attribute keys
556     Glib::ustring node_key, attr_key;
557     _keySplit(path, node_key, attr_key);
558     
559     // set the attribute
560     Inkscape::XML::Node *node = _getNode(node_key, true);
561     node->setAttribute(attr_key.data(), value);
564 // The _extract* methods are where the actual wrok is done - they define how preferences are stored
565 // in the XML file.
567 bool Preferences::_extractBool(Entry const &v)
569     gchar const *s = static_cast<gchar const *>(v._value);
570     if ( !s[0] || !strcmp(s, "0") || !strcmp(s, "false") ) return false;
571     return true;
574 int Preferences::_extractInt(Entry const &v)
576     gchar const *s = static_cast<gchar const *>(v._value);
577     if ( !strcmp(s, "true") ) return true;
578     if ( !strcmp(s, "false") ) return false;
579     return atoi(s);
582 double Preferences::_extractDouble(Entry const &v)
584     gchar const *s = static_cast<gchar const *>(v._value);
585     return g_ascii_strtod(s, NULL);
588 Glib::ustring Preferences::_extractString(Entry const &v)
590     return Glib::ustring(static_cast<gchar const *>(v._value));
593 SPCSSAttr *Preferences::_extractStyle(Entry const &v)
595     SPCSSAttr *style = sp_repr_css_attr_new();
596     sp_repr_css_attr_add_from_string(style, static_cast<gchar const*>(v._value));
597     return style;
600 SPCSSAttr *Preferences::_extractInheritedStyle(Entry const &v)
602     // This is the dirtiest extraction method. Generally we ignore whatever was in v._value
603     // and just get the style using sp_repr_css_attr_inherited. To implement this in GConf,
604     // we'll have to walk up the tree and call sp_repr_css_attr_add_from_string
605     Glib::ustring node_key, attr_key;
606     _keySplit(v._pref_path, node_key, attr_key);
607     
608     Inkscape::XML::Node *node = _getNode(node_key, false);
609     return sp_repr_css_attr_inherited(node, attr_key.data());
612 // XML backend helper: Split the path into a node key and an attribute key.
613 void Preferences::_keySplit(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key)
615     // everything after the last slash
616     attr_key = pref_path.substr(pref_path.rfind('/') + 1, Glib::ustring::npos);
617     // everything before the last slash
618     node_key = pref_path.substr(0, pref_path.rfind('/'));
621 void Preferences::_errorDialog(Glib::ustring const &msg, Glib::ustring const &secondary)
623     if (_quiet) return;
624     if (_use_gui) {
625         Gtk::MessageDialog err(
626             msg, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true);
627         err.set_secondary_text(secondary);
628         err.run();
629     } else {
630         g_message("%s", msg.data());
631         g_message("%s", secondary.data());
632     }
635 Preferences::Entry const Preferences::_create_pref_value(Glib::ustring const &path, void const *ptr)
637     return Entry(path, ptr);
640 Preferences *Preferences::_instance = NULL;
643 } // namespace Inkscape
644  
645 /*
646   Local Variables:
647   mode:c++
648   c-file-style:"stroustrup"
649   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
650   indent-tabs-mode:nil
651   fill-column:99
652   End:
653 */
654 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :