Ticket #11905: patch_multiple_image_core_reviewed.patch

File patch_multiple_image_core_reviewed.patch, 29.9 KB (added by Don-vip, 7 years ago)

core patch, updated and reviewed

  • src/org/openstreetmap/josm/actions/mapmode/SelectAction.java

     
    539539            int dp = (int) lastMousePos.distance(e.getX(), e.getY());
    540540            if (dp < initialMoveThreshold)
    541541                return; // ignore small drags
    542             initialMoveThresholdExceeded = true; //no more ingnoring uintil nex mouse press
     542            initialMoveThresholdExceeded = true; //no more ignoring until next mouse press
    543543        }
    544544        if (e.getPoint().equals(lastMousePos))
    545545            return;
  • src/org/openstreetmap/josm/data/ImageData.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data;
    33
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
    46import java.util.ArrayList;
    57import java.util.Collections;
    68import java.util.List;
     9import java.util.stream.Collectors;
    710
    811import org.openstreetmap.josm.data.coor.LatLon;
    912import org.openstreetmap.josm.gui.layer.geoimage.ImageEntry;
     
    3336
    3437    private final List<ImageEntry> data;
    3538
    36     private int selectedImageIndex = -1;
     39    private final List<Integer> selectedImagesIndex = new ArrayList<>();
    3740
    3841    private final ListenerList<ImageDataUpdateListener> listeners = ListenerList.create();
    3942
     
    5558        } else {
    5659            this.data = new ArrayList<>();
    5760        }
     61        selectedImagesIndex.add(-1);
    5862    }
    5963
    6064    /**
     
    106110    }
    107111
    108112    /**
    109      * Return the current selected image
    110      * @return the selected image as {@link ImageEntry} or null
     113     * Return the first currently selected image
     114     * @return the first selected image as {@link ImageEntry} or null
     115     * @see #getSelectedImages
    111116     */
    112117    public ImageEntry getSelectedImage() {
     118        int selectedImageIndex = selectedImagesIndex.isEmpty() ? -1 : selectedImagesIndex.get(0);
    113119        if (selectedImageIndex > -1) {
    114120            return data.get(selectedImageIndex);
    115121        }
     
    117123    }
    118124
    119125    /**
     126     * Return the current selected images
     127     * @return the selected images as list {@link ImageEntry}
     128     * @since xxx
     129     */
     130    public List<ImageEntry> getSelectedImages() {
     131        return selectedImagesIndex.stream().filter(i -> i > -1).map(data::get).collect(Collectors.toList());
     132    }
     133
     134    /**
    120135     * Select the first image of the sequence
    121136     */
    122137    public void selectFirstImage() {
     
    137152     * @return {@code true} is there is a next image, {@code false} otherwise
    138153     */
    139154    public boolean hasNextImage() {
    140         return selectedImageIndex != data.size() - 1;
     155        return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) != data.size() - 1);
    141156    }
    142157
    143158    /**
     
    145160     */
    146161    public void selectNextImage() {
    147162        if (hasNextImage()) {
    148             setSelectedImageIndex(selectedImageIndex + 1);
     163            setSelectedImageIndex(selectedImagesIndex.get(0) + 1);
    149164        }
    150165    }
    151166
     
    154169     * @return {@code true} is there is a previous image, {@code false} otherwise
    155170     */
    156171    public boolean hasPreviousImage() {
    157         return selectedImageIndex - 1 > -1;
     172        return (selectedImagesIndex.size() == 1 && selectedImagesIndex.get(0) - 1 > -1);
    158173    }
    159174
    160175    /**
     
    164179        if (data.isEmpty()) {
    165180            return;
    166181        }
    167         setSelectedImageIndex(Integer.max(0, selectedImageIndex - 1));
     182        setSelectedImageIndex(Integer.max(0, selectedImagesIndex.get(0) - 1));
    168183    }
    169184
    170185    /**
     
    176191    }
    177192
    178193    /**
    179      * Clear the selected image
     194     * Add image to the list of selected images
     195     * @param image {@link ImageEntry} the image to add
     196     * @since xxx
     197     */
     198    public void addImageToSelection(ImageEntry image) {
     199        int index = data.indexOf(image);
     200        if (selectedImagesIndex.get(0) == -1) {
     201            setSelectedImage(image);
     202        } else if (!selectedImagesIndex.contains(index)) {
     203            selectedImagesIndex.add(index);
     204            listeners.fireEvent(l -> l.selectedImageChanged(this));
     205        }
     206    }
     207
     208    /**
     209     * Remove the image from the list of selected images
     210     * @param image {@link ImageEntry} the image to remove
     211     * @since xxx
     212     */
     213    public void removeImageToSelection(ImageEntry image) {
     214        int index = data.indexOf(image);
     215        selectedImagesIndex.remove(selectedImagesIndex.indexOf(index));
     216        if (selectedImagesIndex.isEmpty()) {
     217            selectedImagesIndex.add(-1);
     218        }
     219        listeners.fireEvent(l -> l.selectedImageChanged(this));
     220    }
     221
     222    /**
     223     * Clear the selected image(s)
    180224     */
    181225    public void clearSelectedImage() {
    182226        setSelectedImageIndex(-1);
     
    187231    }
    188232
    189233    private void setSelectedImageIndex(int index, boolean forceTrigger) {
    190         if (index == selectedImageIndex && !forceTrigger) {
     234        if (selectedImagesIndex.size() > 1) {
     235            selectedImagesIndex.clear();
     236            selectedImagesIndex.add(-1);
     237        }
     238        if (index == selectedImagesIndex.get(0) && !forceTrigger) {
    191239            return;
    192240        }
    193         selectedImageIndex = index;
     241        selectedImagesIndex.set(0, index);
    194242        listeners.fireEvent(l -> l.selectedImageChanged(this));
    195243    }
    196244
     
    198246     * Remove the current selected image from the list
    199247     */
    200248    public void removeSelectedImage() {
    201         data.remove(getSelectedImage());
    202         if (selectedImageIndex == data.size()) {
     249        List<ImageEntry> selected = getSelectedImages();
     250        if (selected.size() > 1) {
     251            throw new IllegalStateException(tr("Multiple images have been selected"));
     252        }
     253        if (selected.isEmpty()) {
     254            return;
     255        }
     256        data.remove(getSelectedImages().get(0));
     257        if (selectedImagesIndex.get(0) == data.size()) {
    203258            setSelectedImageIndex(data.size() - 1);
    204259        } else {
    205             setSelectedImageIndex(selectedImageIndex, true);
     260            setSelectedImageIndex(selectedImagesIndex.get(0), true);
    206261        }
    207262    }
    208263
    209264    /**
     265     * Determines if the image is selected
     266     * @param image the {@link ImageEntry} image
     267     * @return {@code true} is the image is selected, {@code false} otherwise
     268     * @since xxx
     269     */
     270    public boolean isImageSelected(ImageEntry image) {
     271        int index = data.indexOf(image);
     272        return selectedImagesIndex.contains(index);
     273    }
     274
     275    /**
    210276     * Remove the image from the list and trigger update listener
    211277     * @param img the {@link ImageEntry} to remove
    212278     */
  • src/org/openstreetmap/josm/gui/layer/geoimage/GeoImageLayer.java

     
    9898
    9999    /** Mouse position where the last image was selected. */
    100100    private Point lastSelPos;
     101    /** The mouse point */
     102    private Point startPoint;
    101103
    102104    /**
    103105     * Image cycle mode flag.
     
    154156        this.data.addImageDataUpdateListener(this);
    155157    }
    156158
     159    private final class ImageMouseListener extends MouseAdapter {
     160        private boolean isMapModeOk() {
     161            MapMode mapMode = MainApplication.getMap().mapMode;
     162            return mapMode == null || isSupportedMapMode(mapMode);
     163        }
     164
     165        @Override
     166        public void mousePressed(MouseEvent e) {
     167            if (e.getButton() != MouseEvent.BUTTON1)
     168                return;
     169            if (isVisible() && isMapModeOk()) {
     170                cycleModeArmed = true;
     171                invalidate();
     172                startPoint = e.getPoint();
     173            }
     174        }
     175
     176        @Override
     177        public void mouseReleased(MouseEvent ev) {
     178            if (ev.getButton() != MouseEvent.BUTTON1)
     179                return;
     180            if (!isVisible() || !isMapModeOk())
     181                return;
     182            if (!cycleModeArmed) {
     183                return;
     184            }
     185
     186            Rectangle hitBoxClick = new Rectangle((int) startPoint.getX() - 10, (int) startPoint.getY() - 10, 15, 15);
     187            if (!hitBoxClick.contains(ev.getPoint())) {
     188                return;
     189            }
     190
     191            Point mousePos = ev.getPoint();
     192            boolean cycle = cycleModeArmed && lastSelPos != null && lastSelPos.equals(mousePos);
     193            final boolean isShift = (ev.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK) == MouseEvent.SHIFT_DOWN_MASK;
     194            final boolean isCtrl = (ev.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == MouseEvent.CTRL_DOWN_MASK;
     195            int idx = getPhotoIdxUnderMouse(ev, cycle);
     196            if (idx >= 0) {
     197                lastSelPos = mousePos;
     198                cycleModeArmed = false;
     199                ImageEntry img = data.getImages().get(idx);
     200                if (isShift) {
     201                    if (isCtrl && !data.getSelectedImages().isEmpty()) {
     202                        int idx2 = data.getImages().indexOf(data.getSelectedImages().get(data.getSelectedImages().size() - 1));
     203                        int startIndex = Math.min(idx, idx2);
     204                        int endIndex = Math.max(idx, idx2);
     205                        for (int i = startIndex; i <= endIndex; i++) {
     206                            data.addImageToSelection(data.getImages().get(i));
     207                        }
     208                    } else {
     209                        if (data.isImageSelected(img)) {
     210                            data.removeImageToSelection(img);
     211                        } else {
     212                            data.addImageToSelection(img);
     213                        }
     214                    }
     215                } else {
     216                    data.setSelectedImage(img);
     217                }
     218            }
     219        }
     220    }
     221
    157222    /**
    158223     * Loads a set of images, while displaying a dialog that indicates what the plugin is currently doing.
    159224     * In facts, this object is instantiated with a list of files. These files may be JPEG files or
     
    496561                for (ImageEntry e : data.getImages()) {
    497562                    paintImage(e, mv, clip, tempG);
    498563                }
    499                 if (data.getSelectedImage() != null) {
     564                for (ImageEntry img: this.data.getSelectedImages()) {
    500565                    // Make sure the selected image is on top in case multiple images overlap.
    501                     paintImage(data.getSelectedImage(), mv, clip, tempG);
     566                    paintImage(img, mv, clip, tempG);
    502567                }
    503568                updateOffscreenBuffer = false;
    504569            }
     
    515580            }
    516581        }
    517582
    518         ImageEntry e = data.getSelectedImage();
    519         if (e != null && e.getPos() != null) {
    520             Point p = mv.getPoint(e.getPos());
     583        for (ImageEntry e: data.getSelectedImages()) {
     584            if (e != null && e.getPos() != null) {
     585                Point p = mv.getPoint(e.getPos());
    521586
    522             int imgWidth;
    523             int imgHeight;
    524             if (useThumbs && e.hasThumbnail()) {
    525                 Dimension d = scaledDimension(e.getThumbnail());
    526                 if (d != null) {
    527                     imgWidth = d.width;
    528                     imgHeight = d.height;
     587                int imgWidth;
     588                int imgHeight;
     589                if (useThumbs && e.hasThumbnail()) {
     590                    Dimension d = scaledDimension(e.getThumbnail());
     591                    if (d != null) {
     592                        imgWidth = d.width;
     593                        imgHeight = d.height;
     594                    } else {
     595                        imgWidth = -1;
     596                        imgHeight = -1;
     597                    }
    529598                } else {
    530                     imgWidth = -1;
    531                     imgHeight = -1;
     599                    imgWidth = selectedIcon.getIconWidth();
     600                    imgHeight = selectedIcon.getIconHeight();
    532601                }
    533             } else {
    534                 imgWidth = selectedIcon.getIconWidth();
    535                 imgHeight = selectedIcon.getIconHeight();
    536             }
    537602
    538             if (e.getExifImgDir() != null) {
    539                 // Multiplier must be larger than sqrt(2)/2=0.71.
    540                 double arrowlength = Math.max(25, Math.max(imgWidth, imgHeight) * 0.85);
    541                 double arrowwidth = arrowlength / 1.4;
     603                if (e.getExifImgDir() != null) {
     604                    // Multiplier must be larger than sqrt(2)/2=0.71.
     605                    double arrowlength = Math.max(25, Math.max(imgWidth, imgHeight) * 0.85);
     606                    double arrowwidth = arrowlength / 1.4;
    542607
    543                 double dir = e.getExifImgDir();
    544                 // Rotate 90 degrees CCW
    545                 double headdir = (dir < 90) ? dir + 270 : dir - 90;
    546                 double leftdir = (headdir < 90) ? headdir + 270 : headdir - 90;
    547                 double rightdir = (headdir > 270) ? headdir - 270 : headdir + 90;
     608                    double dir = e.getExifImgDir();
     609                    // Rotate 90 degrees CCW
     610                    double headdir = (dir < 90) ? dir + 270 : dir - 90;
     611                    double leftdir = (headdir < 90) ? headdir + 270 : headdir - 90;
     612                    double rightdir = (headdir > 270) ? headdir - 270 : headdir + 90;
    548613
    549                 double ptx = p.x + Math.cos(Utils.toRadians(headdir)) * arrowlength;
    550                 double pty = p.y + Math.sin(Utils.toRadians(headdir)) * arrowlength;
     614                    double ptx = p.x + Math.cos(Utils.toRadians(headdir)) * arrowlength;
     615                    double pty = p.y + Math.sin(Utils.toRadians(headdir)) * arrowlength;
    551616
    552                 double ltx = p.x + Math.cos(Utils.toRadians(leftdir)) * arrowwidth/2;
    553                 double lty = p.y + Math.sin(Utils.toRadians(leftdir)) * arrowwidth/2;
     617                    double ltx = p.x + Math.cos(Utils.toRadians(leftdir)) * arrowwidth/2;
     618                    double lty = p.y + Math.sin(Utils.toRadians(leftdir)) * arrowwidth/2;
    554619
    555                 double rtx = p.x + Math.cos(Utils.toRadians(rightdir)) * arrowwidth/2;
    556                 double rty = p.y + Math.sin(Utils.toRadians(rightdir)) * arrowwidth/2;
     620                    double rtx = p.x + Math.cos(Utils.toRadians(rightdir)) * arrowwidth/2;
     621                    double rty = p.y + Math.sin(Utils.toRadians(rightdir)) * arrowwidth/2;
    557622
    558                 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    559                 g.setColor(new Color(255, 255, 255, 192));
    560                 int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx};
    561                 int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty};
    562                 g.fillPolygon(xar, yar, 4);
    563                 g.setColor(Color.black);
    564                 g.setStroke(new BasicStroke(1.2f));
    565                 g.drawPolyline(xar, yar, 3);
    566             }
     623                    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
     624                    g.setColor(new Color(255, 255, 255, 192));
     625                    int[] xar = {(int) ltx, (int) ptx, (int) rtx, (int) ltx};
     626                    int[] yar = {(int) lty, (int) pty, (int) rty, (int) lty};
     627                    g.fillPolygon(xar, yar, 4);
     628                    g.setColor(Color.black);
     629                    g.setStroke(new BasicStroke(1.2f));
     630                    g.drawPolyline(xar, yar, 3);
     631                }
    567632
    568             if (useThumbs && e.hasThumbnail()) {
    569                 g.setColor(new Color(128, 0, 0, 122));
    570                 g.fillRect(p.x - imgWidth / 2, p.y - imgHeight / 2, imgWidth, imgHeight);
    571             } else {
    572                 selectedIcon.paintIcon(mv, g,
    573                         p.x - imgWidth / 2,
    574                         p.y - imgHeight / 2);
     633                if (useThumbs && e.hasThumbnail()) {
     634                    g.setColor(new Color(128, 0, 0, 122));
     635                    g.fillRect(p.x - imgWidth / 2, p.y - imgHeight / 2, imgWidth, imgHeight);
     636                } else {
     637                    selectedIcon.paintIcon(mv, g,
     638                            p.x - imgWidth / 2,
     639                            p.y - imgHeight / 2);
     640                }
    575641            }
    576642        }
    577643    }
     
    742808
    743809    @Override
    744810    public void hookUpMapView() {
    745         mouseAdapter = new MouseAdapter() {
    746             private boolean isMapModeOk() {
    747                 MapMode mapMode = MainApplication.getMap().mapMode;
    748                 return mapMode == null || isSupportedMapMode(mapMode);
    749             }
    750 
    751             @Override
    752             public void mousePressed(MouseEvent e) {
    753                 if (e.getButton() != MouseEvent.BUTTON1)
    754                     return;
    755                 if (isVisible() && isMapModeOk()) {
    756                     cycleModeArmed = true;
    757                     invalidate();
    758                 }
    759             }
    760 
    761             @Override
    762             public void mouseReleased(MouseEvent ev) {
    763                 if (ev.getButton() != MouseEvent.BUTTON1)
    764                     return;
    765                 if (!isVisible() || !isMapModeOk())
    766                     return;
    767 
    768                 Point mousePos = ev.getPoint();
    769                 boolean cycle = cycleModeArmed && lastSelPos != null && lastSelPos.equals(mousePos);
    770                 int idx = getPhotoIdxUnderMouse(ev, cycle);
    771                 if (idx >= 0) {
    772                     lastSelPos = mousePos;
    773                     cycleModeArmed = false;
    774                     data.setSelectedImage(data.getImages().get(idx));
    775                 }
    776             }
    777         };
     811        mouseAdapter = new ImageMouseListener();
    778812
    779813        mouseMotionAdapter = new MouseMotionAdapter() {
    780814            @Override
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageDisplay.java

     
    6666
    6767    private final ImgDisplayMouseListener imgMouseListener = new ImgDisplayMouseListener();
    6868
     69    private String emptyText;
    6970    private String osdText;
    7071
    7172    private static final BooleanProperty AGPIFO_STYLE =
     
    698699    }
    699700
    700701    /**
     702     * Set the message displayed when there is no image to display.
     703     * By default it display a simple No image
     704     * @param emptyText the string to display
     705     * @since xxx
     706     */
     707    public void setEmptyText(String emptyText) {
     708        this.emptyText = emptyText;
     709    }
     710
     711    /**
    701712     * Sets the On-Screen-Display text.
    702713     * @param text text to display on top of the image
    703714     */
     
    729740        Dimension size = getSize();
    730741        if (entry == null) {
    731742            g.setColor(Color.black);
    732             String noImageStr = tr("No image");
     743            if (emptyText == null) {
     744                emptyText = tr("No image");
     745            }
     746            String noImageStr = emptyText;
    733747            Rectangle2D noImageSize = g.getFontMetrics(g.getFont()).getStringBounds(noImageStr, g);
    734748            g.drawString(noImageStr,
    735749                    (int) ((size.width - noImageSize.getWidth()) / 2),
  • src/org/openstreetmap/josm/gui/layer/geoimage/ImageViewerDialog.java

     
    1313import java.awt.event.WindowEvent;
    1414import java.text.DateFormat;
    1515import java.text.SimpleDateFormat;
     16import java.util.Collections;
     17import java.util.List;
    1618import java.util.Optional;
    1719
    1820import javax.swing.Box;
     
    376378     * Displays image for the given data.
    377379     * @param data geo image data
    378380     * @param entry image entry
     381     * @deprecated Use {@link #displayImage}
    379382     */
     383    @Deprecated
    380384    public static void showImage(ImageData data, ImageEntry entry) {
    381385        getInstance().displayImage(data, entry);
    382386    }
     
    416420    private transient ImageEntry currentEntry;
    417421
    418422    /**
    419      * Displays image for the given layer.
     423     * Displays a single image for the given layer.
    420424     * @param data the image data
    421425     * @param entry image entry
     426     * @see #displayImages
    422427     */
    423428    public void displayImage(ImageData data, ImageEntry entry) {
     429        displayImages(data, Collections.singletonList(entry));
     430    }
     431
     432    /**
     433     * Displays images for the given layer.
     434     * @param data the image data
     435     * @param entries image entries
     436     * @since xxx
     437     */
     438    public void displayImages(ImageData data, List<ImageEntry> entries) {
    424439        boolean imageChanged;
     440        ImageEntry entry = entries != null && entries.size() == 1 ? entries.get(0) : null;
    425441
    426442        synchronized (this) {
    427443            // TODO: pop up image dialog but don't load image again
     
    490506            btnDelete.setEnabled(false);
    491507            btnDeleteFromDisk.setEnabled(false);
    492508            btnCopyPath.setEnabled(false);
     509            if (entries != null && entries.size() > 1) {
     510                imgDisplay.setEmptyText(tr("Multiple images selected"));
     511                btnFirst.setEnabled(!isFirstImageSelected(data));
     512                btnLast.setEnabled(!isLastImageSelected(data));
     513            }
     514            imgDisplay.setImage(null);
     515            imgDisplay.setOsdText("");
    493516            return;
    494517        }
    495518        if (!isDialogShowing()) {
     
    503526        }
    504527    }
    505528
     529    private static boolean isLastImageSelected(ImageData data) {
     530        return data.isImageSelected(data.getImages().get(data.getImages().size() - 1));
     531    }
     532
     533    private static boolean isFirstImageSelected(ImageData data) {
     534        return data.isImageSelected(data.getImages().get(0));
     535    }
     536
    506537    /**
    507538     * When an image is closed, really close it and do not pop
    508539     * up the side dialog.
     
    561592        if (e.getRemovedLayer() instanceof GeoImageLayer) {
    562593            ImageData removedData = ((GeoImageLayer) e.getRemovedLayer()).getImageData();
    563594            if (removedData == currentData) {
    564                 displayImage(null, null);
     595                displayImages(null, null);
    565596            }
    566597            removedData.removeImageDataUpdateListener(this);
    567598        }
     
    591622
    592623    @Override
    593624    public void selectedImageChanged(ImageData data) {
    594         showImage(data, data.getSelectedImage());
     625        displayImages(data, data.getSelectedImages());
    595626    }
    596627
    597628    @Override
    598629    public void imageDataUpdated(ImageData data) {
    599         showImage(data, data.getSelectedImage());
     630        displayImages(data, data.getSelectedImages());
    600631    }
    601632}
  • test/unit/org/openstreetmap/josm/data/ImageDataTest.java

     
    3232    }
    3333
    3434    @Test
    35     public void testWithullData() {
     35    public void testWithNullData() {
    3636        ImageData data = new ImageData();
    3737        assertEquals(0, data.getImages().size());
    3838        assertNull(data.getSelectedImage());
     
    8787
    8888        ImageData data = new ImageData(list);
    8989        data.selectFirstImage();
    90         assertEquals(list.get(0), data.getSelectedImage());
     90        assertEquals(1, data.getSelectedImages().size());
     91        assertEquals(list.get(0), data.getSelectedImages().get(0));
    9192    }
    9293
    9394    @Test
     
    9798
    9899        ImageData data = new ImageData(list);
    99100        data.selectLastImage();
    100         assertEquals(list.get(1), data.getSelectedImage());
     101        assertEquals(1, data.getSelectedImages().size());
     102        assertEquals(list.get(1), data.getSelectedImages().get(0));
    101103    }
    102104
    103105    @Test
     
    107109        ImageData data = new ImageData(list);
    108110        assertTrue(data.hasNextImage());
    109111        data.selectNextImage();
    110         assertEquals(list.get(0), data.getSelectedImage());
     112        assertEquals(1, data.getSelectedImages().size());
     113        assertEquals(list.get(0), data.getSelectedImages().get(0));
    111114        assertFalse(data.hasNextImage());
    112115        data.selectNextImage();
    113         assertEquals(list.get(0), data.getSelectedImage());
     116        assertEquals(list.get(0), data.getSelectedImages().get(0));
    114117    }
    115118
    116119    @Test
     
    123126        data.selectLastImage();
    124127        assertTrue(data.hasPreviousImage());
    125128        data.selectPreviousImage();
    126         assertEquals(list.get(0), data.getSelectedImage());
     129        assertEquals(1, data.getSelectedImages().size());
     130        assertEquals(list.get(0), data.getSelectedImages().get(0));
    127131        data.selectPreviousImage();
    128         assertEquals(list.get(0), data.getSelectedImage());
     132        assertEquals(list.get(0), data.getSelectedImages().get(0));
    129133    }
    130134
    131135    @Test
     
    134138
    135139        ImageData data = new ImageData(list);
    136140        data.setSelectedImage(list.get(0));
    137         assertEquals(list.get(0), data.getSelectedImage());
     141        assertEquals(1, data.getSelectedImages().size());
     142        assertEquals(list.get(0), data.getSelectedImages().get(0));
    138143    }
    139144
    140145    @Test
    141     public void testClearSelectedImage() {
     146    public void testClearSelectedImages() {
    142147        List<ImageEntry> list = getOneImage();
    143148
    144149        ImageData data = new ImageData(list);
    145150        data.setSelectedImage(list.get(0));
    146151        data.clearSelectedImage();
    147         assertNull(data.getSelectedImage());
     152        assertTrue(data.getSelectedImages().isEmpty());
    148153    }
    149154
    150155    @Test
     
    173178        data.selectFirstImage();
    174179        data.removeSelectedImage();
    175180        assertEquals(0, data.getImages().size());
    176         assertNull(data.getSelectedImage());
     181        assertEquals(0, data.getSelectedImages().size());
    177182    }
    178183
    179184    @Test
     
    237242
    238243        data.mergeFrom(data2);
    239244        assertEquals(3, data.getImages().size());
    240         assertEquals(list1.get(0), data.getSelectedImage());
     245        assertEquals(1, data.getSelectedImages().size());
     246        assertEquals(list1.get(0), data.getSelectedImages().get(0));
    241247    }
    242248
    243249    @Test
     
    253259
    254260        data.mergeFrom(data2);
    255261        assertEquals(3, data.getImages().size());
    256         assertEquals(list2.get(0), data.getSelectedImage());
     262        assertEquals(1, data.getSelectedImages().size());
     263        assertEquals(list2.get(0), data.getSelectedImages().get(0));
     264    }
     265
     266    @Test
     267    public void testAddImageToSelection() {
     268        List<ImageEntry> list = getOneImage();
     269        list.add(new ImageEntry(new File("test2")));
     270
     271        ImageData data = new ImageData(list);
     272        data.addImageToSelection(list.get(0));
     273        data.addImageToSelection(list.get(0));
     274        assertEquals(1, data.getSelectedImages().size());
     275        data.addImageToSelection(list.get(1));
     276        assertEquals(2, data.getSelectedImages().size());
     277    }
     278
     279    @Test
     280    public void testRemoveImageToSelection() {
     281        List<ImageEntry> list = getOneImage();
     282        list.add(new ImageEntry());
     283
     284        ImageData data = new ImageData(list);
     285        data.selectLastImage();
     286        data.removeImageToSelection(list.get(1));
     287        assertEquals(0, data.getSelectedImages().size());
     288        data.selectFirstImage();
     289        assertEquals(1, data.getSelectedImages().size());
     290
     291    }
     292
     293    @Test
     294    public void testIsSelected() {
     295        List<ImageEntry> list = getOneImage();
     296        list.add(new ImageEntry(new File("test2")));
     297
     298        ImageData data = new ImageData(list);
     299        assertFalse(data.isImageSelected(list.get(0)));
     300        data.selectFirstImage();
     301        assertTrue(data.isImageSelected(list.get(0)));
     302        data.addImageToSelection(list.get(1));
     303        assertTrue(data.isImageSelected(list.get(0)));
     304        assertTrue(data.isImageSelected(list.get(1)));
     305        assertFalse(data.isImageSelected(new ImageEntry()));
     306    }
     307
     308    @Test
     309    public void testActionsWithMultipleImagesSelected() {
     310        List<ImageEntry> list = this.getOneImage();
     311        list.add(new ImageEntry(new File("test2")));
     312        list.add(new ImageEntry(new File("test3")));
     313        list.add(new ImageEntry(new File("test3")));
     314
     315        ImageData data = new ImageData(list);
     316        data.addImageToSelection(list.get(1));
     317        data.addImageToSelection(list.get(2));
     318
     319        assertFalse(data.hasNextImage());
     320        assertFalse(data.hasPreviousImage());
     321
     322        data.clearSelectedImage();
     323        assertEquals(0, data.getSelectedImages().size());
     324        data.addImageToSelection(list.get(1));
     325        data.selectFirstImage();
     326        assertEquals(1, data.getSelectedImages().size());
     327    }
     328
     329    @Test
     330    public void testTriggerListenerWhenNewImageIsSelectedAndRemoved() {
     331        List<ImageEntry> list = this.getOneImage();
     332        list.add(new ImageEntry());
     333        ImageData data = new ImageData(list);
     334        ImageDataUpdateListener listener = new ImageDataUpdateListener() {
     335            @Override
     336            public void selectedImageChanged(ImageData data) {}
     337
     338            @Override
     339            public void imageDataUpdated(ImageData data) {}
     340        };
     341        new Expectations(listener) {{
     342            listener.selectedImageChanged(data); times = 3;
     343        }};
     344        data.addImageDataUpdateListener(listener);
     345        data.selectFirstImage();
     346        data.addImageToSelection(list.get(1));
     347        data.removeImageToSelection(list.get(0));
    257348    }
    258349
    259350    @Test