/*
* Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
* Copyright [2016-2017] EMBL-European Bioinformatics Institute
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ensembl.healthcheck.eg_gui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Box;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.ensembl.healthcheck.DatabaseRegistryEntry;
import org.ensembl.healthcheck.ReportLine;
import org.ensembl.healthcheck.Reporter;
import org.ensembl.healthcheck.eg_gui.Constants;
import org.ensembl.healthcheck.eg_gui.JPopupTextArea;
import org.ensembl.healthcheck.eg_gui.ReportPanel;
import org.ensembl.healthcheck.testcase.EnsTestCase;
public class GuiReporterTab extends JPanel implements Reporter {
final protected TestClassList testList;
final protected JScrollPane testListScrollPane;
final protected TestClassListModel listModel;
final protected ReportPanel reportPanel;
final protected TestCaseColoredCellRenderer testCaseCellRenderer;
protected boolean userClickedOnTestList = false;
final protected Map<Class<? extends EnsTestCase>,GuiReportPanelData> reportData;
/**
* <p>
* While the testrunner is running, the test selected will be
* automatically updated to be the last one in the list. That way
* the user can monitor what is going on while the tests are running.
* </p>
*
* <p>
* This behaviour is deactivated, if the user has selected a test from
* the list or when he has given the message field focus.
* </p>
*
* @return whether or not it is ok to scroll to the last test of the list automatically.
*/
protected boolean autoScrollTestListOk() {
return !userClickedOnTestList && !reportPanel.messageFieldHasFocus();
}
public void selectDefaultListItem() {
if (autoScrollTestListOk()) {
selectLastListItem();
}
}
/**
* Selects the last item in the list and scrolls to that position.
*/
public void selectLastListItem() {
if (listModel.getSize()>0) {
int indexOfLastComponentInList =listModel.getSize()-1;
testList.setSelectedIndex(indexOfLastComponentInList);
testList.ensureIndexIsVisible(indexOfLastComponentInList);
}
}
public GuiReporterTab() {
this.setBorder(GuiTestRunnerFrameComponentBuilder.defaultEmptyBorder);
reportData = new HashMap<Class<? extends EnsTestCase>,GuiReportPanelData>();
testList = new TestClassList(TestClassList.TestClassListToolTipType.CLASS);
listModel = new TestClassListModel();
reportPanel = new ReportPanel();
testList.setModel(listModel);
testCaseCellRenderer = new TestCaseColoredCellRenderer();
testList.setCellRenderer(testCaseCellRenderer);
testList.addMouseListener(new MouseListener() {
// We want to know, when as soon as the user clicks somewhere on
// the list so we can stop selecting the last item in the list.
//
@Override public void mouseClicked(MouseEvent arg0) {
userClickedOnTestList = true;
}
@Override public void mouseEntered (MouseEvent arg0) {}
@Override public void mouseExited (MouseEvent arg0) {}
@Override public void mousePressed (MouseEvent arg0) {}
@Override public void mouseReleased(MouseEvent arg0) {}
});
// Setting the preferred size causes the scrollbars to not be adapted
// to the list changing size when items are added later on, so
// commented out.
//
// testList.setPreferredSize(
// new Dimension(
// Constants.INITIAL_APPLICATION_WINDOW_WIDTH / 3,
// Constants.INITIAL_APPLICATION_WINDOW_HEIGHT / 3 * 2
// )
// );
setLayout(new BorderLayout());
testListScrollPane = new JScrollPane(testList);
add(
new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
testListScrollPane,
//new JScrollPane(reportPanel)
reportPanel
), BorderLayout.CENTER
);
testList.addListSelectionListener(
new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent arg0) {
if (!arg0.getValueIsAdjusting()) {
reportPanel.setData(
reportData.get(
( (TestClassListItem) listModel.getElementAt(testList.getSelectedIndex()) ).getTestClass()
)
);
}
}
}
);
}
@Override
public void message(final ReportLine reportLine) {
final Class<? extends EnsTestCase> currentKey = reportLine.getTestCase().getClass();
if (!reportData.containsKey(currentKey)) {
reportData.put(currentKey, new GuiReportPanelData(reportLine));
testCaseCellRenderer.setOutcome(currentKey, null);
// This method will be called from a different thread. Therefore
// updates to the components must be run through SwingUtilities.
//
SwingUtilities.invokeLater(
new Runnable() {
@Override public void run() {
listModel.addTest(currentKey);
// If nothing has been selected, then select
// something so the user is not staring at an
// empty report.
//
//if (testList.isSelectionEmpty()) {
if (!userClickedOnTestList) {
selectDefaultListItem();
}
}
}
);
} else {
reportData.get(currentKey).addReportLine(reportLine);
}
// If anything was reported as a problem, the outcome is false.
//
if (reportLine.getLevel()==ReportLine.PROBLEM) {
testCaseCellRenderer.setOutcome(currentKey, false);
}
// If a testcase has been selected, then display the new data in the
// GUI so the user can see the new line in real time.
//
if (!testList.isSelectionEmpty()) {
SwingUtilities.invokeLater(
new Runnable() {
@Override public void run() {
reportPanel.setData(
reportData.get(
( (TestClassListItem) listModel.getElementAt(testList.getSelectedIndex()) ).getTestClass()
)
);
}
}
);
}
}
@Override
public void startTestCase(EnsTestCase testCase, DatabaseRegistryEntry dbre) {}
@Override
public void finishTestCase(
final EnsTestCase testCase,
final boolean result,
DatabaseRegistryEntry dbre
) {
// This method will be called from a different thread. Therefore
// updates to the components must be run through SwingUtilities.
//
SwingUtilities.invokeLater(
new Runnable() {
@Override public void run() {
testCaseCellRenderer.setOutcome(testCase.getClass(), result);
}
}
);
}
}
class ReportPanel extends JPanel implements ActionListener {
final protected JTextField testName;
final protected JPopupTextArea description;
final protected JTextField teamResponsible;
final protected JTextField speciesName;
final protected JPopupTextArea message;
final String copy_selected_text_action = "copy_selected_text_action";
protected boolean updateInProgress;
protected Component createVerticalSpacing() {
return Box.createVerticalStrut(Constants.DEFAULT_VERTICAL_COMPONENT_SPACING);
}
private boolean messageFieldHasFocus = false;
public boolean messageFieldHasFocus() {
return messageFieldHasFocus;
}
public ReportPanel() {
this.setBorder(GuiTestRunnerFrameComponentBuilder.defaultEmptyBorder);
Box singleLineInfo = Box.createVerticalBox();
testName = new JPopupTextField("Name");
description = new JPopupTextArea(3, 0);
teamResponsible = new JPopupTextField("Team Responsible");
speciesName = new JPopupTextField("Species Name");
message = new JPopupTextArea ();
message.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
messageFieldHasFocus = true;
}
@Override
public void focusLost(FocusEvent e) {
messageFieldHasFocus = false;
}
});
description.setLineWrap(true);
description.setWrapStyleWord(true);
GuiTestRunnerFrameComponentBuilder g = null;
singleLineInfo.add(g.createLeftJustifiedText("Test class:"));
singleLineInfo.add(testName);
singleLineInfo.add(createVerticalSpacing());
singleLineInfo.add(g.createLeftJustifiedText("Description:"));
singleLineInfo.add(description);
singleLineInfo.add(createVerticalSpacing());
singleLineInfo.add(g.createLeftJustifiedText("Team responsible:"));
singleLineInfo.add(teamResponsible);
singleLineInfo.add(createVerticalSpacing());
singleLineInfo.add(g.createLeftJustifiedText("Species name:"));
singleLineInfo.add(speciesName);
singleLineInfo.add(createVerticalSpacing());
setLayout(new BorderLayout());
final JPopupMenu popup = new JPopupMenu();
message.add(GuiTestRunnerFrameComponentBuilder.makeMenuItem("Copy selected text", this, copy_selected_text_action));
message.setComponentPopupMenu(popup);
Font currentFont = message.getFont();
Font newFont = new Font(
"Courier",
currentFont.getStyle(),
currentFont.getSize()
);
message.setFont(newFont);
//message.setLineWrap(true);
//message.setWrapStyleWord(true);
singleLineInfo.add(g.createLeftJustifiedText("Output from test:"));
add(singleLineInfo, BorderLayout.NORTH);
add(new JScrollPane(message), BorderLayout.CENTER);
// This has to be set to something small otherwise we will get problems when
// wrapping this in a JSplitPane:
//
// http://docs.oracle.com/javase/6/docs/api/javax/swing/JSplitPane.html
//
// "When the user is resizing the Components the minimum size of the
// Components is used to determine the maximum/minimum position the
// Components can be set to. If the minimum size of the two
// components is greater than the size of the split pane the divider
// will not allow you to resize it."
//
this.setMinimumSize(new Dimension(200,300));
updateInProgress = false;
}
public synchronized void setData(final GuiReportPanelData reportData) {
/*
* Updating the reporter tab may be slow especially when setting the
* message to a long string.
*
* Therefore the update is put into a new Thread to keep the gui
* responsive.
*
* Additional updates are prevented in the meantime. In the worst case
* this could lead to an StackOverflorError, if the method gets
* repeatedly called before it can complete.
*
*/
if (updateInProgress) {
return;
}
Thread t = new Thread() {
public void run() {
testName .setText (reportData.getTestName());
description .setText (reportData.getDescription());
speciesName .setText (reportData.getSpeciesName());
teamResponsible .setText (reportData.getTeamResponsible());
String msg = reportData.getMessage();
message.setText (msg);
if (!messageFieldHasFocus) {
message.setCaretPosition(msg.length());
}
updateInProgress = false;
}
};
t.setName("Updating data in reporter tab.");
updateInProgress = true;
t.start();
}
@Override
public void actionPerformed(ActionEvent arg0) {
if (arg0.paramString().equals(copy_selected_text_action)) {
String selection = message.getSelectedText();
StringSelection data = new StringSelection(selection);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(data, data);
}
}
}