package de.eisfeldj.augendiagnosefx.controller;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.net.URL;
import java.text.CollationKey;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.TreeMap;
import de.eisfeldj.augendiagnosefx.fxelements.EyePhotoPairNode;
import de.eisfeldj.augendiagnosefx.util.DialogUtil;
import de.eisfeldj.augendiagnosefx.util.DialogUtil.ConfirmDialogListener;
import de.eisfeldj.augendiagnosefx.util.DialogUtil.ProgressDialog;
import de.eisfeldj.augendiagnosefx.util.Logger;
import de.eisfeldj.augendiagnosefx.util.PreferenceUtil;
import de.eisfeldj.augendiagnosefx.util.ResourceConstants;
import de.eisfeldj.augendiagnosefx.util.ResourceUtil;
import de.eisfeldj.augendiagnosefx.util.imagefile.EyePhoto;
import de.eisfeldj.augendiagnosefx.util.imagefile.EyePhotoPair;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import static de.eisfeldj.augendiagnosefx.util.PreferenceUtil.KEY_FOLDER_PHOTOS;
import static de.eisfeldj.augendiagnosefx.util.PreferenceUtil.KEY_LAST_NAME;
/**
* BaseController for the "Display Photos" page.
*/
public class DisplayPhotosController extends BaseController implements Initializable {
/**
* The previous selected name.
*/
private String mPreviousName;
/**
* The full "Display Photos" pane.
*/
@FXML
private Pane mDisplayMain;
/**
* The list of names.
*/
@FXML
private ListView<String> mListNames;
/**
* The list of names.
*/
@FXML
private ListView<GridPane> mListPhotos;
/**
* The field for searching names.
*/
@FXML
private TextField mSearchField;
@Override
public final void initialize(final URL location, final ResourceBundle resources) {
initializeNames("", true);
}
/**
* Initialize the list of names with the search string.
*
* @param searchString
* A search string for the names.
* @param loadPhotos
* indicator if photos from the preselected name should be loaded.
*/
private void initializeNames(final String searchString, final boolean loadPhotos) {
List<String> valuesNames =
getFolderNames(new File(PreferenceUtil.getPreferenceString(KEY_FOLDER_PHOTOS)), searchString);
mListNames.setItems(FXCollections.observableList(valuesNames));
String lastName = PreferenceUtil.getPreferenceString(KEY_LAST_NAME);
if (lastName == null && valuesNames.size() > 0) {
lastName = valuesNames.get(0);
}
if (lastName != null && valuesNames.contains(lastName)) {
if (loadPhotos) {
showPicturesForName(lastName);
}
int selectedIndex = valuesNames.indexOf(lastName);
mListNames.getSelectionModel().select(selectedIndex);
mListNames.scrollTo(selectedIndex);
}
}
@Override
public final Parent getRoot() {
return mDisplayMain;
}
/**
* Handler for Click on name on list. Displays the eye photo pairs for that name.
*
* @param event
* The action event.
*/
@FXML
private void handleNameClick(final MouseEvent event) {
String name = mListNames.getSelectionModel().getSelectedItem();
if (event.getButton() == MouseButton.SECONDARY) {
createContextMenu(name).show(getRoot(), event.getScreenX(), event.getScreenY());
}
else {
if (name != null && !name.equals(mPreviousName)) {
showPicturesForName(name);
}
PreferenceUtil.setPreference(KEY_LAST_NAME, name);
}
}
/**
* Handler for change of search text. Filters displayed eye photo pairs.
*
* @param event
* The action event.
*/
@FXML
private void handleSearchText(final KeyEvent event) {
// Need to be in main thread to ensure that text field is already updated
Platform.runLater(new Runnable() {
@Override
public void run() {
initializeNames(mSearchField.getText(), false);
}
});
}
/**
* Create the context menu when clicking on a name.
*
* @param name The name.
* @return The context menu.
*/
private ContextMenu createContextMenu(final String name) {
ContextMenu menu = new ContextMenu();
MenuItem menuItemRemove = new MenuItem();
menuItemRemove.setText(ResourceUtil.getString(ResourceConstants.MENU_DELETE_IMAGES));
menuItemRemove.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(final ActionEvent event) {
DialogUtil.displayConfirmationMessage(new ConfirmDialogListener() {
@Override
public void onDialogPositiveClick() {
File folder = new File(new File(PreferenceUtil.getPreferenceString(KEY_FOLDER_PHOTOS)), name);
File[] children = folder.listFiles();
if (children != null) {
for (File child : children) {
child.delete();
}
}
folder.delete();
if (name.equals(PreferenceUtil.getPreferenceString(KEY_LAST_NAME))) {
PreferenceUtil.removePreference(KEY_LAST_NAME);
}
initializeNames("", true);
}
@Override
public void onDialogNegativeClick() {
// do nothing
}
}, ResourceConstants.BUTTON_DELETE,
ResourceConstants.MESSAGE_DIALOG_CONFIRM_DELETE_FOLDER, name);
}
});
menu.getItems().add(menuItemRemove);
return menu;
}
/**
* Display the pictures for a given name.
*
* @param name
* The name.
*/
private void showPicturesForName(final String name) {
File nameFolder = new File(PreferenceUtil.getPreferenceString(KEY_FOLDER_PHOTOS), name);
ProgressDialog dialog =
DialogUtil.displayProgressDialog(ResourceConstants.MESSAGE_PROGRESS_LOADING_PHOTOS, name);
EyePhotoPair[] eyePhotos = createEyePhotoList(nameFolder);
ObservableList<GridPane> valuesPhotos = FXCollections.observableList(new ArrayList<GridPane>());
for (int i = 0; i < eyePhotos.length; i++) {
EyePhotoPairNode eyePhotoPairNode = new EyePhotoPairNode(eyePhotos[i], this);
valuesPhotos.add(eyePhotoPairNode);
// Workaround to ensure that the scrollbar is correctly resized after the images are loaded.
eyePhotoPairNode.getImagesLoadedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(final ObservableValue<? extends Boolean> observable,
final Boolean oldValue,
final Boolean newValue) {
Platform.runLater(new Runnable() {
@Override
public void run() {
GridPane dummy = new GridPane();
valuesPhotos.add(dummy);
mListPhotos.layout();
valuesPhotos.remove(dummy);
}
});
}
});
}
mPreviousName = name;
Platform.runLater(new Runnable() {
@Override
public void run() {
mListPhotos.setItems(valuesPhotos);
dialog.close();
}
});
}
/**
* Remove the item for one date from the list.
*
* @param node The row to be removed.
*/
public void removeItem(final EyePhotoPairNode node) {
mListPhotos.getItems().remove(node);
}
// METHODS CLONED FROM ANDROID
/**
* Get the list of subfolders, using getFileNameForSorting() for ordering.
*
* @param parentFolder
* The parent folder.
* @param searchString
* A search String for the name.
* @return The list of subfolders.
*/
public static final List<String> getFolderNames(final File parentFolder, final String searchString) {
File[] folders = parentFolder.listFiles(new FileFilter() {
@Override
public boolean accept(final File pathname) {
return pathname.isDirectory() && nameStartsWith(pathname.getName(), searchString);
}
});
List<String> folderNames = new ArrayList<>();
if (folders == null) {
return folderNames;
}
Collator collator = Collator.getInstance();
final Map<File, CollationKey> collationMap = new HashMap<>();
for (File folder : folders) {
collationMap.put(folder, collator.getCollationKey(getFilenameForSorting(folder)));
}
Arrays.sort(folders, new Comparator<File>() {
@Override
public int compare(final File f1, final File f2) {
return collationMap.get(f1).compareTo(collationMap.get(f2));
}
});
for (File f : folders) {
folderNames.add(f.getName());
}
return folderNames;
}
/**
* Check if a name part starts with the given String (case insensitive).
*
* @param name
* The name.
* @param searchString
* The search string.
* @return True if a name part starts with the given String.
*/
private static boolean nameStartsWith(final String name, final String searchString) {
String[] nameParts = name.toLowerCase().split(" ");
String searchStringCaseIndependent = searchString.toLowerCase();
for (int i = 0; i < nameParts.length; i++) {
if (nameParts[i].startsWith(searchStringCaseIndependent)) {
return true;
}
}
return false;
}
/**
* Helper method to return the name of the file for sorting.
*
* @param f
* The file
* @return The name for Sorting
*/
private static String getFilenameForSorting(final File f) {
String name = f.getName();
boolean sortByLastName = PreferenceUtil.getPreferenceBoolean(PreferenceUtil.KEY_SORT_BY_LAST_NAME);
if (sortByLastName) {
int index = name.lastIndexOf(' ');
if (index >= 0) {
String firstName = name.substring(0, index);
String lastName = name.substring(index + 1);
name = lastName + " " + firstName;
}
}
return name;
}
/**
* Create the list of eye photo pairs for display. Photos are arranged in pairs (right-left) by date.
*
* @param folder
* the folder where the photos are located.
* @return The list of eye photo pairs.
*/
private EyePhotoPair[] createEyePhotoList(final File folder) {
Map<Date, EyePhotoPair> eyePhotoMap = new TreeMap<>(new Comparator<Date>() {
@Override
public int compare(final Date lhs, final Date rhs) {
return rhs.compareTo(lhs);
}
});
File[] files = folder.listFiles(new FilenameFilter() {
@Override
public boolean accept(final File dir, final String name) {
return name.toUpperCase().endsWith(".JPG");
}
});
if (files == null) {
return new EyePhotoPair[0];
}
for (File f : files) {
EyePhoto eyePhoto = EyePhoto.fromFile(f);
if (eyePhoto.isFormatted()) {
Date date = eyePhoto.getDate();
if (eyePhotoMap.containsKey(date)) {
EyePhotoPair eyePhotoPair = eyePhotoMap.get(date);
eyePhotoPair.setEyePhoto(eyePhoto);
}
else {
EyePhotoPair eyePhotoPair = new EyePhotoPair();
eyePhotoPair.setEyePhoto(eyePhoto);
eyePhotoMap.put(date, eyePhotoPair);
}
}
else {
Logger.error("Eye photo is not formatted correctly: " + f.getAbsolutePath());
}
}
return eyePhotoMap.values().toArray(new EyePhotoPair[eyePhotoMap.size()]);
}
}