package com.jotuntech.sketcher.client; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.net.URI; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.BoundedRangeModel; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.KeyStroke; import javax.swing.Timer; import cello.jtablet.TabletManager; import cello.jtablet.event.TabletEvent; import cello.jtablet.event.TabletListener; import cello.jtablet.installer.JTabletExtension; import cello.tablet.JTablet; import com.jotuntech.sketcher.client.command.CursorCommand; import com.jotuntech.sketcher.client.command.LineCommand; import com.jotuntech.sketcher.client.command.MergeCommand; import com.jotuntech.sketcher.client.command.MoveCommand; import com.jotuntech.sketcher.client.command.SetColorCommand; import com.jotuntech.sketcher.client.command.UndoCommand; import com.jotuntech.sketcher.common.BitmapLayer; import com.jotuntech.sketcher.common.Brush; import com.jotuntech.sketcher.common.Canvas; import com.jotuntech.sketcher.common.Input; import com.jotuntech.sketcher.common.Layer; import com.jotuntech.sketcher.common.Log; import com.jotuntech.sketcher.common.User; public class Editor extends JComponent implements MouseListener, MouseMotionListener, TabletListener { private Client client; private BufferedImage image; private float zoom = 1; private boolean smoothZoom = true; private Dimension size; private JTablet jTablet1; private boolean jTablet2; private Smoother smoother; private Cursor currentNativeCursor, crosshair, blank, eyedropper, hand; private Timer tagTimer, selectTimer, yieldTimer; private Color canvasBackground = Color.WHITE; private long lastCursorCommand; private int dashPhase = 0; private boolean tagsEnabled = true; public enum State { DISABLED, DRAW_HOVER, DRAW_PRESS, LINE_HOVER, LINE_PRESS, RECT_HOVER, RECT_PRESS, OVAL_HOVER, OVAL_PRESS, BEZIER_HOVER, BEZIER_PRESS, BEZIER_HOVER2, BEZIER_PRESS_P2, BEZIER_PRESS_P3, EYEDROP_PRESS, SCROLL_HOVER, SCROLL_PRESS, SELECT_HOVER, SELECT_PRESS, SELECT_INSIDE, SELECT_MOVE, MOVE_HOVER, MOVE_INSIDE, MOVE_PRESS, AD_HOVER, AD_PRESS; }; private State _state, _oldState = State.DISABLED; private int buttonDown = TabletEvent.NOBUTTON; public enum CursorType { NATIVE, SOFTWARE }; private CursorType cursorType; private boolean cursorVisible = true; private boolean softwareCursorEnabled = false; private BufferedImage ad; private Point adPosition; private int pressure = 0xFF; private float scrollSpeedX, scrollSpeedY, originX, originY, slowLOSX, slowLOSY; private Point los; private Timer scrollTimer; private float currentX, currentY; private float x1, y1, x2, y2, x3, y3, x4, y4; private int iOriginX, iOriginY; /** Current select */ private Rectangle select; public Editor(final Client client) { super(); Toolkit tk = Toolkit.getDefaultToolkit(); crosshair = tk.createCustomCursor(tk.getImage(getClass().getResource("images/crosshair.gif")), new Point(16, 16), "crosshair"); blank = tk.createCustomCursor(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), new Point(0, 0), "blank"); eyedropper = tk.createCustomCursor(tk.getImage(getClass().getResource("images/eyedropper.gif")), new Point(16, 16), "eyedropper"); hand = tk.createCustomCursor(tk.getImage(getClass().getResource("images/hand.gif")), new Point(16, 16), "hand"); setState(State.DISABLED); jTablet2 = false; if(JTabletExtension.checkCompatibility(this, "1.2.0") && JTabletExtension.getInstallStatus("1.2.0").equals(JTabletExtension.InstallStatus.INSTALLED)) { jTablet2 = true; TabletManager.getDefaultManager().addTabletListener(this, this); Log.info("JTablet 2.x installed."); } else { try { jTablet1 = new JTablet(); Log.info("JTablet 0.9.x installed."); } catch (Throwable t) { Log.info("JTablet not installed."); } } smoother = new Smoother(); if(!jTablet2) { addMouseListener(this); addMouseMotionListener(this); } this.client = client; ActionListener tagListener = new ActionListener() { public void actionPerformed(ActionEvent e) { if(client.getConnection() == null || client.getConnection().getUser() == null) { return; } if(lastCursorCommand + 500 < System.currentTimeMillis()) { client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2)))); lastCursorCommand = System.currentTimeMillis(); } if(!tagsEnabled) { return; } for(User u : client.getUserArray()) { if(u != client.getConnection().getUser()) { Input c = u.getCursor(); int cursorX = (int) (c.x * zoom); int cursorY = (int) (c.y * zoom); Rectangle tag = u.getTag(); if(tag.x != cursorX || tag.y != cursorY) { String name = u.getName(); Graphics2D g2 = (Graphics2D) getGraphics(); if(g2 == null) { return; } Rectangle2D stringBounds = g2.getFontMetrics().getStringBounds(name, g2); g2.dispose(); Rectangle newTag = new Rectangle(cursorX, cursorY, (int) stringBounds.getWidth() + 4, (int) stringBounds.getHeight() + 4); u.setTag(newTag); repaint(tag); repaint(newTag); } } } } }; tagTimer = new Timer(1000, tagListener); yieldTimer = new Timer(100, new ActionListener() { public void actionPerformed(ActionEvent e) { client.getCanvas().setDrawing(false); } }); ActionListener selectListener = new ActionListener() { public void actionPerformed(ActionEvent e) { Connection connection = client.getConnection(); if(connection == null) { return; } User user = connection.getUser(); if(user == null) { return; } if(select == null) { return; } repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); ++dashPhase; } }; selectTimer = new Timer(250, selectListener); selectTimer.setCoalesce(true); selectTimer.start(); lastCursorCommand = System.currentTimeMillis(); getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("SPACE"), "drag begin"); getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("released SPACE"), "drag end"); getActionMap().put("drag begin", new AbstractAction() { public void actionPerformed(ActionEvent e) { if(buttonDown != TabletEvent.NOBUTTON || getState() == State.SCROLL_PRESS || getState() == State.SCROLL_HOVER) { return; } if(storeState()) { setState(State.SCROLL_HOVER); } } }); getActionMap().put("drag end", new AbstractAction() { public void actionPerformed(ActionEvent e) { if(getState() == State.SCROLL_PRESS || getState() == State.SCROLL_HOVER) { restoreState(); } } }); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl Z"), "undo"); getActionMap().put("undo", new AbstractAction() { public void actionPerformed(ActionEvent e) { if(buttonDown == TabletEvent.NOBUTTON) { client.getCommandQueue().offer(new CommandEntry(0, new UndoCommand())); } } }); } protected void adjust() { if(image == null) { return; } size = new Dimension((int)(image.getWidth() * zoom), (int)(image.getHeight() * zoom)); setPreferredSize(size); setMaximumSize(size); invalidate(); getParent().validate(); repaint(); } public float getZoom() { return zoom; } public void setZoom(float zoom) { this.zoom = zoom; adjust(); } public void setState(State state) { if(state == this._state) { return; } if(state != State.MOVE_HOVER && state != State.MOVE_INSIDE && state != State.MOVE_PRESS && (this._state == State.MOVE_HOVER || this._state == State.MOVE_INSIDE)) { // TODO: Fix this dirty hack somehow. We must do this here, or the Move tool leaves our phantom layer dirty. client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); select = null; repaint(); System.err.println("Move state hack!"); } switch(state) { case DISABLED: setNativeCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); setCursorType(CursorType.NATIVE); break; case DRAW_HOVER: case DRAW_PRESS: setCursorType(CursorType.SOFTWARE); break; case LINE_HOVER: case RECT_HOVER: case OVAL_HOVER: case BEZIER_HOVER: case BEZIER_HOVER2: setNativeCursor(crosshair); setCursorType(CursorType.NATIVE); break; case EYEDROP_PRESS: setNativeCursor(eyedropper); setCursorType(CursorType.NATIVE); break; case AD_HOVER: setNativeCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); // Don't set to 'hand', this is actually a finger cursor setCursorType(CursorType.NATIVE); break; case SCROLL_HOVER: setNativeCursor(hand); setCursorType(CursorType.NATIVE); break; case SELECT_HOVER: setNativeCursor(crosshair); setCursorType(CursorType.NATIVE); break; case SELECT_INSIDE: setNativeCursor(hand); setCursorType(CursorType.NATIVE); break; case MOVE_HOVER: setNativeCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); setCursorType(CursorType.NATIVE); break; case MOVE_INSIDE: setNativeCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); setCursorType(CursorType.NATIVE); break; } this._state = state; System.err.println("Set state: " + state); } public State getState() { return _state; } private boolean storeState() { if(_oldState != State.DISABLED) { return false; } _oldState = getState(); System.err.println("Stored state, stack: " + _oldState); return true; } private void restoreState() { if(_oldState == State.DISABLED) { throw new RuntimeException("Stack is empty."); } setState(_oldState); _oldState = State.DISABLED; System.err.println("Restored state, stack: " + _oldState); } private void setNativeCursor(Cursor cursor) { Cursor actualCursor = cursorVisible ? cursor : blank; if(cursorType == CursorType.NATIVE && actualCursor != getCursor()) { setCursor(actualCursor); System.err.println("Changed native cursor (setNativeCursor): " + getCursor()); } currentNativeCursor = cursor; } private void setCursorVisible(boolean cursorVisible) { if(cursorVisible == this.cursorVisible) { return; } this.cursorVisible = cursorVisible; if(cursorType == CursorType.SOFTWARE || !cursorVisible) { if(getCursor() != blank) { setCursor(blank); System.err.println("Changed native cursor: " + getCursor()); } repaint(); } else if(currentNativeCursor != getCursor()) { setCursor(currentNativeCursor); } } public void setCursorType(CursorType cursorType) { switch(cursorType) { case NATIVE: this.setCursor(cursorVisible ? currentNativeCursor : blank); this.cursorType = CursorType.NATIVE; System.err.println("Changed native cursor: " + getCursor()); repaint(); break; case SOFTWARE: if(softwareCursorEnabled) { setCursor(blank); this.cursorType = CursorType.SOFTWARE; System.err.println("Changed native cursor: " + getCursor()); } else { currentNativeCursor = crosshair; this.setCursor(cursorVisible ? currentNativeCursor : blank); this.cursorType = CursorType.NATIVE; System.err.println("Changed native cursor: " + getCursor()); } repaint(); break; } } private float bezier(float t, float p0, float p1, float p2, float p3) { return (float) (Math.pow(1 - t, 3) * p0 + 3 * Math.pow(1 - t, 2) * t * p1 + 3 * (1 - t) * Math.pow(t, 2) * p2 + Math.pow(t, 3) * p3); } public void paint(Graphics g) { if(image == null) { return; } // Ensure proper clip area since JScrollView is an asshole Rectangle clip = g.getClipBounds(); Rectangle validRect = new Rectangle(new Point(0, 0), size); if(clip == null) { clip = validRect; } else { clip = clip.intersection(validRect); } g.setClip(clip); Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); if(smoothZoom && zoom != Math.floor(zoom)) { g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); } int zoomWidth = (int) (image.getWidth() * zoom); int zoomHeight = (int) (image.getHeight() * zoom); synchronized(image) { g2.setColor(canvasBackground); g2.fillRect(clip.x, clip.y, Math.min(zoomWidth, clip.width), Math.max(zoomHeight, clip.height)); int sourceX = Math.max(0, (int) Math.floor(clip.x / zoom)); float destinationX = sourceX * zoom; int sourceY = Math.max(0, (int) Math.floor(clip.y / zoom)); float destinationY = sourceY * zoom; int sourceWidth = Math.min(image.getWidth() - sourceX, (int) Math.ceil(clip.width / zoom) + 1); int sourceHeight = Math.min(image.getHeight() - sourceY, (int) Math.ceil(clip.height / zoom) + 1); if(sourceWidth > 0 && sourceHeight > 0) { BufferedImage subImage = image.getSubimage(sourceX, sourceY, sourceWidth, sourceHeight); AffineTransform xform = new AffineTransform(); xform.scale(zoom, zoom); AffineTransform oldXform = g2.getTransform(); g2.translate(destinationX, destinationY); g2.drawImage(subImage, xform, this); g2.setTransform(oldXform); } } for(User u : client.getUserArray()) { if(u != client.getConnection().getUser()) { Rectangle tag = u.getTag(); String name = u.getName(); Rectangle2D stringBounds = g2.getFontMetrics().getStringBounds(name, g2); g2.setColor(Color.YELLOW); g2.fillRect(tag.x, tag.y, tag.width, tag.height); g2.setColor(Color.BLACK); g2.drawRect(tag.x, tag.y, tag.width - 1, tag.height - 1); g2.drawString(name, tag.x + 2, tag.y + (int) stringBounds.getHeight()); } } g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); User user = client.getConnection().getUser(); if(cursorVisible && cursorType == CursorType.SOFTWARE) { Brush brush = user.getBrush(); float hardAdjust = 0.5f + ((brush.getHardness() - 1f) / 12f); Input input = smoother.getIndex(2); float finalRadius = brush.isPressureToRadius() && input.pressure > 0 ? (brush.getRadius() * input.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom; float finalX = input.x * zoom; float finalY = input.y * zoom; Shape s = new Ellipse2D.Float(finalX - finalRadius, finalY - finalRadius, finalRadius * 2, finalRadius * 2); g2.setColor(Color.BLACK); g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 2, 2 }, 0)); g2.draw(s); g2.setColor(Color.WHITE); g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 2, 2 }, 2)); g2.draw(s); } g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10)); g2.setColor(Color.BLACK); State myState = getState(); if(myState == State.LINE_PRESS || myState == State.RECT_PRESS || myState == State.OVAL_PRESS) { switch(myState) { case LINE_PRESS: g2.draw(new Line2D.Float(new Point2D.Float(originX * zoom, originY * zoom), new Point2D.Float(currentX * zoom, currentY * zoom))); break; case RECT_PRESS: g2.draw(new Rectangle2D.Float(originX * zoom, originY * zoom, currentX * zoom - originX * zoom, currentY * zoom - originY * zoom)); break; case OVAL_PRESS: float radiusX = Math.abs(currentX * zoom - originX * zoom); float radiusY = Math.abs(currentY * zoom - originY * zoom); float minX = originX * zoom - radiusX; float minY = originY * zoom - radiusY; g2.draw(new Ellipse2D.Float(minX, minY, radiusX * 2, radiusY * 2)); break; } } else if(myState == State.BEZIER_PRESS || myState == State.BEZIER_HOVER2 || myState == State.BEZIER_PRESS_P2 || myState == State.BEZIER_PRESS_P3) { float t = 0, lx = x1 * zoom, ly = y1 * zoom; for(int i = 0; i <= 64; i++, t += 1f / 64f) { float x = bezier(t, x1 * zoom, x2 * zoom, x3 * zoom, x4 * zoom); float y = bezier(t, y1 * zoom, y2 * zoom, y3 * zoom, y4 * zoom); g2.draw(new Line2D.Float(lx, ly, x, y)); lx = x; ly = y; } g2.setColor(Color.GRAY); g2.draw(new Line2D.Float(x1 * zoom, y1 * zoom, x2 * zoom, y2 * zoom)); g2.draw(new Line2D.Float(x4 * zoom, y4 * zoom, x3 * zoom, y3 * zoom)); g2.draw(new Rectangle2D.Float(x2 * zoom - 3, y2 * zoom - 3, 6, 6)); g2.draw(new Rectangle2D.Float(x3 * zoom - 3, y3 * zoom - 3, 6, 6)); } g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_DEFAULT); if(ad != null) { g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); g2.drawImage(ad, adPosition.x, adPosition.y, null); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); } if(select != null) { g2.setColor(Color.BLACK); g2.setXORMode(Color.WHITE); g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, new float[] { 4, 4 }, dashPhase)); g2.drawRect(Math.round(select.x * zoom), Math.round(select.y * zoom), Math.round(select.width * zoom), Math.round(select.height * zoom)); } } public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { if(img == null) { if(infoflags == ImageObserver.ALLBITS) { x = 0; y = 0; width = client.getCanvas().getWidth(); height = client.getCanvas().getHeight(); } synchronized(image) { Graphics2D g2 = (Graphics2D)image.getGraphics(); g2.setClip(x, y, width, height); AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.DST_OUT, 1); g2.setComposite(ac); g2.fillRect(x, y, width, height); /** Create a map of layers, showing which phantom layers they have */ IdentityHashMap> selectedLayers = new IdentityHashMap>(); for(User u : client.getUserMap().values()) { Layer l = u.getLayer(); if(l == null) { continue; } Set us = selectedLayers.get(l); if(us == null) { us = new HashSet(); selectedLayers.put(l, us); } us.add(u.getPhantomLayer()); } for(Layer l : client.getCanvas().getLayerMap().values()) { Set ls = selectedLayers.get(l); if(l instanceof BitmapLayer && ls != null) { ((BitmapLayer) l).draw(g2, selectedLayers.get(l)); } else { l.draw(g2); } } g2.dispose(); } repaint((int) Math.floor(x * zoom - zoom), (int) Math.floor(y * zoom - zoom), (int) Math.ceil(width * zoom + zoom), (int) Math.ceil(height * zoom + zoom)); return true; } else { return super.imageUpdate(img, infoflags, x, y, width, height); } } public void mouseDragged(MouseEvent e) { cursorDragged(new TabletEvent(e, null, null)); } public void mouseMoved(MouseEvent e) { cursorMoved(new TabletEvent(e, null, null)); } public void mouseClicked(MouseEvent e) { } public void mousePressed(MouseEvent e) { cursorPressed(new TabletEvent(e, null, null)); } public void mouseReleased(MouseEvent e) { cursorReleased(new TabletEvent(e, null, null)); } public void mouseEntered(MouseEvent e) { cursorEntered(new TabletEvent(e, null, null)); } public void mouseExited(MouseEvent e) { cursorExited(new TabletEvent(e, null, null)); } void eyedrop(MouseEvent e) { if(client.getConnection() == null || client.getConnection().getUser() == null || client.getConnection().getUser().getLayer() == null) { return; } Input input = new Input(e.getX() / zoom, e.getY() / zoom, 0); int color = client.getConnection().getUser().getLayer().getColor(input); client.getCommandQueue().add(new CommandEntry(0, new SetColorCommand(color))); client.getUserInterface().setColorSliders(color); } public void setTagTimer(Timer tagTimer) { this.tagTimer = tagTimer; } public Timer getTagTimer() { return tagTimer; } public void setCanvas(Canvas canvas) { image = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_INT_ARGB); image.setAccelerationPriority(1); canvas.addImageObserver(this); adjust(); tagTimer.start(); yieldTimer.start(); } public void setCanvasBackground(Color canvasBackground) { this.canvasBackground = canvasBackground; } public Color getCanvasBackground() { return canvasBackground; } public boolean hasJTablet() { return (jTablet1 != null) || jTablet2; } void paintMouseCursor() { if(cursorVisible && cursorType == CursorType.SOFTWARE) { Brush brush = client.getConnection().getUser().getBrush(); float hardAdjust = 0.5f + ((brush.getHardness() - 1f) / 12f); Input oldInput = smoother.getIndex(1); float oldRadius = brush.isPressureToRadius() && oldInput.pressure > 0 ? (brush.getRadius() * oldInput.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom; float oldX = oldInput.x * zoom; float oldY = oldInput.y * zoom; repaint((int) Math.floor(oldX - oldRadius) - 2, (int) Math.floor(oldY - oldRadius) - 2, (int) Math.ceil(oldRadius * 2) + 4, (int) Math.ceil(oldRadius * 2) + 4); Input input = smoother.getIndex(2); float radius = brush.isPressureToRadius() && input.pressure > 0 ? (brush.getRadius() * input.pressure * hardAdjust * zoom) / 255f : brush.getRadius() * hardAdjust * zoom; float x = input.x * zoom; float y = input.y * zoom; paintImmediately((int) Math.floor(x - oldRadius) - 2, (int) Math.floor(y - oldRadius) - 2, (int) Math.ceil(radius * 2) + 4, (int) Math.ceil(radius * 2) + 4); } } private void yieldForPaint() { if(client.getCanvas() != null) { client.getCanvas().setDrawing(true); yieldTimer.restart(); } } public void cursorEntered(TabletEvent e) { setCursorVisible(true); } public void cursorExited(TabletEvent e) { setCursorVisible(false); } public void cursorGestured(TabletEvent e) { } public void cursorMoved(TabletEvent e) { if(client.getConnection() == null || client.getConnection().getUser() == null) { return; } Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0); smoother.add(input); paintMouseCursor(); if(lastCursorCommand + 500 < System.currentTimeMillis()) { client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2)))); lastCursorCommand = System.currentTimeMillis(); } State myState = getState(); boolean inSelect = select != null && select.contains(e.getX() / zoom, e.getY() / zoom); boolean inAd = ad != null && e.getX() >= adPosition.x && e.getY() >= adPosition.y && e.getX() < adPosition.x + ad.getWidth() && e.getY() < adPosition.y + ad.getHeight(); switch(myState) { case SCROLL_HOVER: case SCROLL_PRESS: break; case SELECT_HOVER: if(inSelect) { setState(State.SELECT_INSIDE); } break; case SELECT_INSIDE: if(!inSelect) { setState(State.SELECT_HOVER); } break; case MOVE_HOVER: if(inSelect) { setState(State.MOVE_INSIDE); } break; case MOVE_INSIDE: if(!inSelect) { setState(State.MOVE_HOVER); } break; case AD_HOVER: if(!inAd) { restoreState(); } break; default: if(inAd && storeState()) { setState(State.AD_HOVER); } break; } } public void cursorPressed(TabletEvent e) { if(buttonDown != TabletEvent.NOBUTTON) { return; } buttonDown = e.getButton(); yieldForPaint(); requestFocusInWindow(); User user = client.getConnection().getUser(); State myState = getState(); int button = e.getButton(); switch(button) { case MouseEvent.BUTTON1: switch(myState) { case DISABLED: return; case DRAW_HOVER: if(user != null && user.isViewer()) { client.getUserInterface().println("Drawing is not permitted in viewer mode. Please sign in to participate."); break; } setState(State.DRAW_PRESS); if(jTablet2) { pressure = Math.round(e.getPressure() * 255f); } else if(jTablet1 != null) { try { if(jTablet1.poll() && jTablet1.hasCursor()) { pressure = (jTablet1.getPressure() * 255) / jTablet1.getPressureExtent(); } } catch(Throwable jte) { } } Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, pressure); smoother.add(input); smoother.setPressure(pressure); client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(smoother.getIndex(2)))); break; case LINE_HOVER: case RECT_HOVER: case OVAL_HOVER: originX = e.getFloatX() / zoom; originY = e.getFloatY() / zoom; currentX = e.getFloatX() / zoom; currentY = e.getFloatY() / zoom; switch(myState) { case LINE_HOVER: setState(State.LINE_PRESS); break; case RECT_HOVER: setState(State.RECT_PRESS); break; case OVAL_HOVER: setState(State.OVAL_PRESS); break; } break; case BEZIER_HOVER: x4 = x3 = x2 = x1 = e.getFloatX() / zoom; y4 = y3 = y2 = y1 = e.getFloatY() / zoom; setState(State.BEZIER_PRESS); break; case BEZIER_HOVER2: float x = e.getFloatX() / zoom; float y = e.getFloatY() / zoom; float pad = 3 / zoom; if(x >= x2 - pad && x <= x2 + pad && y >= y2 - pad && y <= y2 + pad) { setState(State.BEZIER_PRESS_P2); } else if(x >= x3 - pad && x <= x3 + pad && y >= y3 - pad && y <= y3 + pad) { setState(State.BEZIER_PRESS_P3); } break; case SELECT_INSIDE: setState(State.SELECT_MOVE); originX = e.getX() / zoom; originY = e.getY() / zoom; break; case SELECT_HOVER: setState(State.SELECT_PRESS); if(select != null) { Rectangle r = new Rectangle(select); r.grow(1, 1); repaint(Math.round(r.x * zoom), Math.round(r.y * zoom), Math.round(r.width * zoom), Math.round(r.height * zoom)); } select = new Rectangle(e.getX(), e.getY(), 0, 0); iOriginX = Math.round(e.getX() / zoom); iOriginY = Math.round(e.getY() / zoom); break; case MOVE_INSIDE: if(user == null || user.isViewer()) { client.getUserInterface().println("Moving is not permitted in viewer mode. Please sign in to participate."); break; } if(user.getLayer() == null) { break; } client.getCommandQueue().offer(new CommandEntry(0, new MoveCommand(select.x, select.y, select.x, select.y, select.width, select.height))); setState(State.MOVE_PRESS); originX = e.getX() / zoom; originY = e.getY() / zoom; break; case SCROLL_HOVER: los = e.getLocationOnScreen(); originX = los.x; originY = los.y; slowLOSX = los.x; slowLOSY = los.y; scrollSpeedX = 0; scrollSpeedY = 0; setState(State.SCROLL_PRESS); BoundedRangeModel hm = client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel(); BoundedRangeModel vm = client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel(); hm.setValueIsAdjusting(true); vm.setValueIsAdjusting(true); if(scrollTimer != null) { scrollTimer.stop(); scrollTimer = null; } scrollTimer = new Timer(20, new ActionListener() { public void actionPerformed(ActionEvent e) { BoundedRangeModel hm = client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel(); BoundedRangeModel vm = client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel(); if(getState() == State.SCROLL_PRESS) { slowLOSX += (los.x - slowLOSX) * 0.125f; slowLOSY += (los.y - slowLOSY) * 0.125f; scrollSpeedX = -(slowLOSX - originX) - Math.signum(slowLOSX - originX) * (slowLOSX - originX) * (slowLOSX - originX) / 12f; scrollSpeedY = -(slowLOSY - originY) - Math.signum(slowLOSY - originY) * (slowLOSY - originY) * (slowLOSY - originY) / 12f; originX = slowLOSX; originY = slowLOSY; hm.setValue(hm.getValue() + Math.round(scrollSpeedX)); vm.setValue(vm.getValue() + Math.round(scrollSpeedY)); } else { hm.setValue(hm.getValue() + Math.round(scrollSpeedX)); vm.setValue(vm.getValue() + Math.round(scrollSpeedY)); scrollSpeedX *= 0.95f; scrollSpeedY *= 0.95f; if(Math.abs(scrollSpeedX) < 0.5f && Math.abs(scrollSpeedY) < 0.5f) { client.getUserInterface().getEditorPane().getHorizontalScrollBar().getModel().setValueIsAdjusting(false); client.getUserInterface().getEditorPane().getVerticalScrollBar().getModel().setValueIsAdjusting(false); scrollTimer.stop(); scrollTimer = null; } } } }); scrollTimer.setCoalesce(false); scrollTimer.start(); break; case AD_HOVER: setState(State.AD_PRESS); break; } break; case MouseEvent.BUTTON3: if(storeState()) { setState(State.EYEDROP_PRESS); } break; } } public void cursorDragged(TabletEvent e) { if(client.getConnection() == null || client.getConnection().getUser() == null || client.getConnection().getUser().getLayer() == null) { return; } if(jTablet2) { pressure = Math.round(e.getPressure() * 255f); } else if(jTablet1 != null) { try { if(jTablet1.poll() && jTablet1.hasCursor()) { pressure = (jTablet1.getPressure() * 255) / jTablet1.getPressureExtent(); } } catch(Throwable jte) { } } Input input = new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, pressure); smoother.add(input); paintMouseCursor(); State myState = getState(); switch(myState) { case DRAW_PRESS: yieldForPaint(); if(pressure > 0) { List inputList = smoother.get(); for(Iterator i = inputList.iterator(); i.hasNext();) { Input in = i.next(); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(in))); } } break; case LINE_PRESS: case RECT_PRESS: case OVAL_PRESS: yieldForPaint(); // Calculate where shape is right now float radiusX = Math.abs(currentX * zoom - originX * zoom); float radiusY = Math.abs(currentY * zoom - originY * zoom); int minX = (int) Math.floor(originX * zoom - radiusX) - 1; int minY = (int) Math.floor(originY * zoom - radiusY) - 1; int width = (int) Math.ceil(radiusX * 2) + 2; int height = (int) Math.ceil(radiusY * 2) + 2; // Repaint this area, clearing it out repaint(minX, minY, width, height); currentX = e.getFloatX() / zoom; currentY = e.getFloatY() / zoom; // Calculate new position of shape radiusX = Math.abs(currentX * zoom - originX * zoom); radiusY = Math.abs(currentY * zoom - originY * zoom); minX = (int) Math.floor(originX * zoom - radiusX) - 1; minY = (int) Math.floor(originY * zoom - radiusY) - 1; width = (int) Math.ceil(radiusX * 2) + 2; height = (int) Math.ceil(radiusY * 2) + 2; repaint(minX, minY, width, height); break; case BEZIER_PRESS: yieldForPaint(); // Initialize curve to straight line x4 = e.getFloatX() / zoom; y4 = e.getFloatY() / zoom; x2 = (x1 * 2 + x4) / 3; y2 = (y1 * 2 + y4) / 3; x3 = (x1 + x4 * 2) / 3; y3 = (y1 + y4 * 2) / 3; // Calculate extreme corners minX = (int) Math.floor(Math.min(x1, x4) * zoom) - 4; minY = (int) Math.floor(Math.min(y1, y4) * zoom) - 4; int maxX = (int) Math.ceil(Math.max(x1, x4) * zoom) + 4; int maxY = (int) Math.ceil(Math.max(y1, y4) * zoom) + 4; repaint(minX, minY, maxX - minX, maxY - minY); break; case BEZIER_PRESS_P2: yieldForPaint(); minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4; minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4; maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4; maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4; repaint(minX, minY, maxX - minX, maxY - minY); x2 = e.getFloatX() / zoom; y2 = e.getFloatY() / zoom; minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4; minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4; maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4; maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4; repaint(minX, minY, maxX - minX, maxY - minY); break; case BEZIER_PRESS_P3: yieldForPaint(); minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4; minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4; maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4; maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4; repaint(minX, minY, maxX - minX, maxY - minY); x3 = e.getFloatX() / zoom; y3 = e.getFloatY() / zoom; minX = (int) Math.floor(Math.min(x1, Math.min(x2, Math.min(x3, x4))) * zoom) - 4; minY = (int) Math.floor(Math.min(y1, Math.min(y2, Math.min(y3, y4))) * zoom) - 4; maxX = (int) Math.ceil(Math.max(x1, Math.max(x2, Math.max(x3, x4))) * zoom) + 4; maxY = (int) Math.ceil(Math.max(y1, Math.max(y2, Math.max(y3, y4))) * zoom) + 4; repaint(minX, minY, maxX - minX, maxY - minY); break; case SELECT_MOVE: yieldForPaint(); repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); select.translate((int) (e.getX() / zoom - originX), (int) (e.getY() / zoom - originY)); originX = e.getX() / zoom; originY = e.getY() / zoom; ++dashPhase; repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); break; case SELECT_PRESS: yieldForPaint(); minX = Math.min(iOriginX, Math.round(e.getX() / zoom)); minY = Math.min(iOriginY, Math.round(e.getY() / zoom)); maxX = Math.max(iOriginX, Math.round(e.getX() / zoom)); maxY = Math.max(iOriginY, Math.round(e.getY() / zoom)); width = maxX - minX; height = maxY - minY; repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); select.setBounds(minX, minY, maxX - minX, maxY - minY); repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); break; case MOVE_PRESS: break; case SCROLL_PRESS: los = e.getLocationOnScreen(); break; case EYEDROP_PRESS: yieldForPaint(); eyedrop(e); break; } } public void cursorReleased(TabletEvent e) { int button = e.getButton(); if(buttonDown == button) { buttonDown = TabletEvent.NOBUTTON; } else { return; } State myState = getState(); switch(button) { case TabletEvent.BUTTON1: switch(myState) { case DRAW_PRESS: client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); setState(State.DRAW_HOVER); break; case LINE_PRESS: client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, originY, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); setState(State.LINE_HOVER); break; case RECT_PRESS: client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, originY, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, originY, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(e.getFloatX() / zoom, originY, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(e.getFloatX() / zoom, e.getFloatY() / zoom, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(originX, e.getFloatY() / zoom, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(originX, e.getFloatY() / zoom, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(originX, originY, 0xFF)))); client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); setState(State.RECT_HOVER); break; case BEZIER_PRESS: setState(State.BEZIER_HOVER2); break; case BEZIER_HOVER2: client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(x1, y1, 64)))); float t = 1f / 64f; for(int i = 1; i <= 64; i++, t += 1f / 64f) { float x = bezier(t, x1, x2, x3, x4); float y = bezier(t, y1, y2, y3, y4); client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(x, y, 64 + (int) (191 * Math.sin(t * Math.PI)))))); } client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); setState(State.BEZIER_HOVER); break; case BEZIER_PRESS_P2: case BEZIER_PRESS_P3: setState(State.BEZIER_HOVER2); break; case OVAL_PRESS: float centerX = originX; float centerY = originY; float radiusX = Math.abs(e.getFloatX() / zoom - centerX); float radiusY = Math.abs(e.getFloatY() / zoom - centerY); int divisions = 64;//(int) Math.round(Math.max(radiusX, radiusY) * Math.PI); if(divisions == 0) { break; } client.getCommandQueue().offer(new CommandEntry(0, new CursorCommand(new Input(centerX + radiusX, centerY, 0xFF)))); float alpha = 0; for(int i = 0; i <= divisions; i++, alpha += Math.PI * 2 / divisions) { client.getCommandQueue().offer(new CommandEntry(0, new LineCommand(new Input(centerX + (float) Math.cos(alpha) * radiusX, centerY + (float) Math.sin(alpha) * radiusY, 0xFF)))); } client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); setState(State.OVAL_HOVER); break; case AD_PRESS: if(e.getX() >= adPosition.x && e.getY() >= adPosition.y && e.getX() < adPosition.x + ad.getWidth() && e.getY() < adPosition.y + ad.getHeight()) { try { Class desktopClass = Class.forName("java.awt.Desktop"); if((Boolean) desktopClass.getMethod("isDesktopSupported").invoke(null)) { // Desktop.isDesktopSupported() Object desktop = desktopClass.getMethod("getDesktop").invoke(null); // Desktop.getDesktop() Class actionClass = Class.forName("java.awt.Desktop$Action"); if((Boolean) desktopClass.getMethod("isSupported", actionClass).invoke(desktop, actionClass.getField("BROWSE").get(null))) { // desktop.isSupported(java.awt.Desktop.Action.BROWSE) desktopClass.getMethod("browse", URI.class).invoke(desktop, new URI("http://www.jotuntech.com/sketcher/")); // desktop.browse(uri); } } } catch(Throwable t2) { JOptionPane.showMessageDialog(client.getUserInterface(), "Visit www.jotuntech.com/sketcher/ to rent a Sketcher room!"); } } setState(State.AD_HOVER); case SELECT_PRESS: if(select.isEmpty()) { System.err.println("Empty select!"); select = null; } setState(State.SELECT_HOVER); repaint(); break; case SELECT_MOVE: setState(State.SELECT_INSIDE); break; case MOVE_HOVER: client.getCommandQueue().offer(new CommandEntry(0, new MergeCommand())); select = null; repaint(); break; case MOVE_PRESS: repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); int transX = (int) (e.getX() / zoom - originX); int transY = (int) (e.getY() / zoom - originY); client.getCommandQueue().offer(new CommandEntry(0, new MoveCommand(select.x, select.y, select.x + transX, select.y + transY, select.width, select.height))); select.translate(transX, transY); ++dashPhase; repaint(Math.round(select.x * zoom) - 1, Math.round(select.y * zoom) - 1, Math.round(select.width * zoom) + 2, Math.round(select.height * zoom) + 2); setState(State.MOVE_INSIDE); break; case SCROLL_PRESS: restoreState(); break; } break; case TabletEvent.BUTTON3: switch(myState) { case EYEDROP_PRESS: eyedrop(e); restoreState(); break; } break; } } public void cursorScrolled(TabletEvent e) { } public void levelChanged(TabletEvent e) { } public BufferedImage getImage() { return image; } public void setSmoothZoom(boolean enabled) { this.smoothZoom = enabled; } public boolean isSmoothZoom() { return smoothZoom; } public void setTagsEnabled(boolean tagsEnabled) { this.tagsEnabled = tagsEnabled; } public boolean isTagsEnabled() { return tagsEnabled; } public void setAd(BufferedImage ad) { this.ad = ad; } public BufferedImage getAd() { return ad; } public void setAdPosition(Point adPosition) { this.adPosition = adPosition; } public void setAdPosition(int x, int y) { this.adPosition = new Point(x, y); } public Point getAdPosition() { return adPosition; } public Rectangle getSelect() { return select; } public boolean isSoftwareCursorEnabled() { return softwareCursorEnabled; } public void setSoftwareCursorEnabled(boolean softwareCursorEnabled) { this.softwareCursorEnabled = softwareCursorEnabled; } }