com.frostwire.gui.library.LibraryFilesTableMediator.java Source code

Java tutorial

Introduction

Here is the source code for com.frostwire.gui.library.LibraryFilesTableMediator.java

Source

/*
 * Created by Angel Leon (@gubatron), Alden Torres (aldenml)
 * Copyright (c) 2011-2014, FrostWire(R). 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package com.frostwire.gui.library;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import javax.swing.*;
import javax.swing.event.MouseInputListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import com.frostwire.bittorrent.BTDownload;
import com.frostwire.gui.bittorrent.*;
import org.apache.commons.io.FilenameUtils;
import org.limewire.util.FileUtils;
import org.limewire.util.OSUtils;

import com.frostwire.alexandria.Playlist;
import com.frostwire.gui.Librarian;
import com.frostwire.gui.player.MediaPlayer;
import com.frostwire.gui.player.MediaSource;
import com.frostwire.gui.theme.SkinMenu;
import com.frostwire.gui.theme.SkinMenuItem;
import com.frostwire.gui.theme.SkinPopupMenu;
import com.frostwire.torrent.PaymentOptions;
import com.frostwire.util.MP4Muxer;
import com.frostwire.uxstats.UXAction;
import com.frostwire.uxstats.UXStats;
import com.limegroup.gnutella.MediaType;
import com.limegroup.gnutella.gui.ButtonRow;
import com.limegroup.gnutella.gui.CheckBoxList;
import com.limegroup.gnutella.gui.CheckBoxListPanel;
import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.gui.I18n;
import com.limegroup.gnutella.gui.IconManager;
import com.limegroup.gnutella.gui.MPlayerMediator;
import com.limegroup.gnutella.gui.MessageService;
import com.limegroup.gnutella.gui.MultiLineLabel;
import com.limegroup.gnutella.gui.PaddedPanel;
import com.limegroup.gnutella.gui.iTunesMediator;
import com.limegroup.gnutella.gui.actions.AbstractAction;
import com.limegroup.gnutella.gui.actions.LimeAction;
import com.limegroup.gnutella.gui.actions.SearchAction;
import com.limegroup.gnutella.gui.search.GenericCellEditor;
import com.limegroup.gnutella.gui.tables.LimeJTable;
import com.limegroup.gnutella.gui.util.BackgroundExecutorService;
import com.limegroup.gnutella.gui.util.GUILauncher;
import com.limegroup.gnutella.gui.util.GUILauncher.LaunchableProvider;
import com.limegroup.gnutella.util.QueryUtils;

/**
 * This class wraps the JTable that displays files in the library,
 * controlling access to the table and the various table properties.
 * It is the Mediator to the Table part of the Library display.
 *
 * @author gubatron
 * @author aldenml
 *
 */
