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