ch.cyberduck.ui.cocoa.BookmarkTableDataSource.java Source code

Java tutorial

Introduction

Here is the source code for ch.cyberduck.ui.cocoa.BookmarkTableDataSource.java

Source

package ch.cyberduck.ui.cocoa;

/*
 *  Copyright (c) 2005 David Kocher. All rights reserved.
 *  http://cyberduck.ch/
 *
 *  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.
 *
 *  Bug fixes, suggestions and comments should be sent to:
 *  dkocher@cyberduck.ch
 */

import ch.cyberduck.binding.application.NSApplication;
import ch.cyberduck.binding.application.NSDraggingInfo;
import ch.cyberduck.binding.application.NSDraggingSource;
import ch.cyberduck.binding.application.NSEvent;
import ch.cyberduck.binding.application.NSImage;
import ch.cyberduck.binding.application.NSPasteboard;
import ch.cyberduck.binding.application.NSTableColumn;
import ch.cyberduck.binding.application.NSTableView;
import ch.cyberduck.binding.foundation.NSArray;
import ch.cyberduck.binding.foundation.NSIndexSet;
import ch.cyberduck.binding.foundation.NSMutableArray;
import ch.cyberduck.binding.foundation.NSMutableDictionary;
import ch.cyberduck.binding.foundation.NSObject;
import ch.cyberduck.binding.foundation.NSString;
import ch.cyberduck.binding.foundation.NSURL;
import ch.cyberduck.core.*;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.pasteboard.HostPasteboard;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.resources.IconCacheFactory;
import ch.cyberduck.core.serializer.HostDictionary;
import ch.cyberduck.core.threading.ScheduledThreadPool;
import ch.cyberduck.core.threading.WindowMainAction;
import ch.cyberduck.core.transfer.Transfer;
import ch.cyberduck.core.transfer.TransferItem;
import ch.cyberduck.core.transfer.UploadTransfer;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.rococoa.Rococoa;
import org.rococoa.cocoa.foundation.NSInteger;
import org.rococoa.cocoa.foundation.NSPoint;
import org.rococoa.cocoa.foundation.NSRect;
import org.rococoa.cocoa.foundation.NSSize;
import org.rococoa.cocoa.foundation.NSUInteger;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class BookmarkTableDataSource extends ListDataSource {
    private static Logger log = Logger.getLogger(BookmarkTableDataSource.class);

    public enum Column {
        icon, bookmark, status,
    }

    protected BrowserController controller;

    private HostFilter filter;

    private AbstractHostCollection source = AbstractHostCollection.empty();

    /**
     * Subset of the original source
     */
    private AbstractHostCollection filtered;

    private CollectionListener<Host> listener;

    private ScheduledThreadPool timerPool = new ScheduledThreadPool();

    private final HostPasteboard pasteboard = HostPasteboard.getPasteboard();

    public BookmarkTableDataSource(final BrowserController controller) {
        this.controller = controller;
    }

    public void setSource(final AbstractHostCollection source) {
        this.source.removeListener(listener); //Remove previous listener
        this.source = source;
        this.source.addListener(listener = new CollectionListener<Host>() {
            private ScheduledFuture<?> delayed = null;

            @Override
            public void collectionLoaded() {
                controller.invoke(new WindowMainAction(controller) {
                    @Override
                    public void run() {
                        controller.reloadBookmarks();
                    }
                });
            }

            @Override
            public void collectionItemAdded(final Host item) {
                controller.invoke(new WindowMainAction(controller) {
                    @Override
                    public void run() {
                        controller.reloadBookmarks();
                    }
                });
            }

            @Override
            public void collectionItemRemoved(final Host item) {
                controller.invoke(new WindowMainAction(controller) {
                    @Override
                    public void run() {
                        controller.reloadBookmarks();
                    }
                });
            }

            @Override
            public void collectionItemChanged(final Host item) {
                if (null != delayed) {
                    delayed.cancel(false);
                }
                // Delay to 1 second. When typing changes we don't have to save every iteration.
                delayed = timerPool.schedule(new Runnable() {
                    public void run() {
                        controller.invoke(new WindowMainAction(controller) {
                            @Override
                            public void run() {
                                controller.reloadBookmarks();
                            }
                        });
                    }
                }, 1L, TimeUnit.SECONDS);
            }
        });
        this.setFilter(null);
    }

    @Override
    public void invalidate() {
        timerPool.shutdown();
        source.removeListener(listener);
        super.invalidate();
    }

    /**
     * Display only a subset of all bookmarks
     *
     * @param filter Filter for bookmarks
     */
    public void setFilter(HostFilter filter) {
        this.filter = filter;
        this.filtered = null;
    }

    /**
     * @return The filtered collection currently to be displayed within the constraints
     * given by the comparison with the bookmark filter
     * @see HostFilter
     */
    protected AbstractHostCollection getSource() {
        if (null == filter) {
            return source;
        }
        if (null == filtered) {
            filtered = new AbstractHostCollection() {
                private static final long serialVersionUID = -2154002477046004380L;

                @Override
                public String getName() {
                    return source.getName();
                }

                @Override
                public boolean allowsAdd() {
                    return source.allowsAdd();
                }

                @Override
                public boolean allowsDelete() {
                    return source.allowsDelete();
                }

                @Override
                public boolean allowsEdit() {
                    return source.allowsEdit();
                }

                @Override
                public void save() {
                    source.save();
                }

                @Override
                public void load() throws AccessDeniedException {
                    source.load();
                }
            };
            for (final Host bookmark : source) {
                if (filter.accept(bookmark)) {
                    filtered.add(bookmark);
                }
            }
            filtered.addListener(new CollectionListener<Host>() {
                @Override
                public void collectionLoaded() {
                    source.collectionLoaded();
                }

                @Override
                public void collectionItemAdded(final Host item) {
                    source.add(item);
                }

                @Override
                public void collectionItemRemoved(final Host item) {
                    source.remove(item);
                }

                @Override
                public void collectionItemChanged(final Host item) {
                    source.collectionItemChanged(item);
                }
            });
        }
        return filtered;
    }

    @Override
    public NSInteger numberOfRowsInTableView(NSTableView view) {
        return new NSInteger(this.getSource().size());
    }

    @Override
    public NSObject tableView_objectValueForTableColumn_row(final NSTableView view, final NSTableColumn tableColumn,
            final NSInteger row) {
        if (row.intValue() >= this.numberOfRowsInTableView(view).intValue()) {
            return null;
        }
        final String identifier = tableColumn.identifier();
        final Host host = this.getSource().get(row.intValue());
        if (identifier.equals(Column.icon.name())) {
            return IconCacheFactory.<NSImage>get().iconNamed(host.getProtocol().disk(),
                    PreferencesFactory.get().getInteger("bookmark.icon.size"));
        }
        if (identifier.equals(Column.bookmark.name())) {
            final NSMutableDictionary dict = NSMutableDictionary.dictionary();
            dict.setObjectForKey(BookmarkNameProvider.toString(host), "Nickname");
            dict.setObjectForKey(host.getHostname(), "Hostname");
            dict.setObjectForKey(new HostUrlProvider(true, true).get(host), "URL");
            final String comment = this.getSource().getComment(host);
            if (StringUtils.isNotBlank(comment)) {
                dict.setObjectForKey(comment, "Comment");
            }
            return dict;
        }
        if (identifier.equals(Column.status.name())) {
            final Session session = controller.getSession();
            if (session != null) {
                if (host.equals(session.getHost())) {
                    switch (session.getState()) {
                    case open:
                        return IconCacheFactory.<NSImage>get().iconNamed("statusGreen.tiff", 16);
                    case opening:
                    case closing:
                        return IconCacheFactory.<NSImage>get().iconNamed("statusYellow.tiff", 16);
                    }
                }
            }
            return null;
        }
        throw new IllegalArgumentException(String.format("Unknown identifier %s", identifier));
    }

    /**
     * Sets whether the use of modifier keys should have an effect on the type of operation performed.
     *
     * @return Always false
     * @see NSDraggingSource
     */
    @Override
    public boolean ignoreModifierKeysWhileDragging() {
        // If this method is not implemented or returns false, the user can tailor the drag operation by
        // holding down a modifier key during the drag.
        return false;
    }

    @Override
    public NSUInteger tableView_validateDrop_proposedRow_proposedDropOperation(final NSTableView view,
            final NSDraggingInfo info, final NSInteger row, final NSUInteger operation) {
        NSPasteboard draggingPasteboard = info.draggingPasteboard();
        if (!this.getSource().allowsEdit()) {
            // Do not allow drags for non writable collections
            return NSDraggingInfo.NSDragOperationNone;
        } else if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.StringPboardType)) != null) {
            String o = draggingPasteboard.stringForType(NSPasteboard.StringPboardType);
            if (o != null) {
                if (Scheme.isURL(o)) {
                    view.setDropRow(row, NSTableView.NSTableViewDropAbove);
                    return NSDraggingInfo.NSDragOperationCopy;
                }
            }
            return NSDraggingInfo.NSDragOperationNone;
        } else if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.FilenamesPboardType)) != null) {
            final NSObject o = draggingPasteboard.propertyListForType(NSPasteboard.FilenamesPboardType);
            if (o != null) {
                if (o.isKindOfClass(Rococoa.createClass("NSArray", NSArray._Class.class))) {
                    final NSArray elements = Rococoa.cast(o, NSArray.class);
                    for (int i = 0; i < elements.count().intValue(); i++) {
                        String file = elements.objectAtIndex(new NSUInteger(i)).toString();
                        if (file.endsWith(".duck")) {
                            // Allow drag if at least one file is a serialized bookmark
                            view.setDropRow(row, NSTableView.NSTableViewDropAbove);
                            return NSDraggingInfo.NSDragOperationCopy;
                        }
                    }
                    //only allow other files if there is at least one bookmark
                    view.setDropRow(row, NSTableView.NSTableViewDropOn);
                    return NSDraggingInfo.NSDragOperationCopy;
                }
            }
            return NSDraggingInfo.NSDragOperationNone;
        } else if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.URLPboardType)) != null) {
            final NSObject o = draggingPasteboard.propertyListForType(NSPasteboard.URLPboardType);
            if (o != null) {
                if (o.isKindOfClass(Rococoa.createClass("NSArray", NSArray._Class.class))) {
                    final NSArray elements = Rococoa.cast(o, NSArray.class);
                    for (int i = 0; i < elements.count().intValue(); i++) {
                        if (Scheme.isURL(elements.objectAtIndex(new NSUInteger(i)).toString())) {
                            view.setDropRow(row, NSTableView.NSTableViewDropAbove);
                            return NSDraggingInfo.NSDragOperationCopy;
                        }
                    }
                }
            }
            return NSDraggingInfo.NSDragOperationNone;
        } else if (!pasteboard.isEmpty()) {
            view.setDropRow(row, NSTableView.NSTableViewDropAbove);
            if (log.isDebugEnabled()) {
                log.debug(
                        String.format("Drag operation mask is %d", info.draggingSourceOperationMask().intValue()));
            }
            // We accept any file promise within the bounds
            if (info.draggingSourceOperationMask().intValue() == NSDraggingInfo.NSDragOperationCopy.intValue()) {
                return NSDraggingInfo.NSDragOperationCopy;
            }
            return NSDraggingInfo.NSDragOperationMove;
        }
        return NSDraggingInfo.NSDragOperationNone;
    }

    /**
     * @param info contains details on this dragging operation.
     * @param row  The proposed location is row and action is operation.
     *             The data source should incorporate the data from the dragging pasteboard at this time.
     * @see NSTableView.DataSource
     * Invoked by view when the mouse button is released over a table view that previously decided to allow a drop.
     */
    @Override
    public boolean tableView_acceptDrop_row_dropOperation(final NSTableView view, final NSDraggingInfo info,
            final NSInteger row, final NSUInteger operation) {
        NSPasteboard draggingPasteboard = info.draggingPasteboard();
        if (log.isDebugEnabled()) {
            log.debug(String.format("Accept drop at row %s", row));
        }
        view.deselectAll(null);
        final AbstractHostCollection source = this.getSource();
        if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.StringPboardType)) != null) {
            String o = draggingPasteboard.stringForType(NSPasteboard.StringPboardType);
            if (null == o) {
                return false;
            }
            final Host h = HostParser.parse(o);
            source.add(row.intValue(), h);
            view.selectRowIndexes(NSIndexSet.indexSetWithIndex(row), false);
            view.scrollRowToVisible(row);
            return true;
        } else if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.FilenamesPboardType)) != null) {
            // We get a drag from another application e.g. Finder.app proposing some files
            final NSObject object = draggingPasteboard.propertyListForType(NSPasteboard.FilenamesPboardType);
            if (object != null) {
                if (object.isKindOfClass(Rococoa.createClass("NSArray", NSArray._Class.class))) {
                    final NSArray elements = Rococoa.cast(object, NSArray.class);
                    // If regular files are dropped, these will be uploaded to the dropped bookmark location
                    final List<TransferItem> uploads = new ArrayList<TransferItem>();
                    Host host = null;
                    for (int i = 0; i < elements.count().intValue(); i++) {
                        final String filename = elements.objectAtIndex(new NSUInteger(i)).toString();
                        final Local local = LocalFactory.get(filename);
                        if (filename.endsWith(".duck")) {
                            // Adding a previously exported bookmark file from the Finder
                            final Host bookmark;
                            try {
                                bookmark = HostReaderFactory.get().read(local);
                                if (null == bookmark) {
                                    continue;
                                }
                                source.add(row.intValue(), bookmark);
                                view.selectRowIndexes(NSIndexSet.indexSetWithIndex(row), true);
                                view.scrollRowToVisible(row);
                            } catch (AccessDeniedException e) {
                                continue;
                            }
                        } else {
                            // The bookmark this file has been dropped onto
                            final Host h = source.get(row.intValue());
                            if (null == host) {
                                host = h;
                            }
                            // Upload to the remote host this bookmark points to
                            uploads.add(new TransferItem(new Path(
                                    new Path(PathNormalizer.normalize(h.getDefaultPath(), true),
                                            EnumSet.of(Path.Type.directory)),
                                    local.getName(), EnumSet.of(Path.Type.file)), local));
                        }
                    }
                    if (!uploads.isEmpty()) {
                        // If anything has been added to the queue, then process the queue
                        final Transfer t = new UploadTransfer(host, uploads);
                        TransferControllerFactory.get().start(t);
                    }
                    return true;
                }
            }
        } else if (draggingPasteboard
                .availableTypeFromArray(NSArray.arrayWithObject(NSPasteboard.URLPboardType)) != null) {
            final NSObject object = draggingPasteboard.propertyListForType(NSPasteboard.URLPboardType);
            if (object != null) {
                if (object.isKindOfClass(Rococoa.createClass("NSArray", NSArray._Class.class))) {
                    final NSArray elements = Rococoa.cast(object, NSArray.class);
                    for (int i = 0; i < elements.count().intValue(); i++) {
                        final String url = elements.objectAtIndex(new NSUInteger(i)).toString();
                        if (StringUtils.isNotBlank(url)) {
                            final Host h = HostParser.parse(url);
                            source.add(row.intValue(), h);
                            view.selectRowIndexes(NSIndexSet.indexSetWithIndex(row), true);
                            view.scrollRowToVisible(row);
                        }
                    }
                    return true;
                }
            }
            return false;
        } else if (!pasteboard.isEmpty()) {
            if (info.draggingSourceOperationMask().intValue() == NSDraggingInfo.NSDragOperationCopy.intValue()) {
                List<Host> duplicates = new ArrayList<Host>();
                for (Host bookmark : pasteboard) {
                    final Host duplicate = new HostDictionary()
                            .deserialize(bookmark.serialize(SerializerFactory.get()));
                    // Make sure a new UUID is assigned for duplicate
                    duplicate.setUuid(null);
                    source.add(row.intValue(), duplicate);
                    duplicates.add(duplicate);
                }
                for (Host bookmark : duplicates) {
                    int index = source.indexOf(bookmark);
                    view.selectRowIndexes(NSIndexSet.indexSetWithIndex(new NSInteger(index)), true);
                    view.scrollRowToVisible(new NSInteger(index));
                }
            } else {
                int insert = row.intValue();
                for (Host bookmark : pasteboard) {
                    int previous = source.indexOf(bookmark);
                    if (previous == insert) {
                        // No need to move
                        continue;
                    }
                    source.remove(previous);
                    int moved;
                    if (previous < insert) {
                        moved = insert - 1;
                    } else {
                        moved = insert;
                    }
                    source.add(moved, bookmark);
                }
                for (Host bookmark : pasteboard) {
                    int index = source.indexOf(bookmark);
                    view.selectRowIndexes(NSIndexSet.indexSetWithIndex(new NSInteger(index)), true);
                    view.scrollRowToVisible(new NSInteger(index));
                }
            }
            return true;
        }
        return false;
    }

    /**
     * @see NSDraggingSource
     * @see "http://www.cocoabuilder.com/archive/message/2005/10/5/118857"
     */
    @Override
    public void draggedImage_endedAt_operation(final NSImage image, final NSPoint point,
            final NSUInteger operation) {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Drop finished with operation %s", operation));
        }
        if (NSDraggingInfo.NSDragOperationDelete.intValue() == operation.intValue()) {
            controller.deleteBookmarkButtonClicked(null);
        }
        NSPasteboard.pasteboardWithName(NSPasteboard.DragPboard).declareTypes_owner(null, null);
        pasteboard.clear();
    }

    /**
     * @param local indicates that the candidate destination object (the window or view over which the dragged
     *              image is currently poised) is in the same application as the source, while a NO value indicates that
     *              the destination object is in a different application
     * @return A mask, created by combining the dragging operations listed in the NSDragOperation section of
     * NSDraggingInfo protocol reference using the C bitwise OR operator.If the source does not permit
     * any dragging operations, it should return NSDragOperationNone.
     * @see NSDraggingSource
     */
    @Override
    public NSUInteger draggingSourceOperationMaskForLocal(final boolean local) {
        if (local) {
            return new NSUInteger(
                    NSDraggingInfo.NSDragOperationMove.intValue() | NSDraggingInfo.NSDragOperationCopy.intValue());
        }
        return new NSUInteger(
                NSDraggingInfo.NSDragOperationCopy.intValue() | NSDraggingInfo.NSDragOperationDelete.intValue());
    }

    /**
     * @param rowIndexes is the list of row numbers that will be participating in the drag.
     * @return To refuse the drag, return false. To start a drag, return true and place
     * the drag data onto pboard (data, owner, and so on).
     * @see NSTableView.DataSource
     * Invoked by view after it has been determined that a drag should begin, but before the drag has been started.
     * The drag image and other drag-related information will be set up and provided by the table view once this call
     * returns with true.
     */
    @Override
    public boolean tableView_writeRowsWithIndexes_toPasteboard(final NSTableView view, final NSIndexSet rowIndexes,
            final NSPasteboard pboard) {
        for (NSUInteger index = rowIndexes.firstIndex(); !index.equals(NSIndexSet.NSNotFound); index = rowIndexes
                .indexGreaterThanIndex(index)) {
            pasteboard.add(this.getSource().get(index.intValue()));
        }
        NSEvent event = NSApplication.sharedApplication().currentEvent();
        if (event != null) {
            NSPoint dragPosition = view.convertPoint_fromView(event.locationInWindow(), null);
            NSRect imageRect = new NSRect(
                    new NSPoint(dragPosition.x.doubleValue() - 16, dragPosition.y.doubleValue() - 16),
                    new NSSize(32, 32));
            // Writing a promised file of the host as a bookmark file to the clipboard
            view.dragPromisedFilesOfTypes(NSArray.arrayWithObject("duck"), imageRect, this.id(), true, event);
            return true;
        }
        return false;
    }

    /**
     * @return the names (not full paths) of the files that the receiver promises to create at dropDestination.
     * This method is invoked when the drop has been accepted by the destination and the destination,
     * in the case of another Cocoa application, invokes the NSDraggingInfo method
     * namesOfPromisedFilesDroppedAtDestination.
     * For long operations, you can cache dropDestination and defer the creation of the files until the
     * finishedDraggingImage method to avoid blocking the destination application.
     * @see NSTableView.DataSource
     */
    @Override
    public NSArray namesOfPromisedFilesDroppedAtDestination(final NSURL dropDestination) {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Query promised files dropped dat destination %s", dropDestination.path()));
        }
        final NSMutableArray promisedDragNames = NSMutableArray.array();
        if (null != dropDestination) {
            for (Host bookmark : pasteboard) {
                final Local file = LocalFactory.get(dropDestination.path(), String.format("%s.duck",
                        StringUtils.replace(BookmarkNameProvider.toString(bookmark), "/", ":")));
                try {
                    HostWriterFactory.get().write(bookmark, file);
                } catch (AccessDeniedException e) {
                    log.warn(e.getMessage());
                }
                // Adding the filename that is promised to be created at the dropDestination
                promisedDragNames.addObject(NSString.stringWithString(file.getName()));
            }
            pasteboard.clear();
        }
        return promisedDragNames;
    }
}