Code

Fix multiple minor problems in the node tool
authorKrzysztof Kosiński <tweenk.pl@gmail.com>
Tue, 9 Feb 2010 02:20:18 +0000 (03:20 +0100)
committerKrzysztof Kosiński <tweenk.pl@gmail.com>
Tue, 9 Feb 2010 02:20:18 +0000 (03:20 +0100)
src/selection-chemistry.cpp
src/ui/tool/control-point-selection.cpp
src/ui/tool/control-point-selection.h
src/ui/tool/curve-drag-point.cpp
src/ui/tool/curve-drag-point.h
src/ui/tool/node-tool.cpp
src/ui/tool/node-tool.h
src/ui/tool/node.cpp
src/ui/tool/node.h
src/ui/tool/path-manipulator.cpp
src/ui/tool/selector.cpp

index a5c6ae961609b839a04981a907df68585f8adfa0..4dfb4597d0f840f604f95794d22b9a09d812a4cd 100644 (file)
@@ -118,10 +118,12 @@ void SelectionHelper::selectAll(SPDesktop *dt)
 {
     if (tools_isactive(dt, TOOLS_NODES)) {
         InkNodeTool *nt = static_cast<InkNodeTool*>(dt->event_context);
-        nt->_multipath->selectSubpaths();
-    } else {
-        sp_edit_select_all(dt);
+        if (!nt->_multipath->empty()) {
+            nt->_multipath->selectSubpaths();
+            return;
+        }
     }
+    sp_edit_select_all(dt);
 }
 
 void SelectionHelper::selectAllInAll(SPDesktop *dt)
index 638318dbfcacf4992edff39904bafea663679573..df27c2a72ad1929b3654d042e235493919d85e51 100644 (file)
@@ -8,6 +8,7 @@
  * Released under GNU GPL, read the file 'COPYING' for more information
  */
 
+#include <boost/none.hpp>
 #include <2geom/transforms.h>
 #include "desktop.h"
 #include "preferences.h"
@@ -53,15 +54,13 @@ ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_gro
     signal_update.connect( sigc::bind(
         sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
         true));
-    signal_point_changed.connect(
-        sigc::hide( sigc::hide(
-            sigc::bind(
-                sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles),
-                false))));
+    ControlPoint::signal_mouseover_change.connect(
+        sigc::hide(
+            sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged)));
     _handles->signal_transform.connect(
         sigc::mem_fun(*this, &ControlPointSelection::transform));
     _handles->signal_commit.connect(
-        sigc::mem_fun(*this, &ControlPointSelection::_commitTransform));
+        sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform));
 }
 
 ControlPointSelection::~ControlPointSelection()
@@ -81,8 +80,7 @@ std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(c
     found = _points.insert(x).first;
 
     x->updateState();
-    _rot_radius.reset();
-    signal_point_changed.emit(x, true);
+    _pointChanged(x, true);
 
     return std::pair<iterator, bool>(found, true);
 }
@@ -93,8 +91,7 @@ void ControlPointSelection::erase(iterator pos)
     SelectableControlPoint *erased = *pos;
     _points.erase(pos);
     erased->updateState();
-    _rot_radius.reset();
-    signal_point_changed.emit(erased, false);
+    _pointChanged(erased, false);
 }
 ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k)
 {
@@ -175,8 +172,10 @@ void ControlPointSelection::transform(Geom::Matrix const &m)
         SelectableControlPoint *cur = *i;
         cur->transform(m);
     }
+    _updateBounds();
     // TODO preserving the rotation radius needs some rethinking...
     if (_rot_radius) (*_rot_radius) *= m.descrim();
+    if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim();
     signal_update.emit();
 }
 
@@ -233,28 +232,12 @@ void ControlPointSelection::distribute(Geom::Dim2 d)
  *         or nothing if the selection is empty */
 Geom::OptRect ControlPointSelection::pointwiseBounds()
 {
-    Geom::OptRect bound;
-    for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = (*i);
-        Geom::Point p = cur->position();
-        if (!bound) {
-            bound = Geom::Rect(p, p);
-        } else {
-            bound->expandTo(p);
-        }
-    }
-    return bound;
+    return _bounds;
 }
 
 Geom::OptRect ControlPointSelection::bounds()
 {
-    Geom::OptRect bound;
-    for (iterator i = _points.begin(); i != _points.end(); ++i) {
-        SelectableControlPoint *cur = (*i);
-        Geom::OptRect r = cur->bounds();
-        bound.unionWith(r);
-    }
-    return bound;
+    return size() == 1 ? (*_points.begin())->bounds() : _bounds;
 }
 
 void ControlPointSelection::showTransformHandles(bool v, bool one_node)
