summaryrefslogtreecommitdiffstats
path: root/src/ui/dialog/spellcheck.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/dialog/spellcheck.cpp')
-rw-r--r--src/ui/dialog/spellcheck.cpp761
1 files changed, 761 insertions, 0 deletions
diff --git a/src/ui/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp
new file mode 100644
index 0000000..41df154
--- /dev/null
+++ b/src/ui/dialog/spellcheck.cpp
@@ -0,0 +1,761 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Spellcheck dialog.
+ */
+/* Authors:
+ * bulia byak <bulia@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "spellcheck.h"
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "layer-manager.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "display/control/canvas-item-rect.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+#include "object/sp-text.h"
+#include "object/sp-tref.h"
+
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK
+#include "ui/icon-names.h"
+#include "ui/tools/text-tool.h"
+
+#include <glibmm/i18n.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * Get the list of installed dictionaries/languages
+ */
+std::vector<LanguagePair> SpellCheck::get_available_langs()
+{
+ std::vector<LanguagePair> langs;
+
+#if WITH_GSPELL
+ // TODO: write a gspellmm library.
+ // TODO: why is this not const?
+ GList *list = const_cast<GList *>(gspell_language_get_available());
+ g_list_foreach(list, [](gpointer data, gpointer user_data) {
+ GspellLanguage *language = reinterpret_cast<GspellLanguage*>(data);
+ std::vector<LanguagePair> *langs = reinterpret_cast<std::vector<LanguagePair>*>(user_data);
+ const gchar *name = gspell_language_get_name(language);
+ const gchar *code = gspell_language_get_code(language);
+ langs->emplace_back(name, code);
+ }, &langs);
+#endif
+
+ return langs;
+}
+
+static void show_spellcheck_preferences_dialog()
+{
+ Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK);
+ SP_ACTIVE_DESKTOP->getContainer()->new_dialog("Spellcheck");
+}
+
+SpellCheck::SpellCheck()
+ : DialogBase("/dialogs/spellcheck/", "Spellcheck")
+ , _text(nullptr)
+ , _layout(nullptr)
+ , _stops(0)
+ , _adds(0)
+ , _working(false)
+ , _local_change(false)
+ , _prefs(nullptr)
+ , accept_button(_("_Accept"), true)
+ , ignoreonce_button(_("_Ignore once"), true)
+ , ignore_button(_("_Ignore"), true)
+ , add_button(_("A_dd"), true)
+ , dictionary_label(_("Language"))
+ , dictionary_hbox(Gtk::ORIENTATION_HORIZONTAL, 0)
+ , stop_button(_("_Stop"), true)
+ , start_button(_("_Start"), true)
+ , suggestion_hbox(Gtk::ORIENTATION_HORIZONTAL)
+ , changebutton_vbox(Gtk::ORIENTATION_VERTICAL)
+{
+ _prefs = Inkscape::Preferences::get();
+
+ banner_hbox.set_layout(Gtk::BUTTONBOX_START);
+ banner_hbox.add(banner_label);
+
+ if (_langs.empty()) {
+ _langs = get_available_langs();
+
+ if (_langs.empty()) {
+ banner_label.set_markup(Glib::ustring::compose("<i>%1</i>", _("No dictionaries installed")));
+ }
+ }
+
+ scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrolled_window.set_shadow_type(Gtk::SHADOW_IN);
+ scrolled_window.set_size_request(120, 96);
+ scrolled_window.add(tree_view);
+
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ tree_view.append_column(_("Suggestions:"), tree_columns.suggestions);
+
+ if (!_langs.empty()) {
+ for (const LanguagePair &pair : _langs) {
+ dictionary_combo.append(pair.second, pair.first);
+ }
+ // Set previously set language (or the first item)
+ if(!dictionary_combo.set_active_id(_prefs->getString("/dialogs/spellcheck/lang"))) {
+ dictionary_combo.set_active(0);
+ }
+ }
+
+ accept_button.set_tooltip_text(_("Accept the chosen suggestion"));
+ ignoreonce_button.set_tooltip_text(_("Ignore this word only once"));
+ ignore_button.set_tooltip_text(_("Ignore this word in this session"));
+ add_button.set_tooltip_text(_("Add this word to the chosen dictionary"));
+ pref_button.set_tooltip_text(_("Preferences"));
+ pref_button.set_image_from_icon_name("preferences-system");
+
+ dictionary_hbox.pack_start(dictionary_label, false, false, 6);
+ dictionary_hbox.pack_start(dictionary_combo, true, true, 0);
+ dictionary_hbox.pack_start(pref_button, false, false, 0);
+
+ changebutton_vbox.set_spacing(4);
+ changebutton_vbox.pack_start(accept_button, false, false, 0);
+ changebutton_vbox.pack_start(ignoreonce_button, false, false, 0);
+ changebutton_vbox.pack_start(ignore_button, false, false, 0);
+ changebutton_vbox.pack_start(add_button, false, false, 0);
+
+ suggestion_hbox.pack_start (scrolled_window, true, true, 4);
+ suggestion_hbox.pack_end (changebutton_vbox, false, false, 0);
+
+ stop_button.set_tooltip_text(_("Stop the check"));
+ start_button.set_tooltip_text(_("Start the check"));
+
+ actionbutton_hbox.set_layout(Gtk::BUTTONBOX_END);
+ actionbutton_hbox.set_spacing(4);
+ actionbutton_hbox.add(stop_button);
+ actionbutton_hbox.add(start_button);
+
+ /*
+ * Main dialog
+ */
+ set_spacing(6);
+ pack_start (banner_hbox, false, false, 0);
+ pack_start (suggestion_hbox, true, true, 0);
+ pack_start (dictionary_hbox, false, false, 0);
+ pack_start (action_sep, false, false, 6);
+ pack_start (actionbutton_hbox, false, false, 0);
+
+ /*
+ * Signal handlers
+ */
+ accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept));
+ ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce));
+ ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore));
+ add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd));
+ start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart));
+ stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop));
+ tree_view.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange));
+ dictionary_combo.signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged));
+ pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog));
+
+ show_all_children ();
+
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+}
+
+SpellCheck::~SpellCheck()
+{
+ clearRects();
+ disconnect();
+}
+
+void SpellCheck::documentReplaced()
+{
+ if (_working) {
+ // Stop and start on the new desktop
+ finished();
+ onStart();
+ }
+}
+
+void SpellCheck::clearRects()
+{
+ for(auto rect : _rects) {
+ rect->hide();
+ delete rect;
+ }
+ _rects.clear();
+}
+
+void SpellCheck::disconnect()
+{
+ if (_release_connection) {
+ _release_connection.disconnect();
+ }
+ if (_modified_connection) {
+ _modified_connection.disconnect();
+ }
+}
+
+void SpellCheck::allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked)
+{
+ if (SP_IS_DEFS(r))
+ return; // we're not interested in items in defs
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return; // we're not interested in metadata
+ }
+
+ if (auto desktop = getDesktop()) {
+ for (auto& child: r->children) {
+ if (auto item = dynamic_cast<SPItem *>(&child)) {
+ if (!child.cloned && !desktop->layerManager().isLayer(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))
+ l.push_back(item);
+ }
+ }
+ }
+ allTextItems (&child, l, hidden, locked);
+ }
+ }
+ return;
+}
+
+bool
+SpellCheck::textIsValid (SPObject *root, SPItem *text)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ return (std::find(l.begin(), l.end(), text) != l.end());
+}
+
+bool SpellCheck::compareTextBboxes(SPItem const *i1, SPItem const *i2)//returns a<b
+{
+ Geom::OptRect bbox1 = i1->documentVisualBounds();
+ Geom::OptRect bbox2 = i2->documentVisualBounds();
+ if (!bbox1 || !bbox2) {
+ return false;
+ }
+
+ // vector between top left corners
+ Geom::Point diff = bbox1->min() - bbox2->min();
+
+ return diff[Geom::Y] == 0 ? (diff[Geom::X] < 0) : (diff[Geom::Y] < 0);
+}
+
+// We regenerate and resort the list every time, because user could have changed it while the
+// dialog was waiting
+SPItem *SpellCheck::getText (SPObject *root)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ std::sort(l.begin(),l.end(),SpellCheck::compareTextBboxes);
+
+ for (auto item:l) {
+ if(_seen_objects.insert(item).second)
+ return item;
+ }
+ return nullptr;
+}
+
+void
+SpellCheck::nextText()
+{
+ disconnect();
+
+ _text = getText(_root);
+ if (_text) {
+
+ _modified_connection = _text->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified));
+ _release_connection = _text->connectRelease(sigc::mem_fun(*this, &SpellCheck::onObjReleased));
+
+ _layout = te_get_layout (_text);
+ _begin_w = _layout->begin();
+ }
+ _end_w = _begin_w;
+ _word.clear();
+}
+
+void SpellCheck::deleteSpeller() {
+}
+
+bool SpellCheck::updateSpeller() {
+#if WITH_GSPELL
+ auto lang = dictionary_combo.get_active_id();
+ if (!lang.empty()) {
+ const GspellLanguage *language = gspell_language_lookup(lang.c_str());
+ _checker = gspell_checker_new(language);
+ }
+
+ return _checker != nullptr;
+#else
+ return false;
+#endif
+}
+
+void SpellCheck::onStart()
+{
+ if (!getDocument())
+ return;
+
+ start_button.set_sensitive(false);
+
+ _stops = 0;
+ _adds = 0;
+ clearRects();
+
+ if (!updateSpeller())
+ return;
+
+ _root = getDocument()->getRoot();
+
+ // empty the list of objects we've checked
+ _seen_objects.clear();
+
+ // grab first text
+ nextText();
+
+ _working = true;
+
+ doSpellcheck();
+}
+
+void
+SpellCheck::finished ()
+{
+ deleteSpeller();
+
+ clearRects();
+ disconnect();
+
+ tree_view.unset_model();
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+ start_button.set_sensitive(true);
+
+ {
+ gchar *label;
+ if (_stops)
+ label = g_strdup_printf(_("<b>Finished</b>, <b>%d</b> words added to dictionary"), _adds);
+ else
+ label = g_strdup_printf("%s", _("<b>Finished</b>, nothing suspicious found"));
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ _seen_objects.clear();
+
+ _root = nullptr;
+
+ _working = false;
+}
+
+bool
+SpellCheck::nextWord()
+{
+ auto desktop = getDesktop();
+ if (!_working || !desktop)
+ return false;
+
+ if (!_text) {
+ finished();
+ return false;
+ }
+ _word.clear();
+
+ while (_word.size() == 0) {
+ _begin_w = _end_w;
+
+ if (!_layout || _begin_w == _layout->end()) {
+ nextText();
+ return false;
+ }
+
+ if (!_layout->isStartOfWord(_begin_w)) {
+ _begin_w.nextStartOfWord();
+ }
+
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+
+ // try to link this word with the next if separated by '
+ SPObject *char_item = nullptr;
+ Glib::ustring::iterator text_iter;
+ _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (this_char == '\'' || this_char == 0x2019) {
+ Inkscape::Text::Layout::iterator end_t = _end_w;
+ end_t.nextCharacter();
+ _layout->getSourceOfCharacter(end_t, &char_item, &text_iter);
+ if (SP_IS_STRING(char_item)) {
+ int this_char = *text_iter;
+ if (g_ascii_isalpha(this_char)) { // 's
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+ }
+ }
+ }
+
+ // skip words containing digits
+ if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) {
+ bool digits = false;
+ for (unsigned int i : _word) {
+ if (g_unichar_isdigit(i)) {
+ digits = true;
+ break;
+ }
+ }
+ if (digits) {
+ return false;
+ }
+ }
+
+ // skip ALL-CAPS words
+ if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) {
+ bool allcaps = true;
+ for (unsigned int i : _word) {
+ if (!g_unichar_isupper(i)) {
+ allcaps = false;
+ break;
+ }
+ }
+ if (allcaps) {
+ return false;
+ }
+ }
+
+ int have = 0;
+
+#if WITH_GSPELL
+ if (_checker) {
+ GError *error = nullptr;
+ have += gspell_checker_check_word(_checker, _word.c_str(), -1, &error);
+ }
+#endif /* WITH_GSPELL */
+
+ if (have == 0) { // not found in any!
+ _stops ++;
+
+ // display it in window
+ {
+ gchar *label = g_strdup_printf(_("Not in dictionary: <b>%s</b>"), _word.c_str());
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ tree_view.set_sensitive(true);
+ ignore_button.set_sensitive(true);
+ ignoreonce_button.set_sensitive(true);
+ add_button.set_sensitive(true);
+ stop_button.set_sensitive(true);
+
+ // draw rect
+ std::vector<Geom::Point> points =
+ _layout->createSelectionShape(_begin_w, _end_w, _text->i2dt_affine());
+ if (points.size() >= 4) { // We may not have a single quad if this is a clipped part of text on path;
+ // in that case skip drawing the rect
+ Geom::Point tl, br;
+ tl = br = points.front();
+ for (auto & point : points) {
+ if (point[Geom::X] < tl[Geom::X])
+ tl[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] < tl[Geom::Y])
+ tl[Geom::Y] = point[Geom::Y];
+ if (point[Geom::X] > br[Geom::X])
+ br[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] > br[Geom::Y])
+ br[Geom::Y] = point[Geom::Y];
+ }
+
+ // expand slightly
+ Geom::Rect area = Geom::Rect(tl, br);
+ double mindim = fabs(tl[Geom::Y] - br[Geom::Y]);
+ if (fabs(tl[Geom::X] - br[Geom::X]) < mindim)
+ mindim = fabs(tl[Geom::X] - br[Geom::X]);
+ area.expandBy(MAX(0.05 * mindim, 1));
+
+ // Create canvas item rect with red stroke. (TODO: a quad could allow non-axis aligned rects.)
+ auto rect = new Inkscape::CanvasItemRect(desktop->getCanvasSketch(), area);
+ rect->set_stroke(0xff0000ff);
+ rect->show();
+ _rects.push_back(rect);
+
+ // scroll to make it all visible
+ Geom::Point const center = desktop->current_center();
+ area.expandBy(0.5 * mindim);
+ Geom::Point scrollto;
+ double dist = 0;
+ for (unsigned corner = 0; corner < 4; corner ++) {
+ if (Geom::L2(area.corner(corner) - center) > dist) {
+ dist = Geom::L2(area.corner(corner) - center);
+ scrollto = area.corner(corner);
+ }
+ }
+ desktop->scroll_to_point (scrollto, 1.0);
+ }
+
+ // select text; if in Text tool, position cursor to the beginning of word
+ // unless it is already in the word
+ if (desktop->selection->singleItem() != _text) {
+ desktop->selection->set (_text);
+ }
+
+ if (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) {
+ Inkscape::Text::Layout::iterator *cursor =
+ sp_text_context_get_cursor_position(SP_TEXT_CONTEXT(desktop->event_context), _text);
+ if (!cursor) // some other text is selected there
+ desktop->selection->set (_text);
+ else if (*cursor <= _begin_w || *cursor >= _end_w)
+ sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w);
+ }
+
+#if WITH_GSPELL
+
+ // get suggestions
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ unsigned n_sugg = 0;
+
+ if (_checker) {
+ GSList *list = gspell_checker_get_suggestions(_checker, _word.c_str(), -1);
+ std::vector<std::string> suggs;
+
+ // TODO: use a better API for that, or figure out how to make gspellmm.
+ g_slist_foreach(list, [](gpointer data, gpointer user_data) {
+ const gchar *suggestion = reinterpret_cast<const gchar*>(data);
+ std::vector<std::string> *suggs = reinterpret_cast<std::vector<std::string>*>(user_data);
+ suggs->push_back(suggestion);
+ }, &suggs);
+ g_slist_free_full(list, g_free);
+
+ Gtk::TreeModel::iterator iter;
+ for (std::string sugg : suggs) {
+ iter = model->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[tree_columns.suggestions] = sugg;
+
+ // select first suggestion
+ if (++n_sugg == 1) {
+ tree_view.get_selection()->select(iter);
+ }
+ }
+ }
+
+ accept_button.set_sensitive(n_sugg > 0);
+
+#endif /* WITH_GSPELL */
+
+ return true;
+
+ }
+ return false;
+}
+
+
+
+void
+SpellCheck::deleteLastRect ()
+{
+ if (!_rects.empty()) {
+ _rects.back()->hide();
+ delete _rects.back();
+ _rects.pop_back();
+ }
+}
+
+void SpellCheck::doSpellcheck ()
+{
+ if (_langs.empty()) {
+ return;
+ }
+
+ banner_label.set_markup(_("<i>Checking...</i>"));
+
+ while (_working)
+ if (nextWord())
+ break;
+}
+
+void SpellCheck::onTreeSelectionChange()
+{
+ accept_button.set_sensitive(true);
+}
+
+void SpellCheck::onObjModified (SPObject* /* blah */, unsigned int /* bleh */)
+{
+ if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it
+ _local_change = false;
+ return;
+ }
+
+ if (_working && _root) {
+ // user may have edited the text we're checking; try to do the most sensible thing in this
+ // situation
+
+ // just in case, re-get text's layout
+ _layout = te_get_layout (_text);
+
+ // re-get the word
+ _layout->validateIterator(&_begin_w);
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ Glib::ustring word_new = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ if (word_new != _word) {
+ _end_w = _begin_w;
+ deleteLastRect ();
+ doSpellcheck (); // recheck this word and go ahead if it's ok
+ }
+ }
+}
+
+void SpellCheck::onObjReleased (SPObject* /* blah */)
+{
+ if (_working && _root) {
+ // the text object was deleted
+ deleteLastRect ();
+ nextText();
+ doSpellcheck (); // get next text and continue
+ }
+}
+
+void SpellCheck::onAccept ()
+{
+ // insert chosen suggestion
+
+ Glib::RefPtr<Gtk::TreeSelection> selection = tree_view.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring sugg = row[tree_columns.suggestions];
+
+ if (sugg.length() > 0) {
+ //g_print("chosen: %s\n", sugg);
+ _local_change = true;
+ sp_te_replace(_text, _begin_w, _end_w, sugg.c_str());
+ // find the end of the word anew
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ DocumentUndo::done(getDocument(), _("Fix spelling"), INKSCAPE_ICON("draw-text"));
+ }
+ }
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnore ()
+{
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_session(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnoreOnce ()
+{
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onAdd ()
+{
+ _adds++;
+
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_personal(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onStop ()
+{
+ finished();
+}
+
+void SpellCheck::onLanguageChanged()
+{
+ // First, save language for next load
+ auto lang = dictionary_combo.get_active_id();
+ _prefs->setString("/dialogs/spellcheck/lang", lang);
+
+ if (!_working) {
+ onStart();
+ return;
+ }
+
+ if (!updateSpeller()) {
+ return;
+ }
+
+ // recheck current word
+ _end_w = _begin_w;
+ deleteLastRect();
+ doSpellcheck();
+}
+}
+}
+}
+
+/*
+ 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 :