final class LibraryFilesTableMediator
        extends AbstractLibraryTableMediator<LibraryFilesTableModel, LibraryFilesTableDataLine, File> {

    private static final FileShareCellRenderer FILE_SHARE_CELL_RENDERER = new FileShareCellRenderer();

    /**
     * Variables so the PopupMenu & ButtonRow can have the same listeners
     */
    private Action LAUNCH_ACTION;
    private Action LAUNCH_OS_ACTION;
    private Action OPEN_IN_FOLDER_ACTION;
    private Action DEMUX_MP4_AUDIO_ACTION;
    private Action CREATE_TORRENT_ACTION;
    private Action DELETE_ACTION;
    private Action RENAME_ACTION;
    private Action SEND_TO_ITUNES_ACTION;
    private Action WIFI_UNSHARE_ACTION;
    private Action WIFI_SHARE_ACTION;

    /**
     * instance, for singleton access
     */
    private static LibraryFilesTableMediator INSTANCE;

    public static LibraryFilesTableMediator instance() {
        if (INSTANCE == null) {
            INSTANCE = new LibraryFilesTableMediator();
        }
        return INSTANCE;
    }

    /**
     * Build some extra listeners
     */
    protected void buildListeners() {
        super.buildListeners();
        LAUNCH_ACTION = new LaunchAction();
        LAUNCH_OS_ACTION = new LaunchOSAction();
        OPEN_IN_FOLDER_ACTION = new OpenInFolderAction();
        DEMUX_MP4_AUDIO_ACTION = new DemuxMP4AudioAction();
        CREATE_TORRENT_ACTION = new CreateTorrentAction();
        DELETE_ACTION = new RemoveAction();
        RENAME_ACTION = new RenameAction();
        SEND_TO_ITUNES_ACTION = new SendAudioFilesToiTunes();
        WIFI_SHARE_ACTION = new WiFiShareAction(true);
        WIFI_UNSHARE_ACTION = new WiFiShareAction(false);
    }

    @Override
    protected void addListeners() {
        super.addListeners();

        TABLE.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (LibraryUtils.isRefreshKeyEvent(e)) {
                    LibraryMediator.instance().getLibraryExplorer().refreshSelection(true);
                }
            }
        });

    }

    /**
     * Set up the constants
     */
    protected void setupConstants() {
        super.setupConstants();
        MAIN_PANEL = new PaddedPanel();
        DATA_MODEL = new LibraryFilesTableModel();

        //sort by modification time in descending order by default
        //so user can quickly find newest files.
        DATA_MODEL.sort(LibraryFilesTableDataLine.MODIFICATION_TIME_IDX);
        DATA_MODEL.sort(LibraryFilesTableDataLine.MODIFICATION_TIME_IDX);

        TABLE = new LimeJTable(DATA_MODEL);
        DATA_MODEL.setTable(TABLE);
        Action[] aa = new Action[] { LAUNCH_ACTION, OPEN_IN_FOLDER_ACTION, SEND_TO_FRIEND_ACTION, DELETE_ACTION,
                OPTIONS_ACTION };

        BUTTON_ROW = new ButtonRow(aa, ButtonRow.X_AXIS, ButtonRow.NO_GLUE);
    }

    // inherit doc comment
    protected JPopupMenu createPopupMenu() {
        if (TABLE.getSelectionModel().isSelectionEmpty()) {
            return null;
        }

        JPopupMenu menu = new SkinPopupMenu();

        menu.add(new SkinMenuItem(LAUNCH_ACTION));
        if (getMediaType().equals(MediaType.getAudioMediaType())) {
            menu.add(new SkinMenuItem(LAUNCH_OS_ACTION));
        }
        if (hasExploreAction()) {
            menu.add(new SkinMenuItem(OPEN_IN_FOLDER_ACTION));
        }

        if (areAllSelectedFilesMP4s()) {
            menu.add(DEMUX_MP4_AUDIO_ACTION);
            DEMUX_MP4_AUDIO_ACTION.setEnabled(!((DemuxMP4AudioAction) DEMUX_MP4_AUDIO_ACTION).isDemuxing());
        }

        menu.add(new SkinMenuItem(CREATE_TORRENT_ACTION));

        if (areAllSelectedFilesPlayable()) {
            menu.add(createAddToPlaylistSubMenu());
        }

        //sharing takes a while...
        //there's an in between state while the file is changing from
        //unshare to shared, this is "being shared"
        boolean noneSharing = !isAnyBeingShared();
        boolean allShared = areAllSelectedFilesShared();

        WIFI_SHARE_ACTION.setEnabled(noneSharing && !allShared);

        //unsharing is immediate.
        WIFI_UNSHARE_ACTION.setEnabled(noneSharing && allShared);

        //menu.add(new SkinMenuItem(areAllSelectedFilesShared() ? WIFI_UNSHARE_ACTION : WIFI_SHARE_ACTION));
        if (WIFI_SHARE_ACTION.isEnabled()) {
            menu.add(WIFI_SHARE_ACTION);
        }

        if (WIFI_UNSHARE_ACTION.isEnabled()) {
            menu.add(WIFI_UNSHARE_ACTION);
        }

        menu.add(new SkinMenuItem(SEND_TO_FRIEND_ACTION));
        menu.add(new SkinMenuItem(SEND_TO_ITUNES_ACTION));

        menu.addSeparator();
        menu.add(new SkinMenuItem(DELETE_ACTION));
        menu.add(new SkinMenuItem(RENAME_ACTION));
        menu.addSeparator();

        int[] rows = TABLE.getSelectedRows();
        boolean dirSelected = false;
        boolean fileSelected = false;

        for (int i = 0; i < rows.length; i++) {
            File f = DATA_MODEL.get(rows[i]).getFile();
            if (f.isDirectory()) {
                dirSelected = true;
                //            if (IncompleteFileManager.isTorrentFolder(f))
                //               torrentSelected = true;
            } else
                fileSelected = true;

            if (dirSelected && fileSelected)
                break;
        }
        if (dirSelected) {
            DELETE_ACTION.setEnabled(true);
            RENAME_ACTION.setEnabled(false);
        } else {
            DELETE_ACTION.setEnabled(true);
        }

        LibraryFilesTableDataLine line = DATA_MODEL.get(rows[0]);
        menu.add(createSearchSubMenu(line));

        return menu;
    }

    /**
     * If a file in the current selection is being shared, this returns true.
     * @return
     */
    private boolean isAnyBeingShared() {
        boolean oneBeingShared = false;
        int[] selectedRows = TABLE.getSelectedRows();
        for (int i : selectedRows) {
            LibraryFilesTableDataLine libraryFilesTableDataLine = DATA_MODEL.get(i);

            if (Librarian.instance().getFileShareState(libraryFilesTableDataLine.getInitializeObject()
                    .getAbsolutePath()) == Librarian.FILE_STATE_SHARING) {
                oneBeingShared = true;
                break;
            }
        }
        return oneBeingShared;
    }

    private boolean areAllSelectedFilesShared() {
        boolean allAreShared = true;
        int[] selectedRows = TABLE.getSelectedRows();
        for (int i : selectedRows) {
            LibraryFilesTableDataLine libraryFilesTableDataLine = DATA_MODEL.get(i);

            if (Librarian.instance().getFileShareState(libraryFilesTableDataLine.getInitializeObject()
                    .getAbsolutePath()) != Librarian.FILE_STATE_SHARED) {
                allAreShared = false;
                break;
            }
        }
        return allAreShared;
    }

    private boolean areAllSelectedFilesMP4s() {
        boolean selectionIsAllMP4 = true;
        int[] selectedRows = TABLE.getSelectedRows();
        for (int i : selectedRows) {
            if (!DATA_MODEL.getFile(i).getAbsolutePath().toLowerCase().endsWith("mp4")) {
                selectionIsAllMP4 = false;
                break;
            }
        }

        return selectionIsAllMP4;
    }

    private boolean areAllSelectedFilesPlayable() {
        boolean selectionIsAllAudio = true;
        int[] selectedRows = TABLE.getSelectedRows();
        for (int i : selectedRows) {
            if (!MediaPlayer.isPlayableFile(DATA_MODEL.get(i).getInitializeObject())) {
                selectionIsAllAudio = false;
                break;
            }
        }
        return selectionIsAllAudio;
    }

    private JMenu createSearchSubMenu(LibraryFilesTableDataLine dl) {
        SkinMenu menu = new SkinMenu(I18n.tr("Search"));

        if (dl != null) {
            File f = dl.getInitializeObject();
            String keywords = QueryUtils.createQueryString(f.getName());
            if (keywords.length() > 0)
                menu.add(new SkinMenuItem(new SearchAction(keywords)));
        }

        if (menu.getItemCount() == 0)
            menu.setEnabled(false);

        return menu;
    }

    /**
     * Upgrade getScrolledTablePane to public access.
     */
    public JComponent getScrolledTablePane() {
        return super.getScrolledTablePane();
    }

    /* Don't display anything for this.  The LibraryMediator will do it. */
    protected void updateSplashScreen() {
    }

    /**
     * Note: This is set up for this to work.
     * Polling is not needed though, because updates
     * already generate update events.
     */
    private LibraryFilesTableMediator() {
        super("LIBRARY_FILES_TABLE");
    }

    /**
     * Sets up drag & drop for the table.
     */
    protected void setupDragAndDrop() {
        TABLE.setDragEnabled(true);
        TABLE.setTransferHandler(new LibraryFilesTableTransferHandler(this));
    }

    @Override
    protected void setDefaultRenderers() {
        super.setDefaultRenderers();
        TABLE.setDefaultRenderer(PlayableIconCell.class, new PlayableIconCellRenderer());
        TABLE.setDefaultRenderer(PlayableCell.class, new PlayableCellRenderer());
        TABLE.setDefaultRenderer(FileShareCell.class, FILE_SHARE_CELL_RENDERER);
        TABLE.setDefaultRenderer(PaymentOptions.class, new PaymentOptionsRenderer());
    }

    /**
     * Sets the default editors.
     */
    protected void setDefaultEditors() {
        TableColumnModel model = TABLE.getColumnModel();

        TableColumn tc = model.getColumn(LibraryFilesTableDataLine.SHARE_IDX);
        tc.setCellEditor(new FileShareCellEditor(FILE_SHARE_CELL_RENDERER));

        tc = model.getColumn(LibraryFilesTableDataLine.ACTIONS_IDX);
        tc.setCellEditor(new GenericCellEditor(getAbstractActionsRenderer()));

        tc = model.getColumn(LibraryFilesTableDataLine.PAYMENT_OPTIONS_IDX);
        tc.setCellEditor(new GenericCellEditor(new PaymentOptionsRenderer()));
    }

    /**
     * Cancels all editing of fields in the tree and table.
     */
    void cancelEditing() {
        if (TABLE.isEditing()) {
            TableCellEditor editor = TABLE.getCellEditor();
            editor.cancelCellEditing();
        }
    }

    /**
     * Adds the mouse listeners to the wrapped <tt>JTable</tt>.
     *
     * @param listener the <tt>MouseInputListener</tt> that handles mouse events
     *                 for the library
     */
    void addMouseInputListener(final MouseInputListener listener) {
        TABLE.addMouseListener(listener);
        TABLE.addMouseMotionListener(listener);
    }

    /**
     * Updates the Table based on the selection of the given table.
     * Perform lookups to remove any store files from the shared folder
     * view and to only display store files in the store view
     */
    void updateTableFiles(DirectoryHolder dirHolder) {
        if (dirHolder == null)
            return;
        if (dirHolder instanceof MediaTypeSavedFilesDirectoryHolder) {
            setMediaType(((MediaTypeSavedFilesDirectoryHolder) dirHolder).getMediaType());
        } else {
            setMediaType(MediaType.getAnyTypeMediaType());
        }
        clearTable();

        List<List<File>> partitionedFiles = split(100, Arrays.asList(dirHolder.getFiles()));

        for (List<File> partition : partitionedFiles) {
            final List<File> fPartition = partition;

            BackgroundExecutorService.schedule(new Runnable() {

                @Override
                public void run() {
                    for (final File file : fPartition) {
                        GUIMediator.safeInvokeLater(new Runnable() {
                            public void run() {
                                addUnsorted(file);
                            }
                        });
                    }

                    GUIMediator.safeInvokeLater(new Runnable() {
                        public void run() {
                            LibraryMediator.instance().getLibrarySearch().addResults(fPartition.size());
                        }
                    });

                }
            });

        }

        forceResort();
    }

    /**
     * Returns the <tt>File</tt> stored at the specified row in the list.
     *
     * @param row the row of the desired <tt>File</tt> instance in the
     *            list
     *
     * @return a <tt>File</tt> instance associated with the specified row
     *         in the table
     */
    File getFile(int row) {
        return DATA_MODEL.getFile(row);
    }

    /**
     * Accessor for the table that this class wraps.
     *
     * @return The <tt>JTable</tt> instance used by the library.
     */
    JTable getTable() {
        return TABLE;
    }

    ButtonRow getButtonRow() {
        return BUTTON_ROW;
    }

    /**
     * Accessor for the <tt>ListSelectionModel</tt> for the wrapped
     * <tt>JTable</tt> instance.
     */
    ListSelectionModel getSelectionModel() {
        return TABLE.getSelectionModel();
    }

    /**
     * Programatically starts a rename of the selected item.
     */
    void startRename() {
        int row = TABLE.getSelectedRow();
        if (row == -1)
            return;
        //int viewIdx = TABLE.convertColumnIndexToView(LibraryFilesTableDataLine.NAME_IDX);
        //TABLE.editCellAt(row, viewIdx, LibraryTableCellEditor.EVENT);
    }

    /**
     * Shows the license window.
     */
    void showLicenseWindow() {
        //        LibraryTableDataLine ldl = DATA_MODEL.get(TABLE.getSelectedRow());
        //        if(ldl == null)
        //            return;
        //        FileDesc fd = ldl.getFileDesc();
        //        License license = fd.getLicense();
        //        URN urn = fd.getSHA1Urn();
        //        LimeXMLDocument doc = ldl.getXMLDocument();
        //        LicenseWindow window = LicenseWindow.create(license, urn, doc, this);
        //        GUIUtils.centerOnScreen(window);
        //        window.setVisible(true);
    }

    /**
     * Returns the options offered to the user when removing files.
     *
     * Depending on the platform these can be a subset of
     * MOVE_TO_TRASH, DELETE, CANCEL.
     */
    private static Object[] createRemoveOptions() {
        if (OSUtils.supportsTrash()) {
            String trashLabel = OSUtils.isWindows() ? I18n.tr("Move to Recycle Bin") : I18n.tr("Move to Trash");
            return new Object[] { trashLabel, I18n.tr("Delete"), I18n.tr("Cancel") };
        } else {
            return new Object[] { I18n.tr("Delete"), I18n.tr("Cancel") };
        }
    }

    public List<MediaSource> getFilesView() {
        int size = DATA_MODEL.getRowCount();
        List<MediaSource> result = new ArrayList<MediaSource>(size);
        for (int i = 0; i < size; i++) {
            try {
                File file = DATA_MODEL.get(i).getFile();
                if (MediaPlayer.isPlayableFile(file)) {
                    result.add(new MediaSource(DATA_MODEL.get(i).getFile()));
                }
            } catch (Exception e) {
                return Collections.emptyList();
            }
        }
        return result;
    }

    /**
     * Override the default removal so we can actually stop sharing
     * and delete the file.
     * Deletes the selected rows in the table.
     * CAUTION: THIS WILL DELETE THE FILE FROM THE DISK.
     */
    public void removeSelection() {
        int[] rows = TABLE.getSelectedRows();
        if (rows.length == 0)
            return;

        if (TABLE.isEditing()) {
            TableCellEditor editor = TABLE.getCellEditor();
            editor.cancelCellEditing();
        }

        List<File> files = new ArrayList<File>(rows.length);

        // sort row indices and go backwards so list indices don't change when
        // removing the files from the model list
        Arrays.sort(rows);
        for (int i = rows.length - 1; i >= 0; i--) {
            File file = DATA_MODEL.getFile(rows[i]);
            files.add(file);
        }

        CheckBoxListPanel<File> listPanel = new CheckBoxListPanel<File>(files, new FileTextProvider(), true);
        listPanel.getList().setVisibleRowCount(4);

        // display list of files that should be deleted
        Object[] message = new Object[] { new MultiLineLabel(I18n.tr(
                "Are you sure you want to delete the selected file(s), thus removing it from your computer?"), 400),
                Box.createVerticalStrut(ButtonRow.BUTTON_SEP), listPanel,
                Box.createVerticalStrut(ButtonRow.BUTTON_SEP) };

        // get platform dependent options which are displayed as buttons in the dialog
        Object[] removeOptions = createRemoveOptions();

        int option = JOptionPane.showOptionDialog(MessageService.getParentComponent(), message, I18n.tr("Message"),
                JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, removeOptions,
                removeOptions[0] /* default option */);

        if (option == removeOptions.length - 1 /* "cancel" option index */
                || option == JOptionPane.CLOSED_OPTION) {
            return;
        }

        // remove still selected files
        List<File> selected = listPanel.getSelectedElements();
        List<String> undeletedFileNames = new ArrayList<String>();

        for (File file : selected) {
            // stop seeding if seeding
            BittorrentDownload dm = null;
            if ((dm = TorrentUtil.getDownloadManager(file)) != null) {
                dm.setDeleteDataWhenRemove(false);
                dm.setDeleteTorrentWhenRemove(false);
                BTDownloadMediator.instance().remove(dm);
            }

            // close media player if still playing
            if (MediaPlayer.instance().isThisBeingPlayed(file)) {
                MediaPlayer.instance().stop();
                MPlayerMediator.instance().showPlayerWindow(false);
            }

            // removeOptions > 2 => OS offers trash options
            boolean removed = FileUtils.delete(file,
                    removeOptions.length > 2 && option == 0 /* "move to trash" option index */);
            if (removed) {

                DATA_MODEL.remove(DATA_MODEL.getRow(file));
            } else {
                undeletedFileNames.add(getCompleteFileName(file));
            }
        }

        clearSelection();

        if (undeletedFileNames.isEmpty()) {
            return;
        }

        // display list of files that could not be deleted
        message = new Object[] { new MultiLineLabel(I18n.tr(
                "The following files could not be deleted. They may be in use by another application or are currently being downloaded to."),
                400), Box.createVerticalStrut(ButtonRow.BUTTON_SEP),
                new JScrollPane(createFileList(undeletedFileNames)) };

        JOptionPane.showMessageDialog(MessageService.getParentComponent(), message, I18n.tr("Error"),
                JOptionPane.ERROR_MESSAGE);

        super.removeSelection();
    }

    /**
     * Creates a JList of files and sets and makes it non-selectable.
     */
    private static JList<String> createFileList(List<String> fileNames) {
        JList<String> fileList = new JList<String>(fileNames.toArray(new String[0]));
        fileList.setVisibleRowCount(5);
        fileList.setCellRenderer(new FileNameListCellRenderer());
        //fileList.setSelectionForeground(fileList.getForeground());
        //fileList.setSelectionBackground(fileList.getBackground());
        fileList.setFocusable(false);
        return fileList;
    }

    /**
     * Returns the human readable file name for incomplete files or
     * just the regular file name otherwise.
     */
    private String getCompleteFileName(File file) {
        return file.getName();
    }

    public void handleActionKey() {
        LibraryFilesTableDataLine line = DATA_MODEL.get(TABLE.getSelectedRow());
        if (line == null || line.getFile() == null) {
            return;
        }
        if (getMediaType().equals(MediaType.getAudioMediaType()) && MediaPlayer.isPlayableFile(line.getFile())) {
            MediaPlayer.instance().asyncLoadMedia(new MediaSource(line.getFile()), true, true, null,
                    getFilesView());
            UXStats.instance().log(UXAction.LIBRARY_PLAY_AUDIO_FROM_FILE);
            return;
        }

        int[] rows = TABLE.getSelectedRows();
        //LibraryTableModel ltm = DATA_MODEL;
        //File file;
        for (int i = 0; i < rows.length; i++) {
            //file = ltm.getFile(rows[i]);
            // if it's a directory try to select it in the library tree
            // if it could be selected return
            //         if (file.isDirectory()
            //            && LibraryMediator.setSelectedDirectory(file))
            //            return;
        }

        launch(true);
    }

    /**
     * Launches the associated applications for each selected file
     * in the library if it can.
     */
    void launch(boolean playAudio) {
        int[] rows = TABLE.getSelectedRows();
        if (rows.length == 0) {
            return;
        }

        File selectedFile = DATA_MODEL.getFile(rows[0]);

        if (OSUtils.isWindows()) {
            if (selectedFile.isDirectory()) {
                GUIMediator.launchExplorer(selectedFile);
                return;
            } else if (!MediaPlayer.isPlayableFile(selectedFile)) {
                String extension = FilenameUtils.getExtension(selectedFile.getName());
                if (extension != null && extension.equals("torrent")) {
                    GUIMediator.instance().openTorrentFile(selectedFile, true);
                } else {
                    GUIMediator.launchFile(selectedFile);
                }
                return;
            }

        }

        LaunchableProvider[] providers = new LaunchableProvider[rows.length];
        boolean stopAudio = false;
        for (int i = 0; i < rows.length; i++) {
            try {
                MediaType mt = MediaType.getMediaTypeForExtension(
                        FilenameUtils.getExtension(DATA_MODEL.getFile(rows[i]).getName()));
                if (mt.equals(MediaType.getVideoMediaType())) {
                    stopAudio = true;
                }
            } catch (Throwable e) {
                // ignore
            }
            providers[i] = new FileProvider(DATA_MODEL.getFile(rows[i]));
        }
        if (stopAudio || !playAudio) {
            MediaPlayer.instance().stop();
        }

        if (playAudio) {
            GUILauncher.launch(providers);
            UXStats.instance().log(stopAudio ? UXAction.LIBRARY_VIDEO_PLAY : UXAction.LIBRARY_PLAY_AUDIO_FROM_FILE);
        } else {
            GUIMediator.launchFile(selectedFile);
        }
    }

    /**
     * Handles the selection rows in the library window,
     * enabling or disabling buttons and chat menu items depending on
     * the values in the selected rows.
     *
     * @param row the index of the first row that is selected
     */
    public void handleSelection(int row) {
        int[] sel = TABLE.getSelectedRows();
        if (sel.length == 0) {
            handleNoSelection();
            return;
        }

        File selectedFile = getFile(sel[0]);

        //  always turn on Launch, Delete, Magnet Lookup, Bitzi Lookup
        LAUNCH_ACTION.setEnabled(true);
        LAUNCH_OS_ACTION.setEnabled(true);
        DELETE_ACTION.setEnabled(true);

        if (selectedFile != null && !selectedFile.getName().endsWith(".torrent")) {
            CREATE_TORRENT_ACTION.setEnabled(sel.length == 1);
        }

        if (selectedFile != null) {
            SEND_TO_FRIEND_ACTION.setEnabled(sel.length == 1);

            if (getMediaType().equals(MediaType.getAnyTypeMediaType())) {
                boolean atLeastOneIsPlayable = false;

                for (int i : sel) {
                    File f = getFile(i);
                    if (MediaPlayer.isPlayableFile(f) || hasExtension(f.getAbsolutePath(), "mp4")) {
                        atLeastOneIsPlayable = true;
                        break;
                    }
                }

                SEND_TO_ITUNES_ACTION.setEnabled(atLeastOneIsPlayable);
            } else {
                SEND_TO_ITUNES_ACTION.setEnabled(getMediaType().equals(MediaType.getAudioMediaType())
                        || hasExtension(selectedFile.getAbsolutePath(), "mp4"));
            }
        }

        if (sel.length == 1 && selectedFile.isFile() && selectedFile.getParentFile() != null) {
            OPEN_IN_FOLDER_ACTION.setEnabled(true);
        } else {
            OPEN_IN_FOLDER_ACTION.setEnabled(false);
        }

        if (sel.length == 1) {
            LibraryMediator.instance().getLibraryCoverArt().setFile(selectedFile);
        }

        boolean anyBeingShared = isAnyBeingShared();
        WIFI_SHARE_ACTION.setEnabled(!anyBeingShared);
        WIFI_UNSHARE_ACTION.setEnabled(!anyBeingShared);
    }

    /**
     * Handles the deselection of all rows in the library table,
     * disabling all necessary buttons and menu items.
     */
    public void handleNoSelection() {
        LAUNCH_ACTION.setEnabled(false);
        LAUNCH_OS_ACTION.setEnabled(false);
        OPEN_IN_FOLDER_ACTION.setEnabled(false);
        SEND_TO_FRIEND_ACTION.setEnabled(false);
        CREATE_TORRENT_ACTION.setEnabled(false);
        DELETE_ACTION.setEnabled(false);
        RENAME_ACTION.setEnabled(false);
        SEND_TO_ITUNES_ACTION.setEnabled(false);
    }

    /**
     * Refreshes the enabledness of the Enqueue button based
     * on the player enabling state.
     */
    public void setPlayerEnabled(boolean value) {
        handleSelection(TABLE.getSelectedRow());
    }

    public boolean setFileSelected(File file) {
        int i = DATA_MODEL.getRow(file);
        if (i != -1) {
            TABLE.setSelectedRow(i);
            TABLE.ensureSelectionVisible();
            return true;
        }
        return false;
    }

    private boolean hasExploreAction() {
        return OSUtils.isWindows() || OSUtils.isMacOSX();
    }

    /**
     * Split a collection in Lists of up to partitionSize elements.
     * @param <T>
     * @param partitionSize
     * @param collection
     * @return
     */
    public static <T> List<List<T>> split(int partitionSize, List<T> collection) {
        List<List<T>> lists = new LinkedList<List<T>>();

        for (int i = 0; i < collection.size(); i += partitionSize) {
            //the compiler might not know if the collection has changed size
            //so it might not optimize this by itself.
            int jLimit = Math.min(collection.size(), i + partitionSize);
            List<T> newList = new LinkedList<T>();
            for (int j = i; j < jLimit; j++) {
                newList.add(collection.get(j));
            }
            lists.add(newList);
        }

        return lists;
    }

    ///////////////////////////////////////////////////////
    //  ACTIONS
    ///////////////////////////////////////////////////////

    private final class LaunchAction extends AbstractAction {

        /**
         *
         */
        private static final long serialVersionUID = 949208465372392591L;

        public LaunchAction() {
            putValue(Action.NAME, I18n.tr("Launch"));
            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Launch Selected Files"));
            putValue(LimeAction.ICON_NAME, "LIBRARY_LAUNCH");
        }

        public void actionPerformed(ActionEvent ae) {
            launch(true);
        }
    }

    private final class LaunchOSAction extends AbstractAction {

        /**
         *
         */
        private static final long serialVersionUID = 949208465372392592L;

        public LaunchOSAction() {
            String os = "OS";
            if (OSUtils.isWindows()) {
                os = "Windows";
            } else if (OSUtils.isMacOSX()) {
                os = "Mac";
            } else if (OSUtils.isLinux()) {
                os = "Linux";
            }
            putValue(Action.NAME, I18n.tr("Launch in") + " " + os);
            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Launch Selected Files in") + " " + os);
            putValue(LimeAction.ICON_NAME, "LIBRARY_LAUNCH");
        }

        public void actionPerformed(ActionEvent ae) {
            launch(false);
        }
    }

    private final class OpenInFolderAction extends AbstractAction {

        /**
         *
         */
        private static final long serialVersionUID = 1693310684299300459L;

        public OpenInFolderAction() {
            putValue(Action.NAME, I18n.tr("Explore"));
            putValue(LimeAction.SHORT_NAME, I18n.tr("Explore"));
            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Open Folder Containing the File"));
            putValue(LimeAction.ICON_NAME, "LIBRARY_EXPLORE");
        }

        public void actionPerformed(ActionEvent ae) {
            int[] sel = TABLE.getSelectedRows();
            if (sel.length == 0) {
                return;
            }

            File selectedFile = getFile(sel[0]);
            if (selectedFile.isFile() && selectedFile.getParentFile() != null) {
                GUIMediator.launchExplorer(selectedFile);
            }
        }
    }

    private final class CreateTorrentAction extends AbstractAction {

        private static final long serialVersionUID = 1898917632888388860L;

        public CreateTorrentAction() {
            super(I18n.tr("Create New Torrent"));
            putValue(Action.LONG_DESCRIPTION, I18n.tr("Create a new .torrent file"));
        }

        @Override
        public void actionPerformed(ActionEvent arg0) {
            File selectedFile = DATA_MODEL.getFile(TABLE.getSelectedRow());

            //can't create torrents out of empty folders.
            if (selectedFile.isDirectory() && selectedFile.listFiles().length == 0) {
                JOptionPane.showMessageDialog(null, I18n.tr("The folder you selected is empty."),
                        I18n.tr("Invalid Folder"), JOptionPane.ERROR_MESSAGE);
                return;
            }

            //can't create torrents if the folder/file can't be read
            if (!selectedFile.canRead()) {
                JOptionPane.showMessageDialog(null, I18n.tr("Error: You can't read on that file/folder."),
                        I18n.tr("Error"), JOptionPane.ERROR_MESSAGE);
                return;
            }

            CreateTorrentDialog dlg = new CreateTorrentDialog(GUIMediator.getAppFrame());
            dlg.setChosenContent(selectedFile,
                    selectedFile.isFile() ? JFileChooser.FILES_ONLY : JFileChooser.DIRECTORIES_ONLY);
            dlg.setVisible(true);

        }
    }

    private final class RemoveAction extends AbstractAction {

        /**
         *
         */
        private static final long serialVersionUID = -8704093935791256631L;

        public RemoveAction() {
            putValue(Action.NAME, I18n.tr("Delete"));
            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Delete Selected Files"));
            putValue(LimeAction.ICON_NAME, "LIBRARY_DELETE");
        }

        public void actionPerformed(ActionEvent ae) {
            REMOVE_LISTENER.actionPerformed(ae);
        }
    }

    private final class RenameAction extends AbstractAction {

        /**
         *
         */
        private static final long serialVersionUID = 2673219925804729384L;

        public RenameAction() {
            putValue(Action.NAME, I18n.tr("Rename"));
            //  "LIBRARY_RENAME"   ???
            //  "LIBRARY_RENAME_BUTTON_TIP"   ???
        }

        public void actionPerformed(ActionEvent ae) {
            startRename();
        }
    }

    private class SendAudioFilesToiTunes extends AbstractAction {

        private static final long serialVersionUID = 4726989286129406765L;

        public SendAudioFilesToiTunes() {
            if (!OSUtils.isLinux()) {
                putValue(Action.NAME, I18n.tr("Send to iTunes"));
                putValue(Action.SHORT_DESCRIPTION, I18n.tr("Send audio files to iTunes"));
            }
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int[] rows = TABLE.getSelectedRows();
            for (int i = 0; i < rows.length; i++) {
                int index = rows[i]; // current index to add
                File file = DATA_MODEL.getFile(index);

                iTunesMediator.instance().scanForSongs(file);
            }
        }
    }

    private class DemuxMP4AudioAction extends AbstractAction {

        private static final long serialVersionUID = 2994040746359495494L;
        private final ArrayList<File> demuxedFiles;

        private boolean isDemuxing = false;

        public DemuxMP4AudioAction() {
            putValue(Action.NAME, I18n.tr("Extract Audio"));
            putValue(Action.SHORT_DESCRIPTION, I18n.tr("Extract .m4a Audio from this .mp4 video"));
            demuxedFiles = new ArrayList<File>();
        }

        public boolean isDemuxing() {
            return isDemuxing;
        }

        private List<File> getSelectedFiles() {
            int[] rows = TABLE.getSelectedRows();
            List<File> files = new ArrayList<File>(rows.length);
            for (int i = 0; i < rows.length; i++) {
                int index = rows[i]; // current index to add
                File file = DATA_MODEL.getFile(index);
                files.add(file);
            }
            return files;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            final short videoCount = (short) TABLE.getSelectedRows().length;

            //can't happen, but just in case.
            if (videoCount < 1) {
                return;
            }

            //get selected files before we switch to audio and loose the selection
            final List<File> selectedFiles = getSelectedFiles();

            selectAudio();

            String status = I18n.tr("Extracting audio from " + videoCount + " selected videos...");
            if (videoCount == 1) {
                status = I18n.tr("Extracting audio from selected video...");
            }
            LibraryMediator.instance().getLibrarySearch().pushStatus(status);

            SwingWorker<Void, Void> demuxWorker = new SwingWorker<Void, Void>() {

                @Override
                protected Void doInBackground() throws Exception {
                    isDemuxing = true;
                    demuxFiles(selectedFiles);
                    isDemuxing = false;
                    return null;
                }

                @Override
                protected void done() {
                    int failed = videoCount - demuxedFiles.size();
                    String failedStr = (failed > 0) ? " (" + failed + " " + I18n.tr("failed") + ")" : "";
                    LibraryMediator.instance().getLibrarySearch()
                            .pushStatus(I18n.tr("Done extracting audio.") + failedStr);
                }

            };
            demuxWorker.execute();
        }

        private void selectAudio() {
            final LibraryExplorer explorer = LibraryMediator.instance().getLibraryExplorer();
            explorer.enqueueRunnable(new Runnable() {
                @Override
                public void run() {
                    explorer.selectAudio();
                }
            });
            explorer.executePendingRunnables();
        }

        private void demuxFiles(final List<File> files) {
            demuxedFiles.clear();
            for (final File file : files) {

                try {
                    System.out.println("Demuxing file " + file.getAbsolutePath());

                    String mp4 = file.getAbsolutePath();
                    String m4a = new File(file.getParentFile(), FilenameUtils.getBaseName(mp4) + ".m4a")
                            .getAbsolutePath();
                    try {
                        new MP4Muxer().demuxAudio(mp4, m4a, null);
                        demuxedFiles.add(new File(m4a));
                        updateDemuxingStatus(new File(m4a), files.size(), true);
                    } catch (Throwable e) {
                        updateDemuxingStatus(file, files.size(), false);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        private void updateDemuxingStatus(final File demuxed, final int totalDemuxed, final boolean demuxSuccess) {
            GUIMediator.safeInvokeAndWait(new Runnable() {
                @Override
                public void run() {
                    LibraryExplorer explorer = LibraryMediator.instance().getLibraryExplorer();
                    explorer.enqueueRunnable(new Runnable() {

                        @Override
                        public void run() {
                            if (demuxSuccess) {
                                add(demuxed, 0);
                                update(demuxed);
                                LibraryMediator.instance().getLibrarySearch()
                                        .pushStatus(I18n.tr("Finished") + " " + demuxedFiles.size() + " "
                                                + I18n.tr("out of") + " " + totalDemuxed + ". Extracting audio...");
                                System.out.println("Finished" + demuxedFiles.size() + " out of " + totalDemuxed
                                        + ". Extracting audio...");
                            } else {
                                LibraryMediator.instance().getLibrarySearch().pushStatus(
                                        I18n.tr("Could not extract audio from") + " " + demuxed.getName());
                            }
                        }

                    });
                    explorer.executePendingRunnables();
                }
            });
        }
    }

    /**
     * Sets an icon based on the filename extension.      */
    private static class FileNameListCellRenderer extends DefaultListCellRenderer {

        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {
            super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
            String extension = FilenameUtils.getExtension(value.toString());
            if (extension != null) {
                setIcon(IconManager.instance().getIconForExtension(extension));
            }
            return this;
        }
    }

    /**
     * Renders the file part of the Tuple<File, FileDesc> in CheckBoxList<Tuple<File, FileDesc>>.
     */
    private class FileTextProvider implements CheckBoxList.TextProvider<File> {

        public Icon getIcon(File obj) {
            String extension = FilenameUtils.getExtension(obj.getName());
            if (extension != null) {
                return IconManager.instance().getIconForExtension(extension);
            }
            return null;
        }

        public String getText(File obj) {
            return getCompleteFileName(obj);
        }

        public String getToolTipText(File obj) {
            return obj.getAbsolutePath();
        }

    }

    private static class FileProvider implements LaunchableProvider {

        private final File _file;

        public FileProvider(File file) {
            _file = file;
        }

        public File getFile() {
            return _file;
        }
    }

    @Override
    protected void sortAndMaintainSelection(int columnToSort) {
        super.sortAndMaintainSelection(columnToSort);
        resetAudioPlayerFileView();
    }

    public void resetAudioPlayerFileView() {
        Playlist playlist = MediaPlayer.instance().getCurrentPlaylist();
        if (playlist == null) {
            MediaPlayer.instance().setPlaylistFilesView(getFilesView());
        }
    }

    @Override
    protected MediaSource createMediaSource(LibraryFilesTableDataLine line) {
        if (MediaPlayer.isPlayableFile(line.getInitializeObject())) {
            return new MediaSource(line.getInitializeObject());
        } else {
            return null;
        }
    }

    private class WiFiShareAction extends AbstractAction {

        private static final long serialVersionUID = 1889199111839641873L;
        private boolean share;

        public WiFiShareAction(boolean share) {
            super(share ? I18n.tr("Share") : I18n.tr("Unshare"));
            this.share = share;
            String actionName = share ? I18n.tr("Share") : I18n.tr("Unshare");

            putValue(LimeAction.SHORT_NAME, actionName);
            putValue(Action.LONG_DESCRIPTION, actionName + " " + I18n.tr("file on local Wi-Fi network"));
            putValue(Action.SMALL_ICON, GUIMediator.getThemeImage(share ? "file_shared" : "file_unshared"));
            putValue(LimeAction.ICON_NAME, share ? "WIFI_SHARED" : "WIFI_UNSHARED");
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int[] rows = TABLE.getSelectedRows();
            for (int i = 0; i < rows.length; i++) {
                int index = rows[i]; // current index to add
                File file = DATA_MODEL.getFile(index);
                LibraryFilesTableDataLine dataLine = DATA_MODEL.get(i);
                try {
                    //this is so that we avoid re-sharing what's already shared.
                    //we nest this logic for clarity.
                    if (share) {
                        if (!Librarian.instance().isFileShared(file.getAbsolutePath())) {
                            actualShare(dataLine, file);
                        }
                    }
                    //this happens only when.
                    else {
                        actualShare(dataLine, file);
                    }

                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            LibraryMediator.instance().getDeviceDiscoveryClerk().updateLocalPeer();
            UXStats.instance().log(share ? UXAction.WIFI_SHARING_SHARED : UXAction.WIFI_SHARING_UNSHARED);
        }

        private void actualShare(LibraryFilesTableDataLine dataLine, File file) {
            dataLine.setShared(share);
            Librarian.instance().shareFile(file.getAbsolutePath(), share, false);
        }
    }

    public static boolean hasExtension(String filename, String... extensionsWithoutDot) {

        String extension = FilenameUtils.getExtension(filename).toLowerCase();

        for (String ext : extensionsWithoutDot) {
            if (ext.equalsIgnoreCase(extension)) {
                return true;
            }
        }

        return false;
    }
}