@@ -305,6 +288,7 @@ void ControlPointSelection::_pointUngrabbed()
 {
     _dragging = false;
     _grabbed_point = NULL;
+    _updateBounds();
     restoreTransformHandles();
     signal_commit.emit(COMMIT_MOUSE_MOVE);
 }
@@ -320,13 +304,42 @@ bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventBut
     return false;
 }
 
+void ControlPointSelection::_pointChanged(SelectableControlPoint *p, bool selected)
+{
+    _updateBounds();
+    _updateTransformHandles(false);
+    if (_bounds)
+        _handles->rotationCenter().move(_bounds->midpoint());
+
+    signal_point_changed.emit(p, selected);
+}
+
+void ControlPointSelection::_mouseoverChanged()
+{
+    _mouseover_rot_radius = boost::none;
+}
+
+void ControlPointSelection::_updateBounds()
+{
+    _rot_radius = boost::none;
+    _bounds = Geom::OptRect();
+    for (iterator i = _points.begin(); i != _points.end(); ++i) {
+        SelectableControlPoint *cur = (*i);
+        Geom::Point p = cur->position();
+        if (!_bounds) {
+            _bounds = Geom::Rect(p, p);
+        } else {
+            _bounds->expandTo(p);
+        }
+    }
+}
+
 void ControlPointSelection::_updateTransformHandles(bool preserve_center)
 {
     if (_dragging) return;
 
     if (_handles_visible && size() > 1) {
-        Geom::OptRect b = pointwiseBounds();
-        _handles->setBounds(*b, preserve_center);
+        _handles->setBounds(*bounds(), preserve_center);
         _handles->setVisible(true);
     } else if (_one_node_handles && size() == 1) { // only one control point in selection
         SelectableControlPoint *p = *begin();
@@ -365,6 +378,20 @@ bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point
     return true;
 }
 
+/** @brief Computes the distance to the farthest corner of the bounding box.
+ * Used to determine what it means to "rotate by one pixel". */
+double ControlPointSelection::_rotationRadius(Geom::Point const &rc)
+{
+    if (empty()) return 1.0; // some safe value
+    Geom::Rect b = *bounds();
+    double maxlen = 0;
+    for (unsigned i = 0; i < 4; ++i) {
+        double len = Geom::distance(b.corner(i), rc);
+        if (len > maxlen) maxlen = len;
+    }
+    return maxlen;
+}
+
 /** Rotates the selected points in the given direction according to the modifier state
  * from the supplied event.
  * @param event Key event to take modifier state from
@@ -374,15 +401,25 @@ bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
 {
     if (empty()) return false;
 
-    Geom::Point rc = _handles->rotationCenter();
-    if (!_rot_radius) {
-        Geom::Rect b = *(size() == 1 ? bounds() : pointwiseBounds());
-        double maxlen = 0;
-        for (unsigned i = 0; i < 4; ++i) {
-            double len = (b.corner(i) - rc).length();
-            if (len > maxlen) maxlen = len;
+    Geom::Point rc;
+
+    // rotate around the mouseovered point, or the selection's rotation center
+    // if nothing is mouseovered
+    double radius;
+    SelectableControlPoint *scp =
+        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+    if (scp) {
+        rc = scp->position();
+        if (!_mouseover_rot_radius) {
+            _mouseover_rot_radius = _rotationRadius(rc);
+        }
+        radius = *_mouseover_rot_radius;
+    } else {
+        rc = _handles->rotationCenter();
+        if (!_rot_radius) {
+            _rot_radius = _rotationRadius(rc);
         }
-        _rot_radius = maxlen;
+        radius = *_rot_radius;
     }
 
     double angle;
@@ -390,7 +427,7 @@ bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir)
         // Rotate by "one pixel". We interpret this as rotating by an angle that causes
         // the topmost point of a circle circumscribed about the selection's bounding box
         // to move on an arc 1 screen pixel long.
-        angle = atan2(1.0 / _desktop->current_zoom(), *_rot_radius) * dir;
+        angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir;
     } else {
         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
         int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
@@ -410,11 +447,17 @@ bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir)
 {
     if (empty()) return false;
 
-    // TODO should the saved rotation center or the current center be used?
-    Geom::Rect bound = (size() == 1 ? *bounds() : *pointwiseBounds());
-    double maxext = bound.maxExtent();
+    double maxext = bounds()->maxExtent();
     if (Geom::are_near(maxext, 0)) return false;
-    Geom::Point center = _handles->rotationCenter().position();
+
+    Geom::Point center;
+    SelectableControlPoint *scp =
+        dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point);
+    if (scp) {
+        center = scp->position();
+    } else {
+        center = _handles->rotationCenter().position();
+    }
 
     double length_change;
     if (held_alt(event)) {
@@ -455,8 +498,9 @@ bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d)
     return true;
 }
 
-void ControlPointSelection::_commitTransform(CommitEvent ce)
+void ControlPointSelection::_commitHandlesTransform(CommitEvent ce)
 {
+    _updateBounds();
     _updateTransformHandles(true);
     signal_commit.emit(ce);
 }
index 93fba56f55be5b0b24269e1f4f04a9c4be573ef2..48c25c2852ef62736e5914035734c608827bfc55 100644 (file)
@@ -19,6 +19,7 @@
 #include <sigc++/sigc++.h>
 #include <2geom/forward.h>
 #include <2geom/point.h>
+#include <2geom/rect.h>
 #include "display/display-forward.h"
 #include "util/accumulators.h"
 #include "util/hash.h"
@@ -96,6 +97,7 @@ public:
     Geom::OptRect pointwiseBounds();
     Geom::OptRect bounds();
 
+    bool transformHandlesEnabled() { return _handles_visible; }
     void showTransformHandles(bool v, bool one_node);
     // the two methods below do not modify the state; they are for use in manipulators
     // that need to temporarily hide the handles, for example when moving a node
@@ -114,17 +116,24 @@ private:
     void _pointDragged(Geom::Point const &, Geom::Point &, GdkEventMotion *);
     void _pointUngrabbed();
     bool _pointClicked(SelectableControlPoint *, GdkEventButton *);
+    void _pointChanged(SelectableControlPoint *, bool);
+    void _mouseoverChanged();
 
     void _updateTransformHandles(bool preserve_center);
+    void _updateBounds();
     bool _keyboardMove(GdkEventKey const &, Geom::Point const &);
     bool _keyboardRotate(GdkEventKey const &, int);
     bool _keyboardScale(GdkEventKey const &, int);
     bool _keyboardFlip(Geom::Dim2);
     void _keyboardTransform(Geom::Matrix const &);
-    void _commitTransform(CommitEvent ce);
+    void _commitHandlesTransform(CommitEvent ce);
+    double _rotationRadius(Geom::Point const &);
+
     set_type _points;
     set_type _all_points;
     boost::optional<double> _rot_radius;
+    boost::optional<double> _mouseover_rot_radius;
+    Geom::OptRect _bounds;
     TransformHandleSet *_handles;
     SelectableControlPoint *_grabbed_point;
     unsigned _dragging         : 1;
index 88cb72ed5698d8a952e0b1fac165ce71cea0054c..e761daf20c82e8cda27d10bc77248ad9abdf059f 100644 (file)
@@ -44,6 +44,16 @@ CurveDragPoint::CurveDragPoint(PathManipulator &pm)
     setVisible(false);
 }
 
+bool CurveDragPoint::_eventHandler(GdkEvent *event)
+{
+    // do not process any events when the manipulator is empty
+    if (_pm.empty()) {
+        setVisible(false);
+        return false;
+    }
+    return ControlPoint::_eventHandler(event);
+}
+
 bool CurveDragPoint::grabbed(GdkEventMotion */*event*/)
 {
     _pm._selection.hideTransformHandles();
@@ -149,6 +159,7 @@ void CurveDragPoint::_insertNode(bool take_selection)
 
 Glib::ustring CurveDragPoint::_getTip(unsigned state)
 {
+    if (_pm.empty()) return "";
     if (!first || !first.next()) return "";
     bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate();
     if (state_held_shift(state)) {
@@ -162,11 +173,11 @@ Glib::ustring CurveDragPoint::_getTip(unsigned state)
     if (linear) {
         return C_("Path segment tip",
             "<b>Linear segment:</b> drag to convert to a Bezier segment, "
-            "doubleclick to insert node, click to select");
+            "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)");
     } else {
         return C_("Path segment tip",
             "<b>Bezier segment:</b> drag to shape the segment, doubleclick to insert node, "
-            "click to select");
+            "click to select (more: Shift, Ctrl+Alt)");
     }
 }
 
index 147a918371c480dfe8f2153fdbc8c41bb38c5153..288ae6a8e11973e5097a38d59de11a8e6185cb84 100644 (file)
@@ -27,15 +27,16 @@ public:
     void setSize(double sz) { _setSize(sz); }
     void setTimeValue(double t) { _t = t; }
     void setIterator(NodeList::iterator i) { first = i; }
+    virtual bool _eventHandler(GdkEvent *event);
 protected:
     virtual Glib::ustring _getTip(unsigned state);
-private:
     virtual void dragged(Geom::Point &, GdkEventMotion *);
     virtual bool grabbed(GdkEventMotion *);
     virtual void ungrabbed(GdkEventButton *);
     virtual bool clicked(GdkEventButton *);
     virtual bool doubleclicked(GdkEventButton *);
 
+private:
     void _insertNode(bool take_selection);
     double _t;
     PathManipulator &_pm;
index 0c4599e52f5423b7d558cdc59319bd77c37598e3..663504b52491a8caed818a0c77240aa4b9891b39 100644 (file)
@@ -280,7 +280,9 @@ void ink_node_tool_setup(SPEventContext *ec)
     nt->single_node_transform_handles = false;
     nt->flash_tempitem = NULL;
     nt->flashed_item = NULL;
-    // TODO remove this!
+    nt->_last_over = NULL;
+    // TODO long term, fold ShapeEditor into MultiPathManipulator and rename MPM
+    // to something better
     nt->shape_editor = new ShapeEditor(nt->desktop);
 
     // read prefs before adding items to selection to prevent momentarily showing the outline
@@ -442,11 +444,17 @@ gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event)
 
     switch (event->type)
     {
-    case GDK_MOTION_NOTIFY:
-        // create outline
-        if (prefs->getBool("/tools/nodes/pathflash_enabled")) {
-            SPItem *over_item = sp_event_context_find_item (desktop, event_point(event->button),
+    case GDK_MOTION_NOTIFY: {
+        combine_motion_events(desktop->canvas, event->motion, 0);
+        SPItem *over_item = sp_event_context_find_item (desktop, event_point(event->button),
                 FALSE, TRUE);
+        if (over_item != nt->_last_over) {
+            nt->_last_over = over_item;
+            ink_node_tool_update_tip(nt, event);
+        }
+
+        // create pathflash outline
+        if (prefs->getBool("/tools/nodes/pathflash_enabled")) {
             if (over_item == nt->flashed_item) break;
             if (!prefs->getBool("/tools/nodes/pathflash_selected") && selection->includes(over_item)) break;
             if (nt->flash_tempitem) {
@@ -468,7 +476,7 @@ gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event)
                 prefs->getInt("/tools/nodes/pathflash_timeout", 500));
             c->unref();
         }
-        return true;
+        return true;
     case GDK_KEY_PRESS:
         switch (get_group0_keyval(&event->key))
         {
@@ -481,14 +489,9 @@ gint ink_node_tool_root_handler(SPEventContext *event_context, GdkEvent *event)
             ink_node_tool_update_tip(nt, event);
             return TRUE;
         case GDK_a:
-            if (held_control(event->key)) {
-                if (held_alt(event->key)) {
-                    nt->_selected_nodes->selectAll();
-                } else {
-                    // select all nodes in subpaths that have something selected
-                    // if nothing is selected, select everything
-                    nt->_multipath->selectSubpaths();
-                }
+            if (held_control(event->key) && held_alt(event->key)) {
+                nt->_selected_nodes->selectAll();
+                // Ctrl+A is handled in selection-chemistry.cpp via verb
                 ink_node_tool_update_tip(nt, event);
                 return TRUE;
             }
@@ -517,26 +520,49 @@ void ink_node_tool_update_tip(InkNodeTool *nt, GdkEvent *event)
         unsigned new_state = state_after_event(event);
         if (new_state == event->key.state) return;
         if (state_held_shift(new_state)) {
-            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
-                C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection, "
-                "click to toggle object selection"));
+            if (nt->_last_over) {
+                nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
+                    C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection, "
+                    "click to toggle object selection"));
+            } else {
+                nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
+                    C_("Node tool tip", "<b>Shift:</b> drag to add nodes to the selection"));
+            }
             return;
         }
     }
     unsigned sz = nt->_selected_nodes->size();
+    unsigned total = nt->_selected_nodes->allPoints().size();
     if (sz != 0) {
-        char *dyntip = g_strdup_printf(C_("Node tool tip",
-            "Selected <b>%d nodes</b>. Drag to select nodes, click to select a single object "
-            "or unselect all objects"), sz);
-        nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
-        g_free(dyntip);
-    } else if (nt->_multipath->empty()) {
-        nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
-            C_("Node tool tip", "Drag or click to select objects to edit"));
+        if (nt->_last_over) {
+            char *dyntip = g_strdup_printf(C_("Node tool tip",
+                "<b>%u of %u nodes</b> selected. "
+                "Drag to select nodes, click to edit only this object (more: Shift)"), sz, total);
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+            g_free(dyntip);
+        } else {
+            char *dyntip = g_strdup_printf(C_("Node tool tip",
+                "<b>%u of %u nodes</b> selected. "
+                "Drag to select nodes, click clear the selection"), sz, total);
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, dyntip);
+            g_free(dyntip);
+        }
+    } else if (!nt->_multipath->empty()) {
+        if (nt->_last_over) {
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+                "Drag to select nodes, click to edit only this object"));
+        } else {
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+                "Drag to select nodes, click to clear the selection"));
+        }
     } else {
-        nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE,
-            C_("Node tool tip", "Drag to select nodes, click to select an object "
-            "or clear the selection"));
+        if (nt->_last_over) {
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+                "Drag to select objects to edit, click to edit this object (more: Shift)"));
+        } else {
+            nt->_node_message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip",
+                "Drag to select objects to edit"));
+        }
     }
 }
 
index 130e1619815028253c3300ad82e2df2358556ce9..baac642ac97243aa61d47273e5aad004e349f034 100644 (file)
@@ -55,6 +55,7 @@ struct InkNodeTool : public SPEventContext
     SelectorPtr _selector;
     PathSharedDataPtr _path_data;
     SPCanvasGroup *_transform_handle_group;
+    SPItem *_last_over;
 
     unsigned cursor_drag : 1;
     unsigned show_outline : 1;
index 9f1e25877254214e172202eabfa5174501dac323..09ca99c6e1f4b76277f9b3a8dad4af2bd307aa29 100644 (file)
@@ -77,6 +77,7 @@ static Geom::Point direction(Geom::Point const &first, Geom::Point const &second
  * Keeping the invariant on node moves is left to the %Node class.
  */
 
+Geom::Point Handle::_saved_other_pos(0, 0);
 double Handle::_saved_length = 0.0;
 bool Handle::_drag_out = false;
 
@@ -224,6 +225,7 @@ char const *Handle::handle_type_to_localized_string(NodeType type)
 
 bool Handle::grabbed(GdkEventMotion *)
 {
+    _saved_other_pos = other().position();
     _saved_length = _drag_out ? 0 : length();
     _pm()._handleGrabbed();
     return false;
@@ -232,23 +234,45 @@ bool Handle::grabbed(GdkEventMotion *)
 void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event)
 {
     Geom::Point parent_pos = _parent->position();
+    Geom::Point origin = _last_drag_origin();
     // with Alt, preserve length
     if (held_alt(*event)) {
         new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length;
     }
-    // with Ctrl, constrain to M_PI/rotationsnapsperpi increments.
+    // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical
+    // and the original position.
     if (held_control(*event)) {
         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
         int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
-        Geom::Point origin = _last_drag_origin();
-        Geom::Point rel_origin = origin - parent_pos;
-        new_pos = parent_pos + Geom::constrain_angle(Geom::Point(0,0), new_pos - parent_pos, snaps,
-            _drag_out ? Geom::Point(1,0) : Geom::unit_vector(rel_origin));
+
+        // note: if snapping to the original position is only desired in the original
+        // direction of the handle, change 2nd line below to Ray instead of Line
+        Geom::Line original_line(parent_pos, origin);
+        Geom::Point snap_pos = parent_pos + Geom::constrain_angle(
+            Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0));
+        Geom::Point orig_pos = original_line.pointAt(original_line.nearestPoint(new_pos));
+
+        if (Geom::distance(snap_pos, new_pos) < Geom::distance(orig_pos, new_pos)) {
+            new_pos = snap_pos;
+        } else {
+            new_pos = orig_pos;
+        }
+    }
+    // with Shift, if the node is cusp, rotate the other handle as well
+    if (_parent->type() == NODE_CUSP && !_drag_out) {
+        if (held_shift(*event)) {
+            Geom::Point other_relpos = _saved_other_pos - parent_pos;
+            other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos));
+            other().setRelativePos(other_relpos);
+        } else {
+            // restore the position
+            other().setPosition(_saved_other_pos);
+        }
     }
     _pm().update();
 }
 
-void Handle::ungrabbed(GdkEventButton *)
+void Handle::ungrabbed(GdkEventButton *event)
 {
     // hide the handle if it's less than dragtolerance away from the node
     // TODO is this actually desired?
@@ -259,6 +283,12 @@ void Handle::ungrabbed(GdkEventButton *)
     if (dist.length() <= drag_tolerance) {
         move(_parent->position());
     }
+
+    // HACK: If the handle was dragged out, call parent's ungrabbed handler,
+    // so that transform handles reappear
+    if (_drag_out) {
+        _parent->ungrabbed(event);
+    }
     _drag_out = false;
 
     _pm()._handleUngrabbed();
@@ -270,6 +300,12 @@ bool Handle::clicked(GdkEventButton *event)
     return true;
 }
 
+Handle &Handle::other()
+{
+    if (this == &_parent->_front) return _parent->_back;
+    return _parent->_front;
+}
+
 static double snap_increment_degrees() {
     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
     int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000);
@@ -278,29 +314,59 @@ static double snap_increment_degrees() {
 
 Glib::ustring Handle::_getTip(unsigned state)
 {
+    char const *more;
+    bool can_shift_rotate = _parent->type() == NODE_CUSP && !other().isDegenerate();
+    if (can_shift_rotate) {
+        more = C_("Path handle tip", "more: Ctrl, Alt, Ctrl+Alt, Shift");
+    } else {
+        more = C_("Path handle tip", "more: Ctrl, Alt, Ctrl+Alt");
+    }
     if (state_held_alt(state)) {
         if (state_held_control(state)) {
-            return format_tip(C_("Path handle tip",
-                "<b>Ctrl+Alt</b>: preserve length and snap rotation angle to %f° increments"),
-                snap_increment_degrees());
+            if (state_held_shift(state) && can_shift_rotate) {
+                return format_tip(C_("Path handle tip",
+                    "<b>Shift+Ctrl+Alt</b>: preserve length and snap rotation angle to %f° "
+                    "increments while rotating both handles"),
+                    snap_increment_degrees());
+            } else {
+                return format_tip(C_("Path handle tip",
+                    "<b>Ctrl+Alt</b>: preserve length and snap rotation angle to %f° increments"),
+                    snap_increment_degrees());
+            }
         } else {
-            return C_("Path handle tip",
-                "<b>Alt:</b> preserve handle length while dragging");
+            if (state_held_shift(state) && can_shift_rotate) {
+                return C_("Path handle tip",
+                    "<b>Shift+Alt:</b> preserve handle length and rotate both handles");
+            } else {
+                return C_("Path handle tip",
+                    "<b>Alt:</b> preserve handle length while dragging");
+            }
         }
     } else {
         if (state_held_control(state)) {
-            return format_tip(C_("Path handle tip",
-                "<b>Ctrl:</b> snap rotation angle to %f° increments, click to retract"),
-                snap_increment_degrees());
+            if (state_held_shift(state) && can_shift_rotate) {
+                return format_tip(C_("Path handle tip",
+                    "<b>Ctrl:</b> snap rotation angle to %f° increments, click to retract"),
+                    snap_increment_degrees());
+            } else {
+                return format_tip(C_("Path handle tip",
+                    "<b>Shift+Ctrl:</b> snap rotation angle to %f° increments and rotate both handles"),
+                    snap_increment_degrees());
+            }
+        } else if (state_held_shift(state) && can_shift_rotate) {
+            return C_("Path hande tip",
+                "<b>Shift</b>: rotate both handles by the same angle");
         }
     }
+
     switch (_parent->type()) {
     case NODE_AUTO:
-        return C_("Path handle tip",
-            "<b>Auto node handle:</b> drag to convert to smooth node");
+        return format_tip(C_("Path handle tip",
+            "<b>Auto node handle:</b> drag to convert to smooth node (%s)"), more);
     default:
-        return format_tip(C_("Path handle tip", "<b>%s:</b> drag to shape the curve"),
-            handle_type_to_localized_string(_parent->type()));
+        return format_tip(C_("Path handle tip",
+            "<b>%s:</b> drag to shape the segment (%s)"),
+            handle_type_to_localized_string(_parent->type()), more);
     }
 }
 
@@ -497,7 +563,16 @@ void Node::setType(NodeType type, bool update_handles)
             // for degenerate nodes set positions like auto handles
             bool prev_line = _is_line_segment(_prev(), this);
             bool next_line = _is_line_segment(this, _next());
-            if (isDegenerate()) {
+            if (_type == NODE_SMOOTH) {
+                // for a node that is already smooth and at the end of a linear segment,
+                // drag out the second handle to 1/3 the length of the linear segment
+                if (next_line) {
+                    _front.setRelativePos((_prev()->position() - position()) / 3);
+                }
+                if (prev_line) {
+                    _back.setRelativePos((_next()->position() - position()) / 3);
+                }
+            } else if (isDegenerate()) {
                 _updateAutoHandles();
             } else if (_front.isDegenerate()) {
                 // if the front handle is degenerate and this...next is a line segment,
@@ -507,14 +582,14 @@ void Node::setType(NodeType type, bool update_handles)
                     _back.setDirection(*_next(), *this);
                 } else if (_prev()) {
                     Geom::Point dir = direction(_back, *this);
-                    _front.setRelativePos((_prev()->position() - position()).length() / 3 * dir);
+                    _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir);
                 }
             } else if (_back.isDegenerate()) {
                 if (prev_line) {
                     _front.setDirection(*_prev(), *this);
                 } else if (_next()) {
                     Geom::Point dir = direction(_front, *this);
-                    _back.setRelativePos((_next()->position() - position()).length() / 3 * dir);
+                    _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir);
                 }
             } else {
                 // both handles are extended. make colinear while keeping length
@@ -875,13 +950,35 @@ void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event)
         if (held_alt(*event)) {
             // with Ctrl+Alt, constrain to handle lines
             // project the new position onto a handle line that is closer
-            Inkscape::Snapper::ConstraintLine line_front(origin, _front.relativePos());
-            Inkscape::Snapper::ConstraintLine line_back(origin, _back.relativePos());
+            boost::optional<Geom::Point> front_point, back_point;
+            boost::optional<Inkscape::Snapper::ConstraintLine> line_front, line_back;
+            if (_front.isDegenerate()) {
+                if (_is_line_segment(this, _next()))
+                    front_point = _next()->position() - origin;
+            } else {
+                front_point = _front.relativePos();
+            }
+            if (_back.isDegenerate()) {
+                if (_is_line_segment(_prev(), this))
+                    back_point = _prev()->position() - origin;
+            } else {
+                back_point = _back.relativePos();
+            }
+            if (front_point)
+                line_front = Inkscape::Snapper::ConstraintLine(origin, *front_point);
+            if (back_point)
+                line_back = Inkscape::Snapper::ConstraintLine(origin, *back_point);
 
-            // TODO: combine these two branches by modifying snap.h / snap.cpp
+            // TODO: combine the snap and non-snap branches by modifying snap.h / snap.cpp
             if (snap) {
-                fp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_front);
-                bp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(position(), _snapSourceType()), line_back);
+                if (line_front) {
+                    fp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(position(),
+                        _snapSourceType()), *line_front);
+                }
+                if (line_back) {
+                    bp = sm.constrainedSnap(Inkscape::SnapCandidatePoint(position(),
+                        _snapSourceType()), *line_back);
+                }
             }
             if (fp.getSnapped() || bp.getSnapped()) {
                 if (fp.isOtherSnapBetter(bp, false)) {
@@ -890,12 +987,19 @@ void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event)
                     fp.getPoint(new_pos);
                 }
             } else {
-                Geom::Point p_front = line_front.projection(new_pos);
-                Geom::Point p_back = line_back.projection(new_pos);
-                if (Geom::distance(new_pos, p_front) < Geom::distance(new_pos, p_back)) {
-                    new_pos = p_front;
+                boost::optional<Geom::Point> pos;
+                if (line_front) {
+                    pos = line_front->projection(new_pos);
+                }
+                if (line_back) {
+                    Geom::Point pos2 = line_back->projection(new_pos);
+                    if (!pos || (pos && Geom::distance(new_pos, *pos) > Geom::distance(new_pos, pos2)))
+                        pos = pos2;
+                }
+                if (pos) {
+                    new_pos = *pos;
                 } else {
-                    new_pos = p_back;
+                    new_pos = origin;
                 }
             }
         } else {
@@ -949,12 +1053,13 @@ Inkscape::SnapTargetType Node::_snapTargetType()
 Glib::ustring Node::_getTip(unsigned state)
 {
     if (state_held_shift(state)) {
-        if ((_next() && _front.isDegenerate()) || (_prev() && _back.isDegenerate())) {
-            if (state_held_control(state)) {
+        bool can_drag_out = (_next() && _front.isDegenerate()) || (_prev() && _back.isDegenerate());
+        if (can_drag_out) {
+            /*if (state_held_control(state)) {
                 return format_tip(C_("Path node tip",
                     "<b>Shift+Ctrl:</b> drag out a handle and snap its angle "
                     "to %f° increments"), snap_increment_degrees());
-            }
+            }*/
             return C_("Path node tip",
                 "<b>Shift:</b> drag out a handle, click to toggle selection");
         }
@@ -971,8 +1076,16 @@ Glib::ustring Node::_getTip(unsigned state)
 
     // assemble tip from node name
     char const *nodetype = node_type_to_localized_string(_type);
+    if (_selection.transformHandlesEnabled() && selected()) {
+        if (_selection.size() == 1) {
+            return format_tip(C_("Path node tip",
+                "<b>%s:</b> drag to shape the path (more: Shift, Ctrl, Ctrl+Alt)"), nodetype);
+        }
+        return format_tip(C_("Path node tip",
+            "<b>%s:</b> drag to shape the path, click to toggle scale/rotation handles (more: Shift, Ctrl, Ctrl+Alt)"), nodetype);
+    }
     return format_tip(C_("Path node tip",
-        "<b>%s:</b> drag to shape the path, click to select this node"), nodetype);
+        "<b>%s:</b> drag to shape the path, click to select only this node (more: Shift, Ctrl, Ctrl+Alt)"), nodetype);
 }
 
 Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/)
index 581cc9b6fe9e41dab25fcd61d3aa2f5da6ea8b98..c798a1fdbb9cd7b5ae34a269c485f2bb1be95f3e 100644 (file)
@@ -96,6 +96,7 @@ public:
     void setDirection(Geom::Point const &from, Geom::Point const &to);
     void setDirection(Geom::Point const &dir);
     Node *parent() { return _parent; }
+    Handle &other();
 
     static char const *handle_type_to_localized_string(NodeType type);
 protected:
@@ -116,6 +117,7 @@ private:
     SPCanvasItem *_handle_line;
     bool _degenerate; // this is used often internally so it makes sense to cache this
 
+    static Geom::Point _saved_other_pos;
     static double _saved_length;
     static bool _drag_out;
     friend class Node;
index 3a6b15f375d363bb170ec250c1fadffaed179426..fd21970ee0b7cb8e5140e55583e23c60f045e2f0 100644 (file)
@@ -1331,7 +1331,7 @@ double PathManipulator::_getStrokeTolerance()
      * drag tolerance setting.  */
     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
     double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100);
-    if (_path && !SP_OBJECT_STYLE(_path)->stroke.isNone()) {
+    if (_path && SP_OBJECT_STYLE(_path) && !SP_OBJECT_STYLE(_path)->stroke.isNone()) {
         ret += SP_OBJECT_STYLE(_path)->stroke_width.computed * 0.5
             * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords
             * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords
index a30f960253b3ce62baa86efe1601995b57bbbc97..d766d5be3b9897121e07297896beffc77fb974de 100644 (file)
@@ -102,8 +102,8 @@ Selector::~Selector()
 bool Selector::event(GdkEvent *event)
 {
     // The hidden control point will capture all events after it obtains the grab,
-    // but it relies on this function to initiate it. Here we can filter what events
-    // it will receive.
+    // but it relies on this function to initiate it. If we pass only first button
+    // press events here, it won't interfere with any other event handling.
     switch (event->type) {
     case GDK_BUTTON_PRESS:
         // Do not pass button presses other than left button to the control point.