// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Fill style widget. */ /* Authors: * Lauris Kaplinski * Frank Felfe * bulia byak * Jon A. Cruz * Abhishek Sharma * * Copyright (C) 1999-2005 authors * Copyright (C) 2001-2002 Ximian, Inc. * Copyright (C) 2010 Jon A. Cruz * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #define noSP_FS_VERBOSE #include #include #include "desktop-style.h" #include "desktop.h" #include "document-undo.h" #include "fill-n-stroke-factory.h" #include "fill-style.h" #include "gradient-chemistry.h" #include "inkscape.h" #include "selection.h" #include "verbs.h" #include "object/sp-defs.h" #include "object/sp-linear-gradient.h" #include "object/sp-mesh-gradient.h" #include "object/sp-pattern.h" #include "object/sp-radial-gradient.h" #include "object/sp-text.h" #include "style.h" #include "display/sp-canvas.h" #include "widgets/paint-selector.h" // These can be deleted once we sort out the libart dependence. #define ART_WIND_RULE_NONZERO 0 /* Fill */ Gtk::Widget *sp_fill_style_widget_new() { return Inkscape::Widgets::createStyleWidget( FILL ); } namespace Inkscape { class FillNStroke : public Gtk::VBox { public: FillNStroke( FillOrStroke k ); ~FillNStroke() override; void setFillrule( SPPaintSelector::FillRule mode ); void setDesktop(SPDesktop *desktop); private: static void paintModeChangeCB(SPPaintSelector *psel, SPPaintSelector::Mode mode, FillNStroke *self); static void paintChangedCB(SPPaintSelector *psel, FillNStroke *self); static void paintDraggedCB(SPPaintSelector *psel, FillNStroke *self); static gboolean dragDelayCB(gpointer data); static void fillruleChangedCB( SPPaintSelector *psel, SPPaintSelector::FillRule mode, FillNStroke *self ); void selectionModifiedCB(guint flags); void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext); void dragFromPaint(); void updateFromPaint(); void performUpdate(); FillOrStroke kind; SPDesktop *desktop; SPPaintSelector *psel; guint32 lastDrag; guint dragId; bool update; sigc::connection selectChangedConn; sigc::connection subselChangedConn; sigc::connection selectModifiedConn; sigc::connection eventContextConn; }; } // namespace Inkscape void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop) { Inkscape::FillNStroke *fs = dynamic_cast(widget); if (fs) { fs->setDesktop(desktop); } } namespace Inkscape { /** * Create the fill or stroke style widget, and hook up all the signals. */ Gtk::Widget *Inkscape::Widgets::createStyleWidget( FillOrStroke kind ) { FillNStroke *filler = new FillNStroke(kind); return filler; } FillNStroke::FillNStroke( FillOrStroke k ) : Gtk::VBox(), kind(k), desktop(nullptr), psel(nullptr), lastDrag(0), dragId(0), update(false), selectChangedConn(), subselChangedConn(), selectModifiedConn(), eventContextConn() { // Add and connect up the paint selector widget: psel = sp_paint_selector_new(kind); gtk_widget_show(GTK_WIDGET(psel)); gtk_container_add(GTK_CONTAINER(gobj()), GTK_WIDGET(psel)); g_signal_connect( G_OBJECT(psel), "mode_changed", G_CALLBACK(paintModeChangeCB), this ); g_signal_connect( G_OBJECT(psel), "dragged", G_CALLBACK(paintDraggedCB), this ); g_signal_connect( G_OBJECT(psel), "changed", G_CALLBACK(paintChangedCB), this ); if (kind == FILL) { g_signal_connect( G_OBJECT(psel), "fillrule_changed", G_CALLBACK(fillruleChangedCB), this ); } performUpdate(); } FillNStroke::~FillNStroke() { if (dragId) { g_source_remove(dragId); dragId = 0; } psel = nullptr; selectModifiedConn.disconnect(); subselChangedConn.disconnect(); selectChangedConn.disconnect(); eventContextConn.disconnect(); } /** * On signal modified, invokes an update of the fill or stroke style paint object. */ void FillNStroke::selectionModifiedCB( guint flags ) { if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { #ifdef SP_FS_VERBOSE g_message("selectionModifiedCB(%d) on %p", flags, this); #endif performUpdate(); } } void FillNStroke::setDesktop(SPDesktop *desktop) { if (this->desktop != desktop) { if (dragId) { g_source_remove(dragId); dragId = 0; } if (this->desktop) { selectModifiedConn.disconnect(); subselChangedConn.disconnect(); selectChangedConn.disconnect(); eventContextConn.disconnect(); } this->desktop = desktop; if (desktop && desktop->selection) { selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr))); // Must check flags, so can't call performUpdate() directly. selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FillNStroke::selectionModifiedCB))); } performUpdate(); } } /** * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection * is changed back to a selection tool - especially needed for selected gradient stops. */ void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/) { performUpdate(); } /** * Gets the active fill or stroke style property, then sets the appropriate * color, alpha, gradient, pattern, etc. for the paint-selector. * * @param sel Selection to use, or NULL. */ void FillNStroke::performUpdate() { if ( update || !desktop ) { return; } if ( dragId ) { // local change; do nothing, but reset the flag g_source_remove(dragId); dragId = 0; return; } update = true; // create temporary style SPStyle query(desktop->doc()); // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if any, or selection int result = sp_desktop_query_style(desktop, &query, (kind == FILL) ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE); SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast()); switch (result) { case QUERY_STYLE_NOTHING: { /* No paint at all */ psel->setMode(SPPaintSelector::MODE_EMPTY); break; } case QUERY_STYLE_SINGLE: case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere in paint selector case QUERY_STYLE_MULTIPLE_SAME: { SPPaintSelector::Mode pselmode = SPPaintSelector::getModeForStyle(query, kind); psel->setMode(pselmode); if (kind == FILL) { psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO? SPPaintSelector::FILLRULE_NONZERO : SPPaintSelector::FILLRULE_EVENODD); } if (targPaint.set && targPaint.isColor()) { psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value)); } else if (targPaint.set && targPaint.isPaintserver()) { SPPaintServer *server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer(); if (server) { if (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { SPGradient *vector = SP_GRADIENT(server)->getVector(); psel->setSwatch( vector ); } else if (SP_IS_LINEARGRADIENT(server)) { SPGradient *vector = SP_GRADIENT(server)->getVector(); psel->setGradientLinear( vector ); SPLinearGradient *lg = SP_LINEARGRADIENT(server); psel->setGradientProperties( lg->getUnits(), lg->getSpread() ); } else if (SP_IS_RADIALGRADIENT(server)) { SPGradient *vector = SP_GRADIENT(server)->getVector(); psel->setGradientRadial( vector ); SPRadialGradient *rg = SP_RADIALGRADIENT(server); psel->setGradientProperties( rg->getUnits(), rg->getSpread() ); #ifdef WITH_MESH } else if (SP_IS_MESHGRADIENT(server)) { SPGradient *array = SP_GRADIENT(server)->getArray(); psel->setGradientMesh( SP_MESHGRADIENT(array) ); SPMeshGradient *mg = SP_MESHGRADIENT(server); psel->updateMeshList( SP_MESHGRADIENT( array )); #endif } else if (SP_IS_PATTERN(server)) { SPPattern *pat = SP_PATTERN(server)->rootPattern(); psel->updatePatternList( pat ); } } } break; } case QUERY_STYLE_MULTIPLE_DIFFERENT: { psel->setMode(SPPaintSelector::MODE_MULTIPLE); break; } } update = false; } /** * When the mode is changed, invoke a regular changed handler. */ void FillNStroke::paintModeChangeCB( SPPaintSelector * /*psel*/, SPPaintSelector::Mode /*mode*/, FillNStroke *self ) { #ifdef SP_FS_VERBOSE g_message("paintModeChangeCB(psel, mode, self:%p)", self); #endif if (self && !self->update) { self->updateFromPaint(); } } void FillNStroke::fillruleChangedCB( SPPaintSelector * /*psel*/, SPPaintSelector::FillRule mode, FillNStroke *self ) { if (self) { self->setFillrule(mode); } } void FillNStroke::setFillrule( SPPaintSelector::FillRule mode ) { if (!update && desktop) { SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, "fill-rule", (mode == SPPaintSelector::FILLRULE_EVENODD) ? "evenodd":"nonzero"); sp_desktop_set_style(desktop, css); sp_repr_css_attr_unref(css); css = nullptr; DocumentUndo::done(desktop->doc(), SP_VERB_DIALOG_FILL_STROKE, _("Change fill rule")); } } static gchar const *undo_F_label_1 = "fill:flatcolor:1"; static gchar const *undo_F_label_2 = "fill:flatcolor:2"; static gchar const *undo_S_label_1 = "stroke:flatcolor:1"; static gchar const *undo_S_label_2 = "stroke:flatcolor:2"; static gchar const *undo_F_label = undo_F_label_1; static gchar const *undo_S_label = undo_S_label_1; void FillNStroke::paintDraggedCB(SPPaintSelector * /*psel*/, FillNStroke *self) { #ifdef SP_FS_VERBOSE g_message("paintDraggedCB(psel, spw:%p)", self); #endif if (self && !self->update) { self->dragFromPaint(); } } gboolean FillNStroke::dragDelayCB(gpointer data) { gboolean keepGoing = TRUE; if (data) { FillNStroke *self = reinterpret_cast(data); if (!self->update) { if (self->dragId) { g_source_remove(self->dragId); self->dragId = 0; self->dragFromPaint(); self->performUpdate(); } keepGoing = FALSE; } } else { keepGoing = FALSE; } return keepGoing; } /** * This is called repeatedly while you are dragging a color slider, only for flat color * modes. Previously it set the color in style but did not update the repr for efficiency, however * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except * lumps all its changes for undo. */ void FillNStroke::dragFromPaint() { if (!desktop || update) { return; } guint32 when = gtk_get_current_event_time(); // Don't attempt too many updates per second. // Assume a base 15.625ms resolution on the timer. if (!dragId && lastDrag && when && ((when - lastDrag) < 32)) { // local change, do not update from selection dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr); } if (dragId) { // previous local flag not cleared yet; // this means dragged events come too fast, so we better skip this one to speed up display // (it's safe to do this in any case) return; } lastDrag = when; update = true; switch (psel->mode) { case SPPaintSelector::MODE_SOLID_COLOR: { // local change, do not update from selection dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr); psel->setFlatColor( desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); DocumentUndo::maybeDone(desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Set fill color") : _("Set stroke color")); break; } default: g_warning( "file %s: line %d: Paint %d should not emit 'dragged'", __FILE__, __LINE__, psel->mode ); break; } update = false; } /** This is called (at least) when: 1 paint selector mode is switched (e.g. flat color -> gradient) 2 you finished dragging a gradient node and released mouse 3 you changed a gradient selector parameter (e.g. spread) Must update repr. */ void FillNStroke::paintChangedCB( SPPaintSelector * /*psel*/, FillNStroke *self ) { #ifdef SP_FS_VERBOSE g_message("paintChangedCB(psel, spw:%p)", self); #endif if (self && !self->update) { self->updateFromPaint(); } } void FillNStroke::updateFromPaint() { if (!desktop) { return; } update = true; SPDocument *document = desktop->getDocument(); Inkscape::Selection *selection = desktop->getSelection(); std::vector const items(selection->items().begin(), selection->items().end()); switch (psel->mode) { case SPPaintSelector::MODE_EMPTY: // This should not happen. g_warning( "file %s: line %d: Paint %d should not emit 'changed'", __FILE__, __LINE__, psel->mode); break; case SPPaintSelector::MODE_MULTIPLE: // This happens when you switch multiple objects with different gradients to flat color; // nothing to do here. break; case SPPaintSelector::MODE_NONE: { SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none"); sp_desktop_set_style(desktop, css); sp_repr_css_attr_unref(css); css = nullptr; DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Remove fill") : _("Remove stroke")); break; } case SPPaintSelector::MODE_SOLID_COLOR: { if (kind == FILL) { // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in losing release events desktop->getCanvas()->forceFullRedrawAfterInterruptions(0); } psel->setFlatColor( desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); DocumentUndo::maybeDone(desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Set fill color") : _("Set stroke color")); if (kind == FILL) { // resume interruptibility desktop->getCanvas()->endForcedFullRedraws(); } // on release, toggle undo_label so that the next drag will not be lumped with this one if (undo_F_label == undo_F_label_1) { undo_F_label = undo_F_label_2; undo_S_label = undo_S_label_2; } else { undo_F_label = undo_F_label_1; undo_S_label = undo_S_label_1; } break; } case SPPaintSelector::MODE_GRADIENT_LINEAR: case SPPaintSelector::MODE_GRADIENT_RADIAL: case SPPaintSelector::MODE_SWATCH: if (!items.empty()) { SPGradientType const gradient_type = ( psel->mode != SPPaintSelector::MODE_GRADIENT_RADIAL ? SP_GRADIENT_TYPE_LINEAR : SP_GRADIENT_TYPE_RADIAL ); bool createSwatch = (psel->mode == SPPaintSelector::MODE_SWATCH); SPCSSAttr *css = nullptr; if (kind == FILL) { // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, "fill-opacity", "1.0"); } SPGradient *vector = psel->getGradientVector(); if (!vector) { /* No vector in paint selector should mean that we just changed mode */ SPStyle query(desktop->doc()); int result = objects_query_fillstroke(items, &query, kind == FILL); if (result == QUERY_STYLE_MULTIPLE_SAME) { SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); SPColor common; if (!targPaint.isColor()) { common = sp_desktop_get_color(desktop, kind == FILL); } else { common = targPaint.value.color; } vector = sp_document_default_gradient_vector( document, common, createSwatch ); if ( vector && createSwatch ) { vector->setSwatch(); } } for(auto item : items){ //FIXME: see above if (kind == FILL) { sp_repr_css_change_recursive(item->getRepr(), css, "style"); } if (!vector) { SPGradient *gr = sp_gradient_vector_for_object( document, desktop, reinterpret_cast(item), (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, createSwatch ); if ( gr && createSwatch ) { gr->setSwatch(); } sp_item_set_gradient(item, gr, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); } else { sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); } } } else { // We have changed from another gradient type, or modified spread/units within // this gradient type. vector = sp_gradient_ensure_vector_normalized(vector); for(auto item : items){ //FIXME: see above if (kind == FILL) { sp_repr_css_change_recursive(item->getRepr(), css, "style"); } SPGradient *gr = sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); psel->pushAttrsToGradient( gr ); } } if (css) { sp_repr_css_attr_unref(css); css = nullptr; } DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke")); } break; #ifdef WITH_MESH case SPPaintSelector::MODE_GRADIENT_MESH: if (!items.empty()) { SPGradientType const gradient_type = SP_GRADIENT_TYPE_MESH; SPCSSAttr *css = nullptr; if (kind == FILL) { // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, "fill-opacity", "1.0"); } Inkscape::XML::Document *xml_doc = document->getReprDoc(); SPDefs *defs = document->getDefs(); SPMeshGradient * mesh = psel->getMeshGradient(); for(auto item : items){ //FIXME: see above if (kind == FILL) { sp_repr_css_change_recursive(item->getRepr(), css, "style"); } // Check if object already has mesh. bool has_mesh = false; SPStyle *style = item->style; if (style) { SPPaintServer *server = (kind==FILL) ? style->getFillPaintServer():style->getStrokePaintServer(); if (server && SP_IS_MESHGRADIENT(server)) has_mesh = true; } if (!mesh || !has_mesh) { // No mesh in document or object does not already have mesh -> // Create new mesh. // Create mesh element Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); // privates are garbage-collectable repr->setAttribute("inkscape:collect", "always"); // Attach to document defs->getRepr()->appendChild(repr); Inkscape::GC::release(repr); // Get corresponding object SPMeshGradient *mg = static_cast(document->getObjectByRepr(repr)); mg->array.create(mg, item, (kind==FILL) ? item->geometricBounds() : item->visualBounds()); bool isText = SP_IS_TEXT(item); sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), mg, isText); // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); } else { // Using found mesh // Duplicate Inkscape::XML::Node *mesh_repr = mesh->getRepr(); Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc); // privates are garbage-collectable copy_repr->setAttribute("inkscape:collect", "always"); // Attach to document defs->getRepr()->appendChild(copy_repr); Inkscape::GC::release(copy_repr); // Get corresponding object SPMeshGradient *mg = static_cast(document->getObjectByRepr(copy_repr)); // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl; mg->array.read(mg); Geom::OptRect item_bbox = (kind==FILL) ? item->geometricBounds() : item->visualBounds(); mg->array.fill_box( item_bbox ); bool isText = SP_IS_TEXT(item); sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), mg, isText); } } if (css) { sp_repr_css_attr_unref(css); css = nullptr; } DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke")); } break; #endif case SPPaintSelector::MODE_PATTERN: if (!items.empty()) { SPPattern *pattern = psel->getPattern(); if (!pattern) { /* No Pattern in paint selector should mean that we just * changed mode - don't do jack. */ } else { Inkscape::XML::Node *patrepr = pattern->getRepr(); SPCSSAttr *css = sp_repr_css_attr_new(); gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id")); sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext); // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs if (kind == FILL) { sp_repr_css_set_property(css, "fill-opacity", "1.0"); } // cannot just call sp_desktop_set_style, because we don't want to touch those // objects who already have the same root pattern but through a different href // chain. FIXME: move this to a sp_item_set_pattern for(auto item : items){ Inkscape::XML::Node *selrepr = item->getRepr(); if ( (kind == STROKE) && !selrepr) { continue; } SPObject *selobj = item; SPStyle *style = selobj->style; if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) { SPPaintServer *server = (kind == FILL) ? selobj->style->getFillPaintServer() : selobj->style->getStrokePaintServer(); if (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == pattern) // only if this object's pattern is not rooted in our selected pattern, apply continue; } if (kind == FILL) { sp_desktop_apply_css_recursive(selobj, css, true); } else { sp_repr_css_change_recursive(selrepr, css, "style"); } } sp_repr_css_attr_unref(css); css = nullptr; g_free(urltext); } // end if DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Set pattern on fill") : _("Set pattern on stroke")); } // end if break; case SPPaintSelector::MODE_UNSET: if (!items.empty()) { SPCSSAttr *css = sp_repr_css_attr_new(); if (kind == FILL) { sp_repr_css_unset_property(css, "fill"); } else { sp_repr_css_unset_property(css, "stroke"); sp_repr_css_unset_property(css, "stroke-opacity"); sp_repr_css_unset_property(css, "stroke-width"); sp_repr_css_unset_property(css, "stroke-miterlimit"); sp_repr_css_unset_property(css, "stroke-linejoin"); sp_repr_css_unset_property(css, "stroke-linecap"); sp_repr_css_unset_property(css, "stroke-dashoffset"); sp_repr_css_unset_property(css, "stroke-dasharray"); } sp_desktop_set_style(desktop, css); sp_repr_css_attr_unref(css); css = nullptr; DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, (kind == FILL) ? _("Unset fill") : _("Unset stroke")); } break; default: g_warning( "file %s: line %d: Paint selector should not be in " "mode %d", __FILE__, __LINE__, psel->mode ); break; } update = false; } } // 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:textwidth=99 :