1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.common.layout.relative;
17 
18 import static com.android.ide.common.api.MarginType.NO_MARGIN;
19 import static com.android.ide.common.api.SegmentType.BASELINE;
20 import static com.android.ide.common.api.SegmentType.BOTTOM;
21 import static com.android.ide.common.api.SegmentType.CENTER_HORIZONTAL;
22 import static com.android.ide.common.api.SegmentType.CENTER_VERTICAL;
23 import static com.android.ide.common.api.SegmentType.LEFT;
24 import static com.android.ide.common.api.SegmentType.RIGHT;
25 import static com.android.ide.common.api.SegmentType.TOP;
26 import static com.android.SdkConstants.ATTR_ID;
27 
28 import static java.lang.Math.abs;
29 
30 import com.android.SdkConstants;
31 import static com.android.SdkConstants.ANDROID_URI;
32 import com.android.ide.common.api.DropFeedback;
33 import com.android.ide.common.api.IClientRulesEngine;
34 import com.android.ide.common.api.IDragElement;
35 import com.android.ide.common.api.INode;
36 import com.android.ide.common.api.Rect;
37 import com.android.ide.common.api.Segment;
38 import com.android.ide.common.layout.BaseLayoutRule;
39 import com.android.ide.common.layout.relative.DependencyGraph.ViewData;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 
44 /**
45  * A {@link MoveHandler} is a {@link GuidelineHandler} which handles move and drop
46  * gestures, and offers guideline suggestions and snapping.
47  * <p>
48  * Unlike the {@link ResizeHandler}, the {@link MoveHandler} looks for matches for all
49  * different segment types -- the left edge, the right edge, the baseline, the center
50  * edges, and so on -- and picks the best among these.
51  */
52 public class MoveHandler extends GuidelineHandler {
53     private int mDraggedBaseline;
54 
55     /**
56      * Creates a new {@link MoveHandler}.
57      *
58      * @param layout the layout element the handler is operating on
59      * @param elements the elements being dragged in the move operation
60      * @param rulesEngine the corresponding {@link IClientRulesEngine}
61      */
MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine)62     public MoveHandler(INode layout, IDragElement[] elements, IClientRulesEngine rulesEngine) {
63         super(layout, rulesEngine);
64 
65         // Compute list of nodes being dragged within the layout, if any
66         List<INode> nodes = new ArrayList<INode>();
67         for (IDragElement element : elements) {
68             ViewData view = mDependencyGraph.getView(element);
69             if (view != null) {
70                 nodes.add(view.node);
71             }
72         }
73         mDraggedNodes = nodes;
74 
75         mHorizontalDeps = mDependencyGraph.dependsOn(nodes, false /* verticalEdge */);
76         mVerticalDeps = mDependencyGraph.dependsOn(nodes, true /* verticalEdge */);
77 
78         for (INode child : layout.getChildren()) {
79             Rect bc = child.getBounds();
80             if (bc.isValid()) {
81                 // First see if this node looks like it's the same as one of the
82                 // *dragged* bounds
83                 boolean isDragged = false;
84                 for (IDragElement element : elements) {
85                     // This tries to determine if an INode corresponds to an
86                     // IDragElement, by comparing their bounds.
87                     if (bc.equals(element.getBounds())) {
88                         isDragged = true;
89                     }
90                 }
91 
92                 if (!isDragged) {
93                     String id = child.getStringAttr(ANDROID_URI, ATTR_ID);
94                     // It's okay for id to be null; if you apply a constraint
95                     // to a node with a missing id we will generate the id
96 
97                     boolean addHorizontal = !mHorizontalDeps.contains(child);
98                     boolean addVertical = !mVerticalDeps.contains(child);
99 
100                     addBounds(child, id, addHorizontal, addVertical);
101                     if (addHorizontal) {
102                         addBaseLine(child, id);
103                     }
104                 }
105             }
106         }
107 
108         String id = layout.getStringAttr(ANDROID_URI, ATTR_ID);
109         addBounds(layout, id, true, true);
110         addCenter(layout, id, true, true);
111     }
112 
113     @Override
snapVertical(Segment vEdge, int x, Rect newBounds)114     protected void snapVertical(Segment vEdge, int x, Rect newBounds) {
115         int maxDistance = BaseLayoutRule.getMaxMatchDistance();
116         if (vEdge.edgeType == LEFT) {
117             int margin = !mSnap ? 0 : abs(newBounds.x - x);
118             if (margin > maxDistance) {
119                 mLeftMargin = margin;
120             } else {
121                 newBounds.x = x;
122             }
123         } else if (vEdge.edgeType == RIGHT) {
124             int margin = !mSnap ? 0 : abs(newBounds.x - (x - newBounds.w));
125             if (margin > maxDistance) {
126                 mRightMargin = margin;
127             } else {
128                 newBounds.x = x - newBounds.w;
129             }
130         } else if (vEdge.edgeType == CENTER_VERTICAL) {
131             newBounds.x = x - newBounds.w / 2;
132         } else {
133             assert false : vEdge;
134         }
135     }
136 
137     // TODO: Consider unifying this with the snapping logic in ResizeHandler
138     @Override
snapHorizontal(Segment hEdge, int y, Rect newBounds)139     protected void snapHorizontal(Segment hEdge, int y, Rect newBounds) {
140         int maxDistance = BaseLayoutRule.getMaxMatchDistance();
141         if (hEdge.edgeType == TOP) {
142             int margin = !mSnap ? 0 : abs(newBounds.y - y);
143             if (margin > maxDistance) {
144                 mTopMargin = margin;
145             } else {
146                 newBounds.y = y;
147             }
148         } else if (hEdge.edgeType == BOTTOM) {
149             int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h));
150             if (margin > maxDistance) {
151                 mBottomMargin = margin;
152             } else {
153                 newBounds.y = y - newBounds.h;
154             }
155         } else if (hEdge.edgeType == CENTER_HORIZONTAL) {
156             int margin = !mSnap ? 0 : abs(newBounds.y - (y - newBounds.h / 2));
157             if (margin > maxDistance) {
158                 mTopMargin = margin;
159                 // or bottomMargin?
160             } else {
161                 newBounds.y = y - newBounds.h / 2;
162             }
163         } else if (hEdge.edgeType == BASELINE) {
164                 newBounds.y = y - mDraggedBaseline;
165         } else {
166             assert false : hEdge;
167         }
168     }
169 
170     /**
171      * Updates the handler for the given mouse move
172      *
173      * @param feedback the feedback handler
174      * @param elements the elements being dragged
175      * @param offsetX the new mouse X coordinate
176      * @param offsetY the new mouse Y coordinate
177      * @param modifierMask the keyboard modifiers pressed during the drag
178      */
updateMove(DropFeedback feedback, IDragElement[] elements, int offsetX, int offsetY, int modifierMask)179     public void updateMove(DropFeedback feedback, IDragElement[] elements,
180             int offsetX, int offsetY, int modifierMask) {
181         mSnap = (modifierMask & DropFeedback.MODIFIER2) == 0;
182 
183         Rect firstBounds = elements[0].getBounds();
184         INode firstNode = null;
185         if (mDraggedNodes != null && mDraggedNodes.size() > 0) {
186             // TODO - this isn't quite right; this could be a different node than we have
187             // bounds for!
188             firstNode = mDraggedNodes.iterator().next();
189             firstBounds = firstNode.getBounds();
190         }
191 
192         mBounds = new Rect(offsetX, offsetY, firstBounds.w, firstBounds.h);
193         Rect layoutBounds = layout.getBounds();
194         if (mBounds.x2() > layoutBounds.x2()) {
195             mBounds.x -= mBounds.x2() - layoutBounds.x2();
196         }
197         if (mBounds.y2() > layoutBounds.y2()) {
198             mBounds.y -= mBounds.y2() - layoutBounds.y2();
199         }
200         if (mBounds.x < layoutBounds.x) {
201             mBounds.x = layoutBounds.x;
202         }
203         if (mBounds.y < layoutBounds.y) {
204             mBounds.y = layoutBounds.y;
205         }
206 
207         clearSuggestions();
208 
209         Rect b = mBounds;
210         Segment edge = new Segment(b.y, b.x, b.x2(), null, null, TOP, NO_MARGIN);
211         List<Match> horizontalMatches = findClosest(edge, mHorizontalEdges);
212         edge = new Segment(b.y2(), b.x, b.x2(), null, null, BOTTOM, NO_MARGIN);
213         addClosest(edge, mHorizontalEdges, horizontalMatches);
214 
215         edge = new Segment(b.x, b.y, b.y2(), null, null, LEFT, NO_MARGIN);
216         List<Match> verticalMatches = findClosest(edge, mVerticalEdges);
217         edge = new Segment(b.x2(), b.y, b.y2(), null, null, RIGHT, NO_MARGIN);
218         addClosest(edge, mVerticalEdges, verticalMatches);
219 
220         // Match center
221         edge = new Segment(b.centerX(), b.y, b.y2(), null, null, CENTER_VERTICAL, NO_MARGIN);
222         addClosest(edge, mCenterVertEdges, verticalMatches);
223         edge = new Segment(b.centerY(), b.x, b.x2(), null, null, CENTER_HORIZONTAL, NO_MARGIN);
224         addClosest(edge, mCenterHorizEdges, horizontalMatches);
225 
226         // Match baseline
227         if (firstNode != null) {
228             int baseline = firstNode.getBaseline();
229             if (baseline != -1) {
230                 mDraggedBaseline = baseline;
231                 edge = new Segment(b.y + baseline, b.x, b.x2(), firstNode, null, BASELINE,
232                         NO_MARGIN);
233                 addClosest(edge, mHorizontalEdges, horizontalMatches);
234             }
235         } else {
236             int baseline = feedback.dragBaseline;
237             if (baseline != -1) {
238                 mDraggedBaseline = baseline;
239                 edge = new Segment(offsetY + baseline, b.x, b.x2(), null, null, BASELINE,
240                         NO_MARGIN);
241                 addClosest(edge, mHorizontalEdges, horizontalMatches);
242             }
243         }
244 
245         mHorizontalSuggestions = horizontalMatches;
246         mVerticalSuggestions = verticalMatches;
247         mTopMargin = mBottomMargin = mLeftMargin = mRightMargin = 0;
248 
249         Match match = pickBestMatch(mHorizontalSuggestions);
250         if (match != null) {
251             if (mHorizontalDeps.contains(match.edge.node)) {
252                 match.cycle = true;
253             }
254 
255             // Reset top AND bottom bounds regardless of whether both are bound
256             mMoveTop = true;
257             mMoveBottom = true;
258 
259             // TODO: Consider doing the snap logic on all the possible matches
260             // BEFORE sorting, in case this affects the best-pick algorithm (since some
261             // edges snap and others don't).
262             snapHorizontal(match.with, match.edge.at, mBounds);
263 
264             if (match.with.edgeType == TOP) {
265                 mCurrentTopMatch = match;
266             } else if (match.with.edgeType == BOTTOM) {
267                 mCurrentBottomMatch = match;
268             } else {
269                 assert match.with.edgeType == CENTER_HORIZONTAL
270                         || match.with.edgeType == BASELINE : match.with.edgeType;
271                 mCurrentTopMatch = match;
272             }
273         }
274 
275         match = pickBestMatch(mVerticalSuggestions);
276         if (match != null) {
277             if (mVerticalDeps.contains(match.edge.node)) {
278                 match.cycle = true;
279             }
280 
281             // Reset left AND right bounds regardless of whether both are bound
282             mMoveLeft = true;
283             mMoveRight = true;
284 
285             snapVertical(match.with, match.edge.at, mBounds);
286 
287             if (match.with.edgeType == LEFT) {
288                 mCurrentLeftMatch = match;
289             } else if (match.with.edgeType == RIGHT) {
290                 mCurrentRightMatch = match;
291             } else {
292                 assert match.with.edgeType == CENTER_VERTICAL;
293                 mCurrentLeftMatch = match;
294             }
295         }
296 
297         checkCycles(feedback);
298     }
299 }
300