/* Copyright 2016 Evan A. Thompson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.alvanson.xltsearch; import java.io.File; import java.util.Collections; import java.util.List; import java.util.Optional; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; public class App extends Application { public static final String XLT_VERSION = "0.1.1-SNAPSHOT"; private static final String APP_CONFIG_FILE = ".xltsearch"; private static final String APP_CONFIG_COMMENT = "XLTSearch App Configuration"; private static final int DEFAULT_LIMIT = 100; private static final int SCENE_WIDTH = 640; private static final int SCENE_HEIGHT = 480; @FXML private Label folderPathLabel; @FXML private Label indexDetailsLabel; @FXML private TextField queryField; @FXML private Button searchButton; @FXML private TextField limitField; @FXML private Label searchMessageLabel; @FXML private TableView<SearchResult> resultsTable; @FXML private TableColumn<SearchResult,String> fileNameCol; @FXML private TableColumn<SearchResult,String> titleCol; @FXML private TableColumn<SearchResult,String> scoreCol; @FXML private TextArea detailsField; @FXML private Label indexMessageLabel; @FXML private ProgressBar indexProgress; @FXML private Button messagesButton; private Stage stage; private Configurator configurator; private MessageDisplay messageDisplay; private PersistentProperties properties; private final ObjectProperty<Catalog> catalog = new SimpleObjectProperty<>(); public static void main(String[] args) { launch(args); } @Override public void start(final Stage stage) { this.stage = stage; // initialize UI stage.setTitle("XLTSearch " + XLT_VERSION); stage.getIcons().addAll(new Image(getClass().getResourceAsStream("/icon32.png")), new Image(getClass().getResourceAsStream("/icon22.png")), new Image(getClass().getResourceAsStream("/icon16.png"))); FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/app.fxml")); fxmlLoader.setController(this); try { Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT); stage.setScene(scene); stage.show(); } catch (Exception ex) { DetailedAlert alert = new DetailedAlert(DetailedAlert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText("Exception while loading App"); alert.setDetailsText(MessageLogger.getStackTrace(ex)); alert.showAndWait(); } // error notification MessageLogger.messagesProperty().addListener( (ListChangeListener.Change<? extends Message> c) -> { while (c.next()) { notify(c.getAddedSubList()); } }); // load application properties properties = new PersistentProperties( new File(System.getProperty("user.home") + File.separator + APP_CONFIG_FILE), APP_CONFIG_COMMENT, null); // open last folder with last config String lastFolder = properties.getProperty("last.folder"); String lastConfig = properties.getProperty("last.config"); // ok if null // check for existence of last folder, prompt user if null/not found File dir = lastFolder == null ? null : new File(lastFolder); if (dir == null || !dir.isDirectory()) { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Welcome"); alert.setHeaderText("Welcome to XLTSearch!"); alert.setContentText("Please choose a folder to search."); alert.showAndWait(); DirectoryChooser directoryChooser = new DirectoryChooser(); dir = directoryChooser.showDialog(stage); if (dir == null) { // user cancelled "Open Folder" Platform.exit(); return; } } // open folder and load config catalog.set(new Catalog(dir)); catalog.get().open(lastConfig); } @FXML private void initialize() { // replaces CONSTRAINED_RESIZE_POLICY scoreCol.prefWidthProperty().bind( resultsTable.widthProperty() .subtract(fileNameCol.widthProperty()) .subtract(titleCol.widthProperty()) ); searchButton.defaultButtonProperty().bind(searchButton.focusedProperty()); limitField.setText(Integer.toString(DEFAULT_LIMIT)); // DIALOGS configurator = new Configurator(); messageDisplay = new MessageDisplay(); // UI BINDINGS configurator.catalogProperty().bind(catalog); catalog.addListener((o, oldValue, newValue) -> { folderPathLabel.setText(newValue.getPath()); // bind information controls to new catalog indexDetailsLabel.textProperty().unbind(); indexDetailsLabel.textProperty().bind(newValue.indexDetailsProperty()); searchMessageLabel.textProperty().unbind(); searchMessageLabel.textProperty().bind(newValue.searchDetailsProperty()); resultsTable.itemsProperty().unbind(); resultsTable.itemsProperty().bind(newValue.searchResultsProperty()); indexMessageLabel.textProperty().unbind(); indexMessageLabel.textProperty().bind(newValue.indexMessageProperty()); indexProgress.progressProperty().unbind(); indexProgress.progressProperty().bind(newValue.indexProgressProperty()); }); fileNameCol.setCellValueFactory((r) -> new ReadOnlyStringWrapper(r.getValue().file.getName())); titleCol.setCellValueFactory((r) -> new ReadOnlyStringWrapper(r.getValue().title)); scoreCol.setCellValueFactory((r) -> new ReadOnlyStringWrapper(String.format("%.0f", r.getValue().score*100))); // CALLBACKS resultsTable.getSelectionModel().selectedItemProperty().addListener( (o, oldValue, newValue) -> { if (newValue != null) { detailsField.setText(newValue.details); } else { detailsField.setText(""); } }); resultsTable.setOnKeyPressed((event) -> { SearchResult result = resultsTable.getSelectionModel().getSelectedItem(); if (result != null && event.getCode() == KeyCode.ENTER) { catalog.get().openFile(result.file); } }); resultsTable.setRowFactory((tv) -> { final TableRow<SearchResult> row = new TableRow<>(); // open file on double-click row.setOnMouseClicked((event) -> { if (!row.isEmpty() && event.getClickCount() == 2) { catalog.get().openFile(row.getItem().file); } }); // enable drag-and-drop row.setOnDragDetected((event) -> { if (!row.isEmpty()) { Dragboard db = row.startDragAndDrop(TransferMode.COPY, TransferMode.LINK); ClipboardContent content = new ClipboardContent(); content.putFiles(Collections.singletonList(row.getItem().file)); db.setContent(content); event.consume(); } }); return row; }); } @FXML private void openFolder() { if (catalog.get().isIndexing()) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle("Confirmation"); alert.setHeaderText("Indexing in progress"); alert.setContentText("Opening a new folder will cancel indexing. Continue?"); Optional<ButtonType> result = alert.showAndWait(); // escape on cancel/close if (!result.isPresent() || result.get() != ButtonType.OK) { return; } } DirectoryChooser directoryChooser = new DirectoryChooser(); File dir = directoryChooser.showDialog(stage); if (dir != null) { if (catalog.get() != null) { catalog.get().close(); } catalog.set(new Catalog(dir)); } // do nothing on cancel } @FXML private void configure() { configurator.show(); } // validate input and execute search @FXML private void search() { // validate limit field String limitStr = limitField.getText(); limitStr = limitStr.replaceAll("[^\\d]",""); if (limitStr.equals("")) { limitStr = Integer.toString(DEFAULT_LIMIT); } limitField.setText(limitStr); // results table clears automatically detailsField.setText(""); // execute search int limit = Integer.parseInt(limitStr); catalog.get().search(queryField.getText(), limit); } @FXML private void searchOnEnter(KeyEvent event) { if (event.getCode() == KeyCode.ENTER) { search(); } } // handle messages received from MessageLogger private void notify(List<? extends Message> messages) { // immediately display errors for (Message msg : messages) { if (msg.level.compareTo(Message.Level.ERROR) >= 0) { DetailedAlert alert = new DetailedAlert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText(msg.summary); alert.setDetailsText(msg.details); alert.show(); // showAndWait() causes repeats and race conditions } } // restyle button to indicate that new messages are available if (messages.size() > 0 && !messagesButton.getStyleClass().contains("alert")) { messagesButton.getStyleClass().add("alert"); } } @FXML private void displayMessages() { messagesButton.getStyleClass().remove("alert"); messageDisplay.show(); } @Override public void stop() { // save last folder and config, close catalog Catalog c = catalog.get(); if (c != null) { properties.setProperty("last.folder", c.getPath()); String configName = c.getConfigName(); if (configName != null) { properties.setProperty("last.config", configName); } else { properties.remove("last.config"); } c.close(); } } }