Java tutorial
/** * GridStackLayout.java (GridStackLayout) * * Copyright 2015 Vaadin Ltd, Sami Viitanen <sami.viitanen@vaadin.org> * * 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 org.vaadin.alump.gridstack; import com.vaadin.annotations.JavaScript; import com.vaadin.event.LayoutEvents; import com.vaadin.shared.Connector; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; import com.vaadin.ui.AbstractLayout; import com.vaadin.ui.Component; import org.vaadin.alump.gridstack.client.shared.*; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * Vaadin layout using gridstack.js library to layout components * * gridstack.js by Pavel Reznikov: http://troolee.github.io/gridstack.js/ */ @JavaScript({ "jquery-1.11.3.min.js", "jquery-ui.min.js", "lodash.min.js", "gridstack.js" }) public class GridStackLayout extends AbstractLayout implements LayoutEvents.LayoutClickNotifier { public final static String INITIALIZING_STYLENAME = "gridstack-initializing"; protected final List<Component> components = new ArrayList<Component>(); private final List<GridStackMoveEvent.GridStackMoveListener> moveListeners = new ArrayList<GridStackMoveEvent.GridStackMoveListener>(); private final List<GridStackReadyEvent.GridStackReadyListener> readyListeners = new ArrayList<GridStackReadyEvent.GridStackReadyListener>(); private int readyCalls = 0; /** * Use this as x or y coordinate if you want to leave slot selection of component to client side */ public final static int CLIENT_SIDE_SELECTS = -1; private GridStackServerRpc serverRpc = new GridStackServerRpc() { @Override public void layoutClick(MouseEventDetails mouseEventDetails, Connector connector) { fireEvent( LayoutEvents.LayoutClickEvent.createEvent(GridStackLayout.this, mouseEventDetails, connector)); } @Override public void onChildrenMoved(List<GridStackMoveData> moves) { Collection<GridStackMoveEvent> events = new ArrayList<GridStackMoveEvent>(); for (GridStackMoveData move : moves) { Component childComponent = (Component) move.child; GridStackCoordinates oldCoordinates = getCoordinates(childComponent); GridStackChildOptions info = getState(false).childOptions.get(move.child); info.x = move.x; info.y = move.y; info.width = move.width; info.height = move.height; if (!oldCoordinates.equals(getCoordinates(childComponent))) { events.add(createMoveEvent(childComponent, oldCoordinates)); } } fireMoveEvents(events); } @Override public void onReady(int widthPx) { readyCalls++; removeStyleName(INITIALIZING_STYLENAME); final GridStackReadyEvent event = new GridStackReadyEvent(GridStackLayout.this, readyCalls == 1, widthPx); for (GridStackReadyEvent.GridStackReadyListener listener : readyListeners) { listener.onGridStackReady(event); } } }; /** * Creates GridStackLayout with default 3 columns */ public GridStackLayout() { super(); addStyleName(INITIALIZING_STYLENAME); registerRpc(serverRpc, GridStackServerRpc.class); } /** * Create GridStackLayout with defined column count. * @param columns Number of columns, if more than 8 see documentation (extra SCSS including required) */ public GridStackLayout(int columns) { this(); if (columns <= 0) { throw new IllegalArgumentException("Amount of columns can not be 0 or negative"); } addStyleName("with-" + columns + "-columns"); getState().gridStackOptions.width = columns; } /** * Create GridStackLayout with defined column and row count. * @param columns Number of columns, if more than 8 see documentation (extra SCSS including required) * @param rows Maxium amount of rows allowed */ public GridStackLayout(int columns, int rows) { this(columns); if (rows <= 0) { throw new IllegalArgumentException("Amount of rows can not be 0 or negative"); } getState().gridStackOptions.height = rows; } @Override protected GridStackLayoutState getState() { return (GridStackLayoutState) super.getState(); } @Override protected GridStackLayoutState getState(boolean markDirty) { return (GridStackLayoutState) super.getState(markDirty); } /** * {@inheritDoc} */ @Override public void addComponent(Component component) { addComponent(component, CLIENT_SIDE_SELECTS, CLIENT_SIDE_SELECTS); } /** * Add component to layout * @param component Component added * @param useDragHandle true to add component with a separate drag handle, or false to make whole content act as a * drag handle. Notice that using a separate drag handle is recommended if you component * is or contains any active components (buttons etc..) */ public void addComponent(Component component, boolean useDragHandle) { addComponent(component, CLIENT_SIDE_SELECTS, CLIENT_SIDE_SELECTS, useDragHandle); } /** * Add component to given slot * @param component Component added * @param x Slot's X coordinate * @param y Slot's Y coordinate */ public void addComponent(Component component, int x, int y) { addComponent(component, x, y, 1, 1); } /** * Add component to given slot * @param component Component added * @param x Slot's X coordinate * @param y Slot's Y coordinate * @param useDragHandle true to add component with a separate drag handle, or false to make whole content act as a * drag handle. Notice that using a separate drag handle is recommended if you component * is or contains any active components (buttons etc..) */ public void addComponent(Component component, int x, int y, boolean useDragHandle) { addComponent(component, x, y, 1, 1, useDragHandle); } /** * Add component to given slot, and define it's size * @param component Component added * @param x Slot's X coordinate (use negative values if position can be defined on client side) * @param y Slot's Y coordinate (use negative values if position can be defined on client side) * @param width Width of space reserved (in slots) * @param height Height of space reserved (in slots) */ public void addComponent(Component component, int x, int y, int width, int height) { addComponent(component, x, y, width, height, true); } /** * Add component to given slot, and define it's size * @param component Component added * @param x Slot's X coordinate (use negative values if position can be defined on client side) * @param y Slot's Y coordinate (use negative values if position can be defined on client side) * @param width Width of space reserved (in slots) * @param height Height of space reserved (in slots) * @param useDragHandle true to add component with a separate drag handle, or false to make whole content act as a * drag handle. Notice that using a separate drag handle is recommended if you component * is or contains any active components (buttons etc..) */ public void addComponent(Component component, int x, int y, int width, int height, boolean useDragHandle) { super.addComponent(component); components.add(component); GridStackChildOptions info = new GridStackChildOptions(); info.x = x; info.y = y; info.width = width; info.height = height; info.useDragHandle = useDragHandle; getState().childOptions.put(component, info); } /** * Reset component's position and allow child side define new position for it. * @param component Child component which position is reset */ public void resetComponentPosition(Component component) { moveComponent(component, CLIENT_SIDE_SELECTS, CLIENT_SIDE_SELECTS); } /** * Move given child component * @param component Child component moved and/or resized * @param x When defined component's X value is updated, if null old value is kept * @param y When defined component's Y value is updated, if null old value is kept * @throws IllegalArgumentException If given value are invalid (eg. component is not child of this layout) */ public void moveComponent(Component component, Integer x, Integer y) throws IllegalArgumentException { moveAndResizeComponent(component, x, y, null, null); } /** * Move given child component * @param component Child component moved and/or resized * @param width When defined component's width is updated, if null old value is kept * @param height When defined component's height is updated, if null old value is kept * @throws IllegalArgumentException If given value are invalid (eg. component is not child of this layout) */ public void resizeComponent(Component component, Integer width, Integer height) throws IllegalArgumentException { moveAndResizeComponent(component, null, null, width, height); } /** * Move and/or resize given child component * @param component Child component moved and/or resized * @param x When defined component's X value is updated, if null old value is kept * @param y When defined component's Y value is updated, if null old value is kept * @param width When defined component's width is updated, if null old value is kept * @param height When defined component's height is updated, if null old value is kept * @throws IllegalArgumentException If given value are invalid (eg. component is not child of this layout, or * coordinates are invalid). */ public void moveAndResizeComponent(Component component, Integer x, Integer y, Integer width, Integer height) throws IllegalArgumentException { if (x != null & width != null && x >= 0 && x + width > getState(false).gridStackOptions.width) { throw new IllegalArgumentException("Component would go outside the right edge of layout"); } GridStackChildOptions info = getState().childOptions.get(component); if (info == null) { throw new IllegalArgumentException("Given component is not child of GridStackLayout"); } if (x != null) { info.x = x; } if (y != null) { info.y = y; } if (width != null) { info.width = width; } if (height != null) { info.height = height; } } /** * Get component with given slot coordinate * @param x Slot's X coordinate * @param y Slot's Y coordinate * @return Component at slot, or null if component not found */ public Component getComponent(int x, int y) { return getComponent(x, y, false); } /** * Get component with given slot coordinate * @param x Slot's X coordinate * @param y Slot's Y coordinate * @param acceptInsideHit If true also other slots reserved by component are accepted * @return Component at slot, or null if component not found */ public Component getComponent(int x, int y, boolean acceptInsideHit) { for (Connector connector : getState().childOptions.keySet()) { GridStackChildOptions info = getState().childOptions.get(connector); if (acceptInsideHit) { if (x >= info.x && x < (info.x + info.width) && y >= info.y && y < (info.y + info.width)) { return (Component) connector; } } else { if (info.x == x && info.y == y) { return (Component) connector; } } } return null; } /** * {@inheritDoc} */ @Override public void removeComponent(Component component) { getState().childOptions.remove(component); components.remove(component); super.removeComponent(component); } @Override public void replaceComponent(Component oldComponent, Component newComponent) { if (oldComponent == newComponent) { return; } if (oldComponent.getParent() != this) { throw new IllegalArgumentException("Replacable component not child of this layout"); } GridStackChildOptions oldOptions = getState(false).childOptions.get(oldComponent); removeComponent(oldComponent); if (newComponent.getParent() == this) { removeComponent(newComponent); } addComponent(newComponent, oldOptions.x, oldOptions.y, oldOptions.width, oldOptions.height); } /** * {@inheritDoc} */ @Override public int getComponentCount() { return components.size(); } /** * {@inheritDoc} */ @Override public Iterator<Component> iterator() { return components.iterator(); } /** * {@inheritDoc} */ @Override public void addLayoutClickListener(LayoutEvents.LayoutClickListener listener) { addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, LayoutEvents.LayoutClickEvent.class, listener, LayoutEvents.LayoutClickListener.clickMethod); } @Override @Deprecated public void addListener(LayoutEvents.LayoutClickListener layoutClickListener) { addLayoutClickListener(layoutClickListener); } /** * {@inheritDoc} */ @Override public void removeLayoutClickListener(LayoutEvents.LayoutClickListener listener) { removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, LayoutEvents.LayoutClickEvent.class, listener); } @Override @Deprecated public void removeListener(LayoutEvents.LayoutClickListener layoutClickListener) { removeLayoutClickListener(layoutClickListener); } /** * Add listener for component move events * @param listener Listener added */ public void addGridStackMoveListener(GridStackMoveEvent.GridStackMoveListener listener) { moveListeners.add(listener); } /** * Remove listener of component move events * @param listener Listener removed */ public void removeGridStackMoveListener(GridStackMoveEvent.GridStackMoveListener listener) { moveListeners.remove(listener); } /** * Add listener for gridstack ready event * @param listener Listener added */ public void addGridStackReadyListener(GridStackReadyEvent.GridStackReadyListener listener) { readyListeners.add(listener); } /** * Remove listener of GridStack ready event * @param listener Listener removed */ public void removeGridStackReadyListener(GridStackReadyEvent.GridStackReadyListener listener) { readyListeners.remove(listener); } /** * Get coordinates (X,Y,width,height) of component * @param child Child component of layout * @return Coordinates (X,Y,width,height) of component * @throws IllegalArgumentException If child not found */ public GridStackCoordinates getCoordinates(Component child) { GridStackChildOptions opts = getComponentOptions(child, false); return new GridStackCoordinates(opts.x, opts.y, opts.width, opts.height); } protected GridStackMoveEvent createMoveEvent(Component component, GridStackCoordinates oldCoordinates) { return new GridStackMoveEvent(this, component, oldCoordinates, getCoordinates(component)); } protected void fireMoveEvents(Collection<GridStackMoveEvent> events) { if (events.isEmpty()) { return; } for (GridStackMoveEvent.GridStackMoveListener listener : moveListeners) { listener.onGridStackMove(events); } } /** * Define size limitations to child component. For now some values must be defined before child has been rendered * on client side. * @param child Child of this layout * @param minWidth Mininum width in slots (null is undefined) * @param maxWidth Maxium width in slots (null is undefined) * @param minHeight Mininum height in slots (null is undefined) * @param maxHeight Maximum height in slots (null is undefined) */ public void setComponentSizeLimits(Component child, Integer minWidth, Integer maxWidth, Integer minHeight, Integer maxHeight) { GridStackChildOptions childOpts = getComponentOptions(child); childOpts.minWidth = minWidth; childOpts.maxWidth = maxWidth; childOpts.minHeight = minHeight; childOpts.maxHeight = maxHeight; } /** * Check if given child is locked (not allowed to move because of other dragged children) * @param child Child component of layout * @return true if locked, false if not * @throws IllegalArgumentException If child not found */ public boolean isComponentLocked(Component child) { return getComponentOptions(child, false).locked; } /** * Change locked state of child. Locked children will not be moved away from other dragged children. * @param child Child component of layout * @param locked true if locked, false if not * @throws IllegalArgumentException If child not found */ public void setComponentLocked(Component child, boolean locked) { getComponentOptions(child).locked = locked; } protected GridStackChildOptions getComponentOptions(Component child) { return getComponentOptions(child, true, true); } protected GridStackChildOptions getComponentOptions(Component child, boolean modify) { return getComponentOptions(child, modify, true); } protected GridStackChildOptions getComponentOptions(Component child, boolean modify, boolean throwIfMissing) { if (child == null || child.getParent() != this) { throw new IllegalArgumentException("Given component is not child of this layout"); } GridStackChildOptions opt = getState(modify).childOptions.get(child); if (opt == null) { throw new IllegalStateException("Missing child options"); } return opt; } /** * Define if layout is animated when child components are moved * @param animate true to animate, false to not animate * @return This GridStackLayout for command chaining */ public GridStackLayout setAnimate(boolean animate) { getState().gridStackOptions.animate = animate; return this; } /** * Check if layout is animated * @return true if animated, false if not */ public boolean isAnimate() { return getState(false).gridStackOptions.animate; } /** * Set layout static (no dragging of resizing) or dynamic (dragging and resizing allowed) * @param staticGrid true to set static (no dragging of resizing), false to set dynamic (dragging and resizing * allowed) * @return This GridStackLayout for command chaining */ public GridStackLayout setStaticGrid(boolean staticGrid) { getState().gridStackOptions.staticGrid = staticGrid; return this; } /** * Check if layout is in static mode * @return true if in static mode, false if not, null if not defined by server side */ public Boolean isStaticGrid() { return getState(false).gridStackOptions.staticGrid; } /** * Check if component has specific dragging handle * @param child Child component of layout * @return true if component has separate dragging handle, false if whole content acts as dragging handle */ public boolean isComponentWithDragHandle(Component child) { return getComponentOptions(child, false).useDragHandle; } /** * Define vertical margin between components on GridStack layout. Value is only read when rendered on client side * first time, so changing value after that will not have any effect (unless client side is detached). * @param marginPx Vertical margin in pixels * @return This GridStackLayout for command chaining */ public GridStackLayout setVerticalMargin(int marginPx) { getState(true).gridStackOptions.verticalMargin = marginPx; return this; } /** * Get vertical margin between components. Value might be mismatch to actual value used, if changed after client * side was last attached. * @return Vertical margin in pixels, if null the gridstack.js default is used. */ public Integer getVerticalMargin() { return getState(false).gridStackOptions.verticalMargin; } /** * Define height of cell in pixels. * @param heightPx Cell height in pixels * @return This GridStackLayout for command chaining */ public GridStackLayout setCellHeight(int heightPx) { getState(true).gridStackOptions.cellHeight = heightPx; return this; } /** * Get height of cell in pixels. * @return Cell height in pixels, if null the gridstack.js default is used. */ public Integer getCellHeight() { return getState(false).gridStackOptions.cellHeight; } /** * Set minimal width. If window width is less, grid will be shown in one-column mode. Changing value after * it has been attached on client side will not apply until client side is detached and attached. * @param minWidthPx Minimal width in pixels * @return This GridStackLayout for command chaining */ public GridStackLayout setMinWidth(int minWidthPx) { getState(true).gridStackOptions.minWidth = minWidthPx; return this; } /** * Get minimal width. If window width is less, grid will be shown in one-column mode. Value might be mismatch to * actual value used, if changed after client side was last attached. * @return Minimal width in pixels, if null the gridstack.js default is used. */ public Integer getMinWidth() { return getState(false).gridStackOptions.minWidth; } /** * Define if wrapper around child should allow vertical scrolling or not * @param child Child of layout * @param scrolling true to enable vertical scrolling, false to disable it * @throws IllegalArgumentException If child not found */ public void setWrapperScrolling(Component child, boolean scrolling) { getComponentOptions(child, true).disableScrolling = !scrolling; } /** * Check if wrapper around child allows vertical scrolling or not * @param child Child of layout * @return true if wrapper allows vertical scrolling, false if wrapper hides vertical overflow * @throws IllegalArgumentException If child not found */ public boolean isWrapperScrolling(Component child) { return getComponentOptions(child, false).disableScrolling; } /** * Check if given area is empty. Remember that any client side defined positioning not yet reported back to * server side will be unknown and so can result work results. * @param x Left edge coordinate of area * @param x Top edge coordinate of area * @param width Width of area in slots * @param height Height of area in slots * @return true if area is available and valid for use * @throws IllegalArgumentException If invalid values given */ public boolean isAreaEmpty(int x, int y, int width, int height) throws IllegalArgumentException { return isAreaEmpty(new GridStackCoordinates(x, y, width, height)); } /** * Check if given area is empty. Remember that any client side defined positioning not yet reported back to * server side will be unknown and so can result work results. Will also return false if area would go outside the * right edge. * @param coordinates Coordinate area checked (x, y, width, height) * @return true if area is available and valid for use * @throws IllegalArgumentException If invalid values given */ public boolean isAreaEmpty(GridStackCoordinates coordinates) throws IllegalArgumentException { if (coordinates.getX() < 0) { throw new IllegalArgumentException("X can not be negative"); } if (coordinates.getY() < 0) { throw new IllegalArgumentException("Y can not be negative"); } if (coordinates.getWidth() <= 0) { throw new IllegalArgumentException("Width most be larger than zero"); } if (coordinates.getHeight() <= 0) { throw new IllegalArgumentException("Height most be larger than zero"); } // If item would drop out of left side, return false if (coordinates.getX() + coordinates.getWidth() > getState(false).gridStackOptions.width) { return false; } for (int dx = 0; dx < coordinates.getWidth(); ++dx) { for (int dy = 0; dy < coordinates.getHeight(); ++dy) { Component occupant = getComponent(coordinates.getX() + dx, coordinates.getY() + dy, true); if (occupant != null) { return false; } } } return true; } }