/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.base.explorer.list; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.awt.Point; import java.awt.Rectangle; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.net.URI; import java.text.NumberFormat; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import javax.swing.Action; import javax.swing.DefaultListSelectionModel; import javax.swing.Icon; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import org.weasis.base.explorer.JIThumbnailCache; import org.weasis.base.explorer.JIUtility; import org.weasis.base.explorer.Messages; import org.weasis.base.explorer.ThumbnailRenderer; import org.weasis.core.api.gui.util.AppProperties; import org.weasis.core.api.gui.util.GhostGlassPane; import org.weasis.core.api.media.data.Codec; import org.weasis.core.api.media.data.ImageElement; import org.weasis.core.api.media.data.MediaElement; import org.weasis.core.api.media.data.MediaReader; import org.weasis.core.api.media.data.MediaSeries; import org.weasis.core.api.media.data.TagUtil; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.util.FileUtil; import org.weasis.core.api.util.LocalUtil; import org.weasis.core.api.util.StringUtil; import org.weasis.core.ui.docking.UIManager; import org.weasis.core.ui.editor.SeriesViewerFactory; import org.weasis.core.ui.editor.ViewerPluginBuilder; import org.weasis.core.ui.util.DefaultAction; @SuppressWarnings("serial") public abstract class AThumbnailList<E extends MediaElement> extends JList<E> implements IThumbnailList<E> { public static final String SECTION_CHANGED = "SECTION_CHANGED"; //$NON-NLS-1$ public static final String DIRECTORY_SIZE = "DIRECTORY_SIZE"; //$NON-NLS-1$ public static final Dimension ICON_DIM = new Dimension(150, 150); private static final NumberFormat intGroupFormat = LocalUtil.getIntegerInstance(); static { intGroupFormat.setGroupingUsed(true); } private final int editingIndex = -1; private final DefaultListSelectionModel selectionModel; private boolean changed; private Point dragPressed = null; private DragSource dragSource = null; public AThumbnailList() { this(HORIZONTAL_WRAP); } public AThumbnailList(final int scrollMode) { super(); this.setModel(newModel()); this.changed = false; this.selectionModel = new DefaultListSelectionModel(); this.setBackground(new Color(242, 242, 242)); setSelectionModel(this.selectionModel); // setTransferHandler(new ListTransferHandler()); ThumbnailRenderer<E> panel = new ThumbnailRenderer<>(); Dimension dim = panel.getPreferredSize(); setCellRenderer(panel); setFixedCellHeight(dim.height); setFixedCellWidth(dim.width); setVisibleRowCount(-1); setLayoutOrientation(scrollMode); setVerifyInputWhenFocusTarget(false); JIThumbnailCache.getInstance().invalidate(); } @Override public Component asComponent() { return this; } /** * Marks this <tt>Observable</tt> object as having been changed; the <tt>hasChanged</tt> method will now return * <tt>true</tt>. */ @Override public synchronized void setChanged() { this.changed = true; } /** * Indicates that this object has no longer changed, or that it has already notified all of its observers of its * most recent change, so that the <tt>hasChanged</tt> method will now return <tt>false</tt>. This method is called * automatically by the <code>notifyObservers</code> methods. * */ @Override public synchronized void clearChanged() { this.changed = false; } /** * Tests if this object has changed. * * @return <code>true</code> if and only if the <code>setChanged</code> method has been called more recently than * the <code>clearChanged</code> method on this object; <code>false</code> otherwise. */ @Override public synchronized boolean hasChanged() { return this.changed; } @Override public void registerListeners() { registerDragListeners(); addMouseListener(new PopupTrigger()); // TODO prefer the use of Key Bindings rather than keyListener // @see http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { AThumbnailList.this.jiThumbnailKeyPressed(e); } }); } public void registerDragListeners() { if (dragSource != null) { dragSource.removeDragSourceListener(this); dragSource.removeDragSourceMotionListener(this); } dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY, this); dragSource.addDragSourceMotionListener(this); } @Override public IThumbnailModel<E> getThumbnailListModel() { return (IThumbnailModel) getModel(); } public Frame getFrame() { return null; } public boolean isEditing() { if (this.editingIndex > -1) { return true; } return false; } // Subclass JList to workaround bug 4832765, which can cause the // scroll pane to not let the user easily scroll up to the beginning // of the list. An alternative would be to set the unitIncrement // of the JScrollBar to a fixed value. You wouldn't get the nice // aligned scrolling, but it should work. @Override public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation, final int direction) { int row; if ((orientation == SwingConstants.VERTICAL) && (direction < 0) && ((row = getFirstVisibleIndex()) != -1)) { final Rectangle r = getCellBounds(row, row); if ((r.y == visibleRect.y) && (row != 0)) { final Point loc = r.getLocation(); loc.y--; final int prevIndex = locationToIndex(loc); final Rectangle prevR = getCellBounds(prevIndex, prevIndex); if ((prevR == null) || (prevR.y >= r.y)) { return 0; } return prevR.height; } } return super.getScrollableUnitIncrement(visibleRect, orientation, direction); } @Override public String getToolTipText(final MouseEvent evt) { Point pt = evt.getPoint(); final int index = locationToIndex(pt); final Rectangle thumBounds = getCellBounds(index, index); if (thumBounds == null || !thumBounds.contains(pt)) { return null; } final E item = getModel().getElementAt(index); if (item == null || item.getName() == null) { return null; } StringBuilder toolTips = new StringBuilder(); toolTips.append("<html>"); //$NON-NLS-1$ toolTips.append(item.getName()); toolTips.append("<br>"); //$NON-NLS-1$ toolTips.append(Messages.getString("JIThumbnailList.size")); //$NON-NLS-1$ toolTips.append(StringUtil.COLON_AND_SPACE); toolTips.append(FileUtil.formatSize(item.getLength())); toolTips.append("<br>"); //$NON-NLS-1$ toolTips.append(Messages.getString("JIThumbnailList.date")); //$NON-NLS-1$ toolTips.append(StringUtil.COLON_AND_SPACE); toolTips.append(TagUtil.formatDateTime(Instant.ofEpochMilli(item.getLastModified()))); toolTips.append("<br>"); //$NON-NLS-1$ toolTips.append("</html>"); //$NON-NLS-1$ return toolTips.toString(); } public void reset() { setFixedCellHeight(ICON_DIM.height); setFixedCellWidth(ICON_DIM.width); setLayoutOrientation(HORIZONTAL_WRAP); getThumbnailListModel().reload(); setVisibleRowCount(-1); clearSelection(); ensureIndexIsVisible(0); } private MediaSeries buildSeriesFromMediaElement(E mediaElement) { if (mediaElement != null) { MediaReader reader = mediaElement.getMediaReader(); TagW tname; String tvalue; Codec codec = reader.getCodec(); String sUID; String gUID; if (isDicomMedia(mediaElement) && codec != null && codec.isMimeTypeSupported("application/dicom")) { //$NON-NLS-1$ if (reader.getMediaElement() == null) { // DICOM is not readable return null; } sUID = (String) reader.getTagValue(TagW.get("SeriesInstanceUID")); //$NON-NLS-1$ gUID = (String) reader.getTagValue(TagW.get("PatientID")); //$NON-NLS-1$ tname = TagW.get("PatientName"); //$NON-NLS-1$ tvalue = (String) reader.getTagValue(tname); } else { sUID = mediaElement.getMediaURI().toString(); gUID = sUID; tname = TagW.FileName; tvalue = mediaElement.getName(); } return ViewerPluginBuilder.buildMediaSeriesWithDefaultModel(reader, gUID, tname, tvalue, sUID); } return null; } public void openSelection() { E object = getSelectedValue(); openSelection(Arrays.asList(object), true, true, false); } public void openSelection(List<E> selMedias, boolean compareEntryToBuildNewViewer, boolean bestDefaultLayout, boolean inSelView) { if (selMedias != null) { boolean oneFile = selMedias.size() == 1; String sUID = null; String gUID; ArrayList<MediaSeries<? extends MediaElement>> list = new ArrayList<>(); for (E mediaElement : selMedias) { MediaReader reader = mediaElement.getMediaReader(); TagW tname; String tvalue; Codec codec = reader.getCodec(); if (isDicomMedia(mediaElement) && codec != null && codec.isMimeTypeSupported("application/dicom")) { //$NON-NLS-1$ if (reader.getMediaElement() == null) { // DICOM is not readable return; } sUID = (String) reader.getTagValue(TagW.get("SeriesInstanceUID")); //$NON-NLS-1$ gUID = (String) reader.getTagValue(TagW.get("PatientID")); //$NON-NLS-1$ tname = TagW.get("PatientName"); //$NON-NLS-1$ tvalue = (String) reader.getTagValue(tname); } else { sUID = oneFile ? mediaElement.getMediaURI().toString() : sUID == null ? UUID.randomUUID().toString() : sUID; gUID = sUID; tname = TagW.FileName; tvalue = oneFile ? mediaElement.getName() : sUID; } MediaSeries s = ViewerPluginBuilder.buildMediaSeriesWithDefaultModel(reader, gUID, tname, tvalue, sUID); if (s != null && !list.contains(s)) { list.add(s); } } if (!list.isEmpty()) { Map<String, Object> props = Collections.synchronizedMap(new HashMap<String, Object>()); props.put(ViewerPluginBuilder.CMP_ENTRY_BUILD_NEW_VIEWER, compareEntryToBuildNewViewer); props.put(ViewerPluginBuilder.BEST_DEF_LAYOUT, bestDefaultLayout); props.put(ViewerPluginBuilder.SCREEN_BOUND, null); if (inSelView) { props.put(ViewerPluginBuilder.ADD_IN_SELECTED_VIEW, true); } ArrayList<String> mimes = new ArrayList<>(); for (MediaSeries s : list) { String mime = s.getMimeType(); if (mime != null && !mimes.contains(mime)) { mimes.add(mime); } } for (String mime : mimes) { SeriesViewerFactory plugin = UIManager.getViewerFactory(mime); if (plugin != null) { ArrayList<MediaSeries<MediaElement>> seriesList = new ArrayList<>(); for (MediaSeries s : list) { if (mime.equals(s.getMimeType())) { seriesList.add(s); } } ViewerPluginBuilder builder = new ViewerPluginBuilder(plugin, seriesList, ViewerPluginBuilder.DefaultDataModel, props); ViewerPluginBuilder.openSequenceInPlugin(builder); } } } } } public void openGroup(List<E> selMedias, boolean compareEntryToBuildNewViewer, boolean bestDefaultLayout, boolean modeLayout, boolean inSelView) { if (selMedias != null) { String groupUID = null; if (modeLayout) { groupUID = UUID.randomUUID().toString(); } Map<SeriesViewerFactory, List<MediaSeries<MediaElement>>> plugins = new HashMap<>(); for (E m : selMedias) { String mime = m.getMimeType(); if (mime != null) { SeriesViewerFactory plugin = UIManager.getViewerFactory(mime); if (plugin != null) { List<MediaSeries<MediaElement>> list = plugins.get(plugin); if (list == null) { list = new ArrayList<>(modeLayout ? 10 : 1); plugins.put(plugin, list); } // Get only application readers from files MediaReader mreader = m.getMediaReader(); if (modeLayout) { MediaSeries<MediaElement> series = ViewerPluginBuilder .buildMediaSeriesWithDefaultModel(mreader, groupUID, null, null, null); if (series != null) { list.add(series); } } else { MediaSeries<MediaElement> series; if (list.isEmpty()) { series = ViewerPluginBuilder.buildMediaSeriesWithDefaultModel(mreader); if (series != null) { list.add(series); } } else { series = list.get(0); if (series != null) { MediaElement[] ms = mreader.getMediaElement(); if (ms != null) { for (MediaElement media : ms) { media.setTag(TagW.get("SeriesInstanceUID"), //$NON-NLS-1$ series.getTagValue(series.getTagID())); URI uri = media.getMediaURI(); media.setTag(TagW.get("SOPInstanceUID"), //$NON-NLS-1$ uri == null ? UUID.randomUUID().toString() : uri.toString()); series.addMedia(media); } } } } } } } } Map<String, Object> props = Collections.synchronizedMap(new HashMap<String, Object>()); props.put(ViewerPluginBuilder.CMP_ENTRY_BUILD_NEW_VIEWER, compareEntryToBuildNewViewer); props.put(ViewerPluginBuilder.BEST_DEF_LAYOUT, bestDefaultLayout); props.put(ViewerPluginBuilder.SCREEN_BOUND, null); if (inSelView) { props.put(ViewerPluginBuilder.ADD_IN_SELECTED_VIEW, true); } for (Iterator<Entry<SeriesViewerFactory, List<MediaSeries<MediaElement>>>> iterator = plugins.entrySet().iterator(); iterator.hasNext();) { Entry<SeriesViewerFactory, List<MediaSeries<MediaElement>>> item = iterator.next(); ViewerPluginBuilder builder = new ViewerPluginBuilder(item.getKey(), item.getValue(), ViewerPluginBuilder.DefaultDataModel, props); ViewerPluginBuilder.openSequenceInPlugin(builder); } } } private static boolean isDicomMedia(MediaElement mediaElement) { if (mediaElement != null) { String mime = mediaElement.getMimeType(); if (mime != null) { return mime.indexOf("dicom") != -1; //$NON-NLS-1$ } } return false; } public void nextPage(final KeyEvent e) { final int lastIndex = getLastVisibleIndex(); if (getLayoutOrientation() != JList.HORIZONTAL_WRAP) { e.consume(); final int firstIndex = getFirstVisibleIndex(); final int visibleRows = getVisibleRowCount(); final int visibleColums = (int) (((float) (lastIndex - firstIndex) / (float) visibleRows) + .5); final int visibleItems = visibleRows * visibleColums; final int val = (lastIndex + visibleItems >= getModel().getSize()) ? getModel().getSize() - 1 : lastIndex + visibleItems; clearSelection(); setSelectedIndex(val); fireSelectionValueChanged(val, val, false); } else { clearSelection(); setSelectedIndex(lastIndex); fireSelectionValueChanged(lastIndex, lastIndex, false); } } public void lastPage(final KeyEvent e) { final int lastIndex = getLastVisibleIndex(); if (getLayoutOrientation() != JList.HORIZONTAL_WRAP) { e.consume(); final int firstIndex = getFirstVisibleIndex(); final int visibleRows = getVisibleRowCount(); final int visibleColums = (int) (((float) (lastIndex - firstIndex) / (float) visibleRows) + .5); final int visibleItems = visibleRows * visibleColums; final int val = ((firstIndex - 1) - visibleItems < 0) ? 0 : (firstIndex - 1) - visibleItems; clearSelection(); setSelectedIndex(val); fireSelectionValueChanged(val, val, false); } else { clearSelection(); setSelectedIndex(lastIndex); fireSelectionValueChanged(lastIndex, lastIndex, false); } } public void jiThumbnailKeyPressed(final KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_PAGE_DOWN: nextPage(e); break; case KeyEvent.VK_PAGE_UP: lastPage(e); break; case KeyEvent.VK_ENTER: openSelection(); e.consume(); break; } } public Action buildRefreshAction() { // TODO set this action in toolbar return new DefaultAction(Messages.getString("JIThumbnailList.refresh_list"), event -> { //$NON-NLS-1$ final Thread runner = new Thread(AThumbnailList.this.getThumbnailListModel()::reload); runner.start(); }); } public int getLastSelectedIndex() { return super.getSelectedValuesList().indexOf(super.getSelectedValue()); } @Override public void listValueChanged(final ListSelectionEvent e) { setChanged(); notifyObservers(SECTION_CHANGED); clearChanged(); } final class PopupTrigger extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { mouseClickedEvent(e); } @Override public void mousePressed(final MouseEvent evt) { showPopup(evt); } @Override public void mouseReleased(final MouseEvent evt) { showPopup(evt); } private void showPopup(final MouseEvent evt) { // Context menu if (SwingUtilities.isRightMouseButton(evt)) { JPopupMenu popupMenu = AThumbnailList.this.buidContexMenu(evt); if (popupMenu != null) { popupMenu.show(evt.getComponent(), evt.getX(), evt.getY()); } } } } /** * If this object has changed, as indicated by the <code>hasChanged</code> method, then notify all of its observers * and then call the <code>clearChanged</code> method to indicate that this object has no longer changed. * <p> * Each observer has its <code>update</code> method called with two arguments: this observable object and * <code>null</code>. In other words, this method is equivalent to: <blockquote><tt> * notifyObservers(null)</tt> </blockquote> * */ public void notifyObservers() { notifyObservers(null); } /** * If this object has changed, as indicated by the <code>hasChanged</code> method, then notify all of its observers * and then call the <code>clearChanged</code> method to indicate that this object has no longer changed. * <p> * Each observer has its <code>update</code> method called with two arguments: this observable object and the * <code>arg</code> argument. * * @param arg * any object. */ @Override public void notifyObservers(final Object arg) { synchronized (this) { if (!this.changed) { return; } clearChanged(); } } public void notifyStatusBar(final Object arg) { synchronized (this) { clearChanged(); } } // --- DragGestureListener methods ----------------------------------- @Override public void dragGestureRecognized(DragGestureEvent dge) { Component comp = dge.getComponent(); if (comp instanceof AThumbnailList) { int index = getSelectedIndex(); E media = getSelectedValue(); MediaSeries<?> series = buildSeriesFromMediaElement(media); if (series != null) { GhostGlassPane glassPane = AppProperties.glassPane; Icon icon = null; if (media instanceof ImageElement) { icon = JIThumbnailCache.getInstance().getThumbnailFor((ImageElement) media, this, index); } if (icon == null) { icon = JIUtility.getSystemIcon(media); } glassPane.setIcon(icon); dragPressed = new Point(icon.getIconWidth() / 2, icon.getIconHeight() / 2); Point p = (Point) dge.getDragOrigin().clone(); SwingUtilities.convertPointToScreen(p, comp); drawGlassPane(p); glassPane.setVisible(true); dge.startDrag(null, series, this); } } } @Override public void dragMouseMoved(DragSourceDragEvent dsde) { drawGlassPane(dsde.getLocation()); } // --- DragSourceListener methods ----------------------------------- @Override public void dragEnter(DragSourceDragEvent dsde) { } @Override public void dragOver(DragSourceDragEvent dsde) { } @Override public void dragExit(DragSourceEvent dsde) { } @Override public void dragDropEnd(DragSourceDropEvent dsde) { GhostGlassPane glassPane = AppProperties.glassPane; dragPressed = null; glassPane.setImagePosition(null); glassPane.setIcon(null); glassPane.setVisible(false); } @Override public void dropActionChanged(DragSourceDragEvent dsde) { } public void drawGlassPane(Point p) { if (dragPressed != null) { GhostGlassPane glassPane = AppProperties.glassPane; SwingUtilities.convertPointFromScreen(p, glassPane); p.translate(-dragPressed.x, -dragPressed.y); glassPane.setImagePosition(p); } } public List<E> getSelected(final MouseEvent e) { List<E> selected = getSelectedValuesList(); if (!selected.isEmpty()) { int index = locationToIndex(e.getPoint()); if (index >= 0) { E selectedMedia = getModel().getElementAt(index); if (selectedMedia != null && !selected.contains(selectedMedia)) { selected = Arrays.asList(selectedMedia); setSelectedValue(selectedMedia, false); } } } return selected; } public abstract IThumbnailModel<E> newModel(); public abstract JPopupMenu buidContexMenu(final MouseEvent e); public abstract void mouseClickedEvent(MouseEvent e); }