/*
*------------------------------------------------------------------------------
* Copyright (C) 2015 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.metadata.editor;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import omero.gateway.model.AnnotationData;
import omero.gateway.model.ExperimenterData;
import omero.gateway.model.MapAnnotationData;
import omero.model.NamedValue;
import org.openmicroscopy.shoola.agents.metadata.IconManager;
import org.openmicroscopy.shoola.agents.metadata.MetadataViewerAgent;
import org.openmicroscopy.shoola.agents.metadata.editor.EditorModel.MapAnnotationType;
import org.openmicroscopy.shoola.agents.metadata.editor.maptable.MapTable;
import org.openmicroscopy.shoola.agents.metadata.editor.maptable.MapTableModel;
import org.openmicroscopy.shoola.agents.metadata.editor.maptable.MapTableSelectionModel;
import org.openmicroscopy.shoola.agents.metadata.view.MetadataViewerFactory;
import org.openmicroscopy.shoola.agents.util.EditorUtil;
import org.openmicroscopy.shoola.agents.util.ToolTipGenerator;
import org.openmicroscopy.shoola.util.CommonsLangUtils;
import org.openmicroscopy.shoola.util.ui.UIUtilities;
/**
* A {@link AnnotationTaskPaneUI} for displaying {@link MapAnnotationData}
*
* @author Dominik Lindner <a
* href="mailto:d.lindner@dundee.ac.uk">d.lindner@dundee.ac.uk</a>
*/
public class MapTaskPaneUI extends AnnotationTaskPaneUI implements
ListSelectionListener {
/** Maximum width of the tables component before scrollbars are used */
private static final int MAX_TABLES_COMPONENT_WIDTH = 200;
/** Maximum height of the tables component before scrollbars are used */
private static final int MAX_TABLES_COMPONENT_HEIGHT = 300;
/** The toolbar */
private JPanel toolbar;
/** The toolbar buttons */
private JButton addButton, copyButton, pasteButton, deleteButton;
/** Reference to the {@link MapTable}s */
private List<MapTable> mapTables = new ArrayList<MapTable>();
/** Flag to en/disable the ListSelectionListener */
private boolean listenerActive = true;
/** The layout constraints */
private GridBagConstraints c;
/** Reference to the copy/paste storage */
private static List<NamedValue> copiedValues = MetadataViewerFactory
.getCopiedMapAnnotationsEntries();
/** Component displaying the table header */
private JPanel headerPanel = null;
/** Component hosting the tables */
private JPanel tablePanel = null;
/** Scrollpane hosting the tables component */
private JScrollPane sp;
/**
* Creates a new instance
*
* @param model
* Reference to the {@link EditorModel}
* @param view
* Reference to the {@link EditorUI}
* @param controller
* Reference to the {@link EditorControl}
*/
MapTaskPaneUI(EditorModel model, EditorUI view, EditorControl controller) {
super(model, view, controller);
buildUI();
}
@Override
List<AnnotationData> getAnnotationsToSave() {
List<MapAnnotationData> tmp = getMapAnnotations(true, true);
List<AnnotationData> result = new ArrayList<AnnotationData>();
result.addAll(tmp);
return result;
}
@Override
List<Object> getAnnotationsToRemove() {
List<MapAnnotationData> tmp = getEmptyMapAnnotations();
List<Object> result = new ArrayList<Object>();
result.addAll(tmp);
return result;
}
/**
* Builds the component
*/
private void buildUI() {
setLayout(new BorderLayout());
setBackground(UIUtilities.BACKGROUND_COLOR);
toolbar = createToolBar();
toolbar.setBackground(UIUtilities.BACKGROUND_COLOR);
add(toolbar, BorderLayout.NORTH);
c = new GridBagConstraints();
c.anchor = GridBagConstraints.NORTHWEST;
c.insets = new Insets(0, 2, 4, 2);
c.gridx = 0;
c.gridy = 0;
c.weightx = 1;
c.weighty = 0;
c.fill = GridBagConstraints.HORIZONTAL;
tablePanel = new JPanel();
tablePanel.setLayout(new GridBagLayout());
tablePanel.setBackground(UIUtilities.BACKGROUND_COLOR);
sp = new JScrollPane(tablePanel);
sp.setBorder(BorderFactory.createEmptyBorder());
tablePanel.addComponentListener(new ComponentListener() {
@Override
public void componentResized(ComponentEvent e) {
if (view != null)
adjustScrollPane();
}
@Override
public void componentShown(ComponentEvent e) {
}
@Override
public void componentMoved(ComponentEvent e) {
}
@Override
public void componentHidden(ComponentEvent e) {
}
});
add(sp, BorderLayout.CENTER);
sp.setPreferredSize(null);
headerPanel = createHeaderPanel();
tablePanel.add(headerPanel, c);
c.gridy++;
}
/**
* Adjusts the size of the scrollpane hosting the tables
*/
private void adjustScrollPane() {
tablePanel.setPreferredSize(null);
Dimension d = tablePanel.getPreferredSize();
if (d.width > MAX_TABLES_COMPONENT_WIDTH)
d.width = MAX_TABLES_COMPONENT_WIDTH;
if (d.height > MAX_TABLES_COMPONENT_HEIGHT)
d.height = MAX_TABLES_COMPONENT_HEIGHT;
d.width += 5;
d.height += 5;
sp.setPreferredSize(d);
view.revalidate();
}
/**
* Creates the toolbar
*
* @return The panel used as a toolbar
*/
private JPanel createToolBar() {
JPanel bar = new JPanel();
bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS));
bar.add(Box.createHorizontalGlue());
IconManager icons = IconManager.getInstance();
MouseListener ml = new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (!((JButton) e.getSource()).isEnabled())
return;
if (e.getSource() == addButton) {
insertRow();
}
if (e.getSource() == copyButton) {
copySelection();
}
if (e.getSource() == pasteButton) {
pasteSelection();
}
if (e.getSource() == deleteButton) {
deleteSelection();
}
}
};
addButton = new JButton(icons.getIcon(IconManager.PLUS));
UIUtilities.unifiedButtonLookAndFeel(addButton);
addButton.setBackground(UIUtilities.BACKGROUND_COLOR);
addButton.setToolTipText("Insert Row");
addButton.addMouseListener(ml);
addButton.setEnabled(false);
addButton.setFocusable(false);
bar.add(addButton);
copyButton = new JButton(icons.getIcon(IconManager.COPY));
UIUtilities.unifiedButtonLookAndFeel(copyButton);
copyButton.setBackground(UIUtilities.BACKGROUND_COLOR);
copyButton.setToolTipText("Copy Rows");
copyButton.addMouseListener(ml);
copyButton.setEnabled(false);
copyButton.setFocusable(false);
bar.add(copyButton);
pasteButton = new JButton(icons.getIcon(IconManager.PASTE));
UIUtilities.unifiedButtonLookAndFeel(pasteButton);
pasteButton.setBackground(UIUtilities.BACKGROUND_COLOR);
pasteButton.setToolTipText("Paste Rows");
pasteButton.addMouseListener(ml);
pasteButton.setEnabled(false);
pasteButton.setFocusable(false);
bar.add(pasteButton);
deleteButton = new JButton(icons.getIcon(IconManager.DELETE_12));
UIUtilities.unifiedButtonLookAndFeel(deleteButton);
deleteButton.setBackground(UIUtilities.BACKGROUND_COLOR);
deleteButton.setToolTipText("Delete Rows");
deleteButton.addMouseListener(ml);
deleteButton.setEnabled(false);
deleteButton.setFocusable(false);
bar.add(deleteButton);
return bar;
}
/**
* Creates a header panel for the MapAnnotations table
*
* @return See above
*/
private JPanel createHeaderPanel() {
JPanel p = new JPanel(new GridLayout(1, 2));
p.setBackground(UIUtilities.BACKGROUND_COLOR);
JLabel l = constructHeaderLabel("Key");
l.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1,
Color.LIGHT_GRAY));
p.add(l);
p.add(constructHeaderLabel("Value"));
return p;
}
/**
* Constructs a label for the header panel
*
* @return See above
*/
private JLabel constructHeaderLabel(String text) {
JLabel l = new JLabel(" " + text);
Font f = l.getFont().deriveFont(Font.ITALIC);
l.setForeground(UIUtilities.DEFAULT_FONT_COLOR);
l.setFont(f);
return l;
}
@Override
void clearDisplay() {
mapTables.clear();
tablePanel.removeAll();
c.gridy = 0;
tablePanel.add(headerPanel, c);
c.gridy++;
}
/**
* Show only specific types of MapAnnotations
*
* @param filter
* One of:
* {@link org.openmicroscopy.shoola.agents.metadata.editor.AnnotationTaskPaneUI.Filter#SHOW_ALL},
* {@link org.openmicroscopy.shoola.agents.metadata.editor.AnnotationTaskPaneUI.Filter#ADDED_BY_ME},
* {@link org.openmicroscopy.shoola.agents.metadata.editor.AnnotationTaskPaneUI.Filter#ADDED_BY_OTHERS}
*/
public void filter(Filter filter) {
for (MapTable table : mapTables) {
table.getParent().setVisible(false);
if (isUsers(table.getData())
&& (filter == Filter.ADDED_BY_ME || filter == Filter.SHOW_ALL)) {
table.getParent().setVisible(true);
}
if (isOtherUsers(table.getData())
&& (filter == Filter.ADDED_BY_OTHERS || filter == Filter.SHOW_ALL)) {
table.getParent().setVisible(true);
}
if (isOther(table.getData()) && filter == Filter.SHOW_ALL) {
table.getParent().setVisible(true);
}
}
}
private String generateToolTip(MapAnnotationData ma) {
ToolTipGenerator tt = new ToolTipGenerator();
ExperimenterData exp = null;
if (ma.getId() > 0) {
exp = model.getOwner(ma);
tt.addLine("ID", "" + ma.getId(), true);
}
String ns = ma.getNameSpace();
if (!CommonsLangUtils.isEmpty(ns) && !EditorUtil.isInternalNS(ns)) {
tt.addLine("Namespace", ns, true);
}
String desc = ma.getDescription();
if (!CommonsLangUtils.isEmpty(desc)) {
tt.addLine("Description", desc, true);
}
if (exp != null) {
tt.addLine("Owner", EditorUtil.formatExperimenter(exp), true);
}
Timestamp created = ma.getCreated();
if (created != null) {
tt.addLine("Date", UIUtilities.formatDefaultDate(created), true);
}
return tt.toString();
}
/**
* En-/Disables the toolbar buttons with respect to the current model state
*/
private void refreshButtonStates() {
addButton.setEnabled(canInsert());
copyButton.setEnabled(canCopy());
pasteButton.setEnabled(canPaste());
deleteButton.setEnabled(canDelete());
}
/**
* Finds the table corresponding to the given {@link MapAnnotationData}
* object
*
* @param data
* The {@link MapAnnotationData} to look for
* @return The {@link MapTable} or <code>null</code> if it doesn't exist
*/
private MapTable findTable(MapAnnotationData data) {
for (MapTable table : mapTables) {
if (table.getData().getId() == data.getId())
return table;
}
// the user's MapAnnotation might not have an ID yet.
if (isUsers(data)) {
for (MapTable table : mapTables) {
if (table.getData().getId() < 0) {
return table;
}
}
}
return null;
}
/**
* Check if the given {@link MapAnnotationData} is the user's own annotation
*
* @param data
* The {@link MapAnnotationData}
* @return See above
*/
private boolean isUsers(MapAnnotationData data) {
return MapAnnotationData.NS_CLIENT_CREATED.equals(data.getNameSpace())
&& (data.getOwner() == null || MetadataViewerAgent
.getUserDetails().getId() == data.getOwner().getId());
}
/**
* Check if the given {@link MapAnnotationData} belongs to an other user
*
* @param data
* The {@link MapAnnotationData}
* @return See above
*/
private boolean isOtherUsers(MapAnnotationData data) {
return MapAnnotationData.NS_CLIENT_CREATED.equals(data.getNameSpace())
&& !isUsers(data);
}
/**
* Check if the given {@link MapAnnotationData} is a non-user annotation
*
* @param data
* The {@link MapAnnotationData}
* @return See above
*/
private boolean isOther(MapAnnotationData data) {
return !MapAnnotationData.NS_CLIENT_CREATED.equals(data.getNameSpace());
}
/**
* Creates a {@link MapTable} and adds it to the list of mapTables; Returns
* <code>null</code> if the {@link MapAnnotationData} is empty and not
* editable!
*
* @param m
* The data to show
* @return See above
*/
private MapTable createMapTable(MapAnnotationData m) {
boolean editable = (isUsers(m) && (model.canAnnotate()))
|| (model.canEdit(m) && MapAnnotationData.NS_CLIENT_CREATED
.equals(m.getNameSpace()));
if (!editable
&& (m.getContent() == null || ((List<NamedValue>) m
.getContent()).isEmpty()))
return null;
int permissions = editable ? MapTable.PERMISSION_DELETE
| MapTable.PERMISSION_MOVE | MapTable.PERMISSION_EDIT
: MapTable.PERMISSION_NONE;
final MapTable t = new MapTable(permissions);
t.getSelectionModel().addListSelectionListener(this);
t.setData(m);
t.getModel().addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
refreshButtonStates();
MapTableModel m = (MapTableModel) t.getModel();
if (m.isEmpty() && m.getMap().getId() >= 0) {
view.deleteAnnotation(m.getMap());
view.saveData(true);
}
adjustScrollPane();
}
});
mapTables.add(t);
return t;
}
/**
* Get all {@link MapAnnotationData}s handled by this component
*
* @param onlyDirty
* Pass <code>true</code> if you only want the dirty ones
* @param excludeEmpty
* Pass <code>true</code> to exclude {@link MapAnnotationData}s
* without {@link NamedValue} entries
* @return See above.
*/
public List<MapAnnotationData> getMapAnnotations(boolean onlyDirty,
boolean excludeEmpty) {
List<MapAnnotationData> result = new ArrayList<MapAnnotationData>();
for (MapTable t : mapTables) {
if (t.getCellEditor() != null)
t.getCellEditor().stopCellEditing();
if ((!onlyDirty || ((MapTableModel) t.getModel()).isDirty())
&& !(excludeEmpty && t.isEmpty()))
result.add(t.getData());
}
return result;
}
/**
* Get all empty MapAnnotations
*
* @return See above.
*/
public List<MapAnnotationData> getEmptyMapAnnotations() {
List<MapAnnotationData> result = new ArrayList<MapAnnotationData>();
for (MapTable t : mapTables) {
if (t.isEmpty())
result.add(t.getData());
}
return result;
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting() && listenerActive) {
listenerActive = false;
MapTable src = ((MapTableSelectionModel) e.getSource()).getTable();
for (MapTable t : mapTables) {
if (t != src) {
t.getSelectionModel().clearSelection();
}
}
listenerActive = true;
refreshButtonStates();
}
}
/**
* Get the table which holds the current selection
*
* @return See above
*/
public MapTable getSelectedTable() {
for (MapTable t : mapTables) {
if (t.getSelectedRow() != -1)
return t;
}
return null;
}
/**
* Get the table which holds the the user's own {@link MapAnnotationData}
*
* @return See above
*/
public MapTable getUserTable() {
for (MapTable t : mapTables) {
if (isUsers(t.getData()))
return t;
}
return null;
}
/**
* Get the selected {@link NamedValue}s
*
* @return See above.
*/
public List<NamedValue> getSelection() {
MapTable t = getSelectedTable();
if (t != null)
return t.getSelection();
return Collections.emptyList();
}
/**
* Copy the current selection
*/
private void insertRow() {
MapTable t = getSelectedTable();
if (t == null)
t = getUserTable();
if (t == null)
return;
MapTableModel m = (MapTableModel) t.getModel();
// if nothing's selected add to end of table, otherwise below selected
// row
int index = t.getSelectedRow() == -1 ? t.getRowCount() : t
.getSelectedRow() + 1;
m.addEntries(Arrays.asList(new NamedValue("", "")), index);
t.requestFocus();
t.getSelectionModel().setSelectionInterval(index, index);
t.setColumnSelectionInterval(0, 0);
}
/**
* Copy the current selection
*/
private void copySelection() {
copiedValues.clear();
copiedValues.addAll(getSelection());
refreshButtonStates();
}
/**
* Creates a new {@link NamedValue} object with identical name and value as
* the {@link NamedValue} in original
*
* @param original
* The list of {@link NamedValue} objects to copy
* @return A new list with the copied {@link NamedValue}s
*/
private List<NamedValue> deepCopy(List<NamedValue> original) {
List<NamedValue> result = new ArrayList<NamedValue>();
for (NamedValue orig : original) {
NamedValue copy = new NamedValue(orig.name, orig.value);
result.add(copy);
}
return result;
}
/**
* Paste previously copied selection
*/
private void pasteSelection() {
MapTable t = getSelectedTable();
if (t == null)
t = getUserTable();
if (t == null) // no user table and nothing selected, don't know where
// to paste
return;
MapTableModel m = (MapTableModel) t.getModel();
int index = t.getSelectedRow() + 1;
m.addEntries(deepCopy(copiedValues), index);
index += copiedValues.size() - 1;
t.requestFocus();
t.getSelectionModel().setSelectionInterval(index, index);
}
/**
* Delete the currently selected items
*/
private void deleteSelection() {
MapTable t = getSelectedTable();
int index = t.getSelectedRow();
t.deleteSelected();
if (index >= t.getRowCount())
index = t.getRowCount() - 1;
t.requestFocus();
t.getSelectionModel().setSelectionInterval(index, index);
}
/**
* Checks if insert action is possible
*/
private boolean canInsert() {
MapTable t = getSelectedTable();
return ((t == null || t == getUserTable()) && model.canAnnotate())
|| (t != null && t.canEdit());
}
/**
* Checks if delete action is possible
*/
private boolean canDelete() {
return !getSelection().isEmpty() && getSelectedTable().canDelete();
}
/**
* Checks if copy action is possible
*/
private boolean canCopy() {
return !getSelection().isEmpty();
}
/**
* Checks if paste action is possible
*/
private boolean canPaste() {
return !copiedValues.isEmpty()
&& (getSelectedTable() == null || getSelectedTable().canEdit());
}
@Override
void refreshUI() {
List<MapAnnotationData> list = new ArrayList<MapAnnotationData>();
if (filter == Filter.SHOW_ALL || filter == Filter.ADDED_BY_ME) {
list.addAll(model.getMapAnnotations(MapAnnotationType.USER));
if (list.isEmpty()) {
MapAnnotationData newMA = new MapAnnotationData();
newMA.setNameSpace(MapAnnotationData.NS_CLIENT_CREATED);
list.add(newMA);
}
}
if (filter == Filter.SHOW_ALL || filter == Filter.ADDED_BY_OTHERS) {
list.addAll(model.getMapAnnotations(MapAnnotationType.OTHER_USERS));
}
list.addAll(model.getMapAnnotations(MapAnnotationType.OTHER));
for (MapAnnotationData ma : list) {
MapTable t = findTable(ma);
if (t != null) {
t.setData(ma);
} else {
// if there isn't a table yet, create it
String title = ma.getOwner() != null && !isUsers(ma) ? "Added by: "
+ EditorUtil.formatExperimenter(ma.getOwner())
: "";
JPanel p = new JPanel();
p.setBackground(UIUtilities.BACKGROUND_COLOR);
UIUtilities.setBoldTitledBorder(title, p);
p.setToolTipText(generateToolTip(ma));
p.setLayout(new BorderLayout());
t = createMapTable(ma);
if (t != null) {
p.add(t, BorderLayout.CENTER);
// if the MapAnnotation has a custom namespace, display it
if (!CommonsLangUtils.isEmpty(ma.getNameSpace())
&& !MapAnnotationData.NS_CLIENT_CREATED.matches(ma
.getNameSpace())) {
JLabel ns = new JLabel(UIUtilities.formatPartialName(ma
.getNameSpace()));
ns.setFont(ns.getFont().deriveFont(Font.BOLD));
p.add(ns, BorderLayout.NORTH);
}
tablePanel.add(p, c);
c.gridy++;
}
}
}
refreshButtonStates();
setVisible(!mapTables.isEmpty());
adjustScrollPane();
}
@Override
void onRelatedNodesSet() {
clearDisplay();
refreshButtonStates();
}
}