/*
* Copyright 2011 Luke Usherwood.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.download;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.text.JTextComponent;
import net.bettyluke.tracinstant.download.DownloadModel.ListModelView;
import net.bettyluke.tracinstant.prefs.TracInstantProperties;
import net.bettyluke.tracinstant.ui.BrowsePanel;
import net.bettyluke.tracinstant.ui.TracInstantFrame;
import net.bettyluke.util.DocUtils;
// TODO: Enforce single-instance creation - since this is the only anticipated usage
// we are lazy and don't disconnect listeners.
public class DownloadDialog extends JDialog {
private static final String[] COLUMNS = { "", "File", "Status" };
private static final int[] DEFAULT_COL_WIDTH = { 1000, 30000, 10000 };
public class TargetTableModel extends AbstractTableModel {
private ListModelView listModel;
public TargetTableModel(ListModelView listModel) {
this.listModel = listModel;
listModel.addListDataListener(new ListDataListener() {
@Override
public void intervalRemoved(ListDataEvent e) {
fireTableRowsDeleted(e.getIndex0(), e.getIndex1());
}
@Override
public void intervalAdded(ListDataEvent e) {
fireTableRowsInserted(e.getIndex0(), e.getIndex1());
}
@Override
public void contentsChanged(ListDataEvent e) {
fireTableRowsUpdated(e.getIndex0(), e.getIndex1());
}
});
}
@Override
public int getRowCount() {
return listModel.getSize();
}
@Override
public int getColumnCount() {
return COLUMNS.length;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Target target = listModel.getElementAt(rowIndex);
Path file = downloadModel.getAbsolutePath(target);
if ("".equals(COLUMNS[columnIndex])) {
return target.isSelected();
} else if ("File".equals(COLUMNS[columnIndex])) {
return file;
} else if ("Status".equals(COLUMNS[columnIndex])) {
switch (target.getState()) {
case IDLE:
if (target.isOverwriting()) {
return "FILE EXISTS";
}
return "";
case STARTED:
return "Downloading"; // TODO: add percentage ?
case ENDED:
return "Finished";
case ERROR:
return "An error occurred";
default:
break;
}
}
throw new AssertionError();
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
Target target = listModel.getElementAt(rowIndex);
if ("".equals(COLUMNS[columnIndex])) {
target.setSelected((Boolean) aValue);
listModel.modifiedElementAt(rowIndex);
updateControls();
}
}
@Override
public String getColumnName(int column) {
return COLUMNS[column];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
if ("".equals(COLUMNS[columnIndex])) {
return Boolean.class;
}
return String.class;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return columnIndex == 0;
}
}
class StartDownloadAction extends AbstractAction {
public StartDownloadAction() {
super("Start downloading");
}
@Override
public void actionPerformed(ActionEvent e) {
File bugsFolder = browsePanel.getPath();
TracInstantProperties.get().putFilePath("LocalBugsDir", bugsFolder);
if (!bugsFolder.exists()) {
bugsFolder.mkdirs();
}
if (!bugsFolder.isDirectory()) {
showErrorMessage("Directory not found: " + bugsFolder);
return;
}
// TODO: split actions so that setting the bugs folder updates the download
// list immediately, and the OK button is disabled unless the
// folder exists (perhaps with a separate "create this folder" option).
downloadModel.setBugsFolder(bugsFolder);
downloadModel.download();
}
private void showErrorMessage(String message) {
JOptionPane.showMessageDialog(
DownloadDialog.this, message, getTitle(), JOptionPane.ERROR_MESSAGE);
}
}
class StopDownloadAction extends AbstractAction {
public StopDownloadAction() {
super("Stop downloading");
}
@Override
public void actionPerformed(ActionEvent e) {
downloadModel.cancelDownload();
}
}
class HideAction extends AbstractAction {
public HideAction() {
super("Hide");
}
@Override
public void actionPerformed(ActionEvent e) {
// This is intended as a singleton dialog
setVisible(false);
}
}
private static final int GAP = 8;
private final DownloadModel downloadModel;
private final BrowsePanel browsePanel;
private final JComponent tablePanel;
private final JTextComponent statusPanel;
private final Action startAction = new StartDownloadAction();
private final Action stopAction = new StopDownloadAction();
private final Action hideAction = new HideAction();
JButton startButton = new JButton(startAction);
JButton stopButton = new JButton(stopAction);
public DownloadDialog(TracInstantFrame parent, DownloadModel model) {
super(parent, "Download attachments", ModalityType.MODELESS);
this.downloadModel = model;
this.browsePanel = createBrowsePanel();
this.tablePanel = createTablePanel();
this.statusPanel = createFeedbackPanel();
JPanel mainPanel = newBorderedPanel(browsePanel, tablePanel, statusPanel);
getContentPane().add(newBorderedPanel(null, mainPanel, createButtonRow()));
updateControls();
addKeyboardShortcuts();
addBehaviour();
mainPanel.setBorder(BorderFactory.createEmptyBorder(GAP, GAP, 0, GAP));
setSize(640, 500);
setLocationRelativeTo(parent);
}
private void addKeyboardShortcuts() {
getRootPane().setDefaultButton(startButton);
getRootPane().registerKeyboardAction(hideAction,
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
}
private void addBehaviour() {
downloadModel.addChangeListener(e -> updateControls());
JTextComponent ed = browsePanel.getLocationEditor();
ed.getDocument().addDocumentListener(
DocUtils.newOnAnyEventListener(
() -> updateBugsFolder(ed.getText())));
}
private void updateBugsFolder(String filename) {
downloadModel.setBugsFolder(new File(filename));
}
protected void updateControls() {
updateButtonEnabledStates();
statusPanel.setText(getStatusText());
}
private String getStatusText() {
switch (downloadModel.getState()) {
case CANCELLING:
return "Cancelling";
case COUNTING:
return "Finding attachments...";
case DOWNLOADING:
return downloadModel.getDownloadSummary();
case IDLE:
break;
}
if (downloadModel.countComplete() > 0 && downloadModel.countSelected() == 0) {
return "Download complete: " +
downloadModel.countComplete() + " / " +
downloadModel.countFilesToDownloadOrDownloaded() +
" (Tip: double-click to explore to a file)";
}
String s = downloadModel.countSelected() + " files selected.";
int toOverwrite = downloadModel.countFilesToOverwrite();
if (toOverwrite > 0) {
s += " " + toOverwrite + " will be overwritten!";
}
if (!Files.exists(downloadModel.getBugsFolder())) {
s += " The download folder will be created.";
}
return s;
}
private void updateButtonEnabledStates() {
boolean startWasEnabled = startAction.isEnabled();
startAction.setEnabled(!downloadModel.isBusy() && downloadModel.getNumDownloads() > 0);
stopAction.setEnabled(downloadModel.isDownloading());
if (startWasEnabled && !startAction.isEnabled()) {
if (stopAction.isEnabled()) {
stopButton.requestFocusInWindow();
}
} else if (stopButton.hasFocus() && !stopAction.isEnabled()) {
if (startAction.isEnabled()) {
startButton.requestFocusInWindow();
}
}
}
private BrowsePanel createBrowsePanel() {
return new BrowsePanel(downloadModel.getBugsFolder().toFile());
}
private JComponent createTablePanel() {
TargetTableModel tableModel = new TargetTableModel(downloadModel.getListModel());
final JTable table = new JTable(tableModel);
int i = 0;
for (int w : DEFAULT_COL_WIDTH) {
table.getColumnModel().getColumn(i++).setPreferredWidth(w);
}
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
if (SwingUtilities.isLeftMouseButton(evt) && evt.getClickCount() == 2 &&
1 == table.getSelectedRowCount()) {
browseToFolderOfSelectedRow(table);
evt.consume();
}
}
});
table.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent evt) {
if (!KeyEvent.getKeyModifiersText(evt.getModifiers()).isEmpty()) {
return;
}
if (evt.getKeyCode() == KeyEvent.VK_ENTER && 1 == table.getSelectedRowCount()) {
browseToFolderOfSelectedRow(table);
evt.consume();
}
if (evt.getKeyCode() == KeyEvent.VK_SPACE && table.getSelectedRowCount() > 0) {
toggleSelected();
evt.consume();
}
}
// Toggle the checkbox on all selected rows
private void toggleSelected() {
boolean selected = (Boolean) table.getModel().getValueAt(table.getSelectedRow(), 0);
for (int viewRow : table.getSelectedRows()) {
int row = table.convertRowIndexToModel(viewRow);
table.getModel().setValueAt(!selected, row, 0);
}
}
});
return new JScrollPane(table);
}
protected void browseToFolderOfSelectedRow(JTable table) {
int tableViewRow = table.getSelectedRow();
int tableModelRow = table.convertRowIndexToModel(tableViewRow);
browseToTargetFolder(downloadModel.getListModel().getElementAt(tableModelRow));
}
protected void browseToTargetFolder(Object oTarget) {
File file = downloadModel.getAbsolutePath((Target) oTarget).toFile();
while (file != null) {
if (file.isDirectory()) {
try {
Desktop.getDesktop().open(file);
return;
} catch (IOException e) {
e.printStackTrace();
break;
}
} else {
file = file.getParentFile();
}
}
}
private JTextComponent createFeedbackPanel() {
JEditorPane pane = new JEditorPane();
pane.setContentType("text/html");
pane.setText(" ");
pane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
pane.putClientProperty("Nimbus.Overrides.InheritDefaults", true);
UIDefaults defaults = new UIDefaults();
Color bg = new Color(getBackground().getRGB());
defaults.put("EditorPane[Enabled].backgroundPainter", bg);
pane.putClientProperty("Nimbus.Overrides", defaults);
pane.setBackground(bg);
pane.setOpaque(false);
pane.setFocusable(false);
pane.setEditable(false);
Insets in = pane.getInsets();
pane.setBorder(BorderFactory.createEmptyBorder(in.top, in.left, in.bottom, in.right));
return pane;
}
private static JPanel newBorderedPanel(Component north, Component centre, Component south) {
BorderLayout layout = new BorderLayout();
layout.setVgap(GAP);
JPanel panel = new JPanel(layout);
panel.add(centre);
if (north != null) {
panel.add(north, BorderLayout.NORTH);
}
if (south != null) {
panel.add(south, BorderLayout.SOUTH);
}
return panel;
}
private Box createButtonRow() {
Box buttonRow = Box.createHorizontalBox();
buttonRow.add(new JButton(hideAction));
buttonRow.add(Box.createHorizontalGlue());
buttonRow.add(startButton);
buttonRow.add(Box.createHorizontalStrut(6));
buttonRow.add(stopButton);
buttonRow.setBorder(BorderFactory.createEmptyBorder(0, GAP, GAP, GAP));
return buttonRow;
}
}