You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

465 lines
16 KiB

package com.sothr.imagetools.ui.controller
import java.io.{File, IOException}
import java.util.Scanner
import javafx.application.Platform
import javafx.event.ActionEvent
import javafx.fxml.FXML
import javafx.scene.control._
import javafx.scene.layout.{AnchorPane, TilePane, VBox}
import javafx.scene.text.{Text, TextAlignment}
import javafx.scene.web.WebView
import javafx.scene.{Group, Node, Scene}
import javafx.stage.{DirectoryChooser, Stage, StageStyle}
import javafx.util.Callback
import akka.actor._
import com.sothr.imagetools.engine._
import com.sothr.imagetools.engine.image.{Image, SimilarImages}
import com.sothr.imagetools.engine.util.{PropertiesService, ResourceLoader}
import com.sothr.imagetools.ui.component.{ImageTilePane, ImageTileFactory}
import grizzled.slf4j.Logging
import org.markdown4j.Markdown4jProcessor
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent._
import scala.util.{Failure, Success}
/**
* Main Application controller
*
* Created by drew on 12/31/13.
*/
class AppController extends Logging {
//Define controls
@FXML var rootPane: AnchorPane = null
@FXML var rootMenuBar: MenuBar = null
@FXML var scrollPane: ScrollPane = null
@FXML var imageTilePane: TilePane = null
@FXML var tagListView: ListView[String] = null
// Labels
@FXML var selectedDirectoryLabel: Label = null
@FXML var currentDirectoryLabel: Label = null
@FXML var progressLabel: Label = null
// Others
@FXML var progressBar: ProgressBar = null
@FXML var paginator: Pagination = null
@FXML var doRecursiveProcessing: CheckBox = null
// Engine
val engine: Engine = new ConcurrentEngine()
// Current State
var currentDirectory: String = "."
var currentImages: List[Image] = List[Image]()
@FXML def initialize() = {
if (PropertiesService.has("app.ui.lastPath")) {
currentDirectory = PropertiesService.get("app.ui.lastPath", ".")
selectedDirectoryLabel.setText(PropertiesService.get("app.ui.lastPath", ""))
}
//setup the engine listener
val system: ActorSystem = AppConfig.getAppActorSystem
val guiListenerProps: Props = Props.create(classOf[GUIEngineListener])
val guiListener: ActorRef = system.actorOf(guiListenerProps)
// configure the listener
guiListener ! SetupListener(progressBar, progressLabel)
// tell the engine to use our listener
this.engine.setProcessedListener(guiListener)
this.engine.setSimilarityListener(guiListener)
// Initialize the progress label
guiListener ! SubmitMessage("Initialized System... Ready!")
// set the default images per page if it doesn't exist yet
if (!PropertiesService.has("app.ui.thumbsPerPage")) {
PropertiesService.set("app.ui.thumbsPerPage", "100")
}
//setup the paginator
//font size doesn't increase the size of the buttons
//paginator.setStyle("-fx-font-size:13;")
// configure the page factory
paginator.setPageFactory(new Callback[Integer, Node]() {
override def call(pageIndex: Integer): Node = {
// do all of our display logic
showPage(pageIndex)
// override behavior to display anything
new VBox()
}
})
//override the imageTilePane
debug("Replacing the default TilePane with a custom ImageTilePane")
val newImageTilePane = new ImageTilePane()
newImageTilePane.setHgap(this.imageTilePane.getHgap)
newImageTilePane.setVgap(this.imageTilePane.getVgap)
newImageTilePane.setMinHeight(this.imageTilePane.getMinHeight)
newImageTilePane.setMinWidth(this.imageTilePane.getMinWidth)
newImageTilePane.setMaxHeight(this.imageTilePane.getMaxHeight)
newImageTilePane.setMaxWidth(this.imageTilePane.getMaxWidth)
newImageTilePane.setPrefColumns(this.imageTilePane.getPrefColumns)
newImageTilePane.setPrefRows(this.imageTilePane.getPrefRows)
//newImageTilePane.setPrefTileHeight(this.imageTilePane.getPrefTileHeight)
//newImageTilePane.setPrefTileWidth(this.imageTilePane.getPrefTileWidth)
newImageTilePane.setTileAlignment(this.imageTilePane.getTileAlignment)
debug("Assigning the the new ImageTilePane to the ScrollPane")
this.scrollPane.setContent(newImageTilePane)
this.imageTilePane = newImageTilePane
//test
//val testImage = new Image()
//testImage.setThumbnailPath("test.jpg")
//testImage.setImagePath("test.jpg")
//val testImageList = new mutable.MutableList[Image]
//for (i <- 1 to 100) {
// testImageList += testImage
//}
//setPagesContent(testImageList.toList)
//showPage(0)
//val list = FXCollections.observableArrayList[String]()
//for (i <- 1 to 100) {
// list.add(s"test-item ${i}")
//}
//tagListView.setItems(list)
}
//region MenuItem Actions
@FXML
def helpAction(event: ActionEvent) = {
showExternalHTMLUtilityDialog("http://www.sothr.com")
}
@FXML
def aboutAction(event: ActionEvent) = {
debug("Displaying about screen")
var aboutMessage = "Simple About Message"
try {
val scanner = new Scanner(ResourceLoader.get().getResourceStream("documents/about.md"))
aboutMessage = ""
while (scanner.hasNextLine) {
aboutMessage += scanner.nextLine().trim() + "\n"
}
debug(s"Parsed About Message: '$aboutMessage'")
} catch {
case ioe: IOException =>
error("Unable to read about file")
}
showMarkdownUtilityDialog("About", aboutMessage, 400.0, 300.0)
debug("Showing About Dialog")
}
@FXML
def closeAction(event: ActionEvent) = {
debug("Closing application from the menu bar")
val stage: Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
stage.close()
}
//endregion
//region buttons
@FXML
def browseFolders(event: ActionEvent) = {
val chooser = new DirectoryChooser()
chooser.setTitle("ImageTools Browser")
try {
val defaultDirectory = new File(currentDirectory)
chooser.setInitialDirectory(defaultDirectory)
val window = this.rootPane.getScene.getWindow
val selectedDirectory = chooser.showDialog(window)
info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}")
selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
currentDirectory = selectedDirectory.getAbsolutePath
PropertiesService.set("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
} catch {
// fall back on the default because the directory we tried probably didn't exist
case iae: IllegalArgumentException =>
logger.error("The old directory no longer exists", iae)
chooser.setInitialDirectory(null)
val window = this.rootPane.getScene.getWindow
val selectedDirectory = chooser.showDialog(window)
info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}")
selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
currentDirectory = selectedDirectory.getAbsolutePath
PropertiesService.set("app.ui.lastPath", selectedDirectory.getAbsolutePath)
this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
}
}
@FXML
def showAllImages(event: ActionEvent) = {
resetPaginator()
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val images = engine.getImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
images.sortWith((x,y) => x.imagePath < y.imagePath)
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} images")
// This is used so that JavaFX updates on the proper thread
// This is important since UI updates can only happen on that thread
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
@FXML
def showSimilarImages(event: ActionEvent) = {
resetPaginator()
imageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val f: Future[List[Image]] = Future {
val similarImages = engine.getSimilarImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
val tempImages = new mutable.MutableList[Image]()
for (similarImage <- similarImages) {
debug(s"Adding similar images ${similarImage.rootImage.toString} to app")
tempImages += similarImage.rootImage
similarImage.similarImages.foreach(image => tempImages += image)
}
tempImages.toList
}
f onComplete {
case Success(images) =>
info(s"Displaying ${images.length} similar images")
Platform.runLater(new Runnable() {
override def run() {
setPagesContent(images)
showPage(0)
}
})
case Failure(t) =>
error("An Error Occurred", t)
}
}
//endregion
//region pagination
def resetPaginator() = {
this.paginator.setDisable(true)
this.paginator.setPageCount(1)
}
def setPagesContent(images: List[Image]) = {
this.currentImages = images
//set the appropriate size for the pagination
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val pageNum = Math.ceil(this.currentImages.size.toFloat / itemsPerPage).toInt
this.paginator.setPageCount(pageNum)
this.paginator.setDisable(false)
}
def showPage(pageIndex: Integer) = {
val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
val startIndex = pageIndex * itemsPerPage
val endIndex = if ((startIndex + itemsPerPage) > this.currentImages.size) this.currentImages.length else startIndex + itemsPerPage
//clear any selections
getImageTilePane.asInstanceOf[ImageTilePane].clearSelection()
//clear and populate the scrollpane
getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
val images = this.currentImages.slice(startIndex, endIndex)
Platform.runLater(new Runnable() {
override def run() {
for (image <- images) {
debug(s"Adding image ${image.toString} to app")
getImageTilePane.getChildren.add(ImageTileFactory.get(image, getImageTilePane))
}
}
})
}
//endregion
def getImageTilePane :TilePane = {
this.imageTilePane
}
//todo: include a templating engine for rendering information
//todo: show a dialog that is rendered from markdown content
def showMarkdownUtilityDialog(title: String, markdown: String, width: Double = 800.0, height: Double = 600.0) = {
val htmlBody = new Markdown4jProcessor().process(markdown)
showHTMLUtilityDialog(title, htmlBody, width, height)
}
/**
* Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine.
*
* @param title Title of the dialog
* @param htmlBody Body to render
* @param width Desired width of the dialog
* @param height Desired height of the dialog
*/
def showHTMLUtilityDialog(title: String, htmlBody: String, width: Double = 800.0, height: Double = 600.0) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.loadContent(htmlBody)
htmlView.setMinWidth(width)
htmlView.setMinHeight(height)
htmlView.setPrefWidth(width)
htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(title)
dialog.show()
}
def showExternalHTMLUtilityDialog(url: String) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
//setup the HTML view
val htmlView = new WebView
htmlView.getEngine.load(url)
//htmlView.setMinWidth(width)
//htmlView.setMinHeight(height)
//htmlView.setPrefWidth(width)
//htmlView.setPrefHeight(height)
parent.getChildren.add(htmlView)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setTitle(htmlView.getEngine.getTitle)
dialog.show()
}
/**
* Show a plain text utility dialog
*
* @param message Message to display
* @param wrapWidth When to wrap
* @param alignment How it should be aligned
*/
def showUtilityDialog(title: String,
message: String,
wrapWidth: Double = 300.0,
xOffset: Double = 25.0,
yOffset: Double = 25.0,
alignment: TextAlignment = TextAlignment.JUSTIFY) = {
val dialog: Stage = new Stage()
dialog.initStyle(StageStyle.UTILITY)
val parent: Group = new Group()
// fill the text box
val messageText = new Text()
messageText.setText(message)
messageText.setWrappingWidth(wrapWidth)
messageText.setX(xOffset)
messageText.setY(yOffset)
messageText.setTextAlignment(TextAlignment.JUSTIFY)
parent.getChildren.add(messageText)
val scene: Scene = new Scene(parent)
dialog.setScene(scene)
dialog.setResizable(false)
dialog.setMinWidth(wrapWidth + xOffset * 2)
dialog.setTitle(title)
dialog.show()
}
def print(): String = {
"This method works"
}
}
//region EngineListener
case class SetupListener(progressBar: ProgressBar, progressLabel: Label)
/**
* Actor for logging output information
*/
class GUIEngineListener extends EngineListener with ActorLogging {
var progressBar: javafx.scene.control.ProgressBar = null
var progressLabel: javafx.scene.control.Label = null
var isStarted = false
var isFinished = false
override def receive: Actor.Receive = {
case command: SetupListener => setupListener(command)
case command: SubmitMessage => handleMessage(command)
case command: ScannedFileCount => handleScannedFileCount(command)
case command: ComparedFileCount => handleComparedFileCount(command)
case _ => log.info("received unknown message")
}
def setupListener(command: SetupListener) = {
this.progressBar = command.progressBar
this.progressLabel = command.progressLabel
}
override def handleComparedFileCount(command: ComparedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Processed ${command.count}/${command.total}")
}
log.debug("Processed {}/{}", command.count, command.total)
progressBar.setProgress(command.count.toFloat/command.total)
}
})
}
override def handleScannedFileCount(command: ScannedFileCount): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
if (command.message != null) {
log.debug(command.message)
progressLabel.setText(command.message)
} else {
progressLabel.setText(s"Scanned ${command.count}/${command.total} For Similarities")
}
log.debug("Scanned {}/{} For Similarities", command.count, command.total)
progressBar.setProgress(command.count.toFloat/command.total)
}
})
}
override def handleMessage(command: SubmitMessage): Unit = {
Platform.runLater(new Runnable() {
override def run(): Unit = {
log.debug(command.message)
progressLabel.setText(command.message)
}
})
}
}
//endregion