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);
68 path = profile_path(_prefs_basename.data());
69 _prefs_filename = path;
70 g_free(path);
72 _loadDefaults();
73 }
75 Preferences::~Preferences()
76 {
77 // when the preferences are unloaded, save them
78 save();
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;
113 // NOTE: After we upgrade to Glib 2.16, use Glib::ustring::compose
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 }
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 }
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 }
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 }
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 }
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;
218 }
220 /**
221 * @brief Flush all pref changes to the XML file
222 */
223 void Preferences::save()
224 {
225 if (!_writable) return; // no-op if the prefs file is not writable
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());
235 }
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)
246 {
247 std::vector<Entry> temp;
248 Inkscape::XML::Node *node = _getNode(path, false);
249 if (!node) return temp;
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;
256 }
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)
264 {
265 std::vector<Glib::ustring> temp;
266 Inkscape::XML::Node *node = _getNode(path, false);
267 if (!node) return temp;
269 for (Inkscape::XML::NodeSiblingIterator i = node->firstChild(); i; ++i) {
270 temp.push_back(path + '/' + i->attribute("id"));
271 }
272 return temp;
273 }
275 // getter methods
277 Preferences::Entry const Preferences::getEntry(Glib::ustring const &pref_path)
278 {
279 gchar const *v;
280 _getRawValue(pref_path, v);
281 return Entry(pref_path, v);
282 }
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)
292 {
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" ));
297 }
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)
305 {
306 gchar intstr[32];
307 g_snprintf(intstr, 32, "%d", value);
308 _setRawValue(pref_path, intstr);
309 }
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)
317 {
318 gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
319 g_ascii_dtostr(buf, G_ASCII_DTOSTR_BUF_SIZE, value);
320 _setRawValue(pref_path, buf);
321 }
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)
329 {
330 _setRawValue(pref_path, value.data());
331 }
333 void Preferences::setStyle(Glib::ustring const &pref_path, SPCSSAttr *style)
334 {
335 gchar *css_str = sp_repr_css_write_string(style);
336 _setRawValue(pref_path, css_str);
337 g_free(css_str);
338 }
340 void Preferences::mergeStyle(Glib::ustring const &pref_path, SPCSSAttr *style)
341 {
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);
348 }
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)
366 {
367 }
369 Preferences::Observer::~Observer()
370 {
371 // on destruction remove observer to prevent invalid references
372 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
373 prefs->removeObserver(*this);
374 }
376 void Preferences::PrefNodeObserver::notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared<char>, Util::ptr_shared<char> new_value)
377 {
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;
382 _ObserverData *d = static_cast<_ObserverData*>(Preferences::_get_pref_observer_data(_observer));
383 Glib::ustring notify_path = _observer.observed_path;
385 if (!d->_is_attr) {
386 std::vector<gchar const *> path_fragments;
387 notify_path.reserve(256); // this will make appending operations faster
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 }
405 Entry const val = Preferences::_create_pref_value(notify_path, static_cast<void const*>(new_value.pointer()));
406 _observer.notify(val);
407 }
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)
413 {
414 // first assume that the last path element is an entry.
415 _keySplit(pref_path, node_key, attr_key);
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;
431 }
433 void Preferences::addObserver(Observer &o)
434 {
435 // prevent adding the same observer twice
436 if ( _observer_map.find(&o) != _observer_map.end() ) return;
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;
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);
449 _observer_map[&o] = new PrefNodeObserver(o, attr_key);
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 }
457 }
459 void Preferences::removeObserver(Observer &o)
460 {
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;
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);
475 }
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)
489 {
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);
498 if ( splits == NULL ) return node;
500 for (int part_i = 0; splits[part_i]; ++part_i) {
501 // skip empty path segments
502 if (!splits[part_i][0]) continue;
504 for (child = node->firstChild(); child; child = child->next())
505 if (!strcmp(splits[part_i], child->attribute("id"))) break;
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);
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;
531 }
533 void Preferences::_getRawValue(Glib::ustring const &path, gchar const *&result)
534 {
535 // create node and attribute keys
536 Glib::ustring node_key, attr_key;
537 _keySplit(path, node_key, attr_key);
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 }
551 }
553 void Preferences::_setRawValue(Glib::ustring const &path, gchar const *value)
554 {
555 // create node and attribute keys
556 Glib::ustring node_key, attr_key;
557 _keySplit(path, node_key, attr_key);
559 // set the attribute
560 Inkscape::XML::Node *node = _getNode(node_key, true);
561 node->setAttribute(attr_key.data(), value);
562 }
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)
568 {
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;
572 }
574 int Preferences::_extractInt(Entry const &v)
575 {
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);
580 }
582 double Preferences::_extractDouble(Entry const &v)
583 {
584 gchar const *s = static_cast<gchar const *>(v._value);
585 return g_ascii_strtod(s, NULL);
586 }
588 Glib::ustring Preferences::_extractString(Entry const &v)
589 {
590 return Glib::ustring(static_cast<gchar const *>(v._value));
591 }
593 SPCSSAttr *Preferences::_extractStyle(Entry const &v)
594 {
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;
598 }
600 SPCSSAttr *Preferences::_extractInheritedStyle(Entry const &v)
601 {
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);
608 Inkscape::XML::Node *node = _getNode(node_key, false);
609 return sp_repr_css_attr_inherited(node, attr_key.data());
610 }
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)
614 {
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('/'));
619 }
621 void Preferences::_errorDialog(Glib::ustring const &msg, Glib::ustring const &secondary)
622 {
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 }
633 }
635 Preferences::Entry const Preferences::_create_pref_value(Glib::ustring const &path, void const *ptr)
636 {
637 return Entry(path, ptr);
638 }
640 Preferences *Preferences::_instance = NULL;
643 } // namespace Inkscape
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 :