package com.wilutions.itol;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.wilutions.com.BackgTask;
import com.wilutions.itol.db.Attachment;
import com.wilutions.itol.db.ISODate;
import com.wilutions.itol.db.ProgressCallback;
import com.wilutions.itol.db.ProgressCallbackFactory;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.stage.Popup;
import javafx.stage.Window;
import javafx.util.Callback;
import javafx.util.Duration;
public class AttachmentTableViewHandler {
private final static Logger log = Logger.getLogger("AttachmentTableViewHandler");
@SuppressWarnings("unchecked")
public static void apply(MailAttachmentHelper attachmentHelper, TableView<Attachment> table, Attachments observableAttachments, ProgressCallbackFactory progressCallbackFactory) {
long t1 = System.currentTimeMillis();
table.setItems(observableAttachments.getObservableList());
// Render preview in tooltip for image attachments.
TooltipRefCount activeTooltip = new TooltipRefCount(table, attachmentHelper, progressCallbackFactory);
ResourceBundle resb = Globals.getResourceBundle();
TableColumn<Attachment, String> iconColumn = new TableColumn<>("");
final int iconColumnWidth = 24;
iconColumn.setPrefWidth(iconColumnWidth);
// iconColumn.setMaxWidth(iconColumnWidth);
// iconColumn.setMinWidth(iconColumnWidth);
iconColumn.setCellValueFactory(new PropertyValueFactory<Attachment, String>("fileName"));
iconColumn.setCellFactory(new Callback<TableColumn<Attachment, String>, TableCell<Attachment, String>>() {
@Override
public TableCell<Attachment, String> call(TableColumn<Attachment, String> item) {
TableCell<Attachment, String> cell = new TableCell<Attachment, String>() {
@Override
protected void updateItem(String fileName, boolean empty) {
super.updateItem(fileName, empty);
if (fileName != null) {
Image fxImage = FileIconCache.getFileIcon(new File(fileName));
if (fxImage != null) {
ImageView imageView = new ImageView(fxImage);
setGraphic(imageView);
}
}
}
};
return cell;
}
});
iconColumn.setComparator(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return MailAttachmentHelper.getFileExt(o1).compareToIgnoreCase(MailAttachmentHelper.getFileExt(o2));
}
});
TableColumn<Attachment, String> fileNameColumn = new TableColumn<>("Name");
fileNameColumn.setCellValueFactory(new PropertyValueFactory<Attachment, String>("fileName"));
fileNameColumn.setCellFactory(new Callback<TableColumn<Attachment, String>, TableCell<Attachment, String>>() {
@Override
public TableCell<Attachment, String> call(TableColumn<Attachment, String> item) {
TableCell<Attachment, String> cell = new TableCell<Attachment, String>() {
Popup tooltip = new Popup();
// Constructor
{
// Show tooltip if mouse enters this cell.
// Premission: the table has the input focus. This makes it easier to
// be notified, if another application window has been moved to foreground (ALT-TAB).
// The fousedProperty listener below hides the tooltip, if the table
// looses the focus.
this.setOnMouseEntered((event) -> {
Attachment attachment = (Attachment)getTableRow().getItem();
if (attachment != null && table.isFocused()) {
// System.out.println("Enter attachment=" + attachment + " tooltip=" + System.identityHashCode(tooltip));
activeTooltip.handleMouseEnter(attachment, tooltip, event.getScreenX(), event.getScreenY());
}
});
this.setOnMouseClicked((event) -> {
Attachment attachment = (Attachment)getTableRow().getItem();
if (attachment != null) {
// System.out.println("Enter attachment=" + attachment + " tooltip=" + System.identityHashCode(tooltip));
activeTooltip.handleMouseEnter(attachment, tooltip, event.getScreenX(), event.getScreenY());
}
});
// Hide tooltip if mouse leaves this cell
this.setOnMouseExited((event) -> {
Attachment attachment = (Attachment)getTableRow().getItem();
if (attachment != null) {
// System.out.println("Exit attachment=" + attachment + " tooltip=" + System.identityHashCode(tooltip));
activeTooltip.handleMouseExit(tooltip);
}
});
}
@Override
protected void updateItem(String fileName, boolean empty) {
super.updateItem(fileName, empty);
if (fileName != null) {
Attachment attachment = (Attachment)getTableRow().getItem();
if (attachment != null) { // att is null when table.getItems() is modified in IssueHtmlEditor - why?
String style = attachment.getId().isEmpty() ? "-fx-font-weight: bold;" : "fx-font-weight: normal;";
setStyle(style);
String str = MailAttachmentHelper.getFileName(fileName);
setText(str);
}
}
}
};
return cell;
}
});
fileNameColumn.setComparator(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return MailAttachmentHelper.getFileName(o1).compareToIgnoreCase(MailAttachmentHelper.getFileName(o2));
}
});
// final int fileNameColumnWidth = 150;
// fileNameColumn.setPrefWidth(fileNameColumnWidth);
// fileNameColumn.setMinWidth(fileNameColumnWidth);
TableColumn<Attachment, Long> contentLengthColumn = new TableColumn<>("Size");
contentLengthColumn.setCellValueFactory(new PropertyValueFactory<Attachment, Long>("contentLength"));
contentLengthColumn.setCellFactory(new Callback<TableColumn<Attachment, Long>, TableCell<Attachment, Long>>() {
@Override
public TableCell<Attachment, Long> call(TableColumn<Attachment, Long> item) {
TableCell<Attachment, Long> cell = new TableCell<Attachment, Long>() {
@Override
protected void updateItem(Long contentLength, boolean empty) {
super.updateItem(contentLength, empty);
if (contentLength != null) {
String str = MailAttachmentHelper.makeAttachmentSizeString(contentLength);
setText(str);
}
}
};
cell.setStyle("-fx-alignment: CENTER-RIGHT;");
return cell;
}
});
final int contentLengthColumnWidth = 100;
contentLengthColumn.setPrefWidth(contentLengthColumnWidth);
// contentLengthColumn.setMaxWidth(contentLengthColumnWidth);
// contentLengthColumn.setMinWidth(contentLengthColumnWidth);
TableColumn<Attachment, Date> lastModifiedColumn = new TableColumn<>("Date");
lastModifiedColumn.setCellValueFactory(new PropertyValueFactory<Attachment, Date>("lastModified"));
lastModifiedColumn.setCellFactory(new Callback<TableColumn<Attachment, Date>, TableCell<Attachment, Date>>() {
@Override
public TableCell<Attachment, Date> call(TableColumn<Attachment, Date> item) {
TableCell<Attachment, Date> cell = new TableCell<Attachment, Date>() {
@Override
protected void updateItem(Date lastModified, boolean empty) {
super.updateItem(lastModified, empty);
if (lastModified != null) {
String str = DateFormat.getDateTimeInstance().format(lastModified);
setText(str);
}
}
};
cell.setStyle("-fx-alignment: CENTER-RIGHT;");
return cell;
}
});
final int lastModifiedColumnWidth = 150;
lastModifiedColumn.setPrefWidth(lastModifiedColumnWidth);
fileNameColumn.prefWidthProperty()
.bind(table.widthProperty().subtract(iconColumnWidth + contentLengthColumnWidth + lastModifiedColumnWidth + 2));
table.getColumns().clear();
table.getColumns().addAll(iconColumn, fileNameColumn, lastModifiedColumn, contentLengthColumn);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.setPlaceholder(new Label(resb.getString("tabAttachments.emptyMessage")));
lastModifiedColumn.setSortType(TableColumn.SortType.DESCENDING);
table.getSortOrder().add(lastModifiedColumn);
////////////////////
// Drag&Drop
table.setOnDragOver(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
if (db.hasFiles()) {
event.acceptTransferModes(TransferMode.COPY);
}
else {
event.consume();
}
}
});
// Dropping over surface
table.setOnDragDropped(new EventHandler<DragEvent>() {
@Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
boolean success = false;
if (db.hasFiles()) {
success = true;
List<File> files = db.getFiles();
for (File file : files) {
Attachment att = MailAttachmentHelper.createFromFile(file);
table.getItems().add(att);
}
}
event.setDropCompleted(success);
event.consume();
}
});
// Start drag of attachment files.
table.setOnDragDetected((dragEvent) -> {
if (log.isLoggable(Level.FINE)) log.fine("setOnDragEntered(");
List<Attachment> selectedAttachments = table.getSelectionModel().getSelectedItems();
if (!selectedAttachments.isEmpty()) {
Dragboard db = table.startDragAndDrop(TransferMode.COPY);
ClipboardContent content = new ClipboardContent();
try {
CompletableFuture<List<File>> fdownloaded = CompletableFuture.supplyAsync(() -> {
List<File> files = Collections.synchronizedList(new ArrayList<File>());
ProgressCallback cb = progressCallbackFactory.createProgressCallback("Drag attachments");
if (log.isLoggable(Level.FINE)) log.fine("#selectedAttachments=" + selectedAttachments.size());
double progressPerAttachment = 1.0 / (double)selectedAttachments.size();
List<CompletableFuture<Void>> fatts = new ArrayList<>(selectedAttachments.size());
for (Attachment att : selectedAttachments) {
if (log.isLoggable(Level.FINE)) log.fine("download attachment=" + att);
CompletableFuture<Void> fatt = CompletableFuture.supplyAsync(() -> {
try {
URI url = attachmentHelper.downloadAttachment(att, cb.createChild(progressPerAttachment));
File file = new File(url);
files.add(file);
if (log.isLoggable(Level.FINE)) log.fine("file=" + file);
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to download attachment.", e);
}
return null;
});
fatts.add(fatt);
}
cb.setFinished();
try {
CompletableFuture.allOf(fatts.toArray(new CompletableFuture[0])).get();
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to start dragging attachments", e);
}
return files;
});
List<File> files = fdownloaded.get();
log.info("start drag #atts=" + files.size());
content.putFiles(files);
}
catch (Exception e) {
log.log(Level.WARNING, "Failed to start dragging attachments", e);
}
finally {
db.setContent(content);
}
}
dragEvent.consume();
if (log.isLoggable(Level.FINE)) log.fine(")setOnDragEntered");
});
///////////////////////////
// Hide tooltip if focus lost
table.focusedProperty().addListener((property, oldValue, newValue) -> {
if (!newValue) {
activeTooltip.hide();
}
});
long t2 = System.currentTimeMillis();
log.info("[" + (t2-t1) + "] apply(observableAttachments=" + observableAttachments + ")");
}
/**
* This class holds tooltip window and a reference count for the number of calls to show().
* The tooltip is being hidden, when the reference count reaches 0.
*/
private static class TooltipRefCount {
final static Popup NO_TOOLTIP = new Popup();
Node owner;
MailAttachmentHelper attachmentHelper;
ProgressCallbackFactory progressCallbackFactory;
Popup tooltip = NO_TOOLTIP;
int refCount;
public TooltipRefCount(Node owner, MailAttachmentHelper attachmentHelper, ProgressCallbackFactory progressCallbackFactory) {
this.owner = owner;
this.attachmentHelper = attachmentHelper;
this.progressCallbackFactory = progressCallbackFactory;
}
public void handleMouseExit(Popup tooltip) {
this.hide(tooltip);
}
public void handleMouseEnter(Attachment attachment, Popup tooltip, double x, double y) {
if (tooltip.getContent().isEmpty()) {
initThumbnailTooltip(attachment, tooltip, progressCallbackFactory, () -> {
// Add mouse handlers to keep showing tooltip if mouse is over the image.
ImageView imageView = (ImageView)tooltip.getContent().get(0);
imageView.setOnMouseEntered((_ignore) -> {
imageView.getScene().setCursor(Cursor.HAND);
this.show(tooltip, x, y);
});
imageView.setOnMouseExited((_ignore) -> {
this.hide(tooltip);
imageView.getScene().setCursor(Cursor.DEFAULT);
});
// If image is clicked, show full attachment and hide tooltip.
imageView.setOnMouseClicked((_ignore) -> {
BackgTask.run(() -> {
try {
attachmentHelper.showAttachment(attachment, progressCallbackFactory.createProgressCallback("Show attachment"));
} catch (Exception e) {
log.log(Level.WARNING, "Failed to show attachment=" + attachment, e);
}
});
});
this.show(tooltip, x, y);
});
}
else {
this.show(tooltip, x, y);
}
}
public void show(Popup newTooltip, double x, double y) {
Popup oldTooltip = tooltip;
tooltip = newTooltip;
// Hide previous tooltip if another one should be shown.
if (oldTooltip != newTooltip) {
// System.out.println("s.hide oldTooltip=" + System.identityHashCode(oldTooltip));
oldTooltip.hide();
refCount = 0;
}
// Increment number of showing calls for active tooltip.
refCount++;
if (!tooltip.isShowing()) {
// System.out.println("s.show tooltip=" + System.identityHashCode(tooltip));
tooltip.show(owner, x + 20, y);
}
}
public void hide(Popup newTooltip) {
Popup oldTooltip = tooltip;
// If passed tooltip is active...
if (oldTooltip == newTooltip) {
// Decrement number of showing calls.
if (refCount > 0) {
refCount--;
}
// If counted to 0, hide after some time.
if (refCount == 0) {
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1.0), new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
// System.out.println("h.hide tooltip=" + System.identityHashCode(tooltip) + ", refCount=" + refCount);
// Hide tooltip if it is still active and there was no new showing call.
// Example: mouse is moved from table cell into image -> cell exit (refCount--), image enter (refCount++)
if (oldTooltip == tooltip && refCount == 0) {
// System.out.println("h.hide tooltip=" + System.identityHashCode(tooltip));
tooltip.hide();
}
}
}));
timeline.setCycleCount(1);
timeline.play();
}
}
else {
refCount = 0;
// System.out.println("h.hide oldTooltip=" + System.identityHashCode(oldTooltip));
oldTooltip.hide();
}
}
public void hide() {
tooltip.hide();
}
}
/**
* Add a ImageView node to the given tooltip for showing the thumbnail image of the attachment.
* @param attachment
* @param tooltip
* @param progressCallbackFactory
* @param thenRunInFxThread
*/
private static void initThumbnailTooltip(Attachment attachment, Popup tooltip, ProgressCallbackFactory progressCallbackFactory, Runnable thenRunInFxThread) {
if (attachment != null) {
BackgTask.run(() -> {
if (attachment.getThumbnailImage() == null) {
MailAttachmentHelper.getThumbnailImage(attachment, progressCallbackFactory.createProgressCallback("Download thumbnail"));
}
Image image = attachment.getThumbnailImage();
if (image != null) {
Platform.runLater(() -> {
ImageView imageView = new ImageView(image);
imageView.setFitWidth(ThumbnailHelper.THUMBNAIL_WIDTH);
imageView.setFitHeight(ThumbnailHelper.THUMBNAIL_HEIGHT);
imageView.setPreserveRatio(true);
tooltip.getContent().add(imageView);
thenRunInFxThread.run();
});
}
});
}
}
public static List<Attachment> paste(Attachments attachments) {
if (log.isLoggable(Level.FINE)) log.fine("paste(");
List<Attachment> ret = new ArrayList<Attachment>(0);
try {
ret = attachments.addAttachmentsFromClipboard().get();
}
catch (Exception ex) {
log.log(Level.WARNING, "Add attachments from clipboard failed.", ex);
}
if (log.isLoggable(Level.FINE)) log.fine(")paste");
return ret;
}
public static void copy(TableView<Attachment> table, MailAttachmentHelper attachmentHelper, ProgressCallback cb) throws Exception {
if (log.isLoggable(Level.FINE)) log.fine("copy(");
try {
// http://stackoverflow.com/questions/31798646/can-java-system-clipboard-copy-a-file
final String FILE_URL_PREFIX = MailAttachmentHelper.FILE_URL_PREFIX;
List<File> files = new ArrayList<File>();
for (Attachment att : table.getSelectionModel().getSelectedItems()) {
URI fileUri = attachmentHelper.downloadAttachment(att, cb);
files.add(new File(fileUri));
}
FileTransferable ft = new FileTransferable(files);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ft, null);
}
finally {
cb.setFinished();
}
if (log.isLoggable(Level.FINE)) log.fine(")copy");
}
private static class FileTransferable implements Transferable {
private List<File> listOfFiles;
public FileTransferable(List<File> listOfFiles) {
this.listOfFiles = listOfFiles;
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[]{DataFlavor.javaFileListFlavor};
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return DataFlavor.javaFileListFlavor.equals(flavor);
}
@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
return listOfFiles;
}
}
/**
* Add selected attachments to blacklist.
* This attachments should not be added to an issue anymore, e.g. company logos.
* For each attachment, a dialog queries for a name under which the properties of the attachment are stored in the configuration file.
*/
public static void addSelectedAttachmentsToBlacklist(Window owner, MailAttachmentHelper attachmentHelper, ProgressCallback cb, TableView<Attachment> tabAttachments) throws Exception {
try {
ArrayList<Attachment> allItems = new ArrayList<>(tabAttachments.getItems());
ResourceBundle resb = Globals.getResourceBundle();
for (Attachment att : tabAttachments.getSelectionModel().getSelectedItems()) {
TextInputDialog dialog = new TextInputDialog(att.getFileName());
dialog.initOwner(owner);
dialog.setTitle(resb.getString("menuAddToBlacklist"));
dialog.setHeaderText(resb.getString("menuAddToBlacklist.hint"));
Optional<String> selectedName = dialog.showAndWait();
if (!selectedName.isPresent()) break;
URI uri = attachmentHelper.downloadAttachment(att, cb);
File localFile = new File(uri);
MailAttachmentHelper.addBlacklistItem(selectedName.get(), localFile);
allItems.remove(att);
}
if (allItems.size() != tabAttachments.getItems().size()) {
tabAttachments.getItems().clear();
tabAttachments.getItems().addAll(allItems);
tabAttachments.refresh();
}
}
finally {
cb.setFinished();
}
}
}