/* *------------------------------------------------------------------------------ * Copyright (C) 2006-2014 University of Dundee. All rights reserved. * * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * *------------------------------------------------------------------------------ */ package org.openmicroscopy.shoola.agents.dataBrowser.browser; import java.awt.Component; import java.awt.Cursor; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import javax.swing.JComponent; import javax.swing.JLayeredPane; import javax.swing.SwingUtilities; import org.openmicroscopy.shoola.agents.dataBrowser.Colors; import org.openmicroscopy.shoola.agents.dataBrowser.DataBrowserAgent; import org.openmicroscopy.shoola.agents.dataBrowser.visitor.SelectionVisitor; import org.openmicroscopy.shoola.agents.dataBrowser.visitor.RowSelectionVisitor; import org.openmicroscopy.shoola.util.ui.UIUtilities; import org.openmicroscopy.shoola.util.ui.colourpicker.ColourObject; import org.openmicroscopy.shoola.util.ui.colourpicker.ColourPicker; import omero.gateway.model.ImageData; /** * Handles input events originating from the {@link Browser}'s View. * That is, from the {@link RootDisplay} containing all the visualization * trees. * This class takes on the role of the browser's Controller (as in MVC). * * @see BrowserModel * @see RootDisplay * * @author Jean-Marie Burel      * <a href="mailto:j.burel@dundee.ac.uk">j.burel@dundee.ac.uk</a> * @author Donald MacDonald      * <a href="mailto:donald@lifesci.dundee.ac.uk">donald@lifesci.dundee.ac.uk</a> * @version 3.0 * @since OME3.0 */ class BrowserControl implements MouseListener, ImageDisplayVisitor, PropertyChangeListener, MouseMotionListener { //TODO: Implement scroll listener. When the currently selected node is //scrolled out of the parent's viewport then it has to be deselected. /** Indicates to navigate to the left */ private static final int DIRECTION_LEFT = 0; /** Indicates to navigate to the right */ private static final int DIRECTION_RIGHT = 1; /** Indicates to navigate upwards */ private static final int DIRECTION_UP = 2; /** Indicates to navigate downwards */ private static final int DIRECTION_DOWN = 3; /** The Model controlled by this Controller. */ private BrowserModel model; /** The View controlled by this Controller.*/ private RootDisplay view; /** The selected cell, only used when displaying Plate. */ private CellDisplay selectedCell; /** Flag indicating if it is a right-click.*/ private boolean rightClickButton; /** Flag indicating if it is a right-click.*/ private boolean rightClickPad; /** Indicates if the <code>control</code> key is down. */ private boolean ctrl; /** Flag indicating if the left mouse button is pressed. */ private boolean leftMouseButton; /** The location of the mouse pressed.*/ private Point anchor = new Point(); /** The source of the mouse pressed.*/ private Component source; /** The rectangle used to select multiple nodes.*/ private Rectangle selection = new Rectangle(); /** Flag indicating if the <code>Shift</code> is down or not.*/ private boolean shiftDown; /** Component used to select several images.*/ private GlassPane glassPane; /** Flag indicating that the {@link #glassPane} has already been added.*/ private boolean added; /** Flag indicating that the multi-selection is on.*/ private boolean dragging; /** The listener to add to the nodes.*/ private KeyAdapter keyListener; /** Handles the multi-selection using key.*/ private void handleKeySelection() { SelectionVisitor visitor = new SelectionVisitor(null, true); view.accept(visitor); model.setSelectedDisplays(visitor.getSelected()); } /** * Handles the multi-selection. * * @param install Pass <code>true</code> to install the glass pane, * <code>false</code> otherwise. */ private SelectionVisitor handleMultiSelection(boolean install) { SelectionVisitor visitor = new SelectionVisitor(selection, !install); view.accept(visitor); JLayeredPane pane = (JLayeredPane) view.getInternalDesktop(); if (install) { if (glassPane == null) { glassPane = new GlassPane(); } glassPane.setSelection(selection); if (!added) { glassPane.setSize(view.getSize()); pane.add(glassPane, Integer.valueOf(1)); added = true; } } else { if (glassPane != null) { pane.remove(glassPane); } added = false; } pane.repaint(); return visitor; } /** * Reacts to mouse pressed and mouse release event. * * @param me The event to handle. * @param released Pass <code>true</code> if the method is invoked when * the mouse is released, <code>false</code> otherwise. */ private void onClick(MouseEvent me, boolean released) { if (me.getClickCount() == 1) { ImageDisplay d = findParentDisplay(me.getSource()); //if (d == view) return; d.moveToFront(); handleSelection(d, me); me = SwingUtilities.convertMouseEvent((Component) me.getSource(), me, view); Point p = me.getPoint(); model.setPopupPoint(p, false); if ((me.isPopupTrigger() && !released) || (me.isPopupTrigger() && released && !UIUtilities.isMacOS()) || (UIUtilities.isMacOS() && SwingUtilities.isLeftMouseButton(me) && me.isControlDown())) { model.setPopupPoint(p, true); } } else if (me.getClickCount() == 2 && !(me.isMetaDown() || me.isControlDown() || me.isShiftDown())) { Object src = me.getSource(); ImageDisplay d = findParentDisplay(src); if (d == view) return; if (d instanceof ImageNode && !(d.getTitleBar() == src) && isSelectionValid(d)) { model.viewDisplay(d, false); } } } /** * Handles the selection. * * @param d The selected node. * @param me The mouse event to handle. */ private void handleSelection(ImageDisplay d, MouseEvent me) { boolean shiftDown = me.isShiftDown(); Point p = me.getPoint(); ImageDisplay previousDisplay = model.getLastSelectedDisplay(); if (((rightClickButton && !ctrl) || rightClickPad) && model.isMultiSelection()) { //setFoundNode(nodes); return; } if (((ctrl || shiftDown) && leftMouseButton)) { //multi selection ImageDisplay previous = model.getLastSelectedDisplay(); if (previous == null) { model.setSelectedDisplay(d, true, true); return; } Object object = previous.getHierarchyObject(); Class<?> ref = object.getClass(); if (!ref.equals(d.getHierarchyObject().getClass())) return; Collection<ImageDisplay> nodes = model.getSelectedDisplays(); Iterator<ImageDisplay> i = nodes.iterator(); ImageDisplay node; if (nodes.size() == 1) { while (i.hasNext()) { node = i.next(); if (node.equals(d)) return; } } boolean remove = false; while (i.hasNext()) { node = i.next(); if (node.equals(d)) { remove = true; break; } } if (remove) model.removeSelectedDisplay(d); else model.setSelectedDisplay(d, true, true); } else { if (isSelectionValid(d)) { if (d instanceof CellDisplay && !(d.equals(previousDisplay))) { setSelectedCell(p, (CellDisplay) d); } else { boolean b = model.isMultiSelection(); if (rightClickButton && b) return; if (b || !(d.equals(previousDisplay))) model.setSelectedDisplay(d, false, true); } } } } /** * Handles the selection. * * @param d The selected node. * @param multiSelection Passed <code>true</code> if multiple selection is * on, <code>false</code> otherwise. * @param p The point where the mouse is clicked. */ /* private void handleSelection(ImageDisplay d, boolean multiSelection, Point p) { ImageDisplay previousDisplay = model.getLastSelectedDisplay(); if (multiSelection) { //multi selection ImageDisplay previous = model.getLastSelectedDisplay(); if (previous == null) { model.setSelectedDisplay(d, true, true); return; } Object object = previous.getHierarchyObject(); Class ref = object.getClass(); if (!ref.equals(d.getHierarchyObject().getClass())) return; Collection nodes = model.getSelectedDisplays(); Iterator i = nodes.iterator(); ImageDisplay node; boolean remove = false; while (i.hasNext()) { node = (ImageDisplay) i.next(); if (node.equals(d)) { remove = true; break; } } if (remove) model.removeSelectedDisplay(d); else model.setSelectedDisplay(d, true, true); } else { if (isSelectionValid(d)) { if (d instanceof CellDisplay && !(d.equals(previousDisplay))) { setSelectedCell(p, (CellDisplay) d); } else { boolean b = model.isMultiSelection(); if (b || !(d.equals(previousDisplay))) model.setSelectedDisplay(d, false, true); } } } } */ /** * Brings up the color picker to set the color of the node. * * @param p The location of the dialog. * @param node The selected node. */ private void setSelectedCell(Point p, CellDisplay node) { selectedCell = node; SwingUtilities.convertPointToScreen(p, node); ColourPicker picker = new ColourPicker( DataBrowserAgent.getRegistry().getTaskBar().getFrame(), node.getHighlight(), true); picker.setColorDescription(node.getDescription()); picker.addPropertyChangeListener(this); picker.setLocation(p); picker.setVisible(true); } /** * Finds the first {@link ImageDisplay} in <code>x</code>'s containment * hierarchy. * * @param x A component. * @return The parent {@link ImageDisplay} or <code>null</code> if none * was found. */ private ImageDisplay findParentDisplay(Object x) { while (true) { if (x instanceof ImageDisplay) return (ImageDisplay) x; if (x instanceof JComponent) x = ((JComponent) x).getParent(); else break; } return null; } /** * Attaches the listeners to the specified node. * * @param node The node to handle. */ private void attachListeners(ImageNode node) { if (node == null) return; node.addPropertyChangeListener(this); node.addListenerToComponents(this); } /** * Creates a new Controller for the specified <code>model</code> and * <code>view</code>. * You need to call the {@link #initialize() initialize} method after * creation to complete the MVC set up. * * @param model The Model. * @param view The View. */ BrowserControl(final BrowserModel model, RootDisplay view) { if (model == null) throw new NullPointerException("No model."); if (view == null) throw new NullPointerException("No view."); model.addPropertyChangeListener( Browser.SELECTED_DATA_BROWSER_NODE_DISPLAY_PROPERTY, this); model.addPropertyChangeListener(Browser.ROLL_OVER_PROPERTY, this); this.model = model; this.view = view; view.getInternalDesktop().addMouseMotionListener(this); view.getInternalDesktop().setCursor(Cursor.getDefaultCursor()); keyListener = new KeyAdapter() { /** * Selects all the nodes if <code>Ctrl-A</code> or * <code>Cmd-A</code> is pressed. * @see KeyListener#keyPressed(KeyEvent) */ public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_A: if ((UIUtilities.isMacOS() && e.isMetaDown()) || (!UIUtilities.isMacOS() && e.isControlDown())) { handleKeySelection(); } break; case KeyEvent.VK_UP: navigate(DIRECTION_UP, e.isShiftDown()); break; case KeyEvent.VK_DOWN: navigate(DIRECTION_DOWN, e.isShiftDown()); break; case KeyEvent.VK_LEFT: navigate(DIRECTION_LEFT, e.isShiftDown()); break; case KeyEvent.VK_RIGHT: navigate(DIRECTION_RIGHT, e.isShiftDown()); break; } } }; } /** * Moves the current selection to the right, left, up or down. * * @param direction * The direction to move to * @param multiSel * Pass <code>true</code> to add the selection * to a multiple selection */ public void navigate(int direction, boolean multiSel) { ImageDisplay current = model.getLastSelectedDisplay(); if (current == null) return; Rectangle b = current.getBounds(); int x = b.x; int y = b.y; // choose a point which lies 50% of the ImageDisplay width/height // to the left/right/above/under the current ImageDisplay; // (this will work if the gap between the ImageDisplays is not // too big) switch (direction) { case DIRECTION_LEFT: x = b.x - (int) (b.width * 0.5); break; case DIRECTION_RIGHT: x = b.x + (int) (b.width * 1.5); break; case DIRECTION_UP: y = b.y - (int) (b.height * 0.5); break; case DIRECTION_DOWN: y = b.y + (int) (b.height * 1.5); break; } model.setSelectedDisplay(new Point(x, y), multiSel); } /** * Subscribes for mouse events notification with each node in the * various visualization trees. */ void initialize() { model.accept(this); } /** * Checks if the passed image has pixels set related to it. * Returns <code>true</code> if some pixels set are linked, * <code>false</code> otherwise. * * @param node The node to handle. * @return See above. */ boolean isSelectionValid(ImageDisplay node) { if (!(node instanceof ImageNode)) return true; Object ho = node.getHierarchyObject(); if (!(ho instanceof ImageData)) return true; ImageData img = (ImageData) ho; try { img.getDefaultPixels(); return true; } catch (Exception e) { return false; } } /** * Registers this object as mouse listeners with each node. * @see ImageDisplayVisitor#visit(ImageNode) */ public void visit(ImageNode node) { attachListeners(node); if (node instanceof WellSampleNode) { WellSampleNode sample = (WellSampleNode) node; WellImageSet well = sample.getParentWell(); List<WellSampleNode> samples = well.getWellSamples(); Iterator<WellSampleNode> i = samples.iterator(); WellSampleNode wsn; while (i.hasNext()) { wsn = i.next(); if (wsn != node) attachListeners(wsn); } } } /** * Registers this object as mouse listeners with each node. * @see ImageDisplayVisitor#visit(ImageSet) */ public void visit(ImageSet node) { node.getTitleBar().addMouseListener(this); node.getInternalDesktop().addMouseListener(this); node.addPropertyChangeListener(this); //node.addPropertyChangeListener(ImageDisplay.END_MOVING_PROPERTY, this); } /** * Listens to the property event fired by {@link Browser} and * {@link ImageDisplay}. * Necessary for clarity. * @see PropertyChangeListener#propertyChange(PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); if (ImageNode.CLASSIFY_NODE_PROPERTY.equals(name)) { model.setNodeForProperty(Browser.CLASSIFIED_NODE_PROPERTY, evt.getNewValue()); } else if (ImageDisplay.ANNOTATE_NODE_PROPERTY.equals(name)) { model.setNodeForProperty(Browser.ANNOTATED_NODE_PROPERTY, evt.getNewValue()); } else if (Browser.ROLL_OVER_PROPERTY.equals(name)) { RollOverNode n = (RollOverNode) evt.getNewValue(); ImageNode img = null; if (n != null) img = n.getNode(); view.setTitle(model.currentPathString(img)); } else if (ImageNode.PIN_THUMBNAIL_PROPERTY.equals(name)) { ImageNode node = (ImageNode) evt.getNewValue(); model.setThumbSelected(true, node); } else if (ImageDisplay.END_MOVING_PROPERTY.equals(name)) { //ImageDisplay node = model.getLastSelectedDisplay(); /* ImageDisplay node = (ImageDisplay) evt.getNewValue(); int layoutIndex = model.getSelectedLayout(); Layout layout = LayoutFactory.createLayout(layoutIndex, sorter); view.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); node.accept(layout, ImageDisplayVisitor.IMAGE_SET_ONLY); view.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); */ } else if (CellDisplay.DESCRIPTOR_PROPERTY.equals(name)) { CellDisplay node = (CellDisplay) evt.getNewValue(); setSelectedCell(node.getLocation(), node); } else if (ColourPicker.COLOUR_PROPERTY.equals(name)) { ColourObject co = (ColourObject) evt.getNewValue(); if (selectedCell == null) return; selectedCell.setHighlight(co.getColor()); selectedCell.setDescription(co.getDescription()); model.setSelectedCell(selectedCell); } } /** * Sets the currently selected display. * @see MouseListener#mousePressed(MouseEvent) */ public void mousePressed(MouseEvent me) { anchor = me.getPoint(); Component s = ((Component) me.getSource()); s.requestFocus(); s.removeKeyListener(keyListener); s.addKeyListener(keyListener); shiftDown = me.isShiftDown(); if (dragging) return; Collection<ImageDisplay> l = model.getSelectedDisplays(); if (source == null) { if (l.size() == 0) source = (JComponent) me.getSource(); else source = (JComponent) ((List<ImageDisplay>) l).get(0); } if (shiftDown && l.size() > 0) return; rightClickPad = UIUtilities.isMacOS() && SwingUtilities.isLeftMouseButton(me) && me.isControlDown(); rightClickButton = SwingUtilities.isRightMouseButton(me); ctrl = me.isControlDown(); if (UIUtilities.isMacOS()) ctrl = me.isMetaDown(); leftMouseButton = SwingUtilities.isLeftMouseButton(me); if (!UIUtilities.isWindowsOS()) onClick(me, false); } /** * Tells the model that either a pop-up point or a thumbnail selection * was detected. * @see MouseListener#mouseReleased(MouseEvent) */ public void mouseReleased(MouseEvent me) { if (dragging) { SelectionVisitor visitor = handleMultiSelection(false); model.setSelectedDisplays(visitor.getSelected()); dragging = false; return; } Collection<ImageDisplay> l = model.getSelectedDisplays(); if (shiftDown && l.size() >= 1) { if (source == null) source = (JComponent) ((List<ImageDisplay>) l).get(0); ImageDisplay display = findParentDisplay((Component) me.getSource()); Rectangle rS = display.getBounds(); display = findParentDisplay(source); Rectangle rAnchor = display.getBounds(); RowSelectionVisitor visitor = new RowSelectionVisitor(rS, rAnchor, true); view.accept(visitor); shiftDown = me.isShiftDown(); if (!shiftDown) source = null; final List<ImageDisplay> selectedDisplays = new ArrayList<ImageDisplay>(); final List<ImageNode> visibleNodes = model.getVisibleNodes(); final Colors colors = Colors.getInstance(); for (final ImageDisplay node : visitor.getSelected()) if (visibleNodes.contains(node)) selectedDisplays.add(node); else node.setHighlight(colors.getDeselectedHighLight(node)); model.setSelectedDisplays(selectedDisplays); return; } source = null; leftMouseButton = SwingUtilities.isLeftMouseButton(me); if (UIUtilities.isWindowsOS()) onClick(me, true); } /** * Sets the node which has to be zoomed when the roll over flag * is turned on. Note that the {@link ImageNode}s are the only nodes * considered. * @see MouseListener#mouseEntered(MouseEvent) */ public void mouseEntered(MouseEvent me) { boolean mo = model.isMouseOver(); boolean ro = model.isRollOver(); if (!mo && !ro) return; Object src = me.getSource(); ImageDisplay d = findParentDisplay(src); if (mo) { if (d instanceof RootDisplay) { ImageDisplay lastSelected = model.getLastSelectedDisplay(); if (lastSelected != null) { view.setTitle(model.currentPathString(lastSelected)); } else lastSelected = null; model.setNodeForProperty(Browser.MOUSE_OVER_PROPERTY, lastSelected); return; } if (!(d instanceof RootDisplay)) view.setTitle(model.currentPathString(d)); model.setNodeForProperty(Browser.MOUSE_OVER_PROPERTY, d); } if (!ro) return; if (d instanceof ImageNode && !(d.getTitleBar() == src)) { ImageNode img = (ImageNode) d; RollOverNode n = new RollOverNode(img, img.getLocationOnScreen()); model.setRollOverNode(n); } else model.setRollOverNode(null); } /** * Displays the name of the selected node if any when the mouse exited. * @see MouseListener#mouseExited(MouseEvent) */ public void mouseExited(MouseEvent me) { model.setRollOverNode(null); } /** * Highlights the nodes if the <code>SHIFT</code> key is down. * @see MouseMotionListener#mouseDragged(MouseEvent) */ public void mouseDragged(MouseEvent e) { if (e.getSource() != view.getInternalDesktop()) return; dragging = true; Point p = e.getPoint(); if (p == null) p = new Point(); selection.width = Math.abs(p.x-anchor.x); selection.height = Math.abs(p.y-anchor.y); if (anchor.x < p.x) selection.x = anchor.x; else selection.x = p.x; if (anchor.y < p.y) selection.y = anchor.y; else selection.y = p.y; handleMultiSelection(true); } /** * Required by the {@link MouseListener} I/F but no-operation implementation * in our case. * @see MouseListener#mouseClicked(MouseEvent) */ public void mouseClicked(MouseEvent me) {} /** * Required by the {@link MouseMotionListener} I/F but no-operation * implementation in our case. * @see MouseMotionListener#mouseMoved(MouseEvent) */ public void mouseMoved(MouseEvent e) {} }