blob: 7b4e065730f42fc5018db53efc4ee2e1a1f0c05d [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.uibuilder.handlers.relative;
import com.android.annotations.NonNull;
import com.android.tools.idea.uibuilder.api.ViewEditor;
import com.android.tools.idea.uibuilder.handlers.relative.DependencyGraph.ViewData;
import com.android.tools.idea.uibuilder.model.AndroidCoordinate;
import com.android.tools.idea.uibuilder.model.NlComponent;
import com.android.tools.idea.uibuilder.model.Segment;
import com.android.tools.idea.uibuilder.model.SegmentType;
import java.awt.*;
import java.awt.event.InputEvent;
import java.util.ArrayList;
import java.util.List;
import static com.android.tools.idea.uibuilder.graphics.NlConstants.MAX_MATCH_DISTANCE;
import static com.android.tools.idea.uibuilder.model.MarginType.NO_MARGIN;
import static java.lang.Math.abs;
/**
* A {@link RelativeDragHandler} is a {@link GuidelineHandler} which handles move and drop
* gestures, and offers guideline suggestions and snapping.
* <p/>
* Unlike the {@link RelativeResizeHandler}, the {@link RelativeDragHandler} looks for matches for all
* different segment types -- the left edge, the right edge, the baseline, the center
* edges, and so on -- and picks the best among these.
*/
public class RelativeDragHandler extends GuidelineHandler {
private int myDraggedBaseline;
/**
* Creates a new {@link RelativeDragHandler}.
*
* @param viewEditor the editor performing this editing operation
* @param layout the layout element the handler is operating on
* @param elements the elements being dragged in the move operation
*/
public RelativeDragHandler(@NonNull ViewEditor viewEditor, @NonNull NlComponent layout, @NonNull List<NlComponent> elements) {
super(viewEditor, layout);
// Compute list of nodes being dragged within the layout, if any
List<NlComponent> nodes = new ArrayList<NlComponent>();
for (NlComponent element : elements) {
ViewData view = myDependencyGraph.getView(element);
if (view != null) {
nodes.add(view.node);
}
}
myDraggedNodes = nodes;
myHorizontalDeps = myDependencyGraph.dependsOn(nodes, false /* verticalEdge */);
myVerticalDeps = myDependencyGraph.dependsOn(nodes, true /* verticalEdge */);
for (NlComponent child : layout.getChildren()) {
boolean isDragged = myDraggedNodes.contains(child);
if (!isDragged) {
String id = child.getId();
// It's okay for id to be null; if you apply a constraint
// to a node with a missing id we will generate the id
boolean addHorizontal = !myHorizontalDeps.contains(child);
boolean addVertical = !myVerticalDeps.contains(child);
addBounds(child, id, addHorizontal, addVertical, false /*includePadding*/);
if (addHorizontal) {
addBaseLine(child, id);
}
}
}
String id = layout.getId();
addBounds(layout, id, true, true, true /*includePadding*/);
addCenter(layout, id);
}
@Override
protected void snapVertical(Segment vEdge, int x, Rectangle newBounds) {
int maxDistance = MAX_MATCH_DISTANCE;
if (myTextDirection.isLeftSegment(vEdge.edgeType)) {
int margin = !mySnap ? 0 : abs(newBounds.x - x);
if (margin > maxDistance) {
myLeftMargin = margin;
}
else {
newBounds.x = x;
}
}
else if (myTextDirection.isRightSegment(vEdge.edgeType)) {
int margin = !mySnap ? 0 : abs(newBounds.x - (x - newBounds.width));
if (margin > maxDistance) {
myRightMargin = margin;
}
else {
newBounds.x = x - newBounds.width;
}
}
else if (vEdge.edgeType == SegmentType.CENTER_VERTICAL) {
newBounds.x = x - newBounds.width / 2;
}
else {
assert false : vEdge;
}
}
// TODO: Consider unifying this with the snapping logic in RelativeResizeHandler
@Override
protected void snapHorizontal(Segment hEdge, int y, Rectangle newBounds) {
int maxDistance = MAX_MATCH_DISTANCE;
if (hEdge.edgeType == SegmentType.TOP) {
int margin = !mySnap ? 0 : abs(newBounds.y - y);
if (margin > maxDistance) {
myTopMargin = margin;
}
else {
newBounds.y = y;
}
}
else if (hEdge.edgeType == SegmentType.BOTTOM) {
int margin = !mySnap ? 0 : abs(newBounds.y - (y - newBounds.height));
if (margin > maxDistance) {
myBottomMargin = margin;
}
else {
newBounds.y = y - newBounds.height;
}
}
else if (hEdge.edgeType == SegmentType.CENTER_HORIZONTAL) {
int margin = !mySnap ? 0 : abs(newBounds.y - (y - newBounds.height / 2));
if (margin > maxDistance) {
myTopMargin = margin;
// or bottomMargin?
}
else {
newBounds.y = y - newBounds.height / 2;
}
}
else if (hEdge.edgeType == SegmentType.BASELINE) {
newBounds.y = y - myDraggedBaseline;
}
else {
assert false : hEdge;
}
}
/**
* Updates the handler for the given mouse move
*
* @param primary the primary element being dragged
* @param offsetX the X delta
* @param offsetY the Y delta
* @param modifierMask the keyboard modifiers pressed during the drag
*/
public void updateMove(@NonNull NlComponent primary, @AndroidCoordinate int offsetX, @AndroidCoordinate int offsetY, int modifierMask) {
clearSuggestions();
mySnap = (modifierMask & InputEvent.SHIFT_MASK) == 0;
myBounds = new Rectangle(primary.x + offsetX, primary.y + offsetY, primary.w, primary.h);
Rectangle b = myBounds;
Segment edge = new Segment(b.y, b.x, x2(b), null, null, SegmentType.TOP, NO_MARGIN);
List<Match> horizontalMatches = findClosest(edge, myHorizontalEdges);
edge = new Segment(y2(b), b.x, x2(b), null, null, SegmentType.BOTTOM, NO_MARGIN);
addClosest(edge, myHorizontalEdges, horizontalMatches);
// We add the LEFT and RIGHT segments. Also we add the START and END segments that will change based on the current RTL view context.
edge = new Segment(b.x, b.y, y2(b), null, null, SegmentType.LEFT, NO_MARGIN);
List<Match> verticalMatches = findClosest(edge, myVerticalEdges);
edge = new Segment(b.x, b.y, y2(b), null, null, myTextDirection.getLeftSegment(), NO_MARGIN);
addClosest(edge, myVerticalEdges, verticalMatches);
edge = new Segment(x2(b), b.y, y2(b), null, null, SegmentType.RIGHT, NO_MARGIN);
addClosest(edge, myVerticalEdges, verticalMatches);
edge = new Segment(x2(b), b.y, y2(b), null, null, myTextDirection.getRightSegment(), NO_MARGIN);
addClosest(edge, myVerticalEdges, verticalMatches);
// Match center
edge = new Segment(centerX(b), b.y, y2(b), null, null, SegmentType.CENTER_VERTICAL, NO_MARGIN);
addClosest(edge, myCenterVertEdges, verticalMatches);
edge = new Segment(centerY(b), b.x, x2(b), null, null, SegmentType.CENTER_HORIZONTAL, NO_MARGIN);
addClosest(edge, myCenterHorizEdges, horizontalMatches);
// Match baseline
int baseline = primary.getBaseline();
if (baseline != -1) {
myDraggedBaseline = baseline;
edge = new Segment(b.y + baseline, b.x, x2(b), primary, null, SegmentType.BASELINE, NO_MARGIN);
addClosest(edge, myHorizontalEdges, horizontalMatches);
}
// TODO: When dragging from the palette, obtain the baseline from the drag preview, if available
//else {
// int baseline = feedback.dragBaseline;
// if (baseline != -1) {
// myDraggedBaseline = baseline;
// edge = new Segment(offsetY + baseline, b.x, x2(b), null, null, BASELINE, NO_MARGIN);
// addClosest(edge, myHorizontalEdges, horizontalMatches);
// }
//}
myHorizontalSuggestions = horizontalMatches;
myVerticalSuggestions = verticalMatches;
myTopMargin = myBottomMargin = myLeftMargin = myRightMargin = 0;
Match match = pickBestMatch(myHorizontalSuggestions);
if (match != null) {
if (myHorizontalDeps.contains(match.edge.component)) {
match.cycle = true;
}
// Reset top AND bottom bounds regardless of whether both are bound
myMoveTop = true;
myMoveBottom = true;
// TODO: Consider doing the snap logic on all the possible matches
// BEFORE sorting, in case this affects the best-pick algorithm (since some
// edges snap and others don't).
snapHorizontal(match.with, match.edge.at, myBounds);
if (match.with.edgeType == SegmentType.TOP) {
myCurrentTopMatch = match;
}
else if (match.with.edgeType == SegmentType.BOTTOM) {
myCurrentBottomMatch = match;
}
else {
assert match.with.edgeType == SegmentType.CENTER_HORIZONTAL || match.with.edgeType == SegmentType.BASELINE : match.with.edgeType;
myCurrentTopMatch = match;
}
}
match = pickBestMatch(myVerticalSuggestions);
if (match != null) {
if (myVerticalDeps.contains(match.edge.component)) {
match.cycle = true;
}
// Reset left AND right bounds regardless of whether both are bound
myMoveLeft = true;
myMoveRight = true;
snapVertical(match.with, match.edge.at, myBounds);
if (myTextDirection.isLeftSegment(match.with.edgeType)) {
myCurrentLeftMatch = match;
}
else if (myTextDirection.isRightSegment(match.with.edgeType)) {
myCurrentRightMatch = match;
}
else {
assert match.with.edgeType == SegmentType.CENTER_VERTICAL;
myCurrentLeftMatch = match;
}
}
checkCycles();
}
}