package com.baselet.gwt.client.view; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.baselet.command.CommandTarget; import com.baselet.control.basics.geom.Point; import com.baselet.control.basics.geom.Rectangle; import com.baselet.control.config.SharedConfig; import com.baselet.control.constants.MenuConstants; import com.baselet.control.constants.SharedConstants; import com.baselet.control.enums.Direction; import com.baselet.element.GridElementUtils; import com.baselet.element.Selector; import com.baselet.element.facet.common.GroupFacet; import com.baselet.element.interfaces.CursorOwn; import com.baselet.element.interfaces.Diagram; import com.baselet.element.interfaces.GridElement; import com.baselet.element.sticking.StickableMap; import com.baselet.gwt.client.base.Utils; import com.baselet.gwt.client.element.DiagramGwt; import com.baselet.gwt.client.keyboard.Shortcut; import com.baselet.gwt.client.view.EventHandlingUtils.EventHandlingTarget; import com.baselet.gwt.client.view.interfaces.AutoresizeScrollDropTarget; import com.baselet.gwt.client.view.interfaces.HasScrollPanel; import com.baselet.gwt.client.view.widgets.MenuPopup; import com.baselet.gwt.client.view.widgets.MenuPopup.MenuPopupItem; import com.baselet.gwt.client.view.widgets.propertiespanel.PropertiesTextArea; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.event.dom.client.HasMouseOutHandlers; import com.google.gwt.event.dom.client.HasMouseOverHandlers; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.MouseOutEvent; import com.google.gwt.event.dom.client.MouseOutHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.SimplePanel; public abstract class DrawPanel extends SimplePanel implements CommandTarget, HasMouseOutHandlers, HasMouseOverHandlers, EventHandlingTarget, AutoresizeScrollDropTarget { private Diagram diagram = new DiagramGwt(new ArrayList<GridElement>()); protected DrawCanvas canvas = new DrawCanvas(); SelectorNew selector; CommandInvoker commandInvoker = CommandInvoker.getInstance(); DrawPanel otherDrawFocusPanel; HasScrollPanel scrollPanel; private final MainView mainView; PropertiesTextArea propertiesPanel; private final MenuPopup elementContextMenu; private final MenuPopup diagramContextMenu; private Set<Direction> resizeDirections = new HashSet<Direction>(); private final Map<GridElement, StickableMap> stickablesToMove = new HashMap<GridElement, StickableMap>(); public void setOtherDrawFocusPanel(DrawPanel otherDrawFocusPanel) { this.otherDrawFocusPanel = otherDrawFocusPanel; } private Boolean focus = false; @Override public void setFocus(boolean focus) { if (this.focus == focus) { return; } if (focus) { // if focus has switched from diagram <-> palette, reset other selector and redraw otherDrawFocusPanel.getSelector().deselectAllWithoutAfterAction(); otherDrawFocusPanel.redraw(); // redraw is necessary even if other afteractions (properties panel update) are not otherDrawFocusPanel.setFocus(false); } this.focus = focus; } public Boolean getFocus() { return focus; } public DrawPanel(final MainView mainView, final PropertiesTextArea propertiesPanel) { this.setStylePrimaryName("canvasFocusPanel"); this.mainView = mainView; this.propertiesPanel = propertiesPanel; selector = new SelectorNew(diagram) { @Override public void doAfterSelectionChanged() { updatePropertiesPanelWithSelectedElement(); } }; List<MenuPopupItem> diagramItems = Arrays.asList( new MenuPopupItem(MenuConstants.DELETE) { @Override public void execute() { commandInvoker.removeSelectedElements(DrawPanel.this); } }, new MenuPopupItem(MenuConstants.COPY) { @Override public void execute() { commandInvoker.copySelectedElements(DrawPanel.this); } }, new MenuPopupItem(MenuConstants.CUT) { @Override public void execute() { commandInvoker.cutSelectedElements(DrawPanel.this); } }, new MenuPopupItem(MenuConstants.PASTE) { @Override public void execute() { commandInvoker.pasteElements(DrawPanel.this); } }, new MenuPopupItem(MenuConstants.SELECT_ALL) { @Override public void execute() { selector.select(diagram.getGridElements()); } }); List<MenuPopupItem> elementItems = new ArrayList<MenuPopupItem>(diagramItems); elementItems.addAll(Arrays.asList( new MenuPopupItem(MenuConstants.GROUP) { @Override public void execute() { Integer unusedGroup = selector.getUnusedGroup(); commandInvoker.updateSelectedElementsProperty(DrawPanel.this, GroupFacet.KEY, unusedGroup); } }, new MenuPopupItem(MenuConstants.UNGROUP) { @Override public void execute() { commandInvoker.updateSelectedElementsProperty(DrawPanel.this, GroupFacet.KEY, null); } })); diagramContextMenu = new MenuPopup(diagramItems); elementContextMenu = new MenuPopup(elementItems); this.add(canvas.getWidget()); } @Override public void updatePropertiesPanelWithSelectedElement() { List<GridElement> elements = selector.getSelectedElements(); if (!elements.isEmpty()) { // always set properties text of latest selected element (so you also have an element in the prop panel even if you have an active multiselect) propertiesPanel.setGridElement(elements.get(elements.size() - 1), DrawPanel.this); } else { propertiesPanel.setGridElement(diagram, DrawPanel.this); } redraw(); } void keyboardMoveSelectedElements(int diffX, int diffY) { List<GridElement> gridElements = selector.getSelectedElements(); moveElements(diffX, diffY, true, gridElements); dragEndAndRedraw(gridElements); } void moveElements(int diffX, int diffY, boolean firstDrag, List<GridElement> elements) { if (elements.isEmpty()) { // if nothing is selected, move whole diagram elements = diagram.getGridElements(); } for (GridElement ge : elements) { if (firstDrag) { stickablesToMove.put(ge, getStickablesToMoveWhenElementsMove(ge, elements)); } ge.setRectangleDifference(diffX, diffY, 0, 0, firstDrag, stickablesToMove.get(ge), false); // uses setLocationDifference() instead of drag() to avoid special handling (eg: from Relations) } } @Override public Rectangle getVisibleBounds() { return scrollPanel.getVisibleBounds(); } @Override public void redraw() { redraw(true); } void redraw(boolean recalcSize) { List<GridElement> gridElements = diagram.getGridElementsByLayerLowestToHighest(); if (recalcSize) { if (scrollPanel == null) { return; } Rectangle diagramRect = GridElementUtils.getGridElementsRectangle(gridElements); Rectangle visibleRect = getVisibleBounds(); // realign top left corner of the diagram back to the canvas and remove invisible whitespace outside of the diagram final int xTranslate = Math.min(visibleRect.getX(), diagramRect.getX()); // can be positive (to cut upper left whitespace without diagram) or negative (to move diagram back to the visible canvas which starts at (0,0)) final int yTranslate = Math.min(visibleRect.getY(), diagramRect.getY()); if (xTranslate != 0 || yTranslate != 0) { // temp increase of canvas size to make sure scrollbar can be moved canvas.clearAndSetSize(canvas.getWidth() + Math.abs(xTranslate), canvas.getHeight() + Math.abs(yTranslate)); // move scrollbars scrollPanel.moveHorizontalScrollbar(-xTranslate); scrollPanel.moveVerticalScrollbar(-yTranslate); // then move gridelements to correct position for (GridElement ge : gridElements) { ge.setLocationDifference(-xTranslate, -yTranslate); } } // now realign bottom right corner to include the translate-factor and the changed visible and diagram rect int width = Math.max(visibleRect.getX2(), diagramRect.getX2()) - xTranslate; int height = Math.max(visibleRect.getY2(), diagramRect.getY2()) - yTranslate; canvas.clearAndSetSize(width, height); } else { canvas.clearAndSetSize(canvas.getWidth(), canvas.getHeight()); } canvas.draw(true, gridElements, selector); } @Override public GridElement getGridElementOnPosition(Point point) { GridElement returnGe = null; for (GridElement ge : diagram.getGridElementsByLayer(false)) { // get elements, highest layer first if (returnGe != null && returnGe.getLayer() > ge.getLayer()) { break; // because the following elements have lower layers, break if a valid higher layered element has been found } if (ge.isSelectableOn(point)) { if (returnGe == null) { returnGe = ge; } else { boolean newIsSelectedOldNot = selector.isSelected(ge) && !selector.isSelected(returnGe); boolean oldContainsNew = returnGe.getRectangle().contains(ge.getRectangle()); if (newIsSelectedOldNot || oldContainsNew) { returnGe = ge; } } } } return returnGe; } @Override public void setDiagram(Diagram diagram) { this.diagram = diagram; selector.setGridElementProvider(diagram); selector.deselectAll(); // necessary to trigger setting helptext to properties redraw(); } @Override public void addGridElements(List<GridElement> elements) { diagram.getGridElements().addAll(elements); selector.selectOnly(elements); } @Override public void removeGridElements(List<GridElement> elements) { diagram.getGridElements().removeAll(elements); selector.deselect(elements); } @Override public Diagram getDiagram() { return diagram; } @Override public Selector getSelector() { return selector; } @Override public void setAutoresizeScrollDrop(HasScrollPanel scrollPanel) { this.scrollPanel = scrollPanel; } @Override public abstract void onDoubleClick(GridElement ge); @Override public void onMouseDragEnd(GridElement gridElement, Point lastPoint) { dragEndAndRedraw(selector.getSelectedElements()); } private void dragEndAndRedraw(List<GridElement> selectedElements) { for (GridElement ge : selectedElements) { stickablesToMove.remove(ge); ge.dragEnd(); } redraw(); } @Override public void onMouseDownScheduleDeferred(final GridElement element, final boolean isControlKeyDown) { Scheduler.get().scheduleFinally(new ScheduledCommand() { // scheduleDeferred is necessary for mobile (or low performance) browsers @Override public void execute() { onMouseDown(element, isControlKeyDown); } }); } void onMouseDown(final GridElement element, final boolean isControlKeyDown) { if (isControlKeyDown) { if (element != null) { if (selector.isSelected(element)) { selector.deselect(element); } else { selector.select(element); } } } else { if (element != null) { if (selector.isSelected(element)) { selector.moveToLastPosInList(element); propertiesPanel.setGridElement(element, DrawPanel.this); } else { selector.selectOnly(element); } } else { selector.deselectAll(); } } } @Override public void onMouseMoveDraggingScheduleDeferred(final Point dragStart, final int diffX, final int diffY, final GridElement draggedGridElement, final boolean isShiftKeyDown, final boolean isCtrlKeyDown, final boolean firstDrag) { Scheduler.get().scheduleFinally(new ScheduledCommand() { // scheduleDeferred is necessary for mobile (or low performance) browsers @Override public void execute() { onMouseMoveDragging(dragStart, diffX, diffY, draggedGridElement, isShiftKeyDown, isCtrlKeyDown, firstDrag); } }); } void onMouseMoveDragging(Point dragStart, int diffX, int diffY, GridElement draggedGridElement, boolean isShiftKeyDown, boolean isCtrlKeyDown, boolean firstDrag) { if (firstDrag && draggedGridElement != null) { // if draggedGridElement == null the whole diagram is dragged and nothing has to be checked for sticking stickablesToMove.put(draggedGridElement, getStickablesToMoveWhenElementsMove(draggedGridElement, Collections.<GridElement> emptyList())); } if (isCtrlKeyDown) { return; // TODO implement Lasso } else if (!resizeDirections.isEmpty()) { draggedGridElement.drag(resizeDirections, diffX, diffY, getRelativePoint(dragStart, draggedGridElement), isShiftKeyDown, firstDrag, stickablesToMove.get(draggedGridElement), false); } // if a single element is selected, drag it (and pass the dragStart, because it's important for Relations) else if (selector.getSelectedElements().size() == 1) { draggedGridElement.drag(Collections.<Direction> emptySet(), diffX, diffY, getRelativePoint(dragStart, draggedGridElement), isShiftKeyDown, firstDrag, stickablesToMove.get(draggedGridElement), false); } else { // if != 1 elements are selected, move them moveElements(diffX, diffY, firstDrag, selector.getSelectedElements()); } redraw(false); } private Point getRelativePoint(Point dragStart, GridElement draggedGridElement) { return new Point(dragStart.getX() - draggedGridElement.getRectangle().getX(), dragStart.getY() - draggedGridElement.getRectangle().getY()); } protected StickableMap getStickablesToMoveWhenElementsMove(GridElement draggedElement, List<GridElement> excludeList) { return diagram.getStickables(draggedElement, excludeList); } @Override public void onMouseMove(Point absolute) { GridElement geOnPosition = getGridElementOnPosition(absolute); if (geOnPosition != null) { // exactly one gridelement selected which is at the mouseposition resizeDirections = geOnPosition.getResizeArea(absolute.getX() - geOnPosition.getRectangle().getX(), absolute.getY() - geOnPosition.getRectangle().getY()); CursorOwn cursor = geOnPosition.getCursor(absolute, resizeDirections); if (cursor != null) { Utils.showCursor(cursor); } } else { resizeDirections.clear(); Utils.showCursor(CursorOwn.DEFAULT); } } @Override public void onShowMenu(Point point) { Point relativePoint = new Point(point.x - getAbsoluteLeft(), point.y - getAbsoluteTop()); if (getGridElementOnPosition(relativePoint) == null) { // gridelement check must be made with relative coordinates diagramContextMenu.show(point); } else { elementContextMenu.show(point); } } @Override public void handleKeyDown(KeyDownEvent event) { boolean avoidBrowserDefault = true; if (Shortcut.DELETE_ELEMENT.matches(event)) { commandInvoker.removeSelectedElements(DrawPanel.this); } else if (Shortcut.DESELECT_ALL.matches(event)) { selector.deselectAll(); } else if (Shortcut.SELECT_ALL.matches(event)) { selector.select(diagram.getGridElements()); } else if (Shortcut.COPY.matches(event)) { commandInvoker.copySelectedElements(DrawPanel.this); } else if (Shortcut.CUT.matches(event)) { commandInvoker.cutSelectedElements(DrawPanel.this); } else if (Shortcut.PASTE.matches(event)) { commandInvoker.pasteElements(DrawPanel.this); } else if (Shortcut.SAVE.matches(event)) { mainView.getSaveCommand().execute(); } else if (Shortcut.MOVE_UP.matches(event)) { keyboardMoveSelectedElements(0, -SharedConstants.DEFAULT_GRID_SIZE); redraw(); } else if (Shortcut.MOVE_DOWN.matches(event)) { keyboardMoveSelectedElements(0, SharedConstants.DEFAULT_GRID_SIZE); redraw(); } else if (Shortcut.MOVE_LEFT.matches(event)) { keyboardMoveSelectedElements(-SharedConstants.DEFAULT_GRID_SIZE, 0); redraw(); } else if (Shortcut.MOVE_RIGHT.matches(event)) { keyboardMoveSelectedElements(SharedConstants.DEFAULT_GRID_SIZE, 0); redraw(); } else if (Shortcut.DISABLE_STICKING.matches(event)) { SharedConfig.getInstance().setStickingEnabled(false); } else { avoidBrowserDefault = false; } // avoid browser default key handling for all overwritten keys, but not for others (like F5 for refresh or the zoom controls) if (avoidBrowserDefault) { event.preventDefault(); } } @Override public void handleKeyUp(KeyUpEvent event) { if (Shortcut.DISABLE_STICKING.matches(event)) { SharedConfig.getInstance().setStickingEnabled(true); } } @Override public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) { return addDomHandler(handler, MouseOverEvent.getType()); } @Override public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) { return addDomHandler(handler, MouseOutEvent.getType()); } }