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.

461 lines
15 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. package com.sothr.imagetools.ui.controller
  2. import java.io.{File, IOException}
  3. import java.util.Scanner
  4. import javafx.application.Platform
  5. import javafx.event.ActionEvent
  6. import javafx.fxml.FXML
  7. import javafx.scene.control._
  8. import javafx.scene.layout.{AnchorPane, TilePane, VBox}
  9. import javafx.scene.text.{Text, TextAlignment}
  10. import javafx.scene.web.WebView
  11. import javafx.scene.{Group, Node, Scene}
  12. import javafx.stage.{DirectoryChooser, Stage, StageStyle}
  13. import javafx.util.Callback
  14. import akka.actor._
  15. import com.sothr.imagetools.engine._
  16. import com.sothr.imagetools.engine.image.Image
  17. import com.sothr.imagetools.engine.util.{PropertiesService, ResourceLoader}
  18. import com.sothr.imagetools.ui.component.{ImageTileFactory, ImageTilePane}
  19. import grizzled.slf4j.Logging
  20. import org.markdown4j.Markdown4jProcessor
  21. import scala.collection.mutable
  22. import scala.concurrent.ExecutionContext.Implicits.global
  23. import scala.concurrent._
  24. import scala.util.{Failure, Success}
  25. /**
  26. * Main Application controller
  27. *
  28. * Created by drew on 12/31/13.
  29. */
  30. class AppController extends Logging {
  31. // Engine
  32. val engine: Engine = new ConcurrentEngine()
  33. //Define controls
  34. @FXML var rootPane: AnchorPane = null
  35. @FXML var rootMenuBar: MenuBar = null
  36. @FXML var scrollPane: ScrollPane = null
  37. @FXML var imageTilePane: TilePane = null
  38. @FXML var tagListView: ListView[String] = null
  39. // Labels
  40. @FXML var selectedDirectoryLabel: Label = null
  41. @FXML var currentDirectoryLabel: Label = null
  42. @FXML var progressLabel: Label = null
  43. // Others
  44. @FXML var progressBar: ProgressBar = null
  45. @FXML var paginator: Pagination = null
  46. @FXML var doRecursiveProcessing: CheckBox = null
  47. // Current State
  48. var currentDirectory: String = "."
  49. var currentImages: List[Image] = List[Image]()
  50. @FXML def initialize() = {
  51. if (PropertiesService.has("app.ui.lastPath")) {
  52. currentDirectory = PropertiesService.get("app.ui.lastPath", ".")
  53. selectedDirectoryLabel.setText(PropertiesService.get("app.ui.lastPath", ""))
  54. }
  55. //setup the engine listener
  56. val system: ActorSystem = AppConfig.getAppActorSystem
  57. val guiListenerProps: Props = Props.create(classOf[GUIEngineListener])
  58. val guiListener: ActorRef = system.actorOf(guiListenerProps)
  59. // configure the listener
  60. guiListener ! SetupListener(progressBar, progressLabel)
  61. // tell the engine to use our listener
  62. this.engine.setSearchedListener(guiListener)
  63. this.engine.setProcessedListener(guiListener)
  64. this.engine.setSimilarityListener(guiListener)
  65. // Initialize the progress label
  66. guiListener ! SubmitMessage("Initialized System... Ready!")
  67. // set the default images per page if it doesn't exist yet
  68. if (!PropertiesService.has("app.ui.thumbsPerPage")) {
  69. PropertiesService.set("app.ui.thumbsPerPage", "100")
  70. }
  71. //setup the paginator
  72. //font size doesn't increase the size of the buttons
  73. //paginator.setStyle("-fx-font-size:13;")
  74. // configure the page factory
  75. paginator.setPageFactory(new Callback[Integer, Node]() {
  76. override def call(pageIndex: Integer): Node = {
  77. // do all of our display logic
  78. showPage(pageIndex)
  79. // override behavior to display anything
  80. new VBox()
  81. }
  82. })
  83. //override the imageTilePane
  84. debug("Replacing the default TilePane with a custom ImageTilePane")
  85. val newImageTilePane = new ImageTilePane()
  86. newImageTilePane.setHgap(this.imageTilePane.getHgap)
  87. newImageTilePane.setVgap(this.imageTilePane.getVgap)
  88. newImageTilePane.setMinHeight(this.imageTilePane.getMinHeight)
  89. newImageTilePane.setMinWidth(this.imageTilePane.getMinWidth)
  90. newImageTilePane.setMaxHeight(this.imageTilePane.getMaxHeight)
  91. newImageTilePane.setMaxWidth(this.imageTilePane.getMaxWidth)
  92. newImageTilePane.setPrefColumns(this.imageTilePane.getPrefColumns)
  93. newImageTilePane.setPrefRows(this.imageTilePane.getPrefRows)
  94. //newImageTilePane.setPrefTileHeight(this.imageTilePane.getPrefTileHeight)
  95. //newImageTilePane.setPrefTileWidth(this.imageTilePane.getPrefTileWidth)
  96. newImageTilePane.setTileAlignment(this.imageTilePane.getTileAlignment)
  97. debug("Assigning the the new ImageTilePane to the ScrollPane")
  98. this.scrollPane.setContent(newImageTilePane)
  99. this.imageTilePane = newImageTilePane
  100. //test
  101. //val testImage = new Image()
  102. //testImage.setThumbnailPath("test.jpg")
  103. //testImage.setImagePath("test.jpg")
  104. //val testImageList = new mutable.MutableList[Image]
  105. //for (i <- 1 to 100) {
  106. // testImageList += testImage
  107. //}
  108. //setPagesContent(testImageList.toList)
  109. //showPage(0)
  110. //val list = FXCollections.observableArrayList[String]()
  111. //for (i <- 1 to 100) {
  112. // list.add(s"test-item ${i}")
  113. //}
  114. //tagListView.setItems(list)
  115. }
  116. //region MenuItem Actions
  117. @FXML
  118. def helpAction(event: ActionEvent) = {
  119. showExternalHTMLUtilityDialog("http://www.sothr.com")
  120. }
  121. def showExternalHTMLUtilityDialog(url: String) = {
  122. val dialog: Stage = new Stage()
  123. dialog.initStyle(StageStyle.UTILITY)
  124. val parent: Group = new Group()
  125. //setup the HTML view
  126. val htmlView = new WebView
  127. htmlView.getEngine.load(url)
  128. //htmlView.setMinWidth(width)
  129. //htmlView.setMinHeight(height)
  130. //htmlView.setPrefWidth(width)
  131. //htmlView.setPrefHeight(height)
  132. parent.getChildren.add(htmlView)
  133. val scene: Scene = new Scene(parent)
  134. dialog.setScene(scene)
  135. dialog.setResizable(false)
  136. dialog.setTitle(htmlView.getEngine.getTitle)
  137. dialog.show()
  138. }
  139. @FXML
  140. def aboutAction(event: ActionEvent) = {
  141. debug("Displaying about screen")
  142. var aboutMessage = "Simple About Message"
  143. try {
  144. val scanner = new Scanner(ResourceLoader.get().getResourceStream("documents/about.md"))
  145. aboutMessage = ""
  146. while (scanner.hasNextLine) {
  147. aboutMessage += scanner.nextLine().trim() + "\n"
  148. }
  149. debug(s"Parsed About Message: '$aboutMessage'")
  150. } catch {
  151. case ioe: IOException =>
  152. error("Unable to read about file")
  153. }
  154. showMarkdownUtilityDialog("About", aboutMessage, 400.0, 300.0)
  155. debug("Showing About Dialog")
  156. }
  157. //endregion
  158. //region buttons
  159. //todo: show a dialog that is rendered from markdown content
  160. def showMarkdownUtilityDialog(title: String, markdown: String, width: Double = 800.0, height: Double = 600.0) = {
  161. val htmlBody = new Markdown4jProcessor().process(markdown)
  162. showHTMLUtilityDialog(title, htmlBody, width, height)
  163. }
  164. /**
  165. * Render HTML content to a utility dialog. No input or output, just raw rendered content through a webkit engine.
  166. *
  167. * @param title Title of the dialog
  168. * @param htmlBody Body to render
  169. * @param width Desired width of the dialog
  170. * @param height Desired height of the dialog
  171. */
  172. def showHTMLUtilityDialog(title: String, htmlBody: String, width: Double = 800.0, height: Double = 600.0) = {
  173. val dialog: Stage = new Stage()
  174. dialog.initStyle(StageStyle.UTILITY)
  175. val parent: Group = new Group()
  176. //setup the HTML view
  177. val htmlView = new WebView
  178. htmlView.getEngine.loadContent(htmlBody)
  179. htmlView.setMinWidth(width)
  180. htmlView.setMinHeight(height)
  181. htmlView.setPrefWidth(width)
  182. htmlView.setPrefHeight(height)
  183. parent.getChildren.add(htmlView)
  184. val scene: Scene = new Scene(parent)
  185. dialog.setScene(scene)
  186. dialog.setResizable(false)
  187. dialog.setTitle(title)
  188. dialog.show()
  189. }
  190. @FXML
  191. def closeAction(event: ActionEvent) = {
  192. debug("Closing application from the menu bar")
  193. val stage: Stage = this.rootMenuBar.getScene.getWindow.asInstanceOf[Stage]
  194. stage.close()
  195. }
  196. //endregion
  197. //region pagination
  198. @FXML
  199. def browseFolders(event: ActionEvent) = {
  200. val chooser = new DirectoryChooser()
  201. chooser.setTitle("ImageTools Browser")
  202. try {
  203. val defaultDirectory = new File(currentDirectory)
  204. chooser.setInitialDirectory(defaultDirectory)
  205. val window = this.rootPane.getScene.getWindow
  206. val selectedDirectory = chooser.showDialog(window)
  207. info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}")
  208. selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
  209. currentDirectory = selectedDirectory.getAbsolutePath
  210. PropertiesService.set("app.ui.lastPath", selectedDirectory.getAbsolutePath)
  211. this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
  212. } catch {
  213. // fall back on the default because the directory we tried probably didn't exist
  214. case iae: IllegalArgumentException =>
  215. logger.error("The old directory no longer exists", iae)
  216. chooser.setInitialDirectory(null)
  217. val window = this.rootPane.getScene.getWindow
  218. val selectedDirectory = chooser.showDialog(window)
  219. info(s"Selected Directory: ${selectedDirectory.getAbsolutePath}")
  220. selectedDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
  221. currentDirectory = selectedDirectory.getAbsolutePath
  222. PropertiesService.set("app.ui.lastPath", selectedDirectory.getAbsolutePath)
  223. this.currentDirectoryLabel.setText(selectedDirectory.getAbsolutePath)
  224. }
  225. }
  226. @FXML
  227. def showAllImages(event: ActionEvent) = {
  228. resetPaginator()
  229. getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
  230. val f: Future[List[Image]] = Future {
  231. val images = engine.getImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
  232. images.sortWith((x, y) => x.imagePath < y.imagePath)
  233. }
  234. f onComplete {
  235. case Success(images) =>
  236. info(s"Displaying ${images.length} images")
  237. // This is used so that JavaFX updates on the proper thread
  238. // This is important since UI updates can only happen on that thread
  239. Platform.runLater(new Runnable() {
  240. override def run() {
  241. setPagesContent(images)
  242. showPage(0)
  243. }
  244. })
  245. case Failure(t) =>
  246. error("An Error Occurred", t)
  247. }
  248. }
  249. def resetPaginator() = {
  250. this.paginator.setDisable(true)
  251. this.paginator.setPageCount(1)
  252. }
  253. //endregion
  254. def setPagesContent(images: List[Image]) = {
  255. this.currentImages = images
  256. //set the appropriate size for the pagination
  257. val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
  258. val pageNum = Math.ceil(this.currentImages.size.toFloat / itemsPerPage).toInt
  259. this.paginator.setPageCount(pageNum)
  260. this.paginator.setDisable(false)
  261. }
  262. //todo: include a templating engine for rendering information
  263. def showPage(pageIndex: Integer) = {
  264. val itemsPerPage = PropertiesService.get("app.ui.thumbsPerPage", "100").toInt
  265. val startIndex = pageIndex * itemsPerPage
  266. val endIndex = if ((startIndex + itemsPerPage) > this.currentImages.size) this.currentImages.length else startIndex + itemsPerPage
  267. //clear any selections
  268. getImageTilePane.asInstanceOf[ImageTilePane].clearSelection()
  269. //clear and populate the scrollpane
  270. getImageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
  271. val images = this.currentImages.slice(startIndex, endIndex)
  272. Platform.runLater(new Runnable() {
  273. override def run() {
  274. for (image <- images) {
  275. debug(s"Adding image ${image.toString} to app")
  276. getImageTilePane.getChildren.add(ImageTileFactory.get(image, getImageTilePane))
  277. }
  278. }
  279. })
  280. }
  281. def getImageTilePane: TilePane = {
  282. this.imageTilePane
  283. }
  284. @FXML
  285. def showSimilarImages(event: ActionEvent) = {
  286. resetPaginator()
  287. imageTilePane.getChildren.setAll(new java.util.ArrayList[Node]())
  288. val f: Future[List[Image]] = Future {
  289. val similarImages = engine.getSimilarImagesForDirectory(currentDirectory, recursive = doRecursiveProcessing.isSelected)
  290. val tempImages = new mutable.MutableList[Image]()
  291. for (similarImage <- similarImages) {
  292. debug(s"Adding similar images ${similarImage.toString} to app")
  293. similarImage.similarImages.foreach(image => tempImages += image)
  294. }
  295. tempImages.toList
  296. }
  297. f onComplete {
  298. case Success(images) =>
  299. info(s"Displaying ${images.length} similar images")
  300. Platform.runLater(new Runnable() {
  301. override def run() {
  302. setPagesContent(images)
  303. showPage(0)
  304. }
  305. })
  306. case Failure(t) =>
  307. error("An Error Occurred", t)
  308. }
  309. }
  310. /**
  311. * Show a plain text utility dialog
  312. *
  313. * @param message Message to display
  314. * @param wrapWidth When to wrap
  315. * @param alignment How it should be aligned
  316. */
  317. def showUtilityDialog(title: String,
  318. message: String,
  319. wrapWidth: Double = 300.0,
  320. xOffset: Double = 25.0,
  321. yOffset: Double = 25.0,
  322. alignment: TextAlignment = TextAlignment.JUSTIFY) = {
  323. val dialog: Stage = new Stage()
  324. dialog.initStyle(StageStyle.UTILITY)
  325. val parent: Group = new Group()
  326. // fill the text box
  327. val messageText = new Text()
  328. messageText.setText(message)
  329. messageText.setWrappingWidth(wrapWidth)
  330. messageText.setX(xOffset)
  331. messageText.setY(yOffset)
  332. messageText.setTextAlignment(TextAlignment.JUSTIFY)
  333. parent.getChildren.add(messageText)
  334. val scene: Scene = new Scene(parent)
  335. dialog.setScene(scene)
  336. dialog.setResizable(false)
  337. dialog.setMinWidth(wrapWidth + xOffset * 2)
  338. dialog.setTitle(title)
  339. dialog.show()
  340. }
  341. def print(): String = {
  342. "This method works"
  343. }
  344. }
  345. //region EngineListener
  346. case class SetupListener(progressBar: ProgressBar, progressLabel: Label)
  347. /**
  348. * Actor for logging output information
  349. */
  350. class GUIEngineListener extends EngineListener with ActorLogging {
  351. var progressBar: javafx.scene.control.ProgressBar = null
  352. var progressLabel: javafx.scene.control.Label = null
  353. var isStarted = false
  354. var isFinished = false
  355. override def receive: Actor.Receive = {
  356. case command: SetupListener => setupListener(command)
  357. case command: SubmitMessage => handleMessage(command)
  358. case command: ScannedFileCount => handleScannedFileCount(command)
  359. case command: ComparedFileCount => handleComparedFileCount(command)
  360. case _ => log.info("received unknown message")
  361. }
  362. def setupListener(command: SetupListener) = {
  363. this.progressBar = command.progressBar
  364. this.progressLabel = command.progressLabel
  365. }
  366. override def handleComparedFileCount(command: ComparedFileCount): Unit = {
  367. Platform.runLater(new Runnable() {
  368. override def run(): Unit = {
  369. if (command.message != null) {
  370. log.debug(command.message)
  371. progressLabel.setText(command.message)
  372. } else {
  373. progressLabel.setText(s"Processed ${command.count}/${command.total}")
  374. }
  375. log.debug("Processed {}/{}", command.count, command.total)
  376. progressBar.setProgress(command.count.toFloat / command.total)
  377. }
  378. })
  379. }
  380. override def handleScannedFileCount(command: ScannedFileCount): Unit = {
  381. Platform.runLater(new Runnable() {
  382. override def run(): Unit = {
  383. if (command.message != null) {
  384. log.debug(command.message)
  385. progressLabel.setText(command.message)
  386. } else {
  387. progressLabel.setText(s"Scanned ${command.count}/${command.total} For Similarities")
  388. }
  389. log.debug("Scanned {}/{} For Similarities", command.count, command.total)
  390. progressBar.setProgress(command.count.toFloat / command.total)
  391. }
  392. })
  393. }
  394. override def handleMessage(command: SubmitMessage): Unit = {
  395. Platform.runLater(new Runnable() {
  396. override def run(): Unit = {
  397. log.debug(command.message)
  398. progressLabel.setText(command.message)
  399. }
  400. })
  401. }
  402. }
  403. //endregion