/*
* Copyright (c) Henrik Niehaus & Lazy Bones development team
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of the project (Lazy Bones) nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package lazybones.gui.recordings;
import static lazybones.LazyBones.getTranslation;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Observable;
import java.util.Observer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.hampelratte.svdrp.Response;
import org.hampelratte.svdrp.responses.highlevel.DiskStatus;
import org.hampelratte.svdrp.responses.highlevel.Folder;
import org.hampelratte.svdrp.responses.highlevel.Recording;
import org.hampelratte.svdrp.responses.highlevel.TreeNode;
import org.hampelratte.svdrp.sorting.RecordingAlphabeticalComparator;
import org.hampelratte.svdrp.sorting.RecordingIsCutComparator;
import org.hampelratte.svdrp.sorting.RecordingIsNewComparator;
import org.hampelratte.svdrp.sorting.RecordingStarttimeComparator;
import org.hampelratte.svdrp.util.SizeFormatter;
import org.hampelratte.swing.DecoratableTextField;
import org.hampelratte.swing.TextFieldClearButtonDecorator;
import org.hampelratte.swing.TextFieldHintDecorator;
import org.hampelratte.swing.TextFieldIconDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lazybones.LazyBones;
import lazybones.Player;
import lazybones.RecordingManager;
import lazybones.VDRCallback;
import lazybones.actions.DeleteRecordingAction;
import lazybones.actions.GetRecordingDetailsAction;
import lazybones.actions.RenameRecordingAction;
import lazybones.gui.components.RecordingDetailsPanel;
import util.ui.Localizer;
public class RecordingManagerPanel extends JPanel implements ActionListener, ItemListener {
private static final long serialVersionUID = 1L;
private static transient Logger logger = LoggerFactory.getLogger(RecordingManagerPanel.class);
private final JComboBox<SortStrategy> sortStrategySelector = new JComboBox<SortStrategy>();
private final JToggleButton orderSelector = new JToggleButton();
private JScrollPane scrollPane = null;
private RecordingTreeModel recordingTreeModel;
private final JTree recordingTree = new JTree();
private final RecordingDetailsPanel recordingDetailsPanel = new RecordingDetailsPanel();
private JButton buttonSync = null;
private JButton buttonRemove = null;
private JButton expandAll = null;
private JButton collapseAll = null;
private final JPopupMenu popup = new JPopupMenu();
private DecoratableTextField filter = new DecoratableTextField();
// new HintTextField();
private JProgressBar diskUsageProgressBar = new JProgressBar();
private RecordingManager recordingManager;
public RecordingManagerPanel(RecordingManager recordingManager) {
this.recordingManager = recordingManager;
initGUI();
loadSettings();
}
private void loadSettings() {
boolean ascending = Boolean.parseBoolean(LazyBones.getProperties().getProperty("recording.order.ascending", "true"));
int strategyIndex = Integer.parseInt(LazyBones.getProperties().getProperty("recording.order.strategy", "0"));
sortStrategySelector.setSelectedIndex(strategyIndex);
orderSelector.setSelected(!ascending);
SortStrategy strategy = (SortStrategy) sortStrategySelector.getSelectedItem();
recordingTreeModel.sortBy(strategy.getComparator(), ascending);
}
/**
* This method initializes the GUI
*/
private void initGUI() {
createContextMenu();
this.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
int y = 0;
gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = y++;
gbc.weightx = 1.0;
gbc.weighty = 0;
gbc.gridwidth = 2;
gbc.insets = new java.awt.Insets(5, 10, 5, 10);
this.add(createToolbar(), gbc);
gbc.fill = java.awt.GridBagConstraints.BOTH;
gbc.gridx = 0;
gbc.gridy = y++;
gbc.weightx = .6;
gbc.weighty = 1;
gbc.gridwidth = 1;
gbc.insets = new java.awt.Insets(0, 10, 10, 10);
recordingTreeModel = new RecordingTreeModel(recordingManager);
recordingTree.setModel(recordingTreeModel);
recordingTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
recordingTree.setRootVisible(true);
recordingTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent e) {
TreeNode selected = (TreeNode) e.getPath().getLastPathComponent();
TreePath newSelectionPath = e.getNewLeadSelectionPath();
if (newSelectionPath != null && selected instanceof Recording) {
buttonRemove.setEnabled(true);
} else {
buttonRemove.setEnabled(false);
}
}
});
recordingTree.setRowHeight(25);
recordingTree.setCellRenderer(new RecordingTreeRenderer());
scrollPane = new JScrollPane(recordingTree);
scrollPane.getVerticalScrollBar().setUnitIncrement(10);
scrollPane.setPreferredSize(new Dimension(300, 800));
this.add(scrollPane, gbc);
ToolTipManager.sharedInstance().registerComponent(recordingTree);
gbc.gridx = 1;
gbc.gridy = 1;
gbc.weightx = .4;
gbc.gridheight = 1;
gbc.insets = new java.awt.Insets(0, 0, 5, 10);
recordingTree.addTreeSelectionListener(recordingDetailsPanel);
recordingDetailsPanel.setPreferredSize(new Dimension(300, 800));
this.add(recordingDetailsPanel, gbc);
}
private JToolBar createToolbar() {
JToolBar toolBar = new JToolBar();
toolBar.setBorder(BorderFactory.createEmptyBorder());
toolBar.setRollover(true);
toolBar.setFloatable(false);
buttonSync = new JButton();
buttonSync.setToolTipText(getTranslation("resync", "Synchronize"));
buttonSync.setIcon(LazyBones.getInstance().createImageIcon("action", "view-refresh", 22));
buttonSync.addActionListener(this);
buttonSync.setActionCommand("SYNC");
toolBar.add(buttonSync);
buttonRemove = new JButton();
buttonRemove.setToolTipText(getTranslation("delete_recording", "Delete Recording"));
buttonRemove.setIcon(LazyBones.getInstance().createImageIcon("action", "edit-delete", 22));
buttonRemove.addActionListener(this);
buttonRemove.setActionCommand("DELETE");
buttonRemove.setEnabled(false);
toolBar.add(buttonRemove);
toolBar.addSeparator();
sortStrategySelector.addItem(new SortStrategy(new RecordingAlphabeticalComparator(), getTranslation("sort.alphabetical", "alphabetical")));
sortStrategySelector.addItem(new SortStrategy(new RecordingStarttimeComparator(), getTranslation("sort.chronological", "chronological")));
sortStrategySelector.addItem(new SortStrategy(new RecordingIsNewComparator(), getTranslation("sort.new_recordings", "new recordings")));
sortStrategySelector.addItem(new SortStrategy(new RecordingIsCutComparator(), getTranslation("sort.cut_recordings", "cut recordings")));
sortStrategySelector.addItemListener(this);
sortStrategySelector.setPreferredSize(new Dimension(250, 26));
sortStrategySelector.setMaximumSize(new Dimension(250, 26));
toolBar.add(sortStrategySelector);
toolBar.addSeparator(new Dimension(2, 0));
orderSelector.setIcon(LazyBones.getInstance().getIcon("lazybones/sort_ascending.png"));
orderSelector.setSelectedIcon(LazyBones.getInstance().getIcon("lazybones/sort_descending.png"));
orderSelector.setToolTipText(getTranslation("sort.ascending", "ascending"));
orderSelector.addActionListener(this);
toolBar.add(orderSelector);
toolBar.addSeparator();
expandAll = new JButton(LazyBones.getInstance().getIcon("lazybones/list-add.png"));
expandAll.addActionListener(this);
expandAll.setToolTipText(getTranslation("expand_all", "expand all"));
toolBar.add(expandAll);
collapseAll = new JButton(LazyBones.getInstance().getIcon("lazybones/list-remove.png"));
collapseAll.addActionListener(this);
collapseAll.setToolTipText(getTranslation("collapse_all", "collapse all"));
toolBar.add(collapseAll);
toolBar.addSeparator();
filter.setPreferredSize(new Dimension(250, 26));
filter.setMaximumSize(new Dimension(250, 26));
filter.addDecorator(new TextFieldHintDecorator(getTranslation("search", "Search")));
filter.addDecorator(new TextFieldClearButtonDecorator());
ImageIcon searchIcon = LazyBones.getInstance().createImageIcon("action", "system-search", 16);
filter.addDecorator(new TextFieldIconDecorator(searchIcon.getImage()));
toolBar.add(filter);
filter.getDocument().addDocumentListener(new DocumentListener() {
@Override public void removeUpdate(DocumentEvent e) { filter(); }
@Override public void insertUpdate(DocumentEvent e) { filter(); }
@Override public void changedUpdate(DocumentEvent e) { filter(); }
private void filter() {
String query = filter.getText();
recordingTreeModel.filter(query);
if (query.trim().isEmpty()) {
expandAll(recordingTree, false);
recordingTree.expandPath(new TreePath(recordingTreeModel.getRoot()));
} else {
expandAll(recordingTree, true);
}
}
});
toolBar.addSeparator();
toolBar.add(Box.createHorizontalGlue());
toolBar.add(diskUsageProgressBar);
diskUsageProgressBar.setPreferredSize(new Dimension(250, 26));
diskUsageProgressBar.setMaximumSize(new Dimension(250, 26));
diskUsageProgressBar.setMinimum(0);
diskUsageProgressBar.setMaximum(100);
recordingManager.addObserver(new Observer() {
@Override
public void update(Observable o, Object arg) {
if (arg instanceof DiskStatus) {
try {
DiskStatus diskStatus = recordingManager.getDiskStatus();
diskUsageProgressBar.setValue(diskStatus.getUsage());
diskUsageProgressBar.setStringPainted(true);
diskUsageProgressBar.setString(diskStatus.toString());
//@formatter:off
String tooltip = "<html>"
+ SizeFormatter.format(diskStatus.getSpaceFreeInBytes()) + " "
+ LazyBones.getTranslation("free", "free")
+ "<br>"
+ diskStatus.getUsage() + "% "
+ LazyBones.getTranslation("used", "used")
+ "</html>";
//@formatter:on
diskUsageProgressBar.setToolTipText(tooltip);
} catch (Throwable t) {
logger.error("Couldn't determine disk usage", t);
}
}
}
});
return toolBar;
}
@Override
public void actionPerformed(ActionEvent e) {
Recording rec = null;
boolean itemSelected = false;
TreePath selection = recordingTree.getSelectionPath();
if (selection != null) {
itemSelected = true;
TreeNode treeNode = (TreeNode) selection.getLastPathComponent();
if (treeNode instanceof Recording) {
rec = (Recording) treeNode;
}
}
if ("DELETE".equals(e.getActionCommand()) && itemSelected) {
deleteRecording(rec, selection);
} else if ("RENAME".equals(e.getActionCommand()) && itemSelected) {
renameRecording(rec, selection);
} else if ("INFO".equals(e.getActionCommand()) && itemSelected) {
createRecordingDetailsDialog(rec);
} else if ("SYNC".equals(e.getActionCommand())) {
recordingManager.synchronize();
} else if ("PLAY".equals(e.getActionCommand()) && itemSelected) {
Player.play(rec);
} else if ("PLAY_ON_VDR".equals(e.getActionCommand()) && itemSelected) {
recordingManager.playOnVdr(rec);
} else if (e.getSource() == orderSelector) {
SortStrategy strategy = (SortStrategy) sortStrategySelector.getSelectedItem();
boolean ascending = !orderSelector.isSelected();
recordingTreeModel.sortBy(strategy.getComparator(), ascending);
orderSelector.setToolTipText(ascending ? getTranslation("sort.ascending", "ascending") : getTranslation("sort.descending", "descending"));
LazyBones.getProperties().setProperty("recording.order.ascending", Boolean.toString(ascending));
} else if (e.getSource() == expandAll) {
expandAll(recordingTree, true);
} else if (e.getSource() == collapseAll) {
expandAll(recordingTree, false);
}
}
private void deleteRecording(final Recording rec, final TreePath tp) {
if (tp.getLastPathComponent() instanceof Recording) {
blockUserInput(true);
final boolean hasFocus = recordingTree.hasFocus();
VDRCallback<DeleteRecordingAction> callback = new VDRCallback<DeleteRecordingAction>() {
@Override
public void receiveResponse(DeleteRecordingAction cmd, Response response) {
if (!cmd.isSuccess()) {
logger.error(cmd.getResponse().getMessage());
blockUserInput(false);
if (hasFocus) {
recordingTree.requestFocus();
}
} else {
// RecordingManager.getInstance().getRecordings().remove(rec);
// recordingTreeModel.remove(tp);
recordingManager.synchronize(new Runnable() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
blockUserInput(false);
if (hasFocus) {
recordingTree.requestFocus();
}
}
});
}
});
}
}
};
DeleteRecordingAction dra = new DeleteRecordingAction(recordingManager, rec, callback);
dra.enqueue();
}
}
private void renameRecording(final Recording rec, final TreePath tp) {
if (tp.getLastPathComponent() instanceof Recording) {
blockUserInput(true);
final boolean hasFocus = recordingTree.hasFocus();
Object newName = JOptionPane.showInputDialog(null, getTranslation("new_name", "New name?"), getTranslation("rename", "Rename"),
JOptionPane.QUESTION_MESSAGE, null, null, ((Recording)tp.getLastPathComponent()).getTitle());
if (newName == null) {
blockUserInput(false);
return;
}
VDRCallback<RenameRecordingAction> callback = new VDRCallback<RenameRecordingAction>() {
@Override
public void receiveResponse(RenameRecordingAction cmd, Response response) {
if (!cmd.isSuccess()) {
logger.error(cmd.getResponse().getMessage());
blockUserInput(false);
if (hasFocus) {
recordingTree.requestFocus();
}
} else {
recordingManager.synchronize(new Runnable() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
blockUserInput(false);
if (hasFocus) {
recordingTree.requestFocus();
}
}
});
}
});
}
}
};
RenameRecordingAction rra = new RenameRecordingAction(rec, (String) newName, callback);
rra.enqueue();
}
}
private void blockUserInput(boolean yesOrNo) {
scrollPane.setEnabled(!yesOrNo);
recordingTree.setEnabled(!yesOrNo);
buttonRemove.setEnabled(!yesOrNo);
buttonSync.setEnabled(!yesOrNo);
}
private void createRecordingDetailsDialog(Recording rec) {
VDRCallback<GetRecordingDetailsAction> callback = new VDRCallback<GetRecordingDetailsAction>() {
@Override
public void receiveResponse(GetRecordingDetailsAction cmd, Response response) {
if (cmd.isSuccess()) {
final JDialog dialog = new JDialog();
dialog.getContentPane().add(new RecordingDetailsPanel(cmd.getRecording()));
dialog.setSize(400, 300);
dialog.setLocation(LazyBones.getInstance().getMainDialog().getLocation());
dialog.setVisible(true);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
} else {
String mesg = LazyBones.getTranslation("error_retrieve_recording_details", "Couldn't load recording details from VDR: {0}",
response.getMessage());
logger.error(mesg);
}
}
};
GetRecordingDetailsAction grda = new GetRecordingDetailsAction(rec, callback);
grda.enqueue();
}
private void createContextMenu() {
JMenuItem menuDelete = new JMenuItem(Localizer.getLocalization(Localizer.I18N_DELETE));
menuDelete.addActionListener(this);
menuDelete.setActionCommand("DELETE");
menuDelete.setIcon(LazyBones.getInstance().createImageIcon("actions", "edit-delete", 16));
JMenuItem menuRename = new JMenuItem(getTranslation("rename", "Rename"));
menuRename.addActionListener(this);
menuRename.setActionCommand("RENAME");
menuRename.setIcon(LazyBones.getInstance().createImageIcon("action", "document-edit", 16));
JMenuItem menuInfo = new JMenuItem(getTranslation("recording_info", "Show information"));
menuInfo.addActionListener(this);
menuInfo.setActionCommand("INFO");
menuInfo.setIcon(LazyBones.getInstance().createImageIcon("actions", "edit-find", 16));
JMenu menuPlay = new JMenu(getTranslation("playback", "Playback"));
menuPlay.setIcon(LazyBones.getInstance().createImageIcon("actions", "media-playback-start", 16));
JMenuItem menuPlayLocal = new JMenuItem(getTranslation("playback.local", "Play"));
menuPlayLocal.addActionListener(this);
menuPlayLocal.setActionCommand("PLAY");
menuPlayLocal.setIcon(LazyBones.getInstance().createImageIcon("actions", "media-playback-start", 16));
JMenuItem menuPlayOnVdr = new JMenuItem(getTranslation("playback.vdr", "Play on VDR"));
menuPlayOnVdr.addActionListener(this);
menuPlayOnVdr.setActionCommand("PLAY_ON_VDR");
menuPlayOnVdr.setIcon(LazyBones.getInstance().createImageIcon("actions", "media-playback-start", 16));
menuPlay.add(menuPlayLocal);
menuPlay.add(menuPlayOnVdr);
JMenuItem menuSync = new JMenuItem(getTranslation("resync", "Synchronize with VDR"));
menuSync.addActionListener(this);
menuSync.setActionCommand("SYNC");
menuSync.setIcon(LazyBones.getInstance().createImageIcon("actions", "view-refresh", 16));
popup.add(menuInfo);
popup.add(menuPlay);
popup.add(menuDelete);
popup.add(menuRename);
popup.add(menuSync);
recordingTree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
mayTriggerPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
mayTriggerPopup(e);
}
private void mayTriggerPopup(MouseEvent e) {
if (recordingTree.isEnabled()) {
if (e.isPopupTrigger()) {
Point p = e.getPoint();
int selectedRow = recordingTree.getRowForLocation(p.x, p.y);
recordingTree.setSelectionRow(selectedRow);
TreeNode treeNode = (TreeNode) recordingTree.getSelectionPath().getLastPathComponent();
if (treeNode instanceof Recording) {
popup.setLocation(e.getPoint());
popup.show(e.getComponent(), e.getX(), e.getY());
}
}
}
}
});
recordingTree.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (e.getSource() == recordingTree) {
if (e.getKeyCode() == KeyEvent.VK_DELETE) {
TreePath selected = recordingTree.getSelectionPath();
if (selected != null) {
TreeNode treeNode = (TreeNode) selected.getLastPathComponent();
if (treeNode instanceof Folder) {
logger.warn("Deletion of folders is not supported.");
} else {
logger.info("Deleting recording [{}]", treeNode.getDisplayTitle());
deleteRecording((Recording) treeNode, selected);
}
}
}
}
}
});
}
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
SortStrategy strategy = (SortStrategy) e.getItem();
boolean ascending = !orderSelector.isSelected();
recordingTreeModel.sortBy(strategy.getComparator(), ascending);
LazyBones.getProperties().setProperty("recording.order.strategy", Integer.toString(sortStrategySelector.getSelectedIndex()));
}
}
private class SortStrategy {
private final Comparator<Recording> comparator;
private final String description;
public SortStrategy(Comparator<Recording> comparator, String description) {
super();
this.comparator = comparator;
this.description = description;
}
public Comparator<Recording> getComparator() {
return comparator;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return getDescription();
}
}
public void expandAll(JTree tree, boolean expand) {
TreeNode root = (TreeNode)tree.getModel().getRoot();
// Traverse tree from root
expandAll(tree, new TreePath(root), expand);
// reopen the root node, after collapse all
if (!expand) {
tree.expandPath(new TreePath(root));
}
}
private void expandAll(JTree tree, TreePath parent, boolean expand) {
// Traverse children
TreeNode node = (TreeNode) parent.getLastPathComponent();
if (node instanceof Folder) {
Iterator<TreeNode> iter = ((Folder)node).getChildren().iterator();
while (iter.hasNext()) {
TreeNode n = iter.next();
TreePath path = parent.pathByAddingChild(n);
expandAll(tree, path, expand);
}
}
// Expansion or collapse must be done bottom-up
if (expand) {
tree.expandPath(parent);
} else {
tree.collapsePath(parent);
}
}
}