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.

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