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.

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