summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/font-collection-selector.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/widget/font-collection-selector.cpp
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/widget/font-collection-selector.cpp')
-rw-r--r--src/ui/widget/font-collection-selector.cpp674
1 files changed, 674 insertions, 0 deletions
diff --git a/src/ui/widget/font-collection-selector.cpp b/src/ui/widget/font-collection-selector.cpp
new file mode 100644
index 0000000..70d1e11
--- /dev/null
+++ b/src/ui/widget/font-collection-selector.cpp
@@ -0,0 +1,674 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Vaibhav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-collection-selector.h"
+
+#include "libnrtype/font-lister.h"
+
+// For updating from selection
+#include "util/document-fonts.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontCollectionSelector::FontCollectionSelector()
+{
+ // Step 1: Initialize the treeview.
+ treeview = Gtk::manage(new Gtk::TreeView());
+
+ // Step 2: Setup the treeview.
+ setup_tree_view(treeview);
+
+ // Step 3: Intialize the model.
+ store = Gtk::TreeStore::create(FontCollection);
+ // Step 4: Populate the ListStore.
+ treeview->set_model(store);
+
+ // Signals.
+ setup_signals();
+
+ show_all_children();
+}
+
+// Setup the treeview of the widget.
+void FontCollectionSelector::setup_tree_view(Gtk::TreeView *tv)
+{
+ cell_text = new Gtk::CellRendererText();
+ del_icon_renderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ del_icon_renderer->add_icon("edit-delete");
+
+ text_column.pack_start (*cell_text, true);
+ text_column.add_attribute (*cell_text, "text", TEXT_COLUMN);
+ text_column.set_expand(true);
+
+ del_icon_column.pack_start (*del_icon_renderer, false);
+
+ // Attach the cell data functions.
+ text_column.set_cell_data_func(*cell_text, sigc::mem_fun(*this, &FontCollectionSelector::text_cell_data_func));
+
+ treeview->enable_model_drag_dest (Gdk::ACTION_MOVE);
+ treeview->set_headers_visible (false);
+
+ // Target entries for Drag and Drop.
+ target_entries.emplace_back(Gtk::TargetEntry("STRING", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("text/plain", (Gtk::TargetFlags)0, 0));
+
+ treeview->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
+
+ // Append the columns to the treeview.
+ treeview->append_column(text_column);
+ treeview->append_column(del_icon_column);
+
+ scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scroll.set_overlay_scrolling(false);
+ scroll.add (*treeview);
+
+ frame.set_hexpand (true);
+ frame.set_vexpand (true);
+ frame.add (scroll);
+
+ // Grid
+ set_name("FontCollection");
+ set_row_spacing(4);
+ set_column_spacing(1);
+
+ // Add extra columns to the "frame" to change space distribution
+ attach (frame, 0, 0, 1, 2);
+}
+
+void FontCollectionSelector::change_frame_name(const Glib::ustring& name)
+{
+ frame.set_label(name);
+}
+
+void FontCollectionSelector::setup_signals()
+{
+ cell_text->signal_edited().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_rename_collection));
+ del_icon_renderer->signal_activated().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_delete_icon_clicked));
+ treeview->signal_key_press_event().connect([=](GdkEventKey *ev){ return on_key_pressed(ev); });
+ treeview->set_row_separator_func(sigc::mem_fun(*this, &FontCollectionSelector::row_separator_func));
+ treeview->get_column(ICON_COLUMN)->set_cell_data_func(*del_icon_renderer, sigc::mem_fun(*this, &FontCollectionSelector::icon_cell_data_func));
+
+ // Signals for drag and drop.
+ treeview->signal_drag_motion().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_motion), false);
+ treeview->signal_drag_data_received().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_data_received), false);
+ treeview->signal_drag_drop().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_drop), false);
+ // treeview->signal_drag_failed().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_failed), false);
+ treeview->signal_drag_leave().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_leave), false);
+ treeview->signal_drag_end().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_end), false);
+ treeview->get_selection()->signal_changed().connect([=](){ on_selection_changed(); });
+ Inkscape::RecentlyUsedFonts::get()->connectUpdate(sigc::mem_fun(*this, &FontCollectionSelector::populate_system_collections));
+}
+
+// To distinguish the collection name and the font name.
+Glib::ustring FontCollectionSelector::get_text_cell_markup(Gtk::TreeIter const &iter)
+{
+ Glib::ustring markup;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // It is a font.
+ markup = "<span alpha='50%'>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+ else {
+ // It is a collection.
+ markup = "<span>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+
+ return markup;
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::text_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Glib::ustring markup = get_text_cell_markup(iter);
+ renderer->set_property("markup", markup);
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::icon_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // Case: It is a font.
+ bool is_user = (*parent)[FontCollection.is_editable];
+ del_icon_renderer->set_visible(is_user);
+ cell_text->property_editable() = false;
+ } else if((*iter)[FontCollection.is_editable]) {
+ // Case: User font collection.
+ del_icon_renderer->set_visible(true);
+ cell_text->property_editable() = true;
+ } else {
+ // Case: System font collection.
+ del_icon_renderer->set_visible(false);
+ cell_text->property_editable() = false;
+ }
+}
+
+// This function will TURN OFF the visibility of checkbuttons for children in the TreeStore.
+void FontCollectionSelector::check_button_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ renderer->set_visible(false);
+ /*
+ // Append the checkbutton column only if the iterator have some children.
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ if(parent) {
+ renderer->set_visible(false);
+ }
+ else {
+ renderer->set_visible(true);
+ }
+ */
+}
+
+bool FontCollectionSelector::row_separator_func(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter)
+{
+ return (*iter)[FontCollection.name] == "#";
+}
+
+void FontCollectionSelector::populate_collections()
+{
+ store->clear();
+ populate_system_collections();
+ populate_user_collections();
+}
+
+// This function will keep the populate the system collections and their fonts.
+void FontCollectionSelector::populate_system_collections()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = font_collections->get_collections(true);
+
+ // Erase the previous collections.
+ store->freeze_notify();
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter;
+ bool row_0 = false, row_1 = false;
+
+ for(int i = 0; i < 3; i++) {
+ iter = store->get_iter(path);
+ if(iter) {
+ if(treeview->row_expanded(path)) {
+ if(i == 0) {
+ row_0 = true;
+ } else if(i == 1) {
+ row_1 = true;
+ }
+ }
+ store->erase(iter);
+ }
+ }
+
+ // Insert a separator.
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = "#";
+ (*iter)[FontCollection.is_editable] = false;
+ iter = store->children();
+
+ for(auto const &col: system_collections) {
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = col;
+ (*iter)[FontCollection.is_editable] = false;
+ }
+
+ populate_document_fonts();
+ populate_recently_used_fonts();
+ store->thaw_notify();
+
+ if(row_0) {
+ treeview->expand_row(Gtk::TreePath("0"), true);
+ }
+ if(row_1) {
+ treeview->expand_row(Gtk::TreePath("1"), true);
+ }
+}
+
+void FontCollectionSelector::populate_document_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(1);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::DocumentFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+void FontCollectionSelector::populate_recently_used_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::RecentlyUsedFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+// This function will keep the collections_list updated after any event.
+void FontCollectionSelector::populate_user_collections()
+{
+ // Get the list of all the user collections.
+ auto collections = Inkscape::FontCollections::get()->get_collections();
+
+ // Now insert these collections one by one into the treeview.
+ store->freeze_notify();
+ Gtk::TreeModel::iterator iter;
+
+ for(const auto &col: collections) {
+ iter = store->append();
+ (*iter)[FontCollection.name] = col;
+
+ // User collections are editable.
+ (*iter)[FontCollection.is_editable] = true;
+
+ // Alright, now populate the fonts of this collection.
+ populate_fonts(col);
+ }
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::populate_fonts(const Glib::ustring& collection_name)
+{
+ // Get the FontLister instance to get the list of all the collections.
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::set <Glib::ustring> fonts = font_collections->get_fonts(collection_name);
+
+ // First find the location of this collection_name in the map.
+ // +1 for the separator.
+ int index = font_collections->get_user_collection_location(collection_name) + 1;
+
+ store->freeze_notify();
+
+ // Generate the iterator path.
+ Gtk::TreePath path;
+ path.push_back(index);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // auto child_iter = iter->children();
+ auto size = iter->children().size();
+
+ // Clear the previously stored fonts at this path.
+ while(size--) {
+ Gtk::TreeModel::iterator child = iter->children().begin();
+ store->erase(child);
+ }
+
+ for(auto const &font: fonts) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::on_delete_icon_clicked(Glib::ustring const &path)
+{
+ FontCollections *collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ auto parent = (*iter)->parent();
+ if(!parent) {
+ // It is a collection.
+ // No need to confirm in case of empty collections.
+ if (!collections->get_fonts((*iter)[FontCollection.name]).empty()) {
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+ }
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ collections->remove_font((*parent)[FontCollection.name], (*iter)[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+void FontCollectionSelector::on_create_collection()
+{
+ Gtk::TreeModel::iterator iter = store->append();
+ (*iter)[FontCollection.is_editable] = true;
+
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ treeview->set_cursor(path, text_column, true);
+ grab_focus();
+}
+
+void FontCollectionSelector::on_rename_collection(const Glib::ustring& path, const Glib::ustring& new_text)
+{
+ // Fetch the collections.
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ // Check if the same collection is already present.
+ bool is_system = collections->find_collection(new_text, true);
+ bool is_user = collections->find_collection(new_text, false);
+
+ // Return if the new name is empty.
+ // Do not allow user collections to be named as system collections.
+ if (new_text == "" || is_system || is_user) {
+ return;
+ }
+
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // Return if it is not a valid iter.
+ if(!iter) {
+ return;
+ }
+
+ // To check if it's a font-collection or a font.
+ auto parent = (*iter)->parent();
+
+ if(!parent) {
+ // Call the rename_collection function
+ collections->rename_collection((*iter)[FontCollection.name], new_text);
+ }
+ else {
+ collections->rename_font((*parent)[FontCollection.name], (*iter)[FontCollection.name], new_text);
+ }
+
+ (*iter)[FontCollection.name] = new_text;
+ populate_collections();
+}
+
+void FontCollectionSelector::on_delete_button_pressed()
+{
+ // Get the current collection.
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ if(!parent) {
+ // It is a collection.
+ // Check if it is a system collection.
+ bool is_system = collections->find_collection((*iter)[FontCollection.name], true);
+ if(is_system) {
+ return;
+ }
+
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ // Check if it belongs to a system collection.
+ bool is_system = collections->find_collection((*parent)[FontCollection.name], true);
+
+ if(is_system) {
+ return;
+ }
+
+ collections->remove_font((*parent)[FontCollection.name], row[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+// Function to edit the name of the collection or font.
+void FontCollectionSelector::on_edit_button_pressed()
+{
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+
+ if(selection) {
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if(!iter) {
+ return;
+ }
+
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+ bool is_system = Inkscape::FontCollections::get()->find_collection((*iter)[FontCollection.name], true);
+
+ if(!parent && !is_system) {
+ // It is a collection.
+ treeview->set_cursor(Gtk::TreePath(iter), text_column, true);
+ }
+ }
+}
+
+int FontCollectionSelector::deleltion_warning_message_dialog(const Glib::ustring &collection_name)
+{
+ Glib::ustring message =
+ Glib::ustring::compose(_("Are you sure want to delete the \"%1\" font collection?\n"), collection_name);
+ Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true);
+ dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel()));
+ return dialog.run();
+}
+
+bool FontCollectionSelector::on_key_pressed(GdkEventKey *event)
+{
+ if (event->type == GDK_KEY_PRESS && frame.get_label() == "Collections")
+ {
+ // std::cout << "Key pressed" << std::endl;
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Delete:
+ on_delete_button_pressed();
+ break;
+ }
+ // We handled this event.
+ return true;
+ }
+ // We did not handle this event.
+ return false;
+}
+
+bool FontCollectionSelector::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+ treeview->drag_unhighlight();
+
+ if (path) {
+ context->drag_status(Gdk::ACTION_COPY, time);
+ return false;
+ }
+
+ // remove drop highlight
+ context->drag_refuse(time);
+ return true;
+}
+
+void FontCollectionSelector::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> context,
+ int x,
+ int y,
+ const Gtk::SelectionData &selection_data,
+ guint info, guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_data_received()" << std::endl;
+ // 1. Get the row at which the data is dropped.
+ Gtk::TreePath path;
+ treeview->get_path_at_pos(x, y, path);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ bool is_expanded = false;
+
+ // Case when the font is dragged in the empty space.
+ if(!iter) {
+ return;
+ }
+
+ Glib::ustring collection_name = (*iter)[FontCollection.name];
+ auto font_name = Inkscape::FontLister::get_instance()->get_dragging_family();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = collections->get_collections(true);
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ is_expanded = true;
+ collection_name = (*parent)[FontCollection.name];
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ } else {
+ if (treeview->row_expanded(path)) {
+ is_expanded = true;
+ }
+
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ }
+
+ // 2. Get the data that is sent by the source.
+ // std::cout << "Received: " << selection_data.get_data() << std::endl;
+ // std::cout << (*iter)[FontCollection.name] << std::endl;
+ // Add the font into the collection.
+ collections->add_font(collection_name, font_name);
+
+ // Re-populate the collection.
+ populate_fonts(collection_name);
+
+ // Re-expand this row after re-population.
+ if(is_expanded) {
+ treeview->expand_to_path(path);
+ }
+
+ // Call gtk_drag_finish(context, success, del = false, time)
+ gtk_drag_finish(context->gobj(), TRUE, FALSE, time);
+}
+
+bool FontCollectionSelector::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_drop()" << std::endl;
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ // std::cout << "Not on target\n";
+ return false;
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+/*
+bool FontCollectionSelector::on_drag_failed(const Glib::RefPtr<Gdk::DragContext> &context,
+ const Gtk::DragResult result)
+{
+ std::cout << "Drag Failed\n";
+ return true;
+}
+*/
+
+void FontCollectionSelector::on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &context,
+ guint time)
+{
+ // std::cout << "Drag Leave\n";
+}
+
+/*
+void FontCollectionSelector::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollectionSelector::on_drag_start()" << std::endl;
+}
+*/
+
+void FontCollectionSelector::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollection::on_drag_end()" << std::endl;
+ treeview->drag_unhighlight();
+}
+
+void FontCollectionSelector::on_selection_changed()
+{
+ Glib::RefPtr <Gtk::TreeSelection> selection = treeview->get_selection();
+ if(selection) {
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ auto parent = iter->parent();
+
+ // We use 3 states to adjust the sensitivity of the edit and
+ // delete buttons in the font collections manager dialog.
+ int state = 0;
+
+ // State -1: Selection is a system collection or a system
+ // collection font.(Neither edit nor delete)
+
+ // State 0: It's not a system collection or it's font. But it is
+ // a user collection.(Both edit and delete).
+
+ // State 1: It is a font that belongs to a user collection.
+ // (Only delete)
+
+ if(parent) {
+ // It is a font, and thus it is not editable.
+ // Now check if it's parent is a system collection.
+ bool is_system = font_collections->find_collection((*parent)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION_FONT;
+ } else {
+ // Check if it is a system collection.
+ bool is_system = font_collections->find_collection((*iter)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION;
+ }
+
+ signal_changed.emit(state);
+ }
+}
+
+} // namespace Widget
+} // 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 :