diff --git a/.gitignore b/.gitignore
index 7fe49ad3628bf886b4f10cd1ca3a0016c9c52af7..b89812646fed9df2e3607649e978a5ce6b05f605 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,7 @@ genfiles.properties
 *~
 /netbeans-plat
 /docs/doxygen/doxygen_docs
+/docs/doxygen-user/user-docs
 /jdiff-javadocs/*
 /jdiff-logs/*
 /gen_version.txt
@@ -74,3 +75,5 @@ Core/src/org/sleuthkit/autopsy/casemodule/docs/QuickStart.html
 Core/src/org/sleuthkit/autopsy/casemodule/docs/screenshot.png
 /test/script/myconfig.xml
 /test/script/*/*.xml
+.DS_Store
+.*.swp
diff --git a/Core/build.xml b/Core/build.xml
index 9dd720c2d6d49fe0cc7de45907fba7b8c2bb53d6..2d313a2de75f4ecc2d76a67a91f66da7ad0c3f8c 100644
--- a/Core/build.xml
+++ b/Core/build.xml
@@ -6,33 +6,10 @@
     <description>Builds, tests, and runs the project org.sleuthkit.autopsy.core</description>
     <import file="nbproject/build-impl.xml"/>
     
-    <target name="quickstart-add-builddir">
-         <!-- additional docs for quickstart -->
-        <echo message="building quick start guide" />
-        <mkdir dir="build/classes/org/sleuthkit/autopsy/casemodule/docs" />
-        <copy overwrite="true" file="${basedir}/../docs/QuickStartGuide/index.html" tofile="build/classes/org/sleuthkit/autopsy/casemodule/docs/QuickStart.html"/>
-        <copy overwrite="true" file="${basedir}/../docs/QuickStartGuide/screenshot.png" tofile="build/classes/org/sleuthkit/autopsy/casemodule/docs/screenshot.png"/>
-         
-    </target>
     
-      <target name="quickstart-add-src">
-         <!-- additional docs for quickstart -->
-        <echo message="building quick start guide 1" />
-        <mkdir dir="src/org/sleuthkit/autopsy/casemodule/docs" />
-        <copy overwrite="true" file="${basedir}/../docs/QuickStartGuide/index.html" tofile="src/org/sleuthkit/autopsy/casemodule/docs/QuickStart.html"/>
-        <copy overwrite="true" file="${basedir}/../docs/QuickStartGuide/screenshot.png" tofile="src/org/sleuthkit/autopsy/casemodule/docs/screenshot.png"/>
-         
-    </target>
-
-    <target name="quickstart-remove-src">
-         <!-- cleanup additional docs for quickstart -->
-        <echo message="building quick start guide 2" />
-        <delete file="src/org/sleuthkit/autopsy/casemodule/docs/QuickStart.html"/>
-        <delete file="src/org/sleuthkit/autopsy/casemodule/docs/screenshot.png"/>
-    </target>
     
     <target name="javahelp">
-         <antcall target="quickstart-remove-src" />
+         
     </target>
      
     <!-- Verify that the TSK_HOME env variable is set -->
@@ -53,12 +30,7 @@
 
   
     <target name="init" depends="basic-init,files-init,build-init,-javac-init">
-
         <!-- get additional deps -->
         <antcall target="getTSKJars" />
-        
-        <antcall target="quickstart-add-src" />
     </target>
-    
- 
 </project>
diff --git a/ImageAnalyzer/.gitattributes b/ImageAnalyzer/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..ea6aa14ebc2b6e62c55617c5a681521808e75d3c
--- /dev/null
+++ b/ImageAnalyzer/.gitattributes
@@ -0,0 +1,13 @@
+*.java                   text diff=java
+
+*.txt                   text
+*.sh                   text
+*.mf                   text
+*.xml                   text
+*.form                   text
+*.properties                   text
+*.html                   text diff=html
+*.dox                   text
+Doxyfile                text
+
+*.py                  text diff=python
diff --git a/ImageAnalyzer/.gitignore b/ImageAnalyzer/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..9baf66d36165ea3285adcd4b3e8d7692c614c58d
--- /dev/null
+++ b/ImageAnalyzer/.gitignore
@@ -0,0 +1,44 @@
+
+#ignore thumbnails created by windows
+Thumbs.db
+#Ignore files build by Visual Studio
+*.obj
+*.exe
+*.pdb
+*.user
+*.aps
+*.pch
+*.vspscc
+*_i.c
+*_p.c
+*.ncb
+*.suo
+*.tlb
+*.tlh
+*.bak
+*.cache
+*.ilk
+*.log
+[Bb]in
+[Dd]ebug*/
+*.lib
+*.sbr
+obj/
+
+_ReSharper*/
+[Tt]est[Rr]esult*
+
+#Ignore Java class files
+*.class
+build/*
+
+*/javadoc/*
+
+nbproject/private/*
+
+*.hprof
+*.nps
+
+
+
+
diff --git a/ImageAnalyzer/LICENSE-2.0.txt b/ImageAnalyzer/LICENSE-2.0.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/ImageAnalyzer/LICENSE-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/ImageAnalyzer/build.xml b/ImageAnalyzer/build.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4cbe0d1139dc67398397732492952b226228b632
--- /dev/null
+++ b/ImageAnalyzer/build.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- You may freely edit this file. See harness/README in the NetBeans platform -->
+<!-- for some information on what you could do (e.g. targets to override). -->
+<!-- If you delete this file and reopen the project it will be recreated. -->
+<project name="com.basistech.imageanalysis" default="netbeans" basedir=".">
+    <description>Builds, tests, and runs the project com.basistech.imageanalysis.</description>
+    <import file="nbproject/build-impl.xml"/>
+</project>
diff --git a/ImageAnalyzer/manifest.mf b/ImageAnalyzer/manifest.mf
new file mode 100644
index 0000000000000000000000000000000000000000..537c9cf8b0c579a62a14b5dd04cfac05de0096c0
--- /dev/null
+++ b/ImageAnalyzer/manifest.mf
@@ -0,0 +1,5 @@
+Manifest-Version: 1.0
+OpenIDE-Module: org.sleuthkit.autopsy.imageanalyzer
+OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/imageanalyzer/Bundle.properties
+OpenIDE-Module-Specification-Version: 1.0
+
diff --git a/ImageAnalyzer/nbproject/build-impl.xml b/ImageAnalyzer/nbproject/build-impl.xml
new file mode 100644
index 0000000000000000000000000000000000000000..90eec380dc6f8998fa9cafbb2c1e0bb1a8963fb2
--- /dev/null
+++ b/ImageAnalyzer/nbproject/build-impl.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+*** GENERATED FROM project.xml - DO NOT EDIT  ***
+***         EDIT ../build.xml INSTEAD         ***
+-->
+<project name="org.sleuthkit.autopsy.imageanalyzer-impl" basedir="..">
+   <fail message="Please build using Ant 1.7.1 or higher.">
+        <condition>
+            <not>
+                <antversion atleast="1.7.1"/>
+            </not>
+        </condition>
+    </fail>
+    <property file="nbproject/private/suite-private.properties"/>
+    <property file="nbproject/suite.properties"/>
+    <fail unless="suite.dir">You must set 'suite.dir' to point to your containing module suite</fail>
+    <property file="${suite.dir}/nbproject/private/platform-private.properties"/>
+    <property file="${suite.dir}/nbproject/platform.properties"/>
+    <macrodef name="property" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="name"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{name}" value="${@{value}}"/>
+        </sequential>
+    </macrodef>
+    <macrodef name="evalprops" uri="http://www.netbeans.org/ns/nb-module-project/2">
+        <attribute name="property"/>
+        <attribute name="value"/>
+        <sequential>
+            <property name="@{property}" value="@{value}"/>
+        </sequential>
+    </macrodef>
+    <property file="${user.properties.file}"/>
+    <nbmproject2:property name="harness.dir" value="nbplatform.${nbplatform.active}.harness.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:property name="nbplatform.active.dir" value="nbplatform.${nbplatform.active}.netbeans.dest.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <nbmproject2:evalprops property="cluster.path.evaluated" value="${cluster.path}" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/>
+    <fail message="Path to 'platform' cluster missing in $${cluster.path} property or using corrupt Netbeans Platform (missing harness).">
+        <condition>
+            <not>
+                <contains string="${cluster.path.evaluated}" substring="platform"/>
+            </not>
+        </condition>
+    </fail>
+    <import file="${harness.dir}/build.xml"/>
+</project>
diff --git a/ImageAnalyzer/nbproject/genfiles.properties b/ImageAnalyzer/nbproject/genfiles.properties
new file mode 100644
index 0000000000000000000000000000000000000000..96475b54027488db631912d1bc90e97456b1f298
--- /dev/null
+++ b/ImageAnalyzer/nbproject/genfiles.properties
@@ -0,0 +1,8 @@
+build.xml.data.CRC32=673987f7
+build.xml.script.CRC32=eee7b5a4
+build.xml.stylesheet.CRC32=a56c6a5b@2.56.1
+# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
+# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
+nbproject/build-impl.xml.data.CRC32=673987f7
+nbproject/build-impl.xml.script.CRC32=92d95aee
+nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.56.1
diff --git a/ImageAnalyzer/nbproject/project.properties b/ImageAnalyzer/nbproject/project.properties
new file mode 100644
index 0000000000000000000000000000000000000000..259ddd1e7dda6b0126cd1d1c9ab0825146a01e21
--- /dev/null
+++ b/ImageAnalyzer/nbproject/project.properties
@@ -0,0 +1,13 @@
+file.reference.commons-pool2-2.0-javadoc.jar=release/modules/ext/commons-pool2-2.0-javadoc.jar
+file.reference.commons-pool2-2.0-sources.jar=release/modules/ext/commons-pool2-2.0-sources.jar
+file.reference.commons-pool2-2.0.jar=release/modules/ext/commons-pool2-2.0.jar
+file.reference.controlsfx-8.0.6.jar=release/modules/ext/controlsfx-8.0.6.jar
+file.reference.jcip-annotations-src.jar=release/modules/ext/jcip-annotations-src.jar
+file.reference.jcip-annotations.jar=release/modules/ext/jcip-annotations.jar
+file.reference.sqlite-jdbc-3.7.8-SNAPSHOT.jar=release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar
+javac.source=1.8
+javac.compilerargs=-Xlint -Xlint:-serial
+license.file=LICENSE-2.0.txt
+nbm.homepage=http://www.sleuthkit.org/
+nbm.needs.restart=true
+project.license=eureka
diff --git a/ImageAnalyzer/nbproject/project.xml b/ImageAnalyzer/nbproject/project.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7b109729493607c535e167285addf64097b12856
--- /dev/null
+++ b/ImageAnalyzer/nbproject/project.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.apisupport.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/nb-module-project/3">
+            <code-name-base>org.sleuthkit.autopsy.imageanalyzer</code-name-base>
+            <suite-component/>
+            <module-dependencies>
+                <dependency>
+                    <code-name-base>org.netbeans.api.progress</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.32.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.options.api</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.31.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.settings</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>1</release-version>
+                        <specification-version>1.38.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.awt</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.55.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.dialogs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.28.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.explorer</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.50.3</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.modules</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.35.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.nodes</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.33.2</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.29.3</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.util.lookup</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>8.19.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.openide.windows</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>6.60.1</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.sleuthkit.autopsy.core</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>10</release-version>
+                        <specification-version>10.0.11</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
+                    <code-name-base>org.sleuthkit.autopsy.corelibs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <release-version>3</release-version>
+                        <specification-version>1.1</specification-version>
+                    </run-dependency>
+                </dependency>
+            </module-dependencies>
+            <public-packages/>
+            <class-path-extension>
+                <runtime-relative-path>ext/controlsfx-8.0.6.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/controlsfx-8.0.6.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jcip-annotations.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/jcip-annotations.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/commons-pool2-2.0-sources.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/commons-pool2-2.0-sources.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/commons-pool2-2.0.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/commons-pool2-2.0.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jcip-annotations-src.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/jcip-annotations-src.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/commons-pool2-2.0-javadoc.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/commons-pool2-2.0-javadoc.jar</binary-origin>
+            </class-path-extension>
+        </data>
+    </configuration>
+</project>
diff --git a/ImageAnalyzer/nbproject/suite.properties b/ImageAnalyzer/nbproject/suite.properties
new file mode 100644
index 0000000000000000000000000000000000000000..29d7cc9bd6fdd81453543cdf1bcf1dab301e3a92
--- /dev/null
+++ b/ImageAnalyzer/nbproject/suite.properties
@@ -0,0 +1 @@
+suite.dir=${basedir}/..
diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar
new file mode 100644
index 0000000000000000000000000000000000000000..1d1e4226b3ae330e68c7a482942f49eab56627ff
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2dfe051f95116795fa0976ba1c77ef1a781de728
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar
new file mode 100644
index 0000000000000000000000000000000000000000..be6d84f92eaf67b9986cc19fc95a5795ef4ff131
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2f2d1badd0facb374d5c2519ce3525ff8693c222
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar
new file mode 100644
index 0000000000000000000000000000000000000000..39a73c0b9acb47fb4ab23fc5922313885d0e8e91
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar b/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar
new file mode 100644
index 0000000000000000000000000000000000000000..bf52a507df2418b68041436fa3df27c4176cc494
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/jcip-annotations.jar b/ImageAnalyzer/release/modules/ext/jcip-annotations.jar
new file mode 100644
index 0000000000000000000000000000000000000000..ea263af054bd0e3403fb131c3961e8a4964d07e6
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/jcip-annotations.jar differ
diff --git a/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar b/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar
new file mode 100644
index 0000000000000000000000000000000000000000..bcea83745ab32246e315dd0c53209dc6a0cd39b9
Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..5adbb2512d8d51c92e2a043a6c308a6a86c7b9e6
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java
@@ -0,0 +1,165 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import javax.swing.SwingUtilities;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Content;
+
+/** Singleton aggregator for listeners that hook into case and ingest modules.
+ * This class depends on clients to hook up listeners to Autopsy. */
+public class AutopsyListener {
+
+    private static final Logger LOGGER = Logger.getLogger(AutopsyListener.class.getName());
+
+    private final EurekaController controller = EurekaController.getDefault();
+
+    private final PropertyChangeListener ingestJobEventListener = new IngestJobEventListener();
+
+    private final PropertyChangeListener ingestModuleEventListener = new IngestModuleEventListener();
+
+    private final PropertyChangeListener caseListener = new CaseListener();
+
+    public PropertyChangeListener getIngestJobEventListener() {
+        return ingestJobEventListener;
+    }
+
+    public PropertyChangeListener getIngestModuleEventListener() {
+        return ingestModuleEventListener;
+    }
+
+    public PropertyChangeListener getCaseListener() {
+        return caseListener;
+    }
+
+    private static AutopsyListener instance;
+
+    private AutopsyListener() {
+    }
+
+    synchronized public static AutopsyListener getDefault() {
+        if (instance == null) {
+            instance = new AutopsyListener();
+        }
+        return instance;
+    }
+
+    /** listener for ingest events */
+    private class IngestJobEventListener implements PropertyChangeListener {
+
+        @Override
+        synchronized public void propertyChange(PropertyChangeEvent evt) {
+            switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) {
+                case COMPLETED:
+                    if (controller.isListeningEnabled()) {
+                        if (IngestManager.getInstance().isIngestRunning() == false) {
+                            // @@@ Add some logic to not do this if we've done it in the past second
+                            controller.queueTask(controller.new MarkAllFilesAsAnalyzed());
+                        }
+                    } else {
+                        //TODO can we do anything usefull here?
+                    }
+            }
+        }
+    }
+
+    /** listener for ingest events */
+    private class IngestModuleEventListener implements PropertyChangeListener {
+
+        @Override
+        synchronized public void propertyChange(PropertyChangeEvent evt) {
+            switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
+
+                case CONTENT_CHANGED:
+                    //TODO: do we need to do anything here?  -jm
+                    break;
+                case DATA_ADDED:
+                    /* we could listen to DATA events and progressivly update
+                     * files, and get data from DataSource ingest modules, but
+                     * given that most modules don't post new artifacts in the
+                     * events and eureka would have to query for them, without
+                     * knowing which are the new ones, we just ignore these
+                     * events for now. The relevant data should all be captured
+                     * by file done event, anyways -jm */
+                    break;
+                case FILE_DONE:
+                    /** getOldValue has fileID, getNewValue has {@link Abstractfile}
+                     *
+                     * {@link IngestManager#fireModuleDataEvent(org.sleuthkit.autopsy.ingest.ModuleDataEvent) fireModuleDataEvent} */
+                    AbstractFile file = (AbstractFile) evt.getNewValue();
+                    if (controller.isListeningEnabled()) {
+
+                        if (EurekaModule.isSupportedAndNotKnown(file)) {
+                            //this file should be included and we don't already know about it from hash sets (NSRL)
+                            controller.queueTask(controller.new UpdateFile(file));
+                        } else if (EurekaModule.getAllSupportedExtensions().contains(file.getNameExtension())) {
+                            //doing this check results in fewer tasks queued up, and faster completion of db update
+                            //this file would have gotten scooped up in initial grab, but actually we don't need it
+                            controller.queueTask(controller.new RemoveFile(file));
+                        }
+                    } else {
+                        controller.setStale(true);
+                        //TODO: keep track of waht we missed for later
+                    }
+                    break;
+            }
+        }
+    }
+
+    /** listener for case events */
+    private class CaseListener implements PropertyChangeListener {
+
+        @Override
+        synchronized public void propertyChange(PropertyChangeEvent evt) {
+
+            switch (Case.Events.valueOf(evt.getPropertyName())) {
+                case CURRENT_CASE:
+                    Case newCase = (Case) evt.getNewValue();
+                    if (newCase != null) { // case has been opened
+                        //connect db, groupmanager, start worker thread
+                        controller.setCase(newCase);
+
+                    } else { // case is closing
+                        //close eureka window
+                        SwingUtilities.invokeLater(EurekaModule::closeTopComponent);
+                        controller.reset();
+                    }
+                    break;
+
+                case DATA_SOURCE_ADDED:
+                    //copy all file data to eureka databse
+                    Content newDataSource = (Content) evt.getNewValue();
+                    if (controller.isListeningEnabled()) {
+                        controller.queueTask(controller.new PrePopulateDataSourceFiles(newDataSource.getId()));
+                    } else {
+                        controller.setStale(true);
+                        //TODO: keep track of what we missed for later
+                    }
+                    break;
+            }
+        }
+    }
+
+ 
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..5e5062ea192208f43fc214b3ed8e4429a4b511e9
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties
@@ -0,0 +1,8 @@
+OpenIDE-Module-Long-Description=\
+    New image and video analyzer that has been designed to make performing image-intensive investigations more efficient.  \
+    This work has been funded by DHS S&T and this is a beta release. \
+    It is not available on the sleuthkit.org site and has been distributed to limited users.
+OpenIDE-Module-Name=Image Analysis (Beta)
+OpenIDE-Module-Short-Description=Advanced image and video analyzer
+EurekaOptionsPanel.enabledByDefaultBox.text=Enable listening to ingest for new cases.
+EurekaOptionsPanel.enabledForCaseBox.text=Enable listening to ingest for current case.  
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3c9a106e625858d3b463edf5a8c967d1ee8cc3c
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java
@@ -0,0 +1,883 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyIntegerProperty;
+import javafx.beans.property.ReadOnlyIntegerWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.FXCollections;
+import javafx.concurrent.Task;
+import javafx.concurrent.Worker;
+import static javafx.concurrent.Worker.State.CANCELLED;
+import static javafx.concurrent.Worker.State.FAILED;
+import static javafx.concurrent.Worker.State.READY;
+import static javafx.concurrent.Worker.State.RUNNING;
+import static javafx.concurrent.Worker.State.SCHEDULED;
+import static javafx.concurrent.Worker.State.SUCCEEDED;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ProgressIndicator;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import net.jcip.annotations.GuardedBy;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.netbeans.api.progress.ProgressHandle;
+import org.netbeans.api.progress.ProgressHandleFactory;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+import org.sleuthkit.autopsy.imageanalyzer.gui.EurekaToolbar;
+import org.sleuthkit.autopsy.imageanalyzer.gui.NoGroupsDialog;
+import org.sleuthkit.autopsy.imageanalyzer.gui.SummaryTablePane;
+import org.sleuthkit.autopsy.imageanalyzer.progress.ProgressAdapterBase;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.TskData;
+
+/**
+ * Acts as the controller in GroupManager - GroupListPane - EurekaController MVC
+ * Trio
+ *
+ * Connects different parts of Eureka together and is hub for flow of control.
+ */
+public class EurekaController implements FileUpdateListener {
+
+    private static final Logger LOGGER = Logger.getLogger(EurekaController.class.getName());
+
+    private final Region infoOverLayBackground = new Region() {
+        {
+            setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
+            setOpacity(.4);
+        }
+    };
+
+    private static EurekaController instance;
+
+    public static synchronized EurekaController getDefault() {
+        if (instance == null) {
+            instance = new EurekaController();
+        }
+        return instance;
+    }
+
+    /** list based stack to hold history, 'top' is at index 0; */
+    @javax.annotation.concurrent.GuardedBy("this")
+    private final ObservableStack<GroupViewState> historyStack = new ObservableStack<>();
+
+    @javax.annotation.concurrent.GuardedBy("this")
+    private final ObservableStack<GroupViewState> forwardStack = new ObservableStack<>();
+
+    private final ReadOnlyBooleanWrapper listeningEnabled = new ReadOnlyBooleanWrapper(false);
+
+    private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
+
+    private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
+
+    private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);
+
+    private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
+
+    private final FileIDSelectionModel selectionModel = FileIDSelectionModel.getInstance();
+
+    private DBWorkerThread dbWorkerThread;
+
+    private DrawableDB db;
+
+    private final GroupManager groupManager = new GroupManager(this);
+
+    private StackPane fullUIStackPane;
+
+    private StackPane centralStackPane;
+
+    private Node infoOverlay;
+
+    synchronized public ObservableStack<GroupViewState> getHistoryStack() {
+        return historyStack;
+    }
+
+    synchronized public ObservableStack<GroupViewState> getForwardStack() {
+        return forwardStack;
+    }
+
+    public ReadOnlyBooleanProperty getMetaDataCollapsed() {
+        return metaDataCollapsed.getReadOnlyProperty();
+    }
+
+    public void setMetaDataCollapsed(Boolean metaDataCollapsed) {
+        this.metaDataCollapsed.set(metaDataCollapsed);
+    }
+
+    private GroupViewState getViewState() {
+        return viewState.get();
+    }
+
+    public ReadOnlyBooleanProperty regroupDisabled() {
+        return regroupDisabled.getReadOnlyProperty();
+    }
+
+    private final ReadOnlyObjectWrapper<GroupViewState> viewState = new ReadOnlyObjectWrapper<>();
+
+    private void setViewState(GroupViewState newViewState) {
+        Platform.runLater(() -> {
+            viewState.set(newViewState);
+        });
+    }
+
+    public ReadOnlyObjectProperty<GroupViewState> viewState() {
+        return viewState.getReadOnlyProperty();
+    }
+
+    /**
+     * the list of tasks queued to run in the uiBGTaskExecutor. By keeping this
+     * list we can cancel them more gracefully than by {@link ExecutorService#shutdownNow()
+     */
+    @GuardedBy("bgTasks")
+    private final SimpleListProperty<Future<?>> bgTasks = new SimpleListProperty<>(FXCollections.observableArrayList());
+
+    /** an executor to submit async ui related background tasks to. */
+    final ExecutorService bgTaskExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("ui task -%d").build());
+
+    public synchronized FileIDSelectionModel getSelectionModel() {
+
+        return selectionModel;
+    }
+
+    public GroupManager getGroupManager() {
+        return groupManager;
+    }
+
+    public void setListeningEnabled(boolean enabled) {
+        listeningEnabled.set(enabled);
+    }
+
+    ReadOnlyBooleanProperty listeningEnabled() {
+        return listeningEnabled.getReadOnlyProperty();
+    }
+
+    boolean isListeningEnabled() {
+        return listeningEnabled.get();
+    }
+
+    void setStale(Boolean b) {
+        Platform.runLater(() -> {
+            stale.set(b);
+        });
+        if (Case.isCaseOpen()) {
+            new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.STALE, b.toString());
+        }
+    }
+
+    public ReadOnlyBooleanProperty stale() {
+        return stale.getReadOnlyProperty();
+    }
+
+    boolean isStale() {
+        return stale.get();
+    }
+
+    private EurekaController() {
+
+        listeningEnabled.addListener((observable, oldValue, newValue) -> {
+            if (newValue && !oldValue && Case.existsCurrentCase() && EurekaModule.isCaseStale(Case.getCurrentCase())) {
+                queueTask(new CopyAnalyzedFiles());
+            }
+        });
+
+        groupManager.getAnalyzedGroups().addListener((Observable o) -> {
+            checkForGroups();
+        });
+
+        groupManager.getUnSeenGroups().addListener((Observable observable) -> {
+            if (groupManager.getUnSeenGroups().size() > 0 && (getViewState() == null || getViewState().getGroup() == null)) {
+                setViewState(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)));
+            }
+        });
+        regroupDisabled.addListener((Observable observable) -> {
+            checkForGroups();
+        });
+
+        IngestManager.getInstance().addIngestModuleEventListener((evt) -> {
+            Platform.runLater(this::updateRegroupDisabled);
+        });
+        IngestManager.getInstance().addIngestJobEventListener((evt) -> {
+            Platform.runLater(this::updateRegroupDisabled);
+        });
+//        metaDataCollapsed.bind(EurekaToolbar.getDefault().showMetaDataProperty());
+    }
+
+    /** submit a background {@link Task} to be queued for execution
+     * by the thread pool.
+     *
+     * @param task */
+    public void submitBGTask(final Task<?> task) {
+        //listen to task state and remove task from list of tasks once it is 'done'
+        task.stateProperty().addListener((observableState, oldState, newState) -> {
+            switch (newState) {
+                case READY:
+                case SCHEDULED:
+                case RUNNING:
+                    break;
+
+                case FAILED:
+                    LOGGER.log(Level.WARNING, "task :" + task.getTitle() + " failed", task.getException());
+                case CANCELLED:
+                case SUCCEEDED:
+                    Platform.runLater(() -> {
+                        synchronized (bgTasks) {
+                            bgTasks.remove(task);
+                        }
+                    });
+                    break;
+            }
+        });
+
+        synchronized (bgTasks) {
+            bgTasks.add(task);
+        }
+
+        bgTaskExecutor.execute(task);
+    }
+
+    synchronized public void pushGroup(GroupViewState viewState, boolean force) {
+        final GroupViewState currentViewState = getViewState();
+
+        if (force || Objects.equals(currentViewState, viewState) == false) {
+            historyStack.push(currentViewState);
+            setViewState(viewState);
+            if (viewState.equals(forwardStack.peek())) {
+                forwardStack.pop();
+            } else {
+                forwardStack.clear();
+            }
+        }
+    }
+
+    synchronized public GroupViewState goForward() {
+
+        final GroupViewState currentZoom = getViewState();
+
+        final GroupViewState fpeek = forwardStack.peek();
+
+        if (fpeek != null && currentZoom.equals(fpeek) == false) {
+            historyStack.push(currentZoom);
+            setViewState(fpeek);
+            forwardStack.pop();
+        }
+        return fpeek;
+    }
+
+    synchronized public GroupViewState goBack() {
+        final GroupViewState currentZoom = getViewState();
+        final GroupViewState peek = historyStack.peek();
+
+        if (peek != null && peek.equals(currentZoom) == false) {
+            forwardStack.push(currentZoom);
+            setViewState(historyStack.pop());
+        } else if (peek != null && peek.equals(currentZoom)) {
+            historyStack.pop();
+            return goBack();
+        }
+        return peek;
+    }
+
+    private void updateRegroupDisabled() {
+        regroupDisabled.set(getFileUpdateQueueSizeProperty().get() > 0 || IngestManager.getInstance().isIngestRunning());
+    }
+
+    /** Check if there are any fully analyzed groups available
+     * from the GroupManager and remove blocking progress spinners if there
+     * are. If there aren't, add a blocking progress spinner with appropriate
+     * message. */
+    public final void checkForGroups() {
+        if (groupManager.getAnalyzedGroups().isEmpty()) {
+            setViewState(null);
+            if (IngestManager.getInstance().isIngestRunning()) {
+                if (listeningEnabled.get() == false) {
+                    replaceNotification(fullUIStackPane,
+                                        new NoGroupsDialog("No groups are fully analyzed but listening to ingest is disabled. "
+                                                + " No groups will be available until ingest is finished and listening is re-enabled."));
+                } else {
+                    replaceNotification(fullUIStackPane,
+                                        new NoGroupsDialog("No groups are fully analyzed yet, but ingest is still ongoing.  Please Wait.",
+                                                           new ProgressIndicator()));
+                }
+
+            } else if (getFileUpdateQueueSizeProperty().get() > 0) {
+                replaceNotification(fullUIStackPane,
+                                    new NoGroupsDialog("No groups are fully analyzed yet, but image / video data is still being populated.  Please Wait.",
+                                                       new ProgressIndicator()));
+            } else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
+                replaceNotification(fullUIStackPane,
+                                    new NoGroupsDialog("There are no images/videos in the added datasources."));
+
+            } else if (!groupManager.isRegrouping()) {
+                replaceNotification(centralStackPane,
+                                    new NoGroupsDialog("There are no fully analyzed groups to display:"
+                                            + "  the current Group By setting resulted in no groups, "
+                                            + "or no groups are fully analyzed but ingest is not running."));
+            }
+//            else {
+//                replaceNotification(fullUIStackPane,
+//                                    new NoGroupsDialog("Please wait while the images/videos are re grouped.",
+//                                            new ProgressIndicator()));
+//
+//            }
+            // }
+
+        } else {
+            clearNotification();
+        }
+    }
+
+    private void clearNotification() {
+        //remove the ingest spinner
+        if (fullUIStackPane != null) {
+            fullUIStackPane.getChildren().remove(infoOverlay);
+        }
+        //remove the ingest spinner
+        if (centralStackPane != null) {
+            centralStackPane.getChildren().remove(infoOverlay);
+        }
+    }
+
+    private void replaceNotification(StackPane stackPane, Node newNode) {
+        clearNotification();
+
+        infoOverlay = new StackPane(infoOverLayBackground, newNode);
+        if (stackPane != null) {
+            stackPane.getChildren().add(infoOverlay);
+        }
+    }
+
+    private void restartWorker() {
+        if (dbWorkerThread != null) {
+            dbWorkerThread.cancelAllTasks();
+        }
+        dbWorkerThread = new DBWorkerThread();
+
+        getFileUpdateQueueSizeProperty().addListener((o) -> {
+            Platform.runLater(this::updateRegroupDisabled);
+        });
+
+        Thread th = new Thread(dbWorkerThread);
+        th.setDaemon(false); // we want it to go away when it is done
+        th.start();
+    }
+
+    /**
+     * initialize the controller for a specific case.
+     *
+     * @param c
+     */
+    public synchronized void setCase(Case c) {
+
+        this.db = DrawableDB.getDrawableDB(c.getCaseDirectory(), this);
+        db.addUpdatedFileListener(this);
+        setListeningEnabled(EurekaModule.isEnabledforCase(c));
+        setStale(EurekaModule.isCaseStale(c));
+
+        // if we add this line icons are made as files are analyzed rather than on demand.
+        // db.addUpdatedFileListener(IconCache.getDefault());
+        restartWorker();
+
+        groupManager.setDB(db);
+        SummaryTablePane.getDefault().handleCategoryChanged(Collections.EMPTY_LIST);
+    }
+
+    /**
+     * handle {@link FileUpdateEvent} sent from Db when files are
+     * inserted/updated
+     *
+     * @param evt
+     */
+    @Override
+    synchronized public void handleFileUpdate(FileUpdateEvent evt) {
+        final Collection<Long> fileIDs = evt.getUpdatedFiles();
+        switch (evt.getUpdateType()) {
+            case FILE_REMOVED:
+                for (final long fileId : fileIDs) {
+                    //get grouping(s) this file would be in
+                    Set<GroupKey> groupsForFile = groupManager.getGroupKeysForFileID(fileId);
+
+                    for (GroupKey gk : groupsForFile) {
+                        groupManager.removeFromGroup(gk, fileId);
+                    }
+                }
+
+                break;
+            case FILE_UPDATED:
+
+                /**
+                 * TODO: is there a way to optimize this to avoid quering to db
+                 * so much. the problem is that as a new files are analyzed they
+                 * might be in new groups( if we are grouping by say make or
+                 * model)
+                 *
+                 * TODO: Should this be a InnerTask so it can be done by the
+                 * WorkerThread? Is it already done by worker thread because
+                 * handlefileUpdate is invoked through call on db in UpdateTask
+                 * innertask? -jm
+                 */
+                for (final long fileId : fileIDs) {
+
+                    //get grouping(s) this file would be in
+                    Set<GroupKey> groupsForFile = groupManager.getGroupKeysForFileID(fileId);
+
+                    for (GroupKey gk : groupsForFile) {
+                        Grouping g = groupManager.getGroupForKey(gk);
+
+                        //if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it.
+                        if (g != null) {
+                            g.addFile(fileId);
+                        } ////if there wasn't already a group check if there should be one now
+                        else {
+                            //TODO: use method in groupmanager ?
+                            List<Long> checkAnalyzed = groupManager.checkAnalyzed(gk);
+                            if (checkAnalyzed != null) { // => the group is analyzed, so add it to the ui
+                                groupManager.populateAnalyzedGroup(gk, checkAnalyzed);
+                            }
+                        }
+                    }
+                }
+
+                Category.fireChange(fileIDs);
+                if (evt.getChangedAttribute() == DrawableAttribute.TAGS) {
+                    TagUtils.fireChange(fileIDs);
+                }
+                break;
+        }
+    }
+
+    /**
+     * reset the state of the controller (eg if the case is closed)
+     */
+    public synchronized void reset() {
+        LOGGER.info("resetting EurekaControler to initial state.");
+        selectionModel.clearSelection();
+        Platform.runLater(() -> {
+            viewState.set(null);
+            historyStack.clear();
+            forwardStack.clear();
+        });
+
+        EurekaToolbar.getDefault().reset();
+        groupManager.clear();
+        if (db != null) {
+            db.closeDBCon();
+        }
+        db = null;
+    }
+
+    /**
+     * add InnerTask to the queue that the worker thread gets its work from
+     *
+     * @param innerTask
+     */
+    final void queueTask(InnerTask innerTask) {
+        // @@@ We could make a lock for the worker thread
+        if (dbWorkerThread == null) {
+            restartWorker();
+        }
+        dbWorkerThread.addTask(innerTask);
+    }
+
+    public DrawableFile getFileFromId(Long fileID) throws TskCoreException {
+        return db.getFileFromID(fileID);
+    }
+
+    public void setStacks(StackPane fullUIStack, StackPane centralStack) {
+        fullUIStackPane = fullUIStack;
+        this.centralStackPane = centralStack;
+        Platform.runLater(this::checkForGroups);
+    }
+
+    public final ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() {
+        return queueSizeProperty.getReadOnlyProperty();
+    }
+
+    public ReadOnlyIntegerProperty bgTaskQueueSizeProperty() {
+        return bgTasks.sizeProperty();
+    }
+
+    // @@@ REVIEW IF THIS SHOLD BE STATIC...
+    //TODO: concept seems like  the controller deal with how much work to do at a given time
+    // @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.)
+    private class DBWorkerThread implements Runnable {
+
+        // true if the process was requested to stop.  Currently no way to reset it
+        private volatile boolean cancelled = false;
+
+        // list of tasks to run
+        private final BlockingQueue<InnerTask> workQueue = new LinkedBlockingQueue();
+
+        /**
+         * Cancel all of the queued up tasks and the currently scheduled task.
+         * Note that after you cancel, you cannot submit new jobs to this
+         * thread.
+         */
+        public void cancelAllTasks() {
+            cancelled = true;
+            for (InnerTask it : workQueue) {
+                it.cancel();
+            }
+            workQueue.clear();
+            queueSizeProperty.set(workQueue.size());
+        }
+
+        /**
+         * Add a task for the worker thread to perform
+         *
+         * @param it
+         */
+        public void addTask(InnerTask it) {
+            workQueue.add(it);
+            queueSizeProperty.set(workQueue.size());
+        }
+
+        @Override
+        public void run() {
+            // nearly infinite loop waiting for tasks
+            while (true) {
+                if (cancelled) {
+                    return;
+                }
+                try {
+                    // @@@ Could probably do something more fancy here and check if we've been canceled every now and then
+                    InnerTask it = workQueue.take();
+                    if (it.cancelled == false) {
+                        it.run();
+                    }
+
+                    queueSizeProperty.set(workQueue.size());
+
+                } catch (InterruptedException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+
+            }
+        }
+    }
+
+    public SleuthkitCase getSleuthKitCase() throws IllegalStateException {
+        if (Case.isCaseOpen()) {
+            return Case.getCurrentCase().getSleuthkitCase();
+        } else {
+            throw new IllegalStateException("No Case is open!");
+        }
+    }
+
+    /**
+     * Abstract base class for task to be done on {@link DBWorkerThread}
+     */
+    static public abstract class InnerTask extends ProgressAdapterBase implements Runnable {
+
+        protected volatile boolean cancelled = false;
+
+        public void cancel() {
+            updateState(Worker.State.CANCELLED);
+        }
+
+        protected boolean isCancelled() {
+            return getState() == Worker.State.CANCELLED;
+        }
+    }
+
+    /**
+     * Abstract base class for tasks associated with an obj id in the database
+     */
+    static private abstract class TaskWithID extends InnerTask {
+
+        protected Long obj_id;    // id of image or file
+
+        public TaskWithID(Long id) {
+            super();
+            this.obj_id = id;
+        }
+
+        public Long getId() {
+            return obj_id;
+        }
+    }
+
+    /**
+     * Task to mark all unanalyzed files in the DB as analyzed. Just to make
+     * sure that all are displayed. Added because there were rare cases where
+     * something failed and a file was never marked as analyzed and therefore
+     * never displayed. This task should go into the queue at the end after all
+     * of the update tasks.
+     */
+    class MarkAllFilesAsAnalyzed extends InnerTask {
+
+        @Override
+        public void run() {
+            db.markAllFilesAnalyzed();
+//            checkForGroups();
+        }
+    }
+
+    /**
+     * task that updates one file in database with results from ingest
+     */
+    class UpdateFile extends InnerTask {
+
+        private final AbstractFile file;
+
+        public UpdateFile(AbstractFile f) {
+            super();
+            this.file = f;
+        }
+
+        /**
+         * Update a file in the database
+         */
+        @Override
+        public void run() {
+            DrawableFile drawableFile = DrawableFile.create(file, true);
+            db.updateFile(drawableFile);
+        }
+    }
+
+    /**
+     * task that updates one file in database with results from ingest
+     */
+    class RemoveFile extends InnerTask {
+
+        private final AbstractFile file;
+
+        public RemoveFile(AbstractFile f) {
+            super();
+            this.file = f;
+        }
+
+        /**
+         * Update a file in the database
+         */
+        @Override
+        public void run() {
+            boolean removeFile = db.removeFile(file.getId());
+        }
+    }
+
+    /** Task that runs when eureka listening is (re) enabled.
+     *
+     * Uses the presence of TSK_FILE_TYPE_SIG attributes as a approximation to
+     * 'analyzed'. Grabs all files with supported image/video mime types, and
+     * adds
+     * them to the Drawable DB */
+    class CopyAnalyzedFiles extends InnerTask {
+
+        final private String DRAWABLE_QUERY = "name LIKE '%." + StringUtils.join(EurekaModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'";
+
+        private ProgressHandle progressHandle = ProgressHandleFactory.createHandle("populating analyzed image/video database");
+
+        @Override
+        public void run() {
+            progressHandle.start();
+            updateMessage("populating analyzed image/video database");
+
+            try {
+                //grap all files with supported mime types
+                final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY + " or tsk_files.obj_id in (select tsk_files.obj_id from tsk_files , blackboard_artifacts,  blackboard_attributes"
+                        + " where  blackboard_artifacts.obj_id = tsk_files.obj_id"
+                        + " and blackboard_attributes.artifact_id = blackboard_artifacts.artifact_id"
+                        + " and blackboard_artifacts.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO.getTypeID()
+                        + " and blackboard_attributes.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG.getTypeID()
+                        + " and blackboard_attributes.value_text in ('" + StringUtils.join(EurekaModule.getSupportedMimes(), "','") + "'))");
+                progressHandle.switchToDeterminate(files.size());
+
+                updateProgress(0.0);
+
+                //do in transaction
+                DrawableDB.DrawableTransaction tr = db.beginTransaction();
+                int units = 0;
+                for (final AbstractFile f : files) {
+                    if (cancelled) {
+                        LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database");
+                        progressHandle.finish();
+                        break;
+                    }
+                    final Boolean hasMimeType = EurekaModule.hasSupportedMimeType(f);
+                    final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
+
+                    if (known) {
+                        db.removeFile(f.getId(), tr);  //remove known files
+                    } else {
+                        if (hasMimeType == null) {
+                            if (EurekaModule.isSupported(f)) {
+                                //no mime type but supported => not add as not analyzed
+                                db.updatefile(DrawableFile.create(f, false), tr);
+                            } else {
+                                //no mime type, not supported  => remove ( how dd we get here)
+                                db.removeFile(f.getId(), tr);
+                            }
+                        } else {
+                            if (hasMimeType) {  // supported mimetype => analyzed
+                                db.updatefile(DrawableFile.create(f, true), tr);
+                            } else { //unsupported mimtype => analyzed but shouldn't include
+                                db.removeFile(f.getId(), tr);
+                            }
+                        }
+                    }
+
+                    units++;
+                    final int prog = units;
+                    progressHandle.progress(f.getName(), units);
+                    updateProgress(prog - 1 / (double) files.size());
+                    updateMessage(f.getName());
+                }
+
+                progressHandle.finish();
+
+                progressHandle = ProgressHandleFactory.createHandle("commiting image/video database");
+                updateMessage("commiting image/video database");
+                updateProgress(1.0);
+
+                progressHandle.start();
+                db.commitTransaction(tr, true);
+
+            } catch (TskCoreException ex) {
+                Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex);
+            } catch (IllegalStateException ex) {
+                Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.SEVERE, "Case was closed out from underneath CopyDataSource task", ex);
+            }
+
+            progressHandle.finish();
+
+            updateMessage(
+                    "");
+            updateProgress(
+                    -1.0);
+            setStale(false);
+        }
+
+    }
+
+    /** task that does pre-ingest copy over of files from a new datasource
+     * with
+     * (uses fs_obj_id to identify files from new datasource) *
+     *
+     * TODO: create methods to simplify progress value/text updates to both
+     * netbeans and eureka progress/status */
+    class PrePopulateDataSourceFiles extends TaskWithID {
+
+        /** @TODO: for initial grab is there any better way than by
+         * extension?
+         *
+         * in file_done listener we look at file type id attributes and fall
+         * back on jpeg signatures and extensions to check for supported
+         * images */
+        // (name like '.jpg' or name like '.png' ...)
+        final private String DRAWABLE_QUERY = "name LIKE '%." + StringUtils.join(EurekaModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'";
+
+        private ProgressHandle progressHandle = ProgressHandleFactory.createHandle("prepopulating image/video database");
+
+        public PrePopulateDataSourceFiles(Long id) {
+            super(id);
+        }
+
+        /** Copy files from a newly added data source into the DB */
+        @Override
+        public void run() {
+            progressHandle.start();
+            updateMessage("prepopulating image/video database");
+
+            /* Get all "drawable" files, based on extension. After ingest we
+             * use
+             * file type id module and if necessary jpeg signature matching
+             * to
+             * add remove files */
+            final List<AbstractFile> files;
+            try {
+                files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY + "and fs_obj_id = " + this.obj_id);
+                progressHandle.switchToDeterminate(files.size());
+
+//                updateProgress(0.0);
+
+                //do in transaction
+                DrawableDB.DrawableTransaction tr = db.beginTransaction();
+                int units = 0;
+                for (final AbstractFile f : files) {
+                    if (cancelled) {
+                        LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database");
+                        progressHandle.finish();
+                        break;
+                    }
+                    db.updatefile(DrawableFile.create(f, false), tr);
+                    units++;
+                    final int prog = units;
+                    progressHandle.progress(f.getName(), units);
+//                    updateProgress(prog - 1 / (double) files.size());
+//                    updateMessage(f.getName());
+                }
+
+                progressHandle.finish();
+                progressHandle = ProgressHandleFactory.createHandle("commiting image/video database");
+//                updateMessage("commiting image/video database");
+//                updateProgress(1.0);
+
+                progressHandle.start();
+                db.commitTransaction(tr, false);
+
+            } catch (TskCoreException ex) {
+                Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex);
+            } catch (IllegalStateException ex) {
+                Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.SEVERE, "Case was closed out from underneath CopyDataSource task", ex);
+            }
+
+            progressHandle.finish();
+
+//            updateMessage("");
+//            updateProgress(-1.0);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f41e27e535f398a3da780eaf8bbc7cea8494cb2
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java
@@ -0,0 +1,192 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import javax.imageio.ImageIO;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.windows.Mode;
+import org.openide.windows.TopComponent;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.ImageUtils;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.TskData;
+
+/** static definitions and utilities for the Eureka Module
+ *
+ */
+public class EurekaModule {
+
+    static private final Logger LOGGER = Logger.getLogger(EurekaModule.class.getName());
+
+    static final String MODULE_NAME = EurekaModule.class.getSimpleName();
+
+    static private final Set<String> videoExtensions
+            = Sets.newHashSet("aaf", "3gp", "asf", "avi", "m1v", "m2v", "m4v", "mp4",
+                              "mov", "mpeg", "mpg", "mpe", "mp4", "rm", "wmv", "mpv",
+                              "flv", "swf");
+
+    static private final Set<String> imageExtensions = Sets.newHashSet(ImageIO.getReaderFileSuffixes());
+
+    static private final Set<String> supportedExtensions = Sets.union(imageExtensions, videoExtensions);
+
+    static private final Set<String> imageMimes = Sets.newHashSet("image/jpeg", "image/bmp", "image/gif", "image/png");
+
+    static private final Set<String> videoMimes = Sets.newHashSet("video/mp4", "video/x-flv", "video/x-javafx");
+
+    static private final Set<String> supportedMimes = Sets.union(imageMimes, videoMimes);
+
+    public static Set<String> getSupportedMimes() {
+        return Collections.unmodifiableSet(supportedMimes);
+    }
+
+    private EurekaModule() {
+    }
+
+    static boolean isEnabledforCase(Case c) {
+        if (c != null) {
+            String enabledforCaseProp = new PerCaseProperties(c).getConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.ENABLED);
+            return StringUtils.isNotBlank(enabledforCaseProp) ? Boolean.valueOf(enabledforCaseProp) : EurekaPreferences.isEnabledByDefault();
+        } else {
+            return false;
+        }
+    }
+
+    public static boolean isCaseStale(Case c) {
+        if (c != null) {
+            String stale = new PerCaseProperties(c).getConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.STALE);
+            return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : false;
+        } else {
+            return false;
+        }
+    }
+
+    public static Set<String> getAllSupportedExtensions() {
+        return supportedExtensions;
+    }
+
+    public static Boolean isSupported(AbstractFile file) {
+        //if there were no file type attributes, or we failed to read it, fall back on extension and jpeg header
+        return Optional.ofNullable(hasSupportedMimeType(file)).orElseGet(
+                () -> supportedExtensions.contains(getFileExtension(file)) || ImageUtils.isJpegFileHeader(file));
+    }
+
+    /**
+     *
+     * @param file
+     *
+     * @return true if the file had a TSK_FILE_TYPE_SIG attribute on a
+     *         TSK_GEN_INFO that is in the supported list. False if there was an
+     *         unsupported attribute, null if no attributes were found
+     */
+    public static Boolean hasSupportedMimeType(AbstractFile file) {
+        try {
+            ArrayList<BlackboardAttribute> fileSignatureAttrs = file.getGenInfoAttributes(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG);
+            if (fileSignatureAttrs.isEmpty() == false) {
+                return fileSignatureAttrs.stream().anyMatch(attr -> supportedMimes.contains(attr.getValueString()));
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.INFO, "failed to read TSK_FILE_TYPE_SIG attribute for " + file.getName(), ex);
+        }
+        return null;
+    }
+
+    /** @param file
+     *
+     * @return
+     */
+    public static boolean isVideoFile(AbstractFile file) {
+        try {
+            ArrayList<BlackboardAttribute> fileSignatureAttrs = file.getGenInfoAttributes(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG);
+            if (fileSignatureAttrs.isEmpty() == false) {
+                return fileSignatureAttrs.stream().anyMatch(attr -> videoMimes.contains(attr.getValueString()));
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.INFO, "failed to read TSK_FILE_TYPE_SIG attribute for " + file.getName(), ex);
+        }
+        //if there were no file type attributes, or we failed to read it, fall back on extension
+        return videoExtensions.contains(getFileExtension(file));
+    }
+
+    private static String getFileExtension(AbstractFile file) {
+        return Iterables.getLast(Arrays.asList(StringUtils.split(file.getName(), '.')), "");
+    }
+
+    /**
+     * Is the given file 'supported' and not 'known'(nsrl hash hit). If so we
+     * should include it in {@link DrawableDB} and UI
+     *
+     * @param abstractFile
+     *
+     * @return true if the given {@link AbstractFile} is 'supported' and not
+     *         'known', else false
+     */
+    static public boolean isSupportedAndNotKnown(AbstractFile abstractFile) {
+        return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && EurekaModule.isSupported(abstractFile);
+    }
+
+    //TODO: this doesn ot really belong here, move it to EurekaController? Module?
+    @ThreadConfined(type = ThreadConfined.ThreadType.UI)
+    public static void closeTopComponent() {
+        final TopComponent etc = WindowManager.getDefault().findTopComponent("EurekaTopComponent");
+        if (etc != null) {
+            try {
+                etc.close();
+            } catch (Exception e) {
+                LOGGER.log(Level.SEVERE, "failed to close EurekaTopComponent", e);
+            }
+        }
+    }
+
+    @ThreadConfined(type = ThreadConfined.ThreadType.UI)
+    public static void openTopComponent() {
+        //TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case.
+//        synchronized (OpenTimelineAction.class) {
+//            if (timeLineController == null) {
+//                timeLineController = new TimeLineController();
+//                LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S");
+//            }
+//        }
+//        timeLineController.openTimeLine();
+        final EurekaTopComponent EurekaTc = (EurekaTopComponent) WindowManager.getDefault().findTopComponent("EurekaTopComponent");
+        if (EurekaTc != null) {
+            WindowManager.getDefault().isTopComponentFloating(EurekaTc);
+            Mode mode = WindowManager.getDefault().findMode("timeline");
+            if (mode != null) {
+                mode.dockInto(EurekaTc);
+
+            }
+            EurekaTc.open();
+            EurekaTc.requestActive();
+        }
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form
new file mode 100644
index 0000000000000000000000000000000000000000..e811ca7e4002a750ab16d439073342f753cdd511
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Group type="103" groupAlignment="0" attributes="0">
+                  <Component id="enabledForCaseBox" alignment="0" min="-2" max="-2" attributes="0"/>
+                  <Component id="enabledByDefaultBox" alignment="0" min="-2" max="-2" attributes="0"/>
+              </Group>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Group type="102" alignment="0" attributes="0">
+              <EmptySpace max="-2" attributes="0"/>
+              <Component id="enabledByDefaultBox" min="-2" max="-2" attributes="0"/>
+              <EmptySpace type="separate" max="-2" attributes="0"/>
+              <Component id="enabledForCaseBox" min="-2" max="-2" attributes="0"/>
+              <EmptySpace max="32767" attributes="0"/>
+          </Group>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="javax.swing.JCheckBox" name="enabledByDefaultBox">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/sleuthkit/autopsy/imageanalyzer/Bundle.properties" key="EurekaOptionsPanel.enabledByDefaultBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Component class="javax.swing.JCheckBox" name="enabledForCaseBox">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/sleuthkit/autopsy/imageanalyzer/Bundle.properties" key="EurekaOptionsPanel.enabledForCaseBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+    </Component>
+  </SubComponents>
+</Form>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c6d7d35273c8101457e33e9cf108dffc5bbf150
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java
@@ -0,0 +1,115 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.awt.event.ActionEvent;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+
+/** The Image/Video Analyzer panel in the NetBeans provided Options Dialogs
+ * accessed via Tool -> Options */
+final class EurekaOptionsPanel extends javax.swing.JPanel {
+
+    EurekaOptionsPanel(EurekaOptionsPanelController controller) {
+        initComponents();
+
+        IngestManager.getInstance().addIngestJobEventListener(evt -> {
+            enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false);
+        });
+
+        enabledByDefaultBox.addActionListener((ActionEvent e) -> {
+            controller.changed();
+        });
+
+        enabledForCaseBox.addActionListener((ActionEvent e) -> {
+            controller.changed();
+        });
+    }
+
+    /** This method is called from within the constructor to
+     * initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is
+     * always regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        enabledByDefaultBox = new javax.swing.JCheckBox();
+        enabledForCaseBox = new javax.swing.JCheckBox();
+
+        org.openide.awt.Mnemonics.setLocalizedText(enabledByDefaultBox, org.openide.util.NbBundle.getMessage(EurekaOptionsPanel.class, "EurekaOptionsPanel.enabledByDefaultBox.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(enabledForCaseBox, org.openide.util.NbBundle.getMessage(EurekaOptionsPanel.class, "EurekaOptionsPanel.enabledForCaseBox.text")); // NOI18N
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+                    .addComponent(enabledForCaseBox)
+                    .addComponent(enabledByDefaultBox))
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addContainerGap()
+                .addComponent(enabledByDefaultBox)
+                .addGap(18, 18, 18)
+                .addComponent(enabledForCaseBox)
+                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+        );
+    }// </editor-fold>//GEN-END:initComponents
+
+    /** {@inheritDoc} */
+    void load() {
+        enabledByDefaultBox.setSelected(EurekaPreferences.isEnabledByDefault());
+        if (Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false) {
+            enabledForCaseBox.setEnabled(true);
+            enabledForCaseBox.setSelected(EurekaModule.isEnabledforCase(Case.getCurrentCase()));
+        } else {
+            enabledForCaseBox.setEnabled(false);
+            enabledForCaseBox.setSelected(enabledByDefaultBox.isSelected());
+        }
+    }
+
+    /** {@inheritDoc } */
+    void store() {
+        EurekaPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected());
+        EurekaController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected());
+        if (Case.isCaseOpen()) {
+            new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected()));
+        }
+    }
+
+    /** {@inheritDoc }
+     *
+     * @return true, since there is no way for this form to be invalid */
+    boolean valid() {
+        // TODO check whether form is consistent and complete
+        return true;
+    }
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JCheckBox enabledByDefaultBox;
+    private javax.swing.JCheckBox enabledForCaseBox;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java
new file mode 100644
index 0000000000000000000000000000000000000000..21c2516299225993233af7389619be71726d0163
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java
@@ -0,0 +1,107 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.HelpCtx;
+import org.openide.util.Lookup;
+
+@OptionsPanelController.TopLevelRegistration(
+        categoryName = "#OptionsCategory_Name_Options",
+        iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/polaroid_48_silhouette.png",
+        keywords = "#OptionsCategory_Keywords_Options",
+        keywordsCategory = "Options"
+)
+@org.openide.util.NbBundle.Messages({"OptionsCategory_Name_Options=Image / Video Analzyer", "OptionsCategory_Keywords_Options=image video analyzer category "})
+public final class EurekaOptionsPanelController extends OptionsPanelController {
+
+    private EurekaOptionsPanel panel;
+
+    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+
+    private boolean changed;
+
+    @Override
+    public void update() {
+        getPanel().load();
+        changed = false;
+    }
+
+    @Override
+    public void applyChanges() {
+        SwingUtilities.invokeLater(() -> {
+            getPanel().store();
+            changed = false;
+        });
+    }
+
+    @Override
+    public void cancel() {
+        // need not do anything special, if no changes have been persisted yet
+    }
+
+    @Override
+    public boolean isValid() {
+        return getPanel().valid();
+    }
+
+    @Override
+    public boolean isChanged() {
+        return changed;
+    }
+
+    @Override
+    public HelpCtx getHelpCtx() {
+        return null; // new HelpCtx("...ID") if you have a help set
+    }
+
+    @Override
+    public JComponent getComponent(Lookup masterLookup) {
+        return getPanel();
+    }
+
+    @Override
+    public void addPropertyChangeListener(PropertyChangeListener l) {
+        pcs.addPropertyChangeListener(l);
+    }
+
+    @Override
+    public void removePropertyChangeListener(PropertyChangeListener l) {
+        pcs.removePropertyChangeListener(l);
+    }
+
+    private EurekaOptionsPanel getPanel() {
+        if (panel == null) {
+            panel = new EurekaOptionsPanel(this);
+        }
+        return panel;
+    }
+
+    void changed() {
+        if (!changed) {
+            changed = true;
+            pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true);
+        }
+        pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null);
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c3525b78f8b4f6fbf114ba059d882cbe056c063
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java
@@ -0,0 +1,50 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.util.prefs.PreferenceChangeListener;
+import java.util.prefs.Preferences;
+import org.openide.util.NbPreferences;
+
+/**
+ *
+ */
+public class EurekaPreferences {
+
+    private static final Preferences preferences = NbPreferences.forModule(EurekaPreferences.class);
+
+    static final String ENABLED_BY_DEFAULT = "enabled_by_default";
+
+    static boolean isEnabledByDefault() {
+        final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true);
+        return aBoolean;
+    }
+
+    static void setEnabledByDefault(boolean b) {
+        preferences.putBoolean(ENABLED_BY_DEFAULT, b);
+    }
+
+    static void addChangeListener(PreferenceChangeListener l) {
+        preferences.addPreferenceChangeListener(l);
+    }
+
+    static void removeChangeListener(PreferenceChangeListener l) {
+        preferences.removePreferenceChangeListener(l);
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form
new file mode 100644
index 0000000000000000000000000000000000000000..cf17948941e39dcf077b382efeb030fa88f4007f
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.4" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+  </AuxValues>
+
+  <Layout>
+    <DimensionLayout dim="0">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="eurekaJFXPanel" pref="532" max="32767" attributes="0"/>
+          <Component id="listView1" min="-2" max="-2" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+    <DimensionLayout dim="1">
+      <Group type="103" groupAlignment="0" attributes="0">
+          <Component id="eurekaJFXPanel" pref="389" max="32767" attributes="0"/>
+          <Component id="listView1" min="-2" max="-2" attributes="0"/>
+      </Group>
+    </DimensionLayout>
+  </Layout>
+  <SubComponents>
+    <Component class="org.openide.explorer.view.ListView" name="listView1">
+      <Properties>
+        <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[0, 0]"/>
+        </Property>
+        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[0, 0]"/>
+        </Property>
+        <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[0, 0]"/>
+        </Property>
+      </Properties>
+    </Component>
+    <Container class="javafx.embed.swing.JFXPanel" name="eurekaJFXPanel">
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new EurekaPanel()"/>
+      </AuxValues>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout">
+        <Property name="useNullLayout" type="boolean" value="true"/>
+      </Layout>
+    </Container>
+  </SubComponents>
+</Form>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java
new file mode 100644
index 0000000000000000000000000000000000000000..cffb06d93044b02f3295e38b1af38f175af310b6
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java
@@ -0,0 +1,183 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import javafx.application.Platform;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.scene.control.SplitPane;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import org.netbeans.api.settings.ConvertAsProperties;
+import org.openide.explorer.ExplorerManager;
+import org.openide.explorer.ExplorerUtils;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle.Messages;
+import org.openide.windows.TopComponent;
+import org.sleuthkit.autopsy.imageanalyzer.gui.EurekaToolbar;
+import org.sleuthkit.autopsy.imageanalyzer.gui.GroupPane;
+import org.sleuthkit.autopsy.imageanalyzer.gui.MetaDataPane;
+import org.sleuthkit.autopsy.imageanalyzer.gui.StatusBar;
+import org.sleuthkit.autopsy.imageanalyzer.gui.SummaryTablePane;
+import org.sleuthkit.autopsy.imageanalyzer.gui.navpanel.NavPanel;
+
+/**
+ * Top component which displays eureka interface.
+ */
+@ConvertAsProperties(
+        dtd = "-//org.sleuthkit.autopsy.imageanalyzer//Eureka//EN",
+        autostore = false)
+@TopComponent.Description(
+        preferredID = "EurekaTopComponent",
+        //iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png",
+        persistenceType = TopComponent.PERSISTENCE_NEVER)
+@TopComponent.Registration(mode = "timeline", openAtStartup = false)
+@Messages({
+    "CTL_EurekaAction=Image/Video Analysis",
+    "CTL_EurekaTopComponent=Image/Video Analysis",
+    "HINT_EurekaTopComponent=This is a Image/Video Analysis window"
+})
+public final class EurekaTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider {
+
+    public final static String PREFERRED_ID = "EurekaTopComponent";
+
+    private final ExplorerManager em = new ExplorerManager();
+
+    private final Lookup lookup;
+
+    private final EurekaController controller = EurekaController.getDefault();
+
+    private SplitPane splitPane;
+
+    private StackPane centralStack;
+
+    private BorderPane borderPane = new BorderPane();
+
+    private StackPane fullUIStack;
+
+    private MetaDataPane metaDataTable;
+
+    private GroupPane groupPane;
+
+    private NavPanel navPanel;
+
+    private VBox leftPane;
+
+    private Scene myScene;
+
+    public EurekaTopComponent() {
+
+        setName(Bundle.CTL_EurekaTopComponent());
+        setToolTipText(Bundle.HINT_EurekaTopComponent());
+
+        // ...and initialization of lookup variable
+        lookup = (ExplorerUtils.createLookup(em, getActionMap()));
+        initComponents();
+        Platform.runLater(() -> {
+            fullUIStack = new StackPane();
+            myScene = new Scene(fullUIStack);
+            eurekaJFXPanel.setScene(myScene);
+            groupPane = new GroupPane(controller);
+            centralStack = new StackPane(groupPane);
+            fullUIStack.getChildren().add(borderPane);
+            splitPane = new SplitPane();
+            borderPane.setCenter(splitPane);
+            borderPane.setTop(EurekaToolbar.getDefault());
+            borderPane.setBottom(new StatusBar(controller));
+
+            metaDataTable = new MetaDataPane(controller);
+
+            navPanel = new NavPanel(controller);
+            leftPane = new VBox(navPanel, SummaryTablePane.getDefault());
+            SplitPane.setResizableWithParent(leftPane, Boolean.FALSE);
+            SplitPane.setResizableWithParent(groupPane, Boolean.TRUE);
+            SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE);
+            splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
+            splitPane.setDividerPositions(0.0, 1.0);
+
+            EurekaController.getDefault().setStacks(fullUIStack, centralStack);
+        });
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        listView1 = new org.openide.explorer.view.ListView();
+        eurekaJFXPanel = new JFXPanel();
+
+        listView1.setMaximumSize(new java.awt.Dimension(0, 0));
+        listView1.setMinimumSize(new java.awt.Dimension(0, 0));
+        listView1.setPreferredSize(new java.awt.Dimension(0, 0));
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(eurekaJFXPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 532, Short.MAX_VALUE)
+            .addComponent(listView1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+        );
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addComponent(eurekaJFXPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 389, Short.MAX_VALUE)
+            .addComponent(listView1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+        );
+    }// </editor-fold>//GEN-END:initComponents
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javafx.embed.swing.JFXPanel eurekaJFXPanel;
+    private org.openide.explorer.view.ListView listView1;
+    // End of variables declaration//GEN-END:variables
+
+    @Override
+    public void componentOpened() {
+
+    }
+
+    @Override
+    public void componentClosed() {
+        //TODO: we could do some cleanup here
+    }
+
+    void writeProperties(java.util.Properties p) {
+        // better to version settings since initial version as advocated at
+        // http://wiki.apidesign.org/wiki/PropertyFiles
+        p.setProperty("version", "1.0");
+        // TODO store your settings
+    }
+
+    void readProperties(java.util.Properties p) {
+        String version = p.getProperty("version");
+        // TODO read your settings according to their version
+    }
+
+    @Override
+    public ExplorerManager getExplorerManager() {
+        return em;
+    }
+
+    @Override
+    public Lookup getLookup() {
+        return lookup;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d4cbc39d321c7c2a6bc90f0863dd20695b8febd
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java
@@ -0,0 +1,141 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Node;
+import org.openide.util.Exceptions;
+
+/**
+ * This class support both programmer productivity by abstracting frequently
+ * used code to load FXML-defined GUI components, and code performance by
+ * implementing a caching FXMLLoader as described at
+ * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane.
+ */
+public class FXMLConstructor {
+
+    private static final CachingClassLoader CACHING_CLASS_LOADER = new CachingClassLoader((FXMLLoader.getDefaultClassLoader()));
+
+    static public void construct(Node n, String fxmlFileName) {
+        FXMLLoader fxmlLoader = new FXMLLoader(n.getClass().getResource(fxmlFileName));
+        fxmlLoader.setRoot(n);
+        fxmlLoader.setController(n);
+        fxmlLoader.setClassLoader(CACHING_CLASS_LOADER);
+
+        try {
+            fxmlLoader.load();
+        } catch (IOException exception) {
+            try {
+                fxmlLoader.setClassLoader(FXMLLoader.getDefaultClassLoader());
+                fxmlLoader.load();
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+
+    /**
+     * The default FXMLLoader does not cache information about previously loaded
+     * FXML files. See
+     * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane.
+     * for more details. As a partial workaround, we cache information on
+     * previously loaded classes. This does not solve all performance issues,
+     * but is a big improvement.
+     */
+    static public class CachingClassLoader extends ClassLoader {
+
+        private final Map<String, Class> classes = new HashMap<String, Class>();
+
+        private final ClassLoader parent;
+
+        public CachingClassLoader(ClassLoader parent) {
+            this.parent = parent;
+        }
+
+        @Override
+        public Class<?> loadClass(String name) throws ClassNotFoundException {
+            Class<?> c = findClass(name);
+            if (c == null) {
+                throw new ClassNotFoundException(name);
+            }
+            return c;
+        }
+
+        @Override
+        protected Class<?> findClass(String className) throws ClassNotFoundException {
+// System.out.print("try to load " + className); 
+            if (classes.containsKey(className)) {
+                Class<?> result = classes.get(className);
+                return result;
+            } else {
+                try {
+                    Class<?> result = parent.loadClass(className);
+// System.out.println(" -> success!"); 
+                    classes.put(className, result);
+                    return result;
+                } catch (ClassNotFoundException ignore) {
+// System.out.println(); 
+                    classes.put(className, null);
+                    return null;
+                }
+            }
+        }
+
+        // ========= delegating methods ============= 
+        @Override
+        public URL getResource(String name) {
+            return parent.getResource(name);
+        }
+
+        @Override
+        public Enumeration<URL> getResources(String name) throws IOException {
+            return parent.getResources(name);
+        }
+
+        @Override
+        public String toString() {
+            return parent.toString();
+        }
+
+        @Override
+        public void setDefaultAssertionStatus(boolean enabled) {
+            parent.setDefaultAssertionStatus(enabled);
+        }
+
+        @Override
+        public void setPackageAssertionStatus(String packageName, boolean enabled) {
+            parent.setPackageAssertionStatus(packageName, enabled);
+        }
+
+        @Override
+        public void setClassAssertionStatus(String className, boolean enabled) {
+            parent.setClassAssertionStatus(className, enabled);
+        }
+
+        @Override
+        public void clearAssertionStatus() {
+            parent.clearAssertionStatus();
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7e2e2a0900a14facb960c26aa4da89d9fb9809e
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java
@@ -0,0 +1,113 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.util.Arrays;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableSet;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/** Singleton that manages set of fileIds, as well as
+ * last selected fileID.
+ * NOTE: When we had synchronization on selected and lastSelectedProp we got
+ * deadlocks with the tiles during selection
+ */
+public class FileIDSelectionModel {
+
+    private static final Logger LOGGER = Logger.getLogger(FileIDSelectionModel.class.getName());
+
+    private static FileIDSelectionModel instance;
+
+    private final ObservableSet<Long> selected = FXCollections.observableSet();
+
+    private final ReadOnlyObjectWrapper<Long> lastSelectedProp = new ReadOnlyObjectWrapper<>();
+
+    public static synchronized FileIDSelectionModel getInstance() {
+        if (instance == null) {
+            instance = new FileIDSelectionModel();
+        }
+        return instance;
+    }
+
+    public FileIDSelectionModel() {
+        super();
+    }
+
+    public void toggleSelection(Long id) {
+        boolean contained = selected.contains(id);
+
+        if (contained) {
+            selected.remove(id);
+            setLastSelected(null);
+        } else {
+            selected.add(id);
+            setLastSelected(id);
+        }
+    }
+
+    public void clearAndSelectAll(Long... ids) {
+        selected.clear();
+        selected.addAll(Arrays.asList(ids));
+        setLastSelected(ids[ids.length - 1]);
+    }
+
+    public void clearAndSelect(Long id) {
+        selected.clear();
+        selected.add(id);
+        setLastSelected(id);
+    }
+
+    public void select(Long id) {
+        selected.add(id);
+        setLastSelected(id);
+    }
+
+    public void deSelect(Long id) {
+        selected.remove(id);
+        setLastSelected(null);
+    }
+
+    public void clearSelection() {
+        selected.clear();
+        setLastSelected(null);
+    }
+
+    public boolean isSelected(Long id) {
+        return selected.contains(id);
+    }
+
+    public ReadOnlyObjectProperty<Long> lastSelectedProperty() {
+        return lastSelectedProp.getReadOnlyProperty();
+    }
+
+    private void setLastSelected(Long id) {
+        lastSelectedProp.set(id);
+    }
+
+    public ObservableSet<Long> getSelected() {
+        return selected;
+    }
+
+    public void clearAndSelectAll(ObservableList<Long> ids) {
+        clearAndSelectAll(ids.toArray(new Long[ids.size()]));
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..3703a380ee3e65377d00fd14c9b7873e108e364f
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java
@@ -0,0 +1,70 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public class FileUpdateEvent {
+
+    private final Set<Long> updatedFiles;
+
+    private final DrawableAttribute changedAttribute;
+
+    private final UpdateType updateType;
+
+    public UpdateType getUpdateType() {
+        return updateType;
+    }
+
+    public Collection<Long> getUpdatedFiles() {
+        return updatedFiles;
+    }
+
+    public DrawableAttribute getChangedAttribute() {
+        return changedAttribute;
+    }
+
+    public FileUpdateEvent(Collection<? extends Long> updatedFiles, DrawableAttribute changedAttribute, UpdateType updateType) {
+        this.updatedFiles = new HashSet<>(updatedFiles);
+        this.changedAttribute = changedAttribute;
+        this.updateType = updateType;
+    }
+
+    public FileUpdateEvent(Collection<? extends Long> updatedFiles, DrawableAttribute changedAttribute) {
+        this.updatedFiles = new HashSet<>(updatedFiles);
+        this.changedAttribute = changedAttribute;
+        this.updateType = UpdateType.FILE_UPDATED;
+    }
+
+    public FileUpdateEvent(Collection<? extends Long> updatedFiles) {
+        this.updatedFiles = new HashSet<>(updatedFiles);
+        changedAttribute = null;
+        this.updateType = UpdateType.FILE_UPDATED;
+    }
+
+    static public enum UpdateType {
+
+        FILE_UPDATED, FILE_REMOVED;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0fced781b46e96f7307ca50d97c9d76ddb2badc
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java
@@ -0,0 +1,29 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.util.EventListener;
+
+/**
+ *
+ */
+public interface FileUpdateListener extends EventListener {
+
+    public void handleFileUpdate(FileUpdateEvent evt);
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b192441370e405be9ad8b58c73a1690c1404262
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java
@@ -0,0 +1,207 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.image.Image;
+import javafx.scene.image.WritableImage;
+import javax.imageio.IIOException;
+import javax.imageio.ImageIO;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.corelibs.ScalrWrapper;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.ReadContentInputStream;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Manages creation and access of icons. Keeps a cache in memory of most
+ * recently used icons, and a disk cache of all icons. */
+public class IconCache {
+
+    static private IconCache instance;
+
+    private static final int MAX_ICON_SIZE = 300;
+
+    private static final Logger LOGGER = Logger.getLogger(IconCache.class.getName());
+
+    private static final Cache<Long, Optional<Image>> cache = CacheBuilder.newBuilder().maximumSize(1000).softValues().expireAfterAccess(10, TimeUnit.MINUTES).build();
+
+    public SimpleIntegerProperty iconSize = new SimpleIntegerProperty(200);
+
+    private final Executor imageSaver = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("icon saver-%d").build());
+
+    private IconCache() {
+
+    }
+
+    synchronized static public IconCache getDefault() {
+        if (instance == null) {
+            instance = new IconCache();
+        }
+        return instance;
+    }
+
+    public Image get(DrawableFile file) {
+        try {
+            return cache.get(file.getId(), () -> load(file)).orElse(null);
+        } catch (CacheLoader.InvalidCacheLoadException | ExecutionException ex) {
+            LOGGER.log(Level.WARNING, "failed to load icon for file: " + file.getName(), ex);
+            return null;
+        }
+    }
+
+    public Image get(Long fileID) {
+        try {
+            return get(EurekaController.getDefault().getFileFromId(fileID));
+        } catch (TskCoreException ex) {
+            Exceptions.printStackTrace(ex);
+            return null;
+        }
+    }
+
+    public Optional<Image> load(DrawableFile file) throws IIOException {
+
+        Image icon = null;
+        File cacheFile;
+        try {
+            cacheFile = getCacheFile(file.getId());
+        } catch (IllegalStateException e) {
+            LOGGER.log(Level.WARNING, "can't load icon when no case is open");
+            return Optional.empty();
+        }
+
+        // If a thumbnail file is already saved locally
+        if (cacheFile.exists()) {
+            try {
+                int dim = iconSize.get();
+                icon = new Image(cacheFile.toURI().toURL().toString(), dim, dim, true, false, true);
+            } catch (MalformedURLException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+
+        if (icon == null) {
+            //  Logger.getAnonymousLogger().warning("wrong size cache found for image " + getName());
+            icon = generateAndSaveIcon(file);
+        }
+        return Optional.ofNullable(icon);
+
+    }
+
+    private static File getCacheFile(long id) {
+        return new File(Case.getCurrentCase().getCacheDirectory() + File.separator + id + ".png");
+    }
+
+    private Image generateAndSaveIcon(final DrawableFile file) {
+        Image img;
+        //TODO: should we wrap this in a BufferedInputStream? -jm
+        try (ReadContentInputStream inputStream = new ReadContentInputStream(file.getAbstractFile())) {
+            img = new Image(inputStream, MAX_ICON_SIZE, MAX_ICON_SIZE, true, true);
+            if (img.isError()) {
+                LOGGER.log(Level.WARNING, "problem loading image: {0}. {1}", new Object[]{file.getName(), img.getException().getLocalizedMessage()});
+                return fallbackToSwingImage(file);
+            } else {
+                imageSaver.execute(() -> {
+                    saveIcon(file, img);
+                });
+            }
+        } catch (IOException ex) {
+            return fallbackToSwingImage(file);
+        }
+
+        return img;
+
+    }
+    /* Generate a scaled image */
+
+    private Image fallbackToSwingImage(final DrawableFile file) {
+        final BufferedImage generateSwingIcon = generateSwingIcon(file);
+        if (generateSwingIcon != null) {
+            WritableImage toFXImage = SwingFXUtils.toFXImage(generateSwingIcon, null);
+            if (toFXImage != null) {
+                imageSaver.execute(() -> {
+                    saveIcon(file, toFXImage);
+                });
+            }
+
+            return toFXImage;
+        } else {
+            return null;
+        }
+    }
+
+    private BufferedImage generateSwingIcon(DrawableFile file) {
+        try (ReadContentInputStream inputStream = new ReadContentInputStream(file.getAbstractFile())) {
+            BufferedImage bi = ImageIO.read(inputStream);
+            if (bi == null) {
+                LOGGER.log(Level.WARNING, "No image reader for file: {0}", file.getName());
+                return null;
+            } else {
+                try {
+                    if (Math.max(bi.getWidth(), bi.getHeight()) > MAX_ICON_SIZE) {
+                        bi = ScalrWrapper.resizeFast(bi, iconSize.get());
+                    }
+                } catch (IllegalArgumentException e) {
+                    LOGGER.log(Level.WARNING, "scalr could not scale image to 0: {0}", file.getName());
+                } catch (OutOfMemoryError e) {
+                    LOGGER.log(Level.WARNING, "scalr could not scale image (too large): {0}", file.getName());
+                    return null;
+                }
+            }
+            return bi;
+        } catch (IOException ex) {
+            LOGGER.log(Level.WARNING, "Could not read image: " + file.getName(), ex);
+            return null;
+        }
+    }
+
+    private void saveIcon(final DrawableFile file, final Image bi) {
+        try {
+            /* save the icon in a background thread. profiling
+             * showed that it can take as much time as making
+             * the icon? -bc
+             *
+             * We don't do this now as it doesn't fit the
+             * current model of ui-related backgroiund tasks,
+             * and there might be complications to not just
+             * blocking (eg having more than one task to
+             * create the same icon -jm */
+            File f = getCacheFile(file.getId());
+            ImageIO.write(SwingFXUtils.fromFXImage(bi, null), "png", f);
+        } catch (IOException ex) {
+            LOGGER.log(Level.WARNING, "failed to save generated icon ", ex);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..58847bf63dc3af264b275b21f9cb098845fd40ad
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java
@@ -0,0 +1,75 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import org.sleuthkit.autopsy.imageanalyzer.progress.ProgressAdapter;
+import java.util.logging.Level;
+import javafx.concurrent.Task;
+import org.openide.util.Cancellable;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ *
+ */
+public abstract class LoggedTask<T> extends Task<T> implements Cancellable, ProgressAdapter {
+
+    private static final Logger LOGGER = Logger.getLogger(LoggedTask.class.getName());
+    private final String taskName;
+    private final Boolean logStateChanges;
+
+    public String getTaskName() {
+        return taskName;
+    }
+
+    public LoggedTask(String taskName, Boolean logStateChanges) {
+        this.taskName = taskName;
+        this.logStateChanges = logStateChanges;
+    }
+
+    @Override
+    protected void cancelled() {
+        super.cancelled();
+        if (logStateChanges) {
+            LOGGER.log(Level.WARNING, "{0} cancelled!", taskName);
+        }
+    }
+
+    @Override
+    protected void failed() {
+        super.failed();
+        LOGGER.log(Level.SEVERE, taskName + " failed", getException());
+
+    }
+
+    @Override
+    protected void scheduled() {
+        super.scheduled();
+        if (logStateChanges) {
+            LOGGER.log(Level.INFO, "{0} scheduled", taskName);
+        }
+    }
+
+    @Override
+    protected void succeeded() {
+        super.succeeded();
+        if (logStateChanges) {
+            LOGGER.log(Level.INFO, "{0} succeeded", taskName);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java
new file mode 100644
index 0000000000000000000000000000000000000000..096fa54b91ec93141f9c39ed4f86512a1c3c4a78
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java
@@ -0,0 +1,59 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import javafx.beans.property.SimpleListProperty;
+import javafx.collections.FXCollections;
+
+/**
+ *
+ */
+public class ObservableStack<T> extends SimpleListProperty<T> {
+
+    public ObservableStack() {
+        super(FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));
+    }
+
+    public void push(T item) {
+        synchronized (this) {
+            add(0, item);
+        }
+    }
+
+    public T pop() {
+        synchronized (this) {
+            if (isEmpty()) {
+                return null;
+            } else {
+                return remove(0);
+            }
+        }
+    }
+
+    public T peek() {
+        synchronized (this) {
+            if (isEmpty()) {
+                return null;
+            } else {
+                return get(0);
+            }
+        }
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java
new file mode 100644
index 0000000000000000000000000000000000000000..7234fa1b9509d701a005f76d19a9ddb7574e8746
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java
@@ -0,0 +1,55 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import javafx.application.Platform;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+
+/**
+ *
+ * The {@link OnStart} annotation tells NetBeans to invoke this class's
+ * {@link EurekaModule#run()} method
+ */
+@org.openide.modules.OnStart
+public class OnStart implements Runnable {
+
+    static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName());
+
+    /**
+     * make sure that the eureka listeners get setup as early as possible, and
+     * do other setup stuff.
+     *
+     * This method is invoked by virtue of the {@link OnStart} annotation on the
+     * {@link EurekaModule} class
+     */
+    @Override
+    public void run() {
+        Platform.setImplicitExit(false);
+
+        LOGGER.info("setting up eureka listeners");
+
+        IngestManager.getInstance().addIngestJobEventListener(AutopsyListener.getDefault().getIngestJobEventListener());
+        IngestManager.getInstance().addIngestModuleEventListener(AutopsyListener.getDefault().getIngestModuleEventListener());
+
+        Case.addPropertyChangeListener(AutopsyListener.getDefault().getCaseListener());
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f8b2c89e7633f7d92f683833bb71624f0810405
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java
@@ -0,0 +1,297 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.logging.Level;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ *
+ */
+public class PerCaseProperties {
+
+    public static final String ENABLED = "enabled";
+
+    public static final String STALE = "stale";
+
+    private final Case c;
+
+    /** the constructor */
+    PerCaseProperties(Case c) {
+        this.c = c;
+    }
+
+    /**
+     * Makes a new config file of the specified name. Do not include the
+     * extension.
+     *
+     * @param moduleName - The name of the config file to make
+     *
+     * @return True if successfully created, false if already exists or an error
+     *         is thrown.
+     */
+    public synchronized boolean makeConfigFile(String moduleName) {
+        if (!configExists(moduleName)) {
+            File propPath = new File(getPropertyPath(moduleName));
+            File parent = new File(propPath.getParent());
+            if (!parent.exists()) {
+                parent.mkdirs();
+            }
+            Properties props = new Properties();
+            try {
+                propPath.createNewFile();
+                try (FileOutputStream fos = new FileOutputStream(propPath)) {
+                    props.store(fos, "");
+                }
+            } catch (IOException e) {
+                Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Was not able to create a new properties file.", e); //NON-NLS
+                return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Determines if a given properties file exists or not.
+     *
+     * @param moduleName - The name of the config file to evaluate
+     *
+     * @return true if the config exists, false otherwise.
+     */
+    public synchronized boolean configExists(String moduleName) {
+        File f = new File(c.getCaseDirectory() + File.separator + moduleName + ".properties");
+        return f.exists();
+    }
+
+    public synchronized boolean settingExists(String moduleName, String settingName) {
+        if (!configExists(moduleName)) {
+            return false;
+        }
+        try {
+            Properties props = fetchProperties(moduleName);
+            return (props.getProperty(settingName) != null);
+        } catch (IOException e) {
+            return false;
+        }
+
+    }
+
+    /**
+     * Returns the path of the given properties file.
+     *
+     * @param moduleName - The name of the config file to evaluate
+     *
+     * @return The path of the given config file. Returns null if the config
+     *         file doesn't exist.
+     */
+    private synchronized String getPropertyPath(String moduleName) {
+        return c.getCaseDirectory() + File.separator + moduleName + ".properties"; //NON-NLS
+    }
+
+    /**
+     * Returns the given properties file's setting as specific by settingName.
+     *
+     * @param moduleName  - The name of the config file to read from.
+     * @param settingName - The setting name to retrieve.
+     *
+     * @return - the value associated with the setting.
+     *
+     * @throws IOException
+     */
+    public synchronized String getConfigSetting(String moduleName, String settingName) {
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+
+        try {
+            Properties props = fetchProperties(moduleName);
+
+            return props.getProperty(settingName);
+        } catch (IOException e) {
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS
+            return null;
+        }
+
+    }
+
+    /**
+     * Returns the given properties file's map of settings.
+     *
+     * @param moduleName - the name of the config file to read from.
+     *
+     * @return - the map of all key:value pairs representing the settings of the
+     *         config.
+     *
+     * @throws IOException
+     */
+    public synchronized Map< String, String> getConfigSettings(String moduleName) {
+
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+        try {
+            Properties props = fetchProperties(moduleName);
+
+            Set<String> keys = props.stringPropertyNames();
+            Map<String, String> map = new HashMap<>();
+
+            for (String s : keys) {
+                map.put(s, props.getProperty(s));
+            }
+
+            return map;
+        } catch (IOException e) {
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS
+            return null;
+        }
+    }
+
+    /**
+     * Sets the given properties file to the given setting map.
+     *
+     * @param moduleName - The name of the module to be written to.
+     * @param settings   - The mapping of all key:value pairs of settings to add
+     *                   to the config.
+     */
+    public synchronized void setConfigSettings(String moduleName, Map<String, String> settings) {
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+        try {
+            Properties props = fetchProperties(moduleName);
+
+            for (Map.Entry<String, String> kvp : settings.entrySet()) {
+                props.setProperty(kvp.getKey(), kvp.getValue());
+            }
+
+            File path = new File(getPropertyPath(moduleName));
+            try (FileOutputStream fos = new FileOutputStream(path)) {
+                props.store(fos, "Changed config settings(batch)"); //NON-NLS
+            }
+        } catch (IOException e) {
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS
+        }
+    }
+
+    /**
+     * Sets the given properties file to the given settings.
+     *
+     * @param moduleName  - The name of the module to be written to.
+     * @param settingName - The name of the setting to be modified.
+     * @param settingVal  - the value to set the setting to.
+     */
+    public synchronized void setConfigSetting(String moduleName, String settingName, String settingVal) {
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+
+        try {
+            Properties props = fetchProperties(moduleName);
+
+            props.setProperty(settingName, settingVal);
+
+            File path = new File(getPropertyPath(moduleName));
+            try (FileOutputStream fos = new FileOutputStream(path)) {
+                props.store(fos, "Changed config settings(single)"); //NON-NLS
+            }
+        } catch (IOException e) {
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS
+        }
+    }
+
+    /**
+     * Removes the given key from the given properties file.
+     *
+     * @param moduleName - The name of the properties file to be modified.
+     * @param key        - the name of the key to remove.
+     */
+    public synchronized void removeProperty(String moduleName, String key) {
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+
+        try {
+            if (getConfigSetting(moduleName, key) != null) {
+                Properties props = fetchProperties(moduleName);
+
+                props.remove(key);
+                File path = new File(getPropertyPath(moduleName));
+                try (FileOutputStream fos = new FileOutputStream(path)) {
+                    props.store(fos, "Removed " + key); //NON-NLS
+                }
+            }
+        } catch (IOException e) {
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not remove property from file, file not found", e); //NON-NLS
+        }
+    }
+
+    /**
+     * Returns the properties file as specified by moduleName.
+     *
+     * @param moduleName
+     *
+     * @return Properties file as specified by moduleName.
+     *
+     * @throws IOException
+     */
+    private Properties fetchProperties(String moduleName) throws IOException {
+        if (!configExists(moduleName)) {
+            makeConfigFile(moduleName);
+            Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS
+        }
+        Properties props;
+        try (InputStream inputStream = new FileInputStream(getPropertyPath(moduleName))) {
+            props = new Properties();
+            props.load(inputStream);
+        }
+        return props;
+    }
+
+//    /**
+//     * Gets the property file as specified.
+//     *
+//     * @param moduleName
+//     *
+//     * @return A new file handle, returns null if the file does not exist.
+//     */
+//    public File getPropertyFile(String moduleName) {
+//        String path = getPropertyPath(moduleName);
+//        if (path == null) {
+//            return null;
+//        } else {
+//            return new File(getPropertyPath(moduleName));
+//        }
+//    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..362af325685d87afe4c23f6288533c9033a562f3
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java
@@ -0,0 +1,156 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Level;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.image.ImageView;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.services.TagsManager;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Contains static methods for dealing with Tags in Eureka
+ */
+public class TagUtils {
+
+    private static final String follow_Up = "Follow Up";
+
+    private static TagName followUpTagName;
+
+    private final static List<TagListener> listeners = new ArrayList<>();
+
+    synchronized public static TagName getFollowUpTagName() throws TskCoreException {
+        if (followUpTagName == null) {
+            followUpTagName = getTagName(follow_Up);
+        }
+        return followUpTagName;
+    }
+
+    static public Collection<TagName> getNonCategoryTagNames() {
+        List<TagName> nonCatTagNames = new ArrayList<>();
+        List<TagName> allTagNames;
+        try {
+            allTagNames = Case.getCurrentCase().getServices().getTagsManager().getAllTagNames();
+            for (TagName tn : allTagNames) {
+                if (tn.getDisplayName().startsWith(Category.CATEGORY_PREFIX) == false) {
+                    nonCatTagNames.add(tn);
+                }
+            }
+        } catch (TskCoreException | IllegalStateException ex) {
+            Logger.getLogger(TagUtils.class.getName()).log(Level.WARNING, "couldn't access case", ex);
+        }
+
+        return nonCatTagNames;
+    }
+
+    synchronized static public TagName getTagName(String displayName) throws TskCoreException {
+        try {
+            final TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager();
+
+            for (TagName tn : tagsManager.getAllTagNames()) {
+                if (displayName.equals(tn.getDisplayName())) {
+                    return tn;
+                }
+            }
+            try {
+                return tagsManager.addTagName(displayName);
+            } catch (TagsManager.TagNameAlreadyExistsException ex) {
+                throw new TskCoreException("tagame exists but wasn't found", ex);
+            }
+        } catch (IllegalStateException ex) {
+            Logger.getLogger(TagUtils.class.getName()).log(Level.SEVERE, "Case was closed out from underneath", ex);
+            throw new TskCoreException("Case was closed out from underneath", ex);
+        }
+    }
+
+    /**
+     * convert a collection of TagNames to a 'pretty' string by concatenating
+     * their displaynames separated by a semi-colons
+     *
+     * Collection<TagName> => "tagname1, tagname2, tagname3"
+     *
+     * @param tagNames
+     *
+     * @return
+     */
+    static public String collectionToString(Collection<TagName> tagNames) {
+        ArrayList<Object> stringNames = new ArrayList<>();
+        for (TagName tn : tagNames) {
+            stringNames.add(tn.getDisplayName());
+        }
+        final String join = StringUtils.join(stringNames, "; ");
+        return join;
+    }
+
+    public static void fireChange(Collection<Long> ids) {
+        synchronized (listeners) {
+            for (TagListener list : listeners) {
+                list.handleTagsChanged(ids);
+            }
+        }
+    }
+
+    public static void registerListener(TagListener aThis) {
+        synchronized (listeners) {
+            listeners.add(aThis);
+        }
+    }
+
+    public static void unregisterListener(TagListener aThis) {
+        synchronized (listeners) {
+            listeners.remove(aThis);
+        }
+    }
+
+    /**
+     *
+     * @param tn            the value of tn
+     * @param eurekaToolbar the value of eurekaToolbar
+     */
+    static public MenuItem createSelTagMenuItem(final TagName tn, final SplitMenuButton tagSelectedMenuButton) {
+        final MenuItem menuItem = new MenuItem(tn.getDisplayName(), new ImageView(DrawableAttribute.TAGS.getIcon()));
+        menuItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent t) {
+                AddDrawableTagAction.getInstance().addTag(tn, "");
+                tagSelectedMenuButton.setText(tn.getDisplayName());
+                tagSelectedMenuButton.setOnAction(this);
+            }
+        });
+        return menuItem;
+    }
+
+    public static interface TagListener {
+
+        public void handleTagsChanged(Collection<Long> ids);
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java
new file mode 100644
index 0000000000000000000000000000000000000000..067272e291543d17d928aedc09979f7f790aa4b6
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java
@@ -0,0 +1,22 @@
+package org.sleuthkit.autopsy.imageanalyzer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
+@Inherited
+@Documented
+public @interface ThreadConfined {
+
+    ThreadType type();
+
+    public enum ThreadType {
+
+        ANY, UI, JFX, AWT, NOT_UI
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1a34571e842456a7650f40efe3522c5bb938c2b
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
+import javafx.application.Platform;
+import javax.swing.SwingUtilities;
+
+/**
+ *
+ */
+public class ThreadUtils {
+
+    /**
+     * analogous to {@link SwingUtilities#invokeAndWait(java.lang.Runnable) with a return value.
+     * Should be used extremely cautiausly as it blocks the edt thread.
+     *
+     * @param <E> The type of the return value generated by the given Callable,
+     *            r.
+     * @param r   A callable to be invoked on the ui thread.
+     *
+     * @return the result of the Callable's call method;
+     */
+    public static <E> E invokeAndWait(final Callable<E> r) {
+        final AtomicReference<E> ref = new AtomicReference<>();
+        final AtomicReference<Exception> except = new AtomicReference<>();
+        try {
+            SwingUtilities.invokeAndWait(() -> {
+                try {
+                    ref.set(r.call());
+                } catch (Exception e) {
+                    except.set(e);
+                }
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            throw new RuntimeException(ex);
+        } finally {
+            if (except.get() != null) {
+                throw new RuntimeException(except.get());
+            } else {
+                return ref.get();
+            }
+        }
+    }
+
+    /**
+     * analogous to {@link SwingUtilities#invokeAndWait(java.lang.Runnable) for JavaFX.
+     * Should be used extremely cautiausly as it blocks the JavaFx UI  thread.
+     *
+     * @param runnable the runnable to be invoked on the javafx ui thred
+     *
+     * @throws InterruptedException
+     * @throws ExecutionException
+     */
+    public static void runAndWait(Runnable runnable) throws InterruptedException, ExecutionException {
+        FutureTask future = new FutureTask(runnable, null);
+        Platform.runLater(future);
+        future.get();
+    }
+
+    /**
+     *
+     * @param r the value of r
+     */
+    public static void runNowOrLater(Runnable r) {
+        if (Platform.isFxApplicationThread()) {
+            r.run();
+        } else {
+            Platform.runLater(r);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee39b503ddaab312a34b5d9d3c1a6f65ec815cec
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java
@@ -0,0 +1,117 @@
+/*
+ * Autopsy Forensic Browser
+ * 
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ * 
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import javafx.scene.control.Menu;
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+import org.openide.util.Utilities;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Instances of this Action allow users to apply tags to content.
+ *
+ * //TODO: since we are not using actionsGlobalContext anymore and this has
+ * diverged from autopsy action, make this extend from controlsfx Action
+ */
+public class AddDrawableTagAction extends AddTagAction {
+    
+    private static final Logger LOGGER = Logger.getLogger(AddDrawableTagAction.class.getName());
+
+    // This class is a singleton to support multi-selection of nodes, since
+    // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every 
+    // node in the array returns a reference to the same action object from Node.getActions(boolean).    
+    private static AddDrawableTagAction instance;
+    
+    public static synchronized AddDrawableTagAction getInstance() {
+        if (null == instance) {
+            instance = new AddDrawableTagAction();
+        }
+        return instance;
+    }
+    
+    private AddDrawableTagAction() {
+    }
+    
+    public Menu getPopupMenu() {
+        return new TagMenu();
+    }
+    
+    @Override
+    protected String getActionDisplayName() {
+        return Utilities.actionsGlobalContext().lookupAll(AbstractFile.class).size() > 1 ? "Tag Files" : "Tag File";
+    }
+    
+    @Override
+    public void addTag(TagName tagName, String comment) {
+        Set<Long> selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected());
+        
+        new SwingWorker<Void, Void>() {
+            
+            @Override
+            protected Void doInBackground() throws Exception {
+                for (Long fileID : selectedFiles) {
+                    try {
+                        DrawableFile file = EurekaController.getDefault().getFileFromId(fileID);
+                        LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment});
+                        Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment);
+                    } catch (IllegalStateException ex) {
+                        LOGGER.log(Level.SEVERE, "Case was closed out from underneath Updatefile task", ex);
+                    } catch (TskCoreException ex) {
+                        LOGGER.log(Level.SEVERE, "Error tagging result", ex);
+                        JOptionPane.showMessageDialog(null, "Unable to tag " + fileID + ".", "Tagging Error", JOptionPane.ERROR_MESSAGE);
+                    }
+
+                    //make sure rest of ui  hears category change.
+                    EurekaController.getDefault().handleFileUpdate(new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.TAGS));
+
+                   
+                }
+                
+                refreshDirectoryTree();
+                return null;
+            }
+            
+            @Override
+            protected void done() {
+                super.done();
+                try {
+                    get();
+                } catch (InterruptedException | ExecutionException ex) {
+                    LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex);
+                }
+            }
+            
+        }.execute();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..f832800c00e9b30f449220cd9b4b1b1e96d6366a
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java
@@ -0,0 +1,158 @@
+/*
+ * Autopsy Forensic Browser
+ * 
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ * 
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.logging.Level;
+import javafx.event.ActionEvent;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javax.swing.SwingUtilities;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
+import org.sleuthkit.autopsy.actions.GetTagNameDialog;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.services.TagsManager;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * An abstract base class for Actions that allow users to tag SleuthKit data
+ * model objects.
+ */
+abstract class AddTagAction {
+
+    protected void refreshDirectoryTree() {
+        // The way the "directory tree" currently works, a new tags sub-tree 
+        // needs to be made to reflect the results of invoking tag Actions. 
+        SwingUtilities.invokeLater(() -> DirectoryTreeTopComponent.findInstance().refreshContentTreeSafe());
+
+    }
+
+    protected static final String NO_COMMENT = "";
+
+    AddTagAction() {
+    }
+
+    /**
+     * Template method to allow derived classes to provide a string for for a
+     * menu item label.
+     */
+    abstract protected String getActionDisplayName();
+
+    /**
+     * Template method to allow derived classes to add the indicated tag and
+     * comment to one or more a SleuthKit data model objects.
+     */
+    abstract protected void addTag(TagName tagName, String comment);
+
+    /**
+     * Instances of this class implement a context menu user interface for
+     * creating or selecting a tag name for a tag and specifying an optional tag
+     * comment.
+     */
+    // @@@ This user interface has some significant usability issues and needs
+    // to be reworked.
+    protected class TagMenu extends Menu {
+
+        TagMenu() {
+            super(getActionDisplayName());
+
+            // Get the current set of tag names.
+            TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager();
+            List<TagName> tagNames = null;
+            try {
+                tagNames = tagsManager.getAllTagNames();
+            } catch (TskCoreException ex) {
+                Logger.getLogger(TagsManager.class.getName()).log(Level.SEVERE, "Failed to get tag names", ex);
+            }
+
+            // Create a "Quick Tag" sub-menu.
+            Menu quickTagMenu = new Menu("Quick Tag");
+            getItems().add(quickTagMenu);
+
+            // Each tag name in the current set of tags gets its own menu item in
+            // the "Quick Tags" sub-menu. Selecting one of these menu items adds
+            // a tag with the associated tag name. 
+            if (null != tagNames && !tagNames.isEmpty()) {
+                for (final TagName tagName : tagNames) {
+                    if (tagName.getDisplayName().startsWith(Category.CATEGORY_PREFIX) == false) {
+                        MenuItem tagNameItem = new MenuItem(tagName.getDisplayName());
+                        tagNameItem.setOnAction((ActionEvent t) -> {
+                            addTag(tagName, NO_COMMENT);
+                            refreshDirectoryTree();
+                        });
+                        quickTagMenu.getItems().add(tagNameItem);
+                    }
+                }
+            } else {
+                MenuItem empty = new MenuItem("No tags");
+                empty.setDisable(true);
+                quickTagMenu.getItems().add(empty);
+            }
+
+            //   quickTagMenu.addSeparator();
+            // The "Quick Tag" menu also gets an "Choose Tag..." menu item.
+            // Selecting this item initiates a dialog that can be used to create
+            // or select a tag name and adds a tag with the resulting name.
+            MenuItem newTagMenuItem = new MenuItem("New Tag...");
+            newTagMenuItem.setOnAction((ActionEvent t) -> {
+                try {
+                    SwingUtilities.invokeAndWait(() -> {
+                        TagName tagName = GetTagNameDialog.doDialog();
+                        if (tagName != null) {
+                            addTag(tagName, NO_COMMENT);
+                            refreshDirectoryTree();
+                        }
+                    });
+                } catch (InterruptedException | InvocationTargetException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            });
+            quickTagMenu.getItems().add(newTagMenuItem);
+
+            // Create a "Choose Tag and Comment..." menu item. Selecting this item initiates
+            // a dialog that can be used to create or select a tag name with an 
+            // optional comment and adds a tag with the resulting name.
+            MenuItem tagAndCommentItem = new MenuItem("Tag and Comment...");
+            tagAndCommentItem.setOnAction((ActionEvent t) -> {
+                try {
+                    SwingUtilities.invokeAndWait(() -> {
+                        GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog();
+                        if (null != tagNameAndComment) {
+                            if (tagNameAndComment.getTagName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) {
+                                new CategorizeAction().addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
+                            } else {
+                                AddDrawableTagAction.getInstance().addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment());
+                            }
+                            refreshDirectoryTree();
+                        }
+                    });
+                } catch (InterruptedException | InvocationTargetException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            });
+            getItems().add(tagAndCommentItem);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java
new file mode 100644
index 0000000000000000000000000000000000000000..e003612fb30d18401f7f18ff2475ed43c649f554
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java
@@ -0,0 +1,50 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+
+/**
+ *
+ */
+public class Back extends AbstractAction {
+
+    private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png", 16, 16, true, true, true);
+
+    private final EurekaController controller;
+
+    public Back(EurekaController controller) {
+        super("Back");
+        setGraphic(new ImageView(BACK_IMAGE));
+        setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN));
+        this.controller = controller;
+        disabledProperty().bind(controller.getHistoryStack().sizeProperty().isEqualTo(0));
+    }
+
+    @Override
+    public void handle(ActionEvent ae) {
+        controller.goBack();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..785f75f1c6b9291ef34fd68e7fd49e7de029a4c5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java
@@ -0,0 +1,157 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import javafx.event.ActionEvent;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.input.KeyCodeCombination;
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Adaptation of Tag Actions to enforce category-tag uniqueness
+ *
+ * TODO: since we are not using actionsGlobalContext anymore and this has
+ * diverged from autopsy action, make this extend from controlsfx Action
+ */
+public class CategorizeAction extends AddTagAction {
+
+    private static final Logger LOGGER = Logger.getLogger(CategorizeAction.class.getName());
+
+    private final EurekaController controller;
+
+    public CategorizeAction() {
+        super();
+        this.controller = EurekaController.getDefault();
+    }
+
+    static public Menu getPopupMenu() {
+        return new CategoryMenu();
+    }
+
+    @Override
+    protected String getActionDisplayName() {
+        return "Categorize";
+    }
+
+    @Override
+    public void addTag(TagName tagName, String comment) {
+        Set<Long> selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected());
+
+        //TODO: should this get submitted to controller rather than a swingworker ? -jm
+        new SwingWorker<Object, Object>() {
+
+            @Override
+            protected Object doInBackground() throws Exception {
+                Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFiles.toString(), tagName.getDisplayName()});
+                for (Long fileID : selectedFiles) {
+
+                    try {
+                        DrawableFile file = controller.getFileFromId(fileID);
+
+                        Category oldCat = file.getCategory();
+                        // remove file from old category group
+                        controller.getGroupManager().removeFromGroup(new GroupKey(DrawableAttribute.CATEGORY, oldCat), fileID);
+
+                        //remove old category tag if necessary
+                        List<ContentTag> allContentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(file);
+
+                        for (ContentTag ct : allContentTags) {
+                            //this is bad: treating tags as categories as long as thier names start with prefix
+                            //TODO:  abandon using tags for categories and instead add a new column to DrawableDB
+                            if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) {
+                                LOGGER.log(Level.INFO, "removing old category from {0}", file.getName());
+                                Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct);
+                            }
+                        }
+
+                        if (tagName != Category.ZERO.getTagName()) { // no tags for cat-0
+                            Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment);
+                        }
+                        //make sure rest of ui  hears category change.
+                        controller.handleFileUpdate(
+                                new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.CATEGORY));
+
+                    } catch (TskCoreException ex) {
+                        LOGGER.log(Level.SEVERE, "Error categorizing result", ex);
+                        JOptionPane.showMessageDialog(null, "Unable to categorize " + fileID + ".", "Categorizing Error", JOptionPane.ERROR_MESSAGE);
+                    }
+                }
+
+                refreshDirectoryTree();
+                return null;
+            }
+
+            @Override
+            protected void done() {
+                super.done();
+                try {
+                    get();
+                } catch (InterruptedException | ExecutionException ex) {
+                    LOGGER.log(Level.SEVERE, "unexpected exception while categorizing files", ex);
+                }
+            }
+
+        }.execute();
+
+    }
+
+    /**
+     * Instances of this class implement a context menu user interface for
+     * selecting a category
+     */
+    static protected class CategoryMenu extends Menu {
+
+        CategoryMenu() {
+            super("Categorize");
+
+            // Each category get an item in the sub-menu. Selecting one of these menu items adds
+            // a tag with the associated category.
+            for (final Category cat : Category.values()) {
+
+                MenuItem categoryItem = new MenuItem(cat.getDisplayName());
+                categoryItem.setOnAction((ActionEvent t) -> {
+                    final CategorizeAction categorizeAction = new CategorizeAction();
+                    categorizeAction.addTag(cat.getTagName(), NO_COMMENT);
+                });
+                categoryItem.setAccelerator(new KeyCodeCombination(cat.getHotKeycode()));
+                getItems().add(categoryItem);
+            }
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java
new file mode 100644
index 0000000000000000000000000000000000000000..fcd69ec5a4aa887dc531034351c21a55700731b6
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java
@@ -0,0 +1,50 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import org.controlsfx.control.action.AbstractAction;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+
+/**
+ *
+ */
+public class Forward extends AbstractAction {
+
+    private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png", 16, 16, true, true, true);
+
+    private final EurekaController controller;
+
+    public Forward(EurekaController controller) {
+        super("Forward");
+        setGraphic(new ImageView(BACK_IMAGE));
+        setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN));
+        this.controller = controller;
+        disabledProperty().bind(controller.getForwardStack().sizeProperty().isEqualTo(0));
+    }
+
+    @Override
+    public void handle(ActionEvent ae) {
+        controller.goForward();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..783456174195f8445ee30b6ee1f7696bb1873521
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java
@@ -0,0 +1,54 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState;
+import javafx.beans.Observable;
+import javafx.event.ActionEvent;
+import javafx.scene.image.ImageView;
+import org.controlsfx.control.action.AbstractAction;
+
+/**
+ *
+ */
+public class NextUnseenGroup extends AbstractAction {
+
+    private final EurekaController controller;
+
+    public NextUnseenGroup(EurekaController controller) {
+        super("Next Unseen group");
+        this.controller = controller;
+        setGraphic(new ImageView("/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png"));
+        disabledProperty().set(controller.getGroupManager().getUnSeenGroups().size() <= 1);
+        controller.getGroupManager().getUnSeenGroups().addListener((Observable observable) -> {
+            disabledProperty().set(controller.getGroupManager().getUnSeenGroups().size() <= 1);
+        });
+    }
+
+    @Override
+    public void handle(ActionEvent event) {
+        if (controller.viewState() != null && controller.viewState().get() != null && controller.viewState().get().getGroup() != null) {
+            controller.getGroupManager().markGroupSeen(controller.viewState().get().getGroup());
+        }
+        if (controller.getGroupManager().getUnSeenGroups().size() > 0) {
+            controller.pushGroup(GroupViewState.tile(controller.getGroupManager().getUnSeenGroups().get(0)), false);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..e00818b84791f94b30feb18d653070e3bdcf0aae
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java
@@ -0,0 +1,121 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import java.awt.Component;
+import javax.swing.JButton;
+import javax.swing.JOptionPane;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionRegistration;
+import org.openide.util.HelpCtx;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.actions.CallableSystemAction;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.core.Installer;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaModule;
+
+@ActionID(category = "Tools",
+          id = "org.sleuthkit.autopsy.imageanalyzer.OpenAction")
+@ActionReference(path = "Menu/Tools" /* , position = 333 */)
+@ActionRegistration( //        iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png",
+        lazy = false,
+        displayName = "#CTL_OpenAction")
+@Messages("CTL_OpenAction=Analyze Images/Videos")
+public final class OpenAction extends CallableSystemAction {
+
+    private final String EUREKA = "Analyze Images/Videos";
+
+    private static final boolean fxInited = Installer.isJavaFxInited();
+
+    private static final Logger LOGGER = Logger.getLogger(OpenAction.class.getName());
+
+    public OpenAction() {
+        super();
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return Case.isCaseOpen() && fxInited && Case.getCurrentCase().hasData();
+    }
+
+    /** Returns the toolbar component of this action
+     *
+     * @return component the toolbar button */
+    @Override
+    public Component getToolbarPresenter() {
+        JButton toolbarButton = new JButton(this);
+        toolbarButton.setText(EUREKA);
+        toolbarButton.addActionListener(this);
+
+        return toolbarButton;
+    }
+
+    @Override
+    public void performAction() {
+
+        //check case
+        if (!Case.existsCurrentCase()) {
+            return;
+        }
+        final Case currentCase = Case.getCurrentCase();
+
+        if (EurekaModule.isCaseStale(currentCase)) {
+            //case is stale, ask what to do
+            int answer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), "The image / video databse may be out of date. "
+                                                       + "Do you want to update and listen for further ingest results?\n"
+                                                       + "  Choosing 'no' will display the out of date results."
+                                                       + " Choosing 'cancel' will close the image /video analyzer",
+                                                       "The image / video databse may be out of date. ", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+
+            switch (answer) {
+                case JOptionPane.YES_OPTION:
+                    EurekaController.getDefault().setListeningEnabled(true);
+                //fall through
+                case JOptionPane.NO_OPTION:
+                    EurekaModule.openTopComponent();
+                    break;
+                case JOptionPane.CANCEL_OPTION:
+                    break; //do nothing
+
+            }
+        } else {
+            //case is not stale, just open it
+            EurekaModule.openTopComponent();
+        }
+    }
+
+    @Override
+    public String getName() {
+        return EUREKA;
+    }
+
+    @Override
+    public HelpCtx getHelpCtx() {
+        return HelpCtx.DEFAULT_HELP;
+    }
+
+    @Override
+    public boolean asynchronous() {
+        return false; // run on edt
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..9fce64faf7980f07afefbee236ba8e6570692cea
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java
@@ -0,0 +1,51 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+import java.net.URI;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionRegistration;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle.Messages;
+
+@ActionID(
+        category = "Help",
+        id = "org.sleuthkit.autopsy.imageanalyzer.actions.OpenHelpAction"
+)
+@ActionRegistration(
+        displayName = "#CTL_OpenHelpAction"
+)
+@ActionReference(path = "Menu/Help", position = 350)
+@Messages("CTL_OpenHelpAction=Image / Video Analyzer Help")
+public final class OpenHelpAction implements ActionListener {
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        try {
+            Desktop.getDesktop().browse(URI.create("http://sleuthkit.org/autopsy/docs/user-docs/"));
+        } catch (IOException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2d35d2f92641b1f91814147a8fe7ead166a138f6
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java
@@ -0,0 +1,87 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.actions;
+
+import javafx.event.ActionEvent;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.MenuElement;
+
+public class SwingMenuItemAdapter extends MenuItem {
+
+    JMenuItem jMenuItem;
+
+    SwingMenuItemAdapter(final JMenuItem jMenuItem) {
+        super(jMenuItem.getText());
+        this.jMenuItem = jMenuItem;
+        setOnAction((ActionEvent t) -> {
+            jMenuItem.doClick();
+        });
+    }
+
+    public static MenuItem create(MenuElement jmenuItem) {
+        if (jmenuItem instanceof JMenu) {
+            return new SwingMenuAdapter((JMenu) jmenuItem);
+        } else if (jmenuItem instanceof JPopupMenu) {
+            return new SwingMenuAdapter((JPopupMenu) jmenuItem);
+        } else {
+            return new SwingMenuItemAdapter((JMenuItem) jmenuItem);
+        }
+
+    }
+}
+
+class SwingMenuAdapter extends Menu {
+
+    private final MenuElement jMenu;
+
+    SwingMenuAdapter(final JMenu jMenu) {
+        super(jMenu.getText());
+        this.jMenu = jMenu;
+        buildChildren(jMenu);
+
+    }
+
+    SwingMenuAdapter(JPopupMenu jPopupMenu) {
+        super(jPopupMenu.getLabel());
+        this.jMenu = jPopupMenu;
+
+        buildChildren(jMenu);
+    }
+
+    private void buildChildren(MenuElement jMenu) {
+
+        for (MenuElement menuE : jMenu.getSubElements()) {
+            if (menuE instanceof JMenu) {
+                getItems().add(SwingMenuItemAdapter.create((JMenu) menuE));
+            } else if (menuE instanceof JMenuItem) {
+                getItems().add(SwingMenuItemAdapter.create((JMenuItem) menuE));
+            } else if (menuE instanceof JPopupMenu) {
+                buildChildren(menuE);
+            } else {
+
+                System.out.println(menuE.toString());
+//                throw new UnsupportedOperationException();
+            }
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d9e9dfc1c5611625eb7497cdfc9cfe6990394a5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java
@@ -0,0 +1,160 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.paint.Color;
+import javax.annotation.concurrent.GuardedBy;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ *
+ */
+public enum Category implements Comparable<Category> {
+
+    ZERO(Color.LIGHTGREY, 0, "CAT-0, Uncategorized"),
+    ONE(Color.RED, 1, "CAT-1,  Child Exploitation (Illegal)"),
+    TWO(Color.ORANGE, 2, "CAT-2, Child Exploitation (Non-Illegal/Age Difficult)"),
+    THREE(Color.YELLOW, 3, "CAT-3, CGI/Animation (Child Exploitive)"),
+    FOUR(Color.BISQUE, 4, "CAT-4,  Exemplar/Comparison (Internal Use Only)"),
+    FIVE(Color.GREEN, 5, "CAT-5, Non-pertinent");
+
+    final static private Map<String, Category> nameMap = new HashMap();
+
+    private static List<Category> valuesList = Arrays.asList(values());
+
+    static {
+        for (Category cat : values()) {
+            nameMap.put(cat.displayName, cat);
+        }
+    }
+
+    @GuardedBy("listeners")
+    private final static Set<CategoryListener> listeners = new HashSet<>();
+
+    public static void fireChange(Collection<Long> ids) {
+        synchronized (listeners) {
+            for (CategoryListener list : listeners) {
+                list.handleCategoryChanged(ids);
+            }
+        }
+    }
+
+    public static void registerListener(CategoryListener aThis) {
+        synchronized (listeners) {
+            listeners.add(aThis);
+        }
+    }
+
+    public static void unregisterListener(CategoryListener aThis) {
+        synchronized (listeners) {
+            listeners.remove(aThis);
+        }
+    }
+
+    public KeyCode getHotKeycode() {
+        return KeyCode.getKeyCode(Integer.toString(id));
+    }
+
+    public static final String CATEGORY_PREFIX = "CAT-";
+
+    private TagName tagName;
+
+    public static List<Category> valuesList() {
+        return valuesList;
+    }
+
+    private Color color;
+
+    private String displayName;
+
+    private int id;
+
+    public Color getColor() {
+        return color;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public String toString() {
+        return displayName;
+    }
+
+    static public Category fromDisplayName(String displayName) {
+        return nameMap.get(displayName);
+    }
+
+    private Category(Color color, int id, String name) {
+        this.color = color;
+        this.displayName = name;
+        this.id = id;
+    }
+
+    public TagName getTagName() {
+
+        if (tagName == null) {
+            try {
+                tagName = TagUtils.getTagName(displayName);
+            } catch (TskCoreException ex) {
+                Logger.getLogger(Category.class.getName()).log(Level.SEVERE, "failed to get TagName for " + displayName, ex);
+            }
+        }
+        return tagName;
+    }
+
+    public MenuItem createSelCatMenuItem(final SplitMenuButton catSelectedMenuButton) {
+        final MenuItem menuItem = new MenuItem(this.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon()));
+        menuItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent t) {
+                //                        EurekaTileSelectionModel.getInstance().clearAndSelectAll(getTiles().toArray(new DrawableTile[getTiles().size()]));
+                new CategorizeAction().addTag(Category.this.getTagName(), "");
+                catSelectedMenuButton.setText(Category.this.getDisplayName());
+                catSelectedMenuButton.setOnAction(this);
+            }
+        });
+        return menuItem;
+    }
+
+    public static interface CategoryListener {
+
+        public void handleCategoryChanged(Collection<Long> ids);
+
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..32a8a120fc72fe981424b8ac911ece81c259b02a
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java
@@ -0,0 +1,141 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import javafx.beans.property.ReadOnlyStringWrapper;
+import javafx.beans.property.StringProperty;
+import javafx.scene.image.Image;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.datamodel.TagName;
+
+/** psuedo-enum of attributes to filter, sort, and group on. They
+ * mostly correspond to the columns in the db.
+ *
+ * TODO: Review and refactor DrawableAttribute related code with an eye to usage
+ * of type paramaters and multivalued attributes
+ */
+public class DrawableAttribute<T> {
+
+    public final static DrawableAttribute<String> NAME
+            = new DrawableAttribute<>(AttributeName.NAME, "Name", true, "folder-rename.png");
+
+    public final static DrawableAttribute<Boolean> ANALYZED
+            = new DrawableAttribute<>(AttributeName.ANALYZED, "Analyzed", true, "");
+
+    /** since categories are really just tags in autopsy, they are not dealt
+     * with in the DrawableDB. they have special code in various places
+     * to make this transparent.
+     *
+     * //TODO: this had lead to awkward hard to maintain code, and little
+     * advantage. move categories into DrawableDB
+     */
+    public final static DrawableAttribute<Category> CATEGORY
+            = new DrawableAttribute<>(AttributeName.CATEGORY, "Category", false, "category-icon.png");
+
+    public final static DrawableAttribute<Collection<TagName>> TAGS
+            = new DrawableAttribute<>(AttributeName.TAGS, "Tags", false, "tag_red.png");
+
+    public final static DrawableAttribute<String> PATH
+            = new DrawableAttribute<>(AttributeName.PATH, "Path", true, "folder_picture.png");
+
+    public final static DrawableAttribute<Long> CREATED_TIME
+            = new DrawableAttribute<>(AttributeName.CREATED_TIME, "Created Time", true, "clock--plus.png");
+
+    public final static DrawableAttribute<Long> MODIFIED_TIME
+            = new DrawableAttribute<>(AttributeName.MODIFIED_TIME, "Modified Time", true, "clock--pencil.png");
+
+    public final static DrawableAttribute<String> MAKE
+            = new DrawableAttribute<>(AttributeName.MAKE, "Camera Make", true, "camera.png");
+
+    public final static DrawableAttribute<String> MODEL
+            = new DrawableAttribute<>(AttributeName.MODEL, "Camera Model", true, "camera.png");
+
+    //TODO: should this be DrawableAttribute<Collection<String>>?
+    public final static DrawableAttribute<String> HASHSET
+            = new DrawableAttribute<>(AttributeName.HASHSET, "Hashset", false, "hashset_hits.png");
+
+    public final static DrawableAttribute<Long> OBJ_ID
+            = new DrawableAttribute<>(AttributeName.OBJ_ID, "Internal Object ID", true, "");
+
+    public final static DrawableAttribute<Number> WIDTH
+            = new DrawableAttribute<>(AttributeName.WIDTH, "Width", true, "arrow-resize.png");
+
+    public final static DrawableAttribute<Number> HEIGHT
+            = new DrawableAttribute<>(AttributeName.HEIGHT, "Height", true, "arrow-resize-090.png");
+
+    final private static List< DrawableAttribute<?>> groupables
+            = Arrays.asList(PATH, HASHSET, CATEGORY, TAGS, MAKE, MODEL);
+
+    final private static List<DrawableAttribute<?>> values
+            = Arrays.asList(NAME, ANALYZED, CATEGORY, TAGS, PATH, CREATED_TIME,
+                            MODIFIED_TIME, HASHSET, CATEGORY, MAKE, MODEL, OBJ_ID,
+                            WIDTH, HEIGHT);
+
+    private DrawableAttribute(AttributeName name, String displayName, Boolean isDBColumn, String imageName) {
+        this.attrName = name;
+        this.displayName = new ReadOnlyStringWrapper(displayName);
+        this.isDBColumn = isDBColumn;
+        this.imageName = imageName;
+    }
+
+    private Image icon;
+
+    public final boolean isDBColumn;
+
+    public final AttributeName attrName;
+
+    private final StringProperty displayName;
+
+    private final String imageName;
+
+    public Image getIcon() {
+        if (icon == null) {
+            if (StringUtils.isBlank(imageName) == false) {
+                this.icon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/" + imageName, true);
+            }
+        }
+        return icon;
+    }
+
+    public static List<DrawableAttribute<?>> getGroupableAttrs() {
+        return groupables;
+    }
+
+    public static List<DrawableAttribute<?>> getValues() {
+        return Collections.unmodifiableList(values);
+    }
+
+    public StringProperty displayName() {
+        return displayName;
+    }
+
+    public String getDisplayName() {
+        return displayName.get();
+    }
+
+    public static enum AttributeName {
+
+        NAME, ANALYZED, CATEGORY, TAGS, PATH, CREATED_TIME, MODIFIED_TIME, MAKE,
+        MODEL, HASHSET, OBJ_ID, WIDTH, HEIGHT;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java
new file mode 100644
index 0000000000000000000000000000000000000000..8aa88f1dbe6619d9c73afc2868d5ac63ab3dccc1
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java
@@ -0,0 +1,1147 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Level;
+import javax.swing.SortOrder;
+import org.apache.commons.lang.StringUtils;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateListener;
+import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.CATEGORY;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy;
+import static org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy.GROUP_BY_VALUE;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sqlite.SQLiteJDBCLoader;
+
+/** This class is the public interface to the Image / Video Analyzer SQLite
+ * database. This class borrows a lot of ideas and techniques (for good or ill)
+ * from {@link  SleuthkitCase}.
+ *
+ * TODO: Creating an abstract base class for sqlite databases* may make sense
+ * in the future. see also {@link EventsDB} */
+public class DrawableDB {
+
+    private static final java.util.logging.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName());
+
+    //column name constants//////////////////////
+    private static final String ANALYZED = "analyzed";
+
+    private static final String OBJ_ID = "obj_id";
+
+    private static final String HASH_SET_NAME = "hash_set_name";
+
+    private final PreparedStatement insertHashSetStmt;
+
+    private final PreparedStatement groupSeenQueryStmt;
+
+    private final PreparedStatement insertGroupStmt;
+
+    private final List<PreparedStatement> preparedStatements = new ArrayList<>();
+
+    private final PreparedStatement removeFileStmt;
+
+    private final PreparedStatement updateGroupStmt;
+
+    private final PreparedStatement selectHashSetStmt;
+
+    private final PreparedStatement selectHashSetNamesStmt;
+
+    private final PreparedStatement insertHashHitStmt;
+
+    private final PreparedStatement insertFileStmt;
+
+    private final PreparedStatement pathGroupStmt;
+
+    private final PreparedStatement nameGroupStmt;
+
+    private final PreparedStatement created_timeGroupStmt;
+
+    private final PreparedStatement modified_timeGroupStmt;
+
+    private final PreparedStatement makeGroupStmt;
+
+    private final PreparedStatement modelGroupStmt;
+
+    private final PreparedStatement analyzedGroupStmt;
+
+    private final PreparedStatement hashSetGroupStmt;
+
+    /** map from {@link DrawableAttribute} to the {@link PreparedStatement} thet
+     * is used to select groups for that attribute */
+    private final Map<DrawableAttribute, PreparedStatement> groupStatementMap = new HashMap<>();
+
+    /** list of observers to be notified if the database changes */
+    private final HashSet<FileUpdateListener> updateListeners = new HashSet<>();
+
+    private GroupManager manager;
+
+    private EurekaController controller;
+
+    private final String dbPath;
+
+    volatile private Connection con;
+
+    private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy
+
+    private static final Lock DBLock = rwLock.writeLock(); //using exclusing lock for all db ops for now
+
+    static {//make sure sqlite driver is loaded // possibly redundant
+        try {
+            Class.forName("org.sqlite.JDBC");
+        } catch (ClassNotFoundException ex) {
+            LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex);
+        }
+    }
+
+    //////////////general database logic , mostly borrowed from sleuthkitcase
+    /**
+     * Lock to protect against concurrent write accesses to case database and to
+     * block readers while database is in write transaction. Should be utilized
+     * by all db code where underlying storage supports max. 1 concurrent writer
+     * MUST always call dbWriteUnLock() as early as possible, in the same thread
+     * where dbWriteLock() was called
+     */
+    public static void dbWriteLock() {
+        //Logger.getLogger("LOCK").log(Level.INFO, "Locking " + rwLock.toString());
+        DBLock.lock();
+    }
+
+    /**
+     * Release previously acquired write lock acquired in this thread using
+     * dbWriteLock(). Call in "finally" block to ensure the lock is always
+     * released.
+     */
+    public static void dbWriteUnlock() {
+        //Logger.getLogger("LOCK").log(Level.INFO, "UNLocking " + rwLock.toString());
+        DBLock.unlock();
+    }
+
+    /**
+     * Lock to protect against read while it is in a write transaction
+     * state.
+     * Supports multiple concurrent readers if there is no writer. MUST
+     * always call dbReadUnLock() as early as possible, in the same thread
+     * where dbReadLock() was called.
+     */
+    void dbReadLock() {
+        DBLock.lock();
+    }
+
+    /**
+     * Release previously acquired read lock acquired in this thread using
+     * dbReadLock(). Call in "finally" block to ensure the lock is always
+     * released.
+     */
+    void dbReadUnlock() {
+        DBLock.unlock();
+    }
+
+    /** @param dbPath the path to the db file
+     *
+     * @throws SQLException if there is problem creating or configuring the db */
+    private DrawableDB(String dbPath) throws SQLException, ExceptionInInitializerError {
+        this.dbPath = dbPath;
+
+        if (initializeDB()) {
+            insertFileStmt = prepareStatement(
+                    "INSERT OR REPLACE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) "
+                    + "VALUES (?,?,?,?,?,?,?,?)");
+            removeFileStmt = prepareStatement("delete from drawable_files where obj_id = ?");
+
+            pathGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  path  = ? ", DrawableAttribute.PATH);
+            nameGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  name  = ? ", DrawableAttribute.NAME);
+            created_timeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  created_time  = ? ", DrawableAttribute.CREATED_TIME);
+            modified_timeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  modified_time  = ? ", DrawableAttribute.MODIFIED_TIME);
+            makeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  make  = ? ", DrawableAttribute.MAKE);
+            modelGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where  model  = ? ", DrawableAttribute.MODEL);
+            analyzedGroupStmt = prepareStatement("Select obj_id , analyzed from drawable_files where analyzed = ?", DrawableAttribute.ANALYZED);
+            hashSetGroupStmt = prepareStatement("select drawable_files.obj_id as obj_id, analyzed from drawable_files natural join hash_sets natural join hash_set_hits  where hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET);
+
+            updateGroupStmt = prepareStatement("update groups set seen = 1 where value = ? and attribute = ?");
+            insertGroupStmt = prepareStatement("insert or replace into groups (value, attribute) values (?,?)");
+
+            groupSeenQueryStmt = prepareStatement("select seen from groups where value = ? and attribute = ?");
+
+            selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets");
+            insertHashSetStmt = prepareStatement("insert or ignore into hash_sets (hash_set_name)  values (?)");
+            selectHashSetStmt = prepareStatement("select hash_set_id from hash_sets where hash_set_name = ?");
+
+            insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)");
+
+        } else {
+            throw new ExceptionInInitializerError();
+        }
+    }
+
+    /** create PreparedStatement with the supplied string, and add the new
+     * statement to the list of PreparedStatements used in {@link DrawableDB#closeStatements()
+     *
+     * @param stmtString the string representation of the sqlite statement to
+     *                   prepare
+     *
+     * @return the prepared statement
+     *
+     * @throws SQLException if unable to prepare the statement */
+    private PreparedStatement prepareStatement(String stmtString) throws SQLException {
+        PreparedStatement prepareStatement = con.prepareStatement(stmtString);
+        preparedStatements.add(prepareStatement);
+        return prepareStatement;
+    }
+
+    /** calls {@link DrawableDB#prepareStatement(java.lang.String) ,
+     *  and then add the statement to the groupStatmentMap used to lookup
+     * statements by the attribute/column they group on
+     *
+     * @param stmtString the string representation of the sqlite statement to
+     *                   prepare
+     * @param attr       the {@link DrawableAttribute} this query groups by
+     *
+     * @return the prepared statement
+     *
+     * @throws SQLExceptionif unable to prepare the statement */
+    private PreparedStatement prepareStatement(String stmtString, DrawableAttribute<?> attr) throws SQLException {
+        PreparedStatement prepareStatement = prepareStatement(stmtString);
+        if (attr != null) {
+            groupStatementMap.put(attr, prepareStatement);
+        }
+
+        return prepareStatement;
+    }
+
+    /** public factory method. Creates and opens a connection to a new
+     * database * at the given path.
+     *
+     * @param dbPath
+     *
+     * @return
+     */
+    public static DrawableDB getDrawableDB(String dbPath, EurekaController controller) {
+
+        try {
+            DrawableDB drawableDB = new DrawableDB(dbPath + File.separator + "drawable.db");
+            drawableDB.controller = controller;
+            return drawableDB;
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "sql error creating database connection", ex);
+            return null;
+        } catch (ExceptionInInitializerError ex) {
+            LOGGER.log(Level.SEVERE, "error creating database connection", ex);
+            return null;
+        }
+    }
+
+    private void setPragmas() throws SQLException {
+
+        //this should match Sleuthkit db setupt
+        try (Statement statement = con.createStatement()) {
+            //reduce i/o operations, we have no OS crash recovery anyway
+            statement.execute("PRAGMA synchronous = OFF;");
+            //allow to query while in transaction - no need read locks
+            statement.execute("PRAGMA read_uncommitted = True;");
+
+            //TODO: do we need this?
+            statement.execute("PRAGMA foreign_keys = ON");
+
+            //TODO: test this
+            statement.execute("PRAGMA journal_mode  = MEMORY");
+//
+            //we don't use this feature, so turn it off for minimal speed up on queries
+            //this is deprecated and not recomended
+            statement.execute("PRAGMA count_changes = OFF;");
+            //this made a big difference to query speed
+            statement.execute("PRAGMA temp_store = MEMORY");
+            //this made a modest improvement in query speeds
+            statement.execute("PRAGMA cache_size = 50000");
+            //we never delete anything so...
+            statement.execute("PRAGMA auto_vacuum = 0");
+        }
+
+        try {
+            LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode",
+                                                 SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode()
+                                                                                ? "native" : "pure-java"));
+        } catch (Exception exception) {
+            LOGGER.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception);
+        }
+
+    }
+
+    /** create the table and indices if they don't already exist
+     *
+     * @return the number of rows in the table , count > 0 indicating an
+     *         existing table */
+    private boolean initializeDB() {
+        try {
+            if (isClosed()) {
+                openDBCon();
+            }
+            setPragmas();
+
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem accessing  database", ex);
+            return false;
+        }
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE TABLE  if not exists drawable_files "
+                    + "( obj_id INTEGER PRIMARY KEY, "
+                    + " path VARCHAR(255), "
+                    + " name VARCHAR(255), "
+                    + " created_time integer, "
+                    + " modified_time integer, "
+                    + " make VARCHAR(255), "
+                    + " model VARCHAR(255), "
+                    + " analyzed integer DEFAULT 0)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem creating drawable_files table", ex);
+            return false;
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE TABLE  if not exists groups "
+                    + "(group_id INTEGER PRIMARY KEY, "
+                    + " value VARCHAR(255) not null, "
+                    + " attribute VARCHAR(255) not null, "
+                    + " seen integer DEFAULT 0, "
+                    + " UNIQUE(value, attribute) )";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem creating groups table", ex);
+            return false;
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE TABLE  if not exists hash_sets "
+                    + "( hash_set_id INTEGER primary key,"
+                    + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem creating hash_sets table", ex);
+            return false;
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE TABLE  if not exists hash_set_hits "
+                    + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, "
+                    + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, "
+                    + " PRIMARY KEY (hash_set_id, obj_id))";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex);
+            return false;
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE UNIQUE INDEX if not exists obj_id_idx ON drawable_files(obj_id)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating obj_id_idx", ex);
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE  INDEX if not exists path_idx ON drawable_files(path)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating path_idx", ex);
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE  INDEX if not exists name_idx ON drawable_files(name)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating name_idx", ex);
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE  INDEX if not exists make_idx ON drawable_files(make)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating make_idx", ex);
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE  INDEX if not exists model_idx ON drawable_files(model)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating model_idx", ex);
+        }
+
+        try (Statement stmt = con.createStatement()) {
+            String sql = "CREATE  INDEX if not exists analyzed_idx ON drawable_files(analyzed)";
+            stmt.execute(sql);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem creating analyzed_idx", ex);
+        }
+
+        return true;
+    }
+
+    @Override
+    public void finalize() throws Throwable {
+        try {
+            closeDBCon();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    public void closeDBCon() {
+        if (con != null) {
+            try {
+                closeStatements();
+                con.close();
+            } catch (SQLException ex) {
+                LOGGER.log(Level.WARNING, "Failed to close connection to drawable.db", ex);
+            }
+        }
+        con = null;
+    }
+
+    public void openDBCon() {
+        try {
+            if (con == null || con.isClosed()) {
+                con = DriverManager.getConnection("jdbc:sqlite:" + dbPath);
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "Failed to open connection to drawable.db", ex);
+        }
+    }
+
+    public boolean isClosed() throws SQLException {
+        if (con == null) {
+            return true;
+        }
+        return con.isClosed();
+    }
+
+    /** get all the hash set names used in the db
+     *
+     * @return a set of the names of all the hash sets that have hash set hits */
+    public Set<String> getHashSetNames() {
+        Set<String> names = new HashSet<>();
+        // "SELECT DISTINCT hash_set_name FROM hash_sets"
+        dbReadLock();
+        try (ResultSet rs = selectHashSetNamesStmt.executeQuery();) {
+            while (rs.next()) {
+                names.add(rs.getString(HASH_SET_NAME));
+            }
+        } catch (SQLException sQLException) {
+            LOGGER.log(Level.WARNING, "failed to get hash set names", sQLException);
+        } finally {
+            dbReadUnlock();
+        }
+        return names;
+    }
+
+    public boolean isGroupSeen(GroupKey groupKey) {
+        dbReadLock();
+        try {
+            groupSeenQueryStmt.clearParameters();
+            groupSeenQueryStmt.setString(1, groupKey.getValueDisplayName());
+            groupSeenQueryStmt.setString(2, groupKey.getAttribute().attrName.toString());
+            try (ResultSet rs = groupSeenQueryStmt.executeQuery()) {
+                while (rs.next()) {
+                    return rs.getBoolean("seen");
+                }
+            }
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return false;
+    }
+
+    public void markGroupSeen(GroupKey gk) {
+        dbWriteLock();
+        try {
+            //PreparedStatement updateGroup = con.prepareStatement("update groups set seen = 1 where value = ? and attribute = ?");
+            updateGroupStmt.clearParameters();
+            updateGroupStmt.setString(1, gk.getValueDisplayName());
+            updateGroupStmt.setString(2, gk.getAttribute().attrName.toString());
+            updateGroupStmt.execute();
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    public boolean removeFile(long id) {
+        DrawableTransaction trans = beginTransaction();
+        boolean removeFile = removeFile(id, trans);
+        commitTransaction(trans, true);
+        return removeFile;
+    }
+
+    public void updateFile(DrawableFile f) {
+        DrawableTransaction trans = beginTransaction();
+        updatefile(f, trans);
+        commitTransaction(trans, true);
+    }
+
+    /**
+     * use transactions to update files
+     *
+     * @param f
+     * @param tr
+     *
+     *
+     *
+     */
+    public void updatefile(DrawableFile f, DrawableTransaction tr) {
+
+        //TODO:      implement batch version -jm
+        if (tr.isClosed()) {
+            throw new IllegalArgumentException("can't update database with closed transaction");
+        }
+        dbWriteLock();
+        try {
+
+            // "INSERT OR REPLACE INTO drawable_files (path, name, created_time, modified_time, make, model, analyzed)"
+            insertFileStmt.setLong(1, f.getId());
+            insertFileStmt.setString(2, f.getDrawablePath());
+            insertFileStmt.setString(3, f.getName());
+            insertFileStmt.setLong(4, f.getCrtime());
+            insertFileStmt.setLong(5, f.getMtime());
+            insertFileStmt.setString(6, f.getMake());
+            insertFileStmt.setString(7, f.getModel());
+            insertFileStmt.setInt(8, f.isAnalyzed() ? 1 : 0);
+            insertFileStmt.executeUpdate();
+
+            final List<String> hashSetNames = (List<String>) f.getValueOfAttribute(DrawableAttribute.HASHSET);
+
+            if (hashSetNames.isEmpty() == false) {
+                for (String name : hashSetNames) {
+
+                    // "insert or ignore into hash_sets (hash_set_name)  values (?)"
+                    insertHashSetStmt.setString(1, name);
+                    insertHashSetStmt.executeUpdate();
+
+                    //TODO: use nested select to get hash_set_id rather than seperate statement/query
+                    //"select hash_set_id from hash_sets where hash_set_name = ?"
+                    selectHashSetStmt.setString(1, name);
+                    try (ResultSet rs = selectHashSetStmt.executeQuery()) {
+                        while (rs.next()) {
+                            int hashsetID = rs.getInt("hash_set_id");
+                            //"insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)";
+                            insertHashHitStmt.setInt(1, hashsetID);
+                            insertHashHitStmt.setLong(2, f.getId());
+                            insertHashHitStmt.executeUpdate();
+                            break;
+                        }
+                    }
+                }
+            }
+
+            //and update all groups this file is in
+            for (DrawableAttribute attr : DrawableAttribute.getGroupableAttrs()) {
+                Object valueOfAttribute = f.getValueOfAttribute(attr);
+                Collection vals;
+                if (valueOfAttribute instanceof Collection) {
+                    vals = (Collection) valueOfAttribute;
+                } else {
+                    vals = Collections.singleton(valueOfAttribute);
+                }
+                for (Object val : vals) {
+                    insertGroup(val.toString(), attr);
+                }
+            }
+
+            tr.addUpdatedFile(f.getId());
+
+        } catch (SQLException ex) {
+            LOGGER.log(Level.SEVERE, "failed to update file" + f.getName(), ex);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    public DrawableTransaction beginTransaction() {
+        return new DrawableTransaction();
+    }
+
+    public void commitTransaction(DrawableTransaction tr, Boolean notify) {
+        if (tr.isClosed()) {
+            throw new IllegalArgumentException("can't close already closed transaction");
+        }
+        tr.commit(notify);
+    }
+
+    public void addUpdatedFileListener(FileUpdateListener l) {
+        updateListeners.add(l);
+    }
+
+    /**
+     *
+     * @param f the value of f
+     */
+    private void fireUpdatedFiles(Collection<Long> f) {
+        for (FileUpdateListener listener : updateListeners) {
+            listener.handleFileUpdate(new FileUpdateEvent(f, null, FileUpdateEvent.UpdateType.FILE_UPDATED));
+        }
+    }
+
+    private void fireRemovedFiles(Collection<Long> f) {
+        for (FileUpdateListener listener : updateListeners) {
+            listener.handleFileUpdate(new FileUpdateEvent(f, null, FileUpdateEvent.UpdateType.FILE_REMOVED));
+        }
+    }
+
+    public Boolean isFileAnalyzed(DrawableFile f) {
+        return isFileAnalyzed(f.getId());
+    }
+
+    public Boolean isFileAnalyzed(long fileId) {
+        dbReadLock();
+        try (Statement stmt = con.createStatement();
+             ResultSet analyzedQuery = stmt.executeQuery("select analyzed from drawable_files where obj_id = " + fileId)) {
+            while (analyzedQuery.next()) {
+                return analyzedQuery.getBoolean(ANALYZED);
+            }
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return false;
+    }
+
+    public Boolean areFilesAnalyzed(Collection<Long> fileIds) {
+
+        dbReadLock();
+        try (Statement stmt = con.createStatement();
+             //Can't make this a preprared statement because of the IN ( ... )
+             ResultSet analyzedQuery = stmt.executeQuery("select count(analyzed) as analyzed from drawable_files where analyzed = 1 and obj_id in (" + StringUtils.join(fileIds, ", ") + ")")) {
+            while (analyzedQuery.next()) {
+                return analyzedQuery.getInt(ANALYZED) == fileIds.size();
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return false;
+    }
+
+    public Boolean isGroupAnalyzed(GroupKey gk) {
+        dbReadLock();
+        try {
+            List<Long> fileIDsInGroup = getFileIDsInGroup(gk);
+
+            try (Statement stmt = con.createStatement();
+                 //Can't make this a preprared statement because of the IN ( ... )
+                 ResultSet analyzedQuery = stmt.executeQuery("select count(analyzed) as analyzed from drawable_files where analyzed = 1 and obj_id in (" + StringUtils.join(fileIDsInGroup, ", ") + ")")) {
+                while (analyzedQuery.next()) {
+                    return analyzedQuery.getInt(ANALYZED) == fileIDsInGroup.size();
+                }
+            } catch (SQLException ex) {
+                LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex);
+            }
+        } catch (TskCoreException tskCoreException) {
+            LOGGER.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return false;
+    }
+
+    /**
+     * Find and return list of all ids of files matching the specific
+     * Where clause
+     *
+     * @param sqlWhereClause a SQL where clause appropriate for the desired
+     *                       files (do not begin the WHERE clause with the word WHERE!)
+     *
+     * @return a list of file ids each of which satisfy the given WHERE clause
+     *
+     * @throws TskCoreException
+     */
+    public List<Long> findAllFileIdsWhere(String sqlWhereClause) throws TskCoreException {
+        Statement statement = null;
+        ResultSet rs = null;
+        List<Long> ret = new ArrayList<>();
+        dbReadLock();
+        try {
+            statement = con.createStatement();
+            rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause);
+            while (rs.next()) {
+                ret.add(rs.getLong(1));
+            }
+        } catch (SQLException e) {
+            throw new TskCoreException("SQLException thrown when calling 'DrawableDB.findAllFileIdsWhere(): " + sqlWhereClause, e);
+        } finally {
+            if (rs != null) {
+                try {
+                    rs.close();
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error closing result set after executing  findAllFileIdsWhere", ex);
+                }
+            }
+            if (statement != null) {
+                try {
+                    statement.close();
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error closing statement after executing  findAllFileIdsWhere", ex);
+                }
+            }
+            dbReadUnlock();
+        }
+        return ret;
+    }
+
+    /**
+     *
+     *
+     *
+     * @param groupBy
+     * @param sortBy
+     * @param sortOrder
+     *
+     * @return
+     */
+    public <A> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
+
+        List<A> vals = new ArrayList<>();
+
+        switch (groupBy.attrName) {
+            case ANALYZED:
+            case CATEGORY:
+            case HASHSET:
+                //these are somewhat special cases for now as they have fixed values, or live in the main autopsy database
+                //they should have special handling at a higher level of the stack.
+                throw new UnsupportedOperationException();
+            default:
+                dbReadLock();
+                //TODO: convert this to prepared statement 
+                StringBuilder query = new StringBuilder("select " + groupBy.attrName.name() + ", count(*) from drawable_files group by " + groupBy.attrName.name());
+
+                String orderByClause = "";
+                switch (sortBy) {
+                    case GROUP_BY_VALUE:
+                        orderByClause = " order by " + groupBy.attrName.name();
+                        break;
+                    case FILE_COUNT:
+                        orderByClause = " order by count(*)";
+                        break;
+                    case NONE:
+//                    case PRIORITY:
+                        break;
+                }
+
+                query.append(orderByClause);
+
+                if (orderByClause.equals("") == false) {
+                    String sortOrderClause = "";
+
+                    switch (sortOrder) {
+                        case DESCENDING:
+                            sortOrderClause = " DESC";
+                            break;
+                        case ASCENDING:
+                            sortOrderClause = " ASC";
+                            break;
+                        default:
+                            orderByClause = "";
+                    }
+                    query.append(sortOrderClause);
+                }
+
+                try (Statement stmt = con.createStatement();
+                     ResultSet valsResults = stmt.executeQuery(query.toString())) {
+
+                    while (valsResults.next()) {
+                        final Object object = valsResults.getObject(groupBy.attrName.name());
+                        vals.add((A) object);
+                    }
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.WARNING, "Unable to get values for attribute", ex);
+                } finally {
+                    dbReadUnlock();
+                }
+        }
+
+        return vals;
+    }
+
+    public void insertGroup(final String value, DrawableAttribute groupBy) {
+        dbWriteLock();
+
+        try {
+            //PreparedStatement insertGroup = con.prepareStatement("insert or ignore into groups (value, attribute, seen) values (?,?,0)");
+            insertGroupStmt.clearParameters();
+            insertGroupStmt.setString(1, value);
+            insertGroupStmt.setString(2, groupBy.attrName.name());
+            insertGroupStmt.execute();
+        } catch (SQLException sQLException) {
+            LOGGER.log(Level.SEVERE, "Unable to insert group", sQLException);
+        } finally {
+            dbWriteUnlock();
+        }
+    }
+
+    /**
+     * @param id       the obj_id of the file to return
+     * @param analyzed the analyzed state of the file
+     *
+     * @return a DrawableFile for the given obj_id and analyzed state
+     *
+     * @throws TskCoreException if unable to get a file from the currently open
+     *                          {@link SleuthkitCase}
+     */
+    private DrawableFile getFileFromID(Long id, boolean analyzed) throws TskCoreException {
+        try {
+            return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id), analyzed);
+        } catch (IllegalStateException ex) {
+            LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex);
+            return null;
+        }
+    }
+
+    /**
+     * @param id the obj_id of the file to return
+     *
+     * @return a DrawableFile for the given obj_id
+     *
+     * @throws TskCoreException if unable to get a file from the currently open
+     *                          {@link SleuthkitCase}
+     */
+    public DrawableFile getFileFromID(Long id) throws TskCoreException {
+        try {
+            return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id),
+                                       areFilesAnalyzed(Collections.singleton(id)));
+        } catch (IllegalStateException ex) {
+            LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex);
+            return null;
+        }
+    }
+
+    public List<Long> getFileIDsInGroup(GroupKey groupKey) throws TskCoreException {
+
+        if (groupKey.getAttribute().isDBColumn) {
+            switch (groupKey.getAttribute().attrName) {
+                case CATEGORY:
+                    return manager.getFileIDsWithCategory((Category) groupKey.getValue());
+                case TAGS:
+                    return manager.getFileIDsWithTag((TagName) groupKey.getValue());
+            }
+        }
+        List<Long> files = new ArrayList<>();
+        dbReadLock();
+        try {
+            PreparedStatement statement = getGroupStatment(groupKey.getAttribute());
+            statement.setObject(1, groupKey.getValue());
+
+            try (ResultSet valsResults = statement.executeQuery()) {
+                while (valsResults.next()) {
+                    files.add(valsResults.getLong(OBJ_ID));
+                }
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return files;
+    }
+
+    public List<DrawableFile> getFilesInGroup(GroupKey key) throws TskCoreException {
+        List<DrawableFile> files = new ArrayList<>();
+        dbReadLock();
+        try {
+            PreparedStatement statement = null;
+
+            /* I hate this! not flexible/generic/maintainable we could have the
+             * DrawableAttribute provide/create/configure the correct statement
+             * but they shouldn't be coupled like that -jm */
+            switch (key.getAttribute().attrName) {
+                case CATEGORY:
+                    return getFilesWithCategory((Category) key.getValue());
+                default:
+                    statement = getGroupStatment(key.getAttribute());
+            }
+
+            statement.setObject(1, key.getValue());
+            //I hate this! not flexible/generic/maintainable -jm
+            //even special enums like for blackboard artifacts seems clunky
+//            switch (key.getAttribute().getValueType()) {
+//                case STRING:
+//                    statement.setString(1, (String) key.getValue());
+//                    break;
+//                case LONG:
+//                    statement.setLong(1, ((Integer) key.getValue()).longValue());
+//                    break;
+//                case BOOLEAN:
+//                    statement.setInt(1, (int) key.getValue());
+//            }
+            try (ResultSet valsResults = statement.executeQuery()) {
+                while (valsResults.next()) {
+                    files.add(getFileFromID(valsResults.getLong(OBJ_ID), valsResults.getBoolean(ANALYZED)));
+                }
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "failed to get file for group:" + key.getAttribute() + " == " + key.getValue(), ex);
+        } finally {
+            dbReadUnlock();
+        }
+
+        return files;
+    }
+
+    private void closeStatements() throws SQLException {
+        for (PreparedStatement pStmt : preparedStatements) {
+            pStmt.close();
+        }
+    }
+
+    public List<DrawableFile> getFilesWithCategory(Category cat) throws TskCoreException, IllegalArgumentException {
+        try {
+            List<DrawableFile> files = new ArrayList<>();
+            List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName());
+            for (ContentTag ct : contentTags) {
+                if (ct.getContent() instanceof AbstractFile) {
+                    files.add(DrawableFile.create((AbstractFile) ct.getContent(), isFileAnalyzed(ct.getContent().getId())));
+                }
+            }
+            return files;
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + cat.getDisplayName(), ex);
+            throw ex;
+        }
+    }
+
+    private PreparedStatement getGroupStatment(DrawableAttribute groupBy) {
+        return groupStatementMap.get(groupBy);
+
+    }
+
+    public int countAllFiles() {
+        int result = -1;
+        dbReadLock();
+        try (ResultSet rs = con.createStatement().executeQuery("select count(*) as COUNT from drawable_files")) {
+            while (rs.next()) {
+
+                result = rs.getInt("COUNT");
+                break;
+            }
+        } catch (SQLException ex) {
+            Exceptions.printStackTrace(ex);
+        } finally {
+            dbReadUnlock();
+        }
+        return result;
+    }
+
+    /**
+     * delete the row with obj_id = id.
+     *
+     * @param id the obj_id of the row to be deleted
+     *
+     * @return true if a row was deleted, 0 if not.
+     */
+    public boolean removeFile(long id, DrawableTransaction tr) {
+        if (tr.isClosed()) {
+            throw new IllegalArgumentException("can't update database with closed transaction");
+        }
+        int valsResults = 0;
+        dbWriteLock();
+
+        try {
+            //"delete from drawable_files where (obj_id = " + id + ")"
+            removeFileStmt.setLong(1, id);
+            removeFileStmt.executeUpdate();
+            tr.addRemovedFile(id);
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex);
+        } finally {
+            dbWriteUnlock();
+        }
+
+        //indicates succesfull removal of 1 file
+        return valsResults == 1;
+    }
+
+    /**
+     * Mark all un analyzed files as analyzed.
+     *
+     * TODO: This is a hack we only do because their is
+     * a bug that even after ingest is done, their are sometimes still files
+     * that are not marked as analyzed. Ultimately we should track down the
+     * underlying bug. -jm
+     *
+     * @return the ids of files that we marked as analyzed
+     */
+    public ArrayList<Long> markAllFilesAnalyzed() {
+        DrawableTransaction trans = beginTransaction();
+        ArrayList<Long> ids = new ArrayList<>();
+        dbWriteLock();
+        try (Statement statement = con.createStatement();
+             ResultSet executeQuery = statement.executeQuery("select obj_id from drawable_files where analyzed = 0")) {
+
+            while (executeQuery.next()) {
+                ids.add(executeQuery.getLong("obj_id"));
+            }
+
+            if (ids.isEmpty() == false) {
+                Logger.getAnonymousLogger().log(Level.INFO, "marking as analyzed " + ids);
+                statement.executeUpdate("update drawable_files set analyzed = 1 where obj_id in (" + StringUtils.join(ids, ",") + ")");
+                trans.updatedFiles.addAll(ids);
+            }
+        } catch (SQLException ex) {
+            LOGGER.log(Level.WARNING, "failed to mark files as analyzed", ex);
+        } finally {
+            dbWriteUnlock();
+        }
+
+        trans.commit(Boolean.TRUE);
+
+        return ids;
+    }
+
+    public class MultipleTransactionException extends IllegalStateException {
+
+        private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction";
+
+        public MultipleTransactionException() {
+            super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO);
+        }
+    }
+
+    /**
+     * inner class that can reference access database connection
+     */
+    public class DrawableTransaction {
+
+        private final Set<Long> updatedFiles;
+
+        private final Set<Long> removedFiles;
+
+        private boolean closed = false;
+
+        /**
+         * factory creation method
+         *
+         * @param con the {@link  ava.sql.Connection}
+         *
+         * @return a LogicalFileTransaction for the given connection
+         *
+         * @throws SQLException
+         */
+        private DrawableTransaction() {
+            this.updatedFiles = new HashSet<>();
+            this.removedFiles = new HashSet<>();
+            //get the write lock, released in close()
+            dbWriteLock();
+            try {
+                con.setAutoCommit(false);
+
+            } catch (SQLException ex) {
+                LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex);
+            }
+
+        }
+
+        synchronized public void rollback() {
+            if (!closed) {
+                try {
+                    con.rollback();
+                    updatedFiles.clear();
+                } catch (SQLException ex1) {
+                    LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1);
+                } finally {
+                    close();
+                }
+            }
+        }
+
+        synchronized private void commit(Boolean notify) {
+            if (!closed) {
+                try {
+                    con.commit();
+                    // make sure we close before we update, bc they'll need locks
+                    close();
+
+                    if (notify) {
+                        fireUpdatedFiles(updatedFiles);
+                        fireRemovedFiles(removedFiles);
+                    }
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error commiting drawable.db.", ex);
+                    rollback();
+                }
+            }
+        }
+
+        synchronized private void close() {
+            if (!closed) {
+                try {
+                    con.setAutoCommit(true);
+                } catch (SQLException ex) {
+                    LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex);
+                } finally {
+                    closed = true;
+                    dbWriteUnlock();
+                }
+            }
+        }
+
+        synchronized public Boolean isClosed() {
+            return closed;
+        }
+
+        synchronized private void addUpdatedFile(Long f) {
+            updatedFiles.add(f);
+        }
+
+        synchronized private void addRemovedFile(long id) {
+            removedFiles.add(id);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..c29d125790c839a84cc280718ada9a77e487ecd4
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java
@@ -0,0 +1,370 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.util.Pair;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.text.WordUtils;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaModule;
+import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.WIDTH;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE;
+import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE;
+import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER;
+import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG;
+import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.ContentVisitor;
+import org.sleuthkit.datamodel.SleuthkitItemVisitor;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * @TODO: There is something I don't understand or have done wrong about
+ * implementing this class,as it is unreadable by
+ * {@link ReadContentInputStream}. As a work around I kept a reference to the
+ * original {@link AbstractFile} to use when reading the image. -jm
+ */
+public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile {
+
+    public static DrawableFile create(AbstractFile abstractFileById, boolean b) {
+        if (EurekaModule.isVideoFile(abstractFileById)) {
+            return new VideoFile(abstractFileById, b);
+        } else {
+            return new ImageFile(abstractFileById, b);
+        }
+    }
+
+    public static DrawableFile create(Long id, boolean b) throws TskCoreException, IllegalStateException {
+
+        AbstractFile abstractFileById = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id);
+        if (EurekaModule.isVideoFile(abstractFileById)) {
+            return new VideoFile(abstractFileById, b);
+        } else {
+            return new ImageFile(abstractFileById, b);
+        }
+    }
+
+    private String drawablePath;
+
+    abstract public boolean isVideo();
+
+    protected T file;
+
+    private final SimpleBooleanProperty analyzed;
+
+    private final SimpleObjectProperty<Category> category = new SimpleObjectProperty<>(null);
+
+    private final ObservableList<String> hashHitSetNames = FXCollections.observableArrayList();
+
+    public ObservableList<String> getHashHitSetNames() {
+        return hashHitSetNames;
+    }
+
+    private String make;
+
+    private String model;
+
+    protected DrawableFile(T file, Boolean analyzed) {
+        /* @TODO: the two 'new Integer(0).shortValue()' values and null are
+         * placeholders because the super constructor expects values i can't get
+         * easily at the moment */
+
+        super(file.getSleuthkitCase(), file.getId(), file.getAttrType(), file.getAttrId(), file.getName(), file.getType(), file.getMetaAddr(), (int) file.getMetaSeq(), file.getDirType(), file.getMetaType(), null, new Integer(0).shortValue(), file.getSize(), file.getCtime(), file.getCrtime(), file.getAtime(), file.getMtime(), new Integer(0).shortValue(), file.getUid(), file.getGid(), file.getMd5Hash(), file.getKnown(), file.getParentPath());
+        this.analyzed = new SimpleBooleanProperty(analyzed);
+        this.file = file;
+        updateHashSetHits();
+    }
+
+    @Override
+    public boolean isRoot() {
+        return false;
+    }
+
+    @Override
+    public <T> T accept(SleuthkitItemVisitor<T> v) {
+
+        return file.accept(v);
+    }
+
+    @Override
+    public <T> T accept(ContentVisitor<T> v) {
+        return file.accept(v);
+    }
+
+    @Override
+    public List<Content> getChildren() throws TskCoreException {
+        return new ArrayList<>();
+    }
+
+    @Override
+    public List<Long> getChildrenIds() throws TskCoreException {
+        return new ArrayList<>();
+    }
+
+    public ObservableList<Pair<DrawableAttribute, ? extends Object>> getAttributesList() {
+        final ObservableList<Pair<DrawableAttribute, ? extends Object>> attributeList = FXCollections.observableArrayList();
+        for (DrawableAttribute attr : DrawableAttribute.getValues()) {
+            attributeList.add(new Pair<>(attr, getValueOfAttribute(attr)));
+        }
+        return attributeList;
+    }
+
+    public Object getValueOfAttribute(DrawableAttribute attr) {
+        switch (attr.attrName) {
+            case OBJ_ID:
+                return file.getId();
+            case PATH:
+                return getDrawablePath();
+            case NAME:
+                return getName();
+            case CREATED_TIME:
+                return getCrtimeAsDate();
+            case MODIFIED_TIME:
+                return getMtimeAsDate();
+            case MAKE:
+                if (make == null) {
+                    make = WordUtils.capitalizeFully((String) getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE));
+                }
+                return make;
+            case MODEL:
+                if (model == null) {
+                    model = WordUtils.capitalizeFully((String) getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL));
+                }
+                return model;
+
+            case CATEGORY:
+
+                updateCategory();
+
+                return category.get();
+
+            case TAGS:
+                try {
+                    List<ContentTag> contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(this);
+                    Set<TagName> values = new HashSet<>();
+
+                    for (ContentTag ct : contentTagsByContent) {
+                        values.add(ct.getName());
+                    }
+                    return new ArrayList<>(values);
+
+                } catch (TskCoreException ex) {
+                    Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up " + attr.getDisplayName() + " for " + file.getName(), ex);
+                    return new ArrayList<>();
+                }
+
+            case ANALYZED:
+                return analyzed.get();
+            case HASHSET:
+                updateHashSetHits();
+                return hashHitSetNames;
+            case WIDTH:
+                return getWidth();
+            case HEIGHT:
+                return getHeight();
+
+            default:
+                throw new UnsupportedOperationException("DrawableFile.getValueOfAttribute does not yet support " + attr.getDisplayName());
+
+        }
+    }
+
+    public List<? extends Object> getValuesOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE artType, BlackboardAttribute.ATTRIBUTE_TYPE attrType) {
+        ArrayList<Object> vals = new ArrayList<>();
+        try {
+            //why doesn't file.getArtifacts() work?
+            //TODO: this seams like overkill, use a more targeted query
+            ArrayList<BlackboardArtifact> artifacts = getAllArtifacts();
+
+            for (BlackboardArtifact artf : artifacts) {
+                if (artf.getArtifactTypeID() == artType.getTypeID()) {
+                    for (BlackboardAttribute attr : artf.getAttributes()) {
+                        if (attr.getAttributeTypeID() == attrType.getTypeID()) {
+
+                            switch (attr.getValueType()) {
+                                case BYTE:
+                                    vals.add(attr.getValueBytes());
+                                case DOUBLE:
+                                    vals.add(attr.getValueDouble());
+                                case INTEGER:
+                                    vals.add(attr.getValueInt());
+                                case LONG:
+                                    vals.add(attr.getValueLong());
+                                case STRING:
+                                    vals.add(attr.getValueString());
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (TskCoreException ex) {
+            Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up {0}/{1}" + " " + " for {2}", new Object[]{artType.getDisplayName(), attrType.getDisplayName(), getName()});
+        }
+
+        return vals;
+    }
+
+    private Object getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE artType, BlackboardAttribute.ATTRIBUTE_TYPE attrType) {
+        try {
+
+            //why doesn't file.getArtifacts() work?
+            //TODO: this seams like overkill, use a more targeted query
+            ArrayList<BlackboardArtifact> artifacts = file.getArtifacts(artType);// getAllArtifacts();
+
+            for (BlackboardArtifact artf : artifacts) {
+                if (artf.getArtifactTypeID() == artType.getTypeID()) {
+                    for (BlackboardAttribute attr : artf.getAttributes()) {
+                        if (attr.getAttributeTypeID() == attrType.getTypeID()) {
+                            switch (attr.getValueType()) {
+                                case BYTE:
+                                    return attr.getValueBytes();
+                                case DOUBLE:
+                                    return attr.getValueDouble();
+                                case INTEGER:
+                                    return attr.getValueInt();
+                                case LONG:
+                                    return attr.getValueLong();
+                                case STRING:
+                                    return attr.getValueString();
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (TskCoreException ex) {
+            Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up {0}/{1}" + " " + " for {2}", new Object[]{artType.getDisplayName(), attrType.getDisplayName(), getName()});
+        }
+        return "";
+    }
+
+    public String getMake() {
+        return getValueOfAttribute(DrawableAttribute.MAKE).toString();
+    }
+
+    public String getModel() {
+        return getValueOfAttribute(DrawableAttribute.MODEL).toString();
+    }
+
+    public void setCategory(Category category) {
+        categoryProperty().set(category);
+
+    }
+
+    public Category getCategory() {
+        return (Category) getValueOfAttribute(DrawableAttribute.CATEGORY);
+    }
+
+    public SimpleObjectProperty<Category> categoryProperty() {
+        return category;
+    }
+
+    public void updateCategory() {
+        try {
+            List<ContentTag> contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(this);
+            Category cat = null;
+            for (ContentTag ct : contentTagsByContent) {
+                if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) {
+                    cat = Category.fromDisplayName(ct.getName().getDisplayName());
+                    break;
+                }
+            }
+            if (cat == null) {
+                category.set(Category.ZERO);
+            } else {
+                category.set(cat);
+            }
+        } catch (TskCoreException ex) {
+            Logger.getLogger(DrawableFile.class.getName()).log(Level.WARNING, "problem looking up category for file " + this.getName(), ex);
+        }
+    }
+
+    public abstract Node getFullsizeDisplayNode();
+
+    public abstract Image getIcon();
+
+    public void setAnalyzed(Boolean analyzed) {
+        this.analyzed.set(analyzed);
+    }
+
+    public boolean isAnalyzed() {
+        return analyzed.get();
+    }
+
+    public T getAbstractFile() {
+        return this.file;
+    }
+
+    private void updateHashSetHits() {
+        hashHitSetNames.setAll((Collection<? extends String>) getValuesOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME));
+    }
+
+    abstract Number getWidth();
+
+    abstract Number getHeight();
+
+    private static final String[] SLASH = new String[]{"/"};
+
+    private static final String[] DOUBLE_SLASH = new String[]{"//"};
+
+    public String getDrawablePath() {
+        if (drawablePath != null) {
+            return drawablePath;
+        } else {
+            try {
+                drawablePath = StringUtils.removeEnd(getUniquePath(), getName());
+//                drawablePath = StringUtils.replaceEachRepeatedly(drawablePath, DOUBLE_SLASH, SLASH);
+                return drawablePath;
+            } catch (TskCoreException ex) {
+                Logger.getLogger(DrawableFile.class.getName()).log(Level.WARNING, "failed to get drawablePath from " + getName());
+                return "";
+            }
+        }
+    }
+
+    private long getRootID() throws TskCoreException {
+
+        Content myParent = getParent();
+        long id = -1;
+
+        while (myParent != null) {
+            id = myParent.getId();
+            myParent = myParent.getParent();
+        }
+
+        return id;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..70c0c050521e5852c61bbacd08ecc39f2df7aeac
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java
@@ -0,0 +1,118 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import org.sleuthkit.autopsy.imageanalyzer.IconCache;
+import org.sleuthkit.autopsy.imageanalyzer.gui.Fitable;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.util.logging.Level;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javax.imageio.ImageIO;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.ReadContentInputStream;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Eureka data model object that represents an image file. It is a
+ * wrapper(/decorator?/adapter?) around {@link AbstractFile} and provides
+ * methods to get an icon sized and a full sized {@link  Image}.
+ *
+ *
+ */
+public class ImageFile extends DrawableFile {
+
+    private SoftReference<Image> imageRef;
+
+    ImageFile(AbstractFile f, Boolean analyzed) {
+        super(f, analyzed);
+
+    }
+
+    ImageFile(Long id, Boolean analyzed) throws TskCoreException {
+        super(Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id), analyzed);
+    }
+
+    @Override
+    public Image getIcon() {
+        return IconCache.getDefault().get(this);
+    }
+
+    @Override
+    public Node getFullsizeDisplayNode() {
+        return new FitableImageView(getFullSizeImage());
+    }
+
+    public Image getFullSizeImage() {
+        Image image = null;
+        if (imageRef != null) {
+            image = imageRef.get();
+        }
+
+        if (image == null) {
+
+            try (ReadContentInputStream readContentInputStream = new ReadContentInputStream(this.getAbstractFile())) {
+                BufferedImage read = ImageIO.read(readContentInputStream);
+                image = SwingFXUtils.toFXImage(read, null);
+            } catch (IOException | NullPointerException ex) {
+                Logger.getLogger(ImageFile.class.getName()).log(Level.WARNING, "unable to read file" + getName());
+                return null;
+            }
+            imageRef = new SoftReference<>(image);
+        }
+        return image;
+    }
+
+    @Override
+    Number getWidth() {
+        final Image fullSizeImage = getFullSizeImage();
+        if (fullSizeImage != null) {
+            return fullSizeImage.getWidth();
+        }
+        return -1;
+    }
+
+    @Override
+    Number getHeight() {
+        final Image fullSizeImage = getFullSizeImage();
+        if (fullSizeImage != null) {
+            return fullSizeImage.getHeight();
+        }
+        return -1;
+    }
+
+    @Override
+    public boolean isVideo() {
+        return false;
+    }
+
+    private class FitableImageView extends ImageView implements Fitable {
+
+        public FitableImageView(Image image) {
+            super(image);
+        }
+
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bb16fe5b3ca334b18569e04b971ff420164a44c
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java
@@ -0,0 +1,132 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.datamodel;
+
+import org.sleuthkit.autopsy.imageanalyzer.gui.MediaControl;
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.nio.file.Paths;
+import java.util.logging.Level;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaException;
+import javafx.scene.media.MediaPlayer;
+import javafx.scene.text.Text;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.ContentUtils;
+import org.sleuthkit.datamodel.AbstractFile;
+
+public class VideoFile extends DrawableFile {
+
+    private static final Image VIDEO_ICON = new Image("org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png");
+
+    VideoFile(AbstractFile file, Boolean analyzed) {
+        super(file, analyzed);
+    }
+
+    @Override
+    public Image getIcon() {
+        //TODO: implement video thumbnailing here
+        return VIDEO_ICON;
+    }
+    SoftReference<Media> mediaRef;
+
+    public Media getMedia() throws IOException, MediaException {
+        Media media = null;
+        if (mediaRef != null) {
+            media = mediaRef.get();
+        }
+        if (media != null) {
+            return media;
+        }
+        final File cacheFile = getCacheFile(this.getId());
+        if (cacheFile.exists() == false) {
+
+            ContentUtils.writeToFile(this.getAbstractFile(), cacheFile);
+
+        }
+        try {
+            media = new Media(Paths.get(cacheFile.getAbsolutePath()).toUri().toString());
+            mediaRef = new SoftReference<>(media);
+            return media;
+        } catch (MediaException ex) {
+            throw ex;
+        }
+    }
+
+    @Override
+    public Node getFullsizeDisplayNode() {
+        final File cacheFile = getCacheFile(this.getId());
+
+        try {
+            if (cacheFile.exists() == false) {
+                ContentUtils.writeToFile(this.getAbstractFile(), cacheFile);
+            }
+            final Media media = getMedia();
+            final MediaPlayer mediaPlayer = new MediaPlayer(media);
+            final MediaControl mediaView = new MediaControl(mediaPlayer);
+            mediaPlayer.statusProperty().addListener((observableStatus, oldStatus, newStatus) -> {
+                Logger.getAnonymousLogger().log(Level.INFO, "media player: {0}", newStatus);
+            });
+            return mediaView;
+        } catch (IOException ex) {
+            Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "failed to initialize MediaControl for file " + getName(), ex);
+            return new Text(ex.getLocalizedMessage() + "\nSee the logs for details.");
+        } catch (MediaException ex) {
+            Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, ex.getType() + " Failed to initialize MediaControl for file " + getName(), ex);
+            Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "caused by " + ex.getCause().getLocalizedMessage(), ex.getCause());
+            return new Text(ex.getType() + "\nSee the logs for details.");
+        } catch (OutOfMemoryError ex) {
+            Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "failed to initialize MediaControl for file " + getName(), ex);
+            return new Text("There was a problem playing video file.\nSee the logs for details.");
+        }
+    }
+
+    private File getCacheFile(long id) {
+        return new File(Case.getCurrentCase().getCacheDirectory() + File.separator + id);
+    }
+
+    @Override
+    Number getWidth() {
+        try {
+            return getMedia().getWidth();
+        } catch (IOException | MediaException ex) {
+//            Exceptions.printStackTrace(ex);
+            return -1;
+        }
+    }
+
+    @Override
+    public boolean isVideo() {
+        return true;
+    }
+
+    @Override
+    Number getHeight() {
+        try {
+            return getMedia().getHeight();
+        } catch (IOException | MediaException ex) {
+//            Exceptions.printStackTrace(ex);
+            return -1;
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css
new file mode 100644
index 0000000000000000000000000000000000000000..937a27b23e50a40cf9841074b4b02b477aedf6f2
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css
@@ -0,0 +1,6 @@
+.list-cell:odd {
+    -fx-background-color: transparent; /* derive(-fx-control-inner-background,-5%); */
+}
+.list-cell:even {
+    -fx-background-color: transparent; /* derive(-fx-control-inner-background,-5%); */
+}
\ No newline at end of file
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..b9e198f27458b23c6d57e87679cae828b537963e
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.net.*?>
+<?import java.util.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.control.TitledPane" alignment="TOP_LEFT" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minWidth="-1.0" text="filter label" wrapText="true" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <content>
+    <VBox fx:id="filtersBox" prefHeight="-1.0" prefWidth="-1.0" />
+  </content>
+  <graphic>
+    <HBox alignment="CENTER_LEFT">
+      <children>
+        <CheckBox fx:id="selectedBox" mnemonicParsing="false" selected="true" />
+      </children>
+      <padding>
+        <Insets left="5.0" />
+      </padding>
+    </HBox>
+  </graphic>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..d2294c700d08827e6093b088beadad22a00bf895
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java
@@ -0,0 +1,99 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.filtering;
+
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.FilterRow;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AtomicFilter;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.UnionFilter;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import java.net.URL;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.TreeMap;
+import javafx.collections.ListChangeListener;
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.TitledPane;
+import javafx.scene.layout.VBox;
+
+/**
+ * FXML Controller class
+ *
+ */
+public class FilterPane extends TitledPane {
+
+    @FXML
+    private ResourceBundle resources;
+    @FXML
+    private URL location;
+    @FXML
+    private VBox filtersBox;
+    @FXML
+    private CheckBox selectedBox;
+    private UnionFilter<AtomicFilter> filter;
+    private Map<AtomicFilter, FilterRow> filterRowMap = new TreeMap<>(AtomicFilter.ALPHABETIC_COMPARATOR);
+
+    @FXML
+    void initialize() {
+        assert filtersBox != null : "fx:id=\"filtersBox\" was not injected: check your FXML file 'FilterPane.fxml'.";
+        assert selectedBox != null : "fx:id=\"selectedBox\" was not injected: check your FXML file 'FilterPane.fxml'.";
+    }
+
+    public FilterPane() {
+        FXMLConstructor.construct(this, "FilterPane.fxml");
+    }
+
+    private void rebuildChildren() {
+        filtersBox.getChildren().clear();
+        for (FilterRow af : filterRowMap.values()) {
+            filtersBox.getChildren().add(af);
+        }
+    }
+
+    void setFilter(UnionFilter<AtomicFilter> filter) {
+//TODO : do this more reasonably
+        this.filter = filter;
+        this.setText(filter.getDisplayName());
+        filterRowMap.clear();
+        filtersBox.getChildren().clear();
+        for (AtomicFilter af : filter.subFilters) {
+            final FilterRow filterRow = af.getUI();
+            filterRowMap.put(af, filterRow);
+        }
+        rebuildChildren();
+        
+        this.filter.subFilters.addListener(new ListChangeListener<AtomicFilter>() {
+            @Override
+            public void onChanged(ListChangeListener.Change<? extends AtomicFilter> change) {
+                while (change.next()) {
+                    for (AtomicFilter af : change.getAddedSubList()) {
+                        FilterRow filterRow = af.getUI();
+                        filterRowMap.put(af, filterRow);
+                    }
+                    for (AtomicFilter af : change.getRemoved()) {
+                      filterRowMap.remove(af);
+                    }
+                }
+                rebuildChildren();
+            }
+        });
+
+        this.filter.active.bindBidirectional(selectedBox.selectedProperty());
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..41636f42d7e8414fda90926aa68c529855737048
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.util.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="1.7976931348623157E308" maxWidth="-1.0" minHeight="-1.0" minWidth="200.0" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <children>
+    <TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" rotateGraphic="true" side="TOP" tabClosingPolicy="UNAVAILABLE" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <tabs>
+        <Tab closable="false" text="Filters">
+          <content>
+            <ListView fx:id="filtersList" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" />
+          </content>
+          <graphic>
+            <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+              <image>
+                <Image url="@../images/funnel.png" />
+              </image>
+            </ImageView>
+          </graphic>
+        </Tab>
+        <Tab closable="false" disable="true" text="File Tree">
+          <content>
+            <AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0" />
+          </content>
+        </Tab>
+      </tabs>
+    </TabPane>
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..2281841d21ffef236ce311738627d4bbe15dc9c4
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java
@@ -0,0 +1,353 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.filtering;
+
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.FilterSet;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AtomicFilter;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AttributeFilter;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaModule;
+import org.sleuthkit.autopsy.imageanalyzer.LoggedTask;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateListener;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AbstractFilter;
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.NameFilter;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.fxml.FXML;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.layout.AnchorPane;
+import javafx.util.Callback;
+import javax.swing.SortOrder;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadUtils;
+
+/**
+ * This singleton acts as the controller for the Filters. It creates filters
+ * based on values in the database, and broadcasts events when the activation
+ * state or other ui configuration of the filters changes.
+ *
+ * deprecated until we revisit filtering
+ */
+@Deprecated
+public class FiltersPanel extends AnchorPane implements FileUpdateListener {
+
+    public static final String FILTER_STATE_CHANGED = "FILTER_STATE_CHANGED";
+    @FXML
+    private ResourceBundle resources;
+    @FXML
+    private URL location;
+    @FXML
+    private ListView< AttributeFilter> filtersList;
+    private static final Logger LOGGER = Logger.getLogger(FiltersPanel.class.getName());
+    volatile private DrawableDB db;
+    final Map<DrawableAttribute, AttributeFilter> attrFilterMap = new HashMap<>();
+    private static FiltersPanel instance;
+
+    /**
+     * clear/reset state
+     */
+    public void clear() {
+        Platform.runLater(new Runnable() {
+            @Override
+            public void run() {
+                db = null;
+                filterSet.clear();
+                attrFilterMap.clear();
+            }
+        });
+    }
+    /**
+     * listen to changes in individual filters and forward to external listeners
+     * via the pcs
+     */
+    private final ChangeListener filterForwardingListener = new ChangeListener<Object>() {
+        @Override
+        public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
+            pcs.firePropertyChange(new PropertyChangeEvent(observable, FILTER_STATE_CHANGED, oldValue, newValue));
+        }
+    };
+
+    public FilterSet getFilterSet() {
+        return filterSet;
+    }
+    /* top level filter */
+    private FilterSet filterSet = new FilterSet();
+    /**
+     * {@link Service} to (re)build filterset based on values in the database
+     */
+    private final RebuildFiltersService rebuildFiltersService = new RebuildFiltersService();
+    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+
+    /**
+     * register a {@link PropertyChangeListener}
+     *
+     * @param pcl
+     */
+    public void addListener(PropertyChangeListener pcl) {
+        pcs.addPropertyChangeListener(pcl);
+    }
+
+    @FXML
+    void initialize() {
+        assert filtersList != null : "fx:id=\"filtersList\" was not injected: check your FXML file 'FiltersPanel.fxml'.";
+
+        filtersList.setItems(filterSet.subFilters);
+
+        filtersList.setCellFactory(new Callback<ListView<AttributeFilter>, ListCell<AttributeFilter>>() {
+            @Override
+            public ListCell<AttributeFilter> call(ListView<AttributeFilter> p) {
+                return new FilterPaneListCell();
+            }
+        });
+        for (final DrawableAttribute attr : DrawableAttribute.getValues()) {
+            if (attr != DrawableAttribute.NAME && attr != DrawableAttribute.CREATED_TIME && attr != DrawableAttribute.MODIFIED_TIME) {
+                final AttributeFilter attrFilter = new AttributeFilter(attr);
+                filterSet.subFilters.add(attrFilter);
+                attrFilterMap.put(attr, attrFilter);
+            }
+        }
+    }
+
+    static synchronized public FiltersPanel getDefault() {
+        if (instance == null) {
+            instance = new FiltersPanel();
+        }
+        return instance;
+    }
+
+    private FiltersPanel() {
+
+        FXMLConstructor.construct(this, "FiltersPanel.fxml");
+    }
+
+    public void setDB(DrawableDB drawableDb) {
+        db = drawableDb;
+        db.addUpdatedFileListener(this);
+        rebuildFilters();
+    }
+
+    @Override
+    synchronized public void handleFileUpdate(FileUpdateEvent evt) {
+        //updateFilters(evt.getUpdatedFiles());
+    }
+
+    synchronized public void rebuildFilters() {
+        /*
+         * Platform.runLater(new Runnable() {
+         * @Override
+         * public void run() {
+         * rebuildFiltersService.restart();
+         * }
+         * });
+         * */
+    }
+
+    synchronized public void updateFilters(final Collection< DrawableFile> files) {
+        /*
+         * for (DrawableFile file : files) {
+         * for (final DrawableAttribute attr : DrawableAttribute.getValues()) {
+         * AttributeFilter attributeFilter;
+         * AbstractFilter.FilterComparison comparison = AtomicFilter.EQUALS;
+         * switch (attr.attrName) {
+         * case NAME:
+         * case PATH:
+         * case CREATED_TIME:
+         * case MODIFIED_TIME:
+         * case CATEGORY:
+         * case OBJ_ID:
+         * case ANALYZED:
+         * //fall through all attributes that don't have per value filters
+         * break;
+         * case HASHSET:
+         * comparison = AtomicFilter.CONTAINED_IN;
+         * break;
+         * default:
+         * //default is make one == filter for each value in database
+         * attributeFilter = getFilterForAttr(attr);
+         *
+         * ObservableList<Object> vals =
+         * FXCollections.singletonObservableList(file.getValueOfAttribute(attr));
+         *
+         * addFilterForAttrValues(vals, attributeFilter, comparison);
+         * }
+         * }
+         * }
+         */
+    }
+
+    synchronized private AttributeFilter getFilterForAttr(final DrawableAttribute attr) {
+        AttributeFilter attributeFilter = attrFilterMap.get(attr);
+        if (attributeFilter == null) {
+            attributeFilter = new AttributeFilter(attr);
+            attributeFilter.active.addListener(filterForwardingListener);
+        }
+
+        final AttributeFilter finalFilter = attributeFilter;
+
+//        Platform.runLater(new Runnable() {
+//            @Override
+//            public void run() {
+        if (filterSet.subFilters.contains(finalFilter) == false) {
+            filterSet.subFilters.add(finalFilter);
+        }
+//            }
+//        });
+
+        attrFilterMap.put(attr, attributeFilter);
+        return attributeFilter;
+    }
+
+    synchronized private void addFilterForAttrValues(List<? extends Object> vals, final AttributeFilter attributeFilter, final AbstractFilter.FilterComparison filterComparison) {
+        for (final Object val : vals) {
+            if (attributeFilter.containsSubFilterForValue(val) == false) {
+                final AtomicFilter filter = new AtomicFilter(attributeFilter.getAttribute(), filterComparison, val);
+                Platform.runLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        attributeFilter.subFilters.add(filter);
+                    }
+                });
+
+                filter.active.addListener(filterForwardingListener);
+//                Logger.getAnonymousLogger().log(Level.INFO, "created filter " + filter);
+            }
+
+        }
+    }
+
+    /**
+     *
+     */
+    static class FilterPaneListCell extends ListCell<AttributeFilter> {
+
+        private final FilterPane filterPane;
+
+        public FilterPaneListCell() {
+            super();
+            filterPane = new FilterPane();
+        }
+
+        @Override
+        protected void updateItem(final AttributeFilter item, final boolean empty) {
+            super.updateItem(item, empty);
+            Platform.runLater(new Runnable() {
+                @Override
+                public void run() {
+                    if (empty || item == null) {
+                        setGraphic(null);
+                    } else {
+                        setGraphic(filterPane);
+                        filterPane.setFilter(item);
+                    }
+                }
+            });
+
+
+        }
+    }
+
+    private class RebuildFiltersService extends Service<Void> {
+
+        @Override
+        protected Task<Void> createTask() {
+            return new RebuildFiltersTask();
+        }
+
+        private class RebuildFiltersTask extends LoggedTask<Void> {
+
+            public RebuildFiltersTask() {
+                super("rebuilding filters", true);
+            }
+
+            @Override
+            protected Void call() throws Exception {
+                ThreadUtils.runAndWait(new Runnable() {
+                    @Override
+                    public void run() {
+                        filterSet.subFilters.clear();
+                        attrFilterMap.clear();
+                    }
+                });
+
+                LOGGER.log(Level.INFO, "rebuilding filters started");
+
+                for (final DrawableAttribute attr : DrawableAttribute.getValues()) {
+                    AbstractFilter.FilterComparison comparison = AtomicFilter.EQUALS;
+                    switch (attr.attrName) {
+                        case NAME:
+                            final AttributeFilter nameFilter = getFilterForAttr(attr);
+                            final NameFilter filter = new NameFilter();
+                            Platform.runLater(new Runnable() {
+                                @Override
+                                public void run() {
+                                    nameFilter.subFilters.add(filter);
+                                }
+                            });
+
+                            filter.active.addListener(filterForwardingListener);
+                            filter.filterValue.addListener(filterForwardingListener);
+
+                            LOGGER.log(Level.INFO, "createdfilter {0}", filter);
+                            break;
+
+                        case PATH:
+                        case CREATED_TIME:
+                        case MODIFIED_TIME:
+                        case OBJ_ID:
+                            break;
+                        case HASHSET:
+                            comparison = AtomicFilter.CONTAINED_IN;
+                            break;
+                        default:
+
+                            //default is make one == filter per attribute value in db
+                            final AttributeFilter attributeFilter = getFilterForAttr(attr);
+                            //TODO: FILE_COUNT is arbitrarty but maybe better than NONE, we can include file counts in labels in future
+                            List<? extends Object> vals = EurekaController.getDefault().getGroupManager().findValuesForAttribute(attr, GroupSortBy.FILE_COUNT, SortOrder.DESCENDING);
+                            addFilterForAttrValues(vals, attributeFilter, comparison);
+                    }
+                }
+                return null;
+            }
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8bf253293f884d4f75834d3c5dd3e64eecd9dc9
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java
@@ -0,0 +1,131 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import java.util.List;
+import javafx.beans.property.SimpleBooleanProperty;
+
+/**
+ *
+ * @author jonathan
+ */
+public abstract class AbstractFilter<R> {
+
+    public abstract void clear();
+    public static final AtomicFilter.FilterComparison EQUALS = new AtomicFilter.EQUALS();
+    public static final AtomicFilter.FilterComparison EQUALS_IGNORECASE = new AtomicFilter.EQUALS_IGNORECASE();
+    public static final AtomicFilter.FilterComparison NOT_EQUALS = new AtomicFilter.NOT_EQUALS();
+    public static final AtomicFilter.FilterComparison SUBSTRING = new AtomicFilter.SUBSTRING();
+    public static final AtomicFilter.FilterComparison CONTAINED_IN = new AtomicFilter.CONTAINS();
+    public final SimpleBooleanProperty active = new SimpleBooleanProperty(true);
+
+    public abstract String getDisplayName();
+
+    abstract public Boolean accept(DrawableFile df);
+
+    public boolean isActive() {
+        return active.get();
+    }
+
+    final protected static class CONTAINS extends AtomicFilter.FilterComparison<List<String>, String> {
+
+        @Override
+        public String getSqlOperator() {
+            return " in ";
+        }
+
+        @Override
+        public Boolean compare(List<String> attrVal, String filterVal) {
+            return attrVal.contains(filterVal);
+        }
+    }
+
+    final protected static class SUBSTRING extends AtomicFilter.FilterComparison<String, String> {
+
+        @Override
+        public Boolean compare(String attrVal, String filterVal) {
+            return attrVal.toLowerCase().contains(filterVal.toLowerCase());
+        }
+
+        @Override
+        public String getSqlOperator() {
+            return " like ";
+        }
+
+        @Override
+        public String toString() {
+            return "in";
+        }
+    }
+
+    final protected static class EQUALS_IGNORECASE extends AtomicFilter.FilterComparison<String, String> {
+
+        @Override
+        public Boolean compare(String attrVal, String filterVal) {
+            return attrVal.equals(filterVal);
+        }
+
+        @Override
+        public String getSqlOperator() {
+            return " == ";
+        }
+
+        @Override
+        public String toString() {
+            return "=";
+        }
+    }
+
+    final protected static class EQUALS<T> extends AtomicFilter.FilterComparison<T, T> {
+
+        @Override
+        public Boolean compare(T attrVal, T filterVal) {
+            return attrVal.equals(filterVal);
+        }
+
+        @Override
+        public String getSqlOperator() {
+            return " == ";
+        }
+
+        @Override
+        public String toString() {
+            return "=";
+        }
+    }
+
+    final protected static class NOT_EQUALS<T> extends AtomicFilter.FilterComparison<T, T> {
+
+        @Override
+        public Boolean compare(T attrVal, T filterVal) {
+            return attrVal != filterVal;
+        }
+
+        @Override
+        public String getSqlOperator() {
+            return " != ";
+        }
+
+        @Override
+        public String toString() {
+            return "≠";
+        }
+    }
+
+    /**
+     *
+     *
+     */
+    public static abstract class FilterComparison<A, F> {
+
+        private FilterComparison() {
+        }
+
+        abstract public Boolean compare(A attrVal, F filterVal);
+
+        abstract public String getSqlOperator();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..77c545e8952fcdfeaf9e21c6cfd5c8f07cf05bba
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java
@@ -0,0 +1,168 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import java.net.URL;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.Label;
+
+/**
+ *
+ */
+public class AtomicFilter<A, F> extends AbstractFilter {
+
+    private DrawableAttribute<A> filterAttribute;
+
+    public DrawableAttribute<A> getFilterAttribute() {
+        return filterAttribute;
+    }
+
+    public F getFilterValue() {
+        return filterValue.get();
+    }
+
+    public FilterComparison<A, F> getFilterComparisson() {
+        return filterComparisson;
+    }
+    public SimpleObjectProperty<F> filterValue;
+    private FilterComparison<A, F> filterComparisson;
+
+    public AtomicFilter(DrawableAttribute filterAttribute, FilterComparison<A, F> filterComparisson, F filterValue) {
+        this.filterAttribute = filterAttribute;
+        this.filterValue = new SimpleObjectProperty<>(filterValue);
+        this.filterComparisson = filterComparisson;
+    }
+
+    @Override
+    public Boolean accept(DrawableFile df) {
+//        Logger.getAnonymousLogger().log(Level.INFO, getDisplayName() + " : " + filterValue + " filtered " + df.getName() + " = " + compare);
+        return isActive()
+                ? filterComparisson.compare((A) df.getValueOfAttribute(filterAttribute), filterValue.get())
+                : false;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 79 * hash + Objects.hashCode(this.filterAttribute);
+        hash = 79 * hash + Objects.hashCode(this.filterValue);
+        hash = 79 * hash + Objects.hashCode(this.filterComparisson);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AtomicFilter other = (AtomicFilter) obj;
+        if (this.filterAttribute != other.filterAttribute) {
+            return false;
+        }
+        if (!Objects.equals(this.filterValue, other.filterValue)) {
+            return false;
+        }
+        if (!Objects.equals(this.filterComparisson, other.filterComparisson)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return filterValue.get().toString();
+    }
+
+    public FilterRow getUI() {
+        return new AtomicFilterRow(this);
+    }
+
+    @Override
+    public void clear() {
+        active.set(true);
+    }
+    public static final Comparator<AtomicFilter> ALPHABETIC_COMPARATOR = new Comparator<AtomicFilter>() {
+        @Override
+        public int compare(AtomicFilter o1, AtomicFilter o2) {
+            return o1.getDisplayName().compareTo(o2.getDisplayName());
+        }
+    };
+
+    /**
+     *
+     * @author jonathan
+     */
+    public static class AtomicFilterRow<A, F> extends FilterRow<AtomicFilter<A, F>> {
+
+        @FXML
+        protected ResourceBundle resources;
+        @FXML
+        protected URL location;
+        @FXML
+        protected ChoiceBox comparisonBox;
+        @FXML
+        protected Label filterLabel;
+        @FXML
+        protected CheckBox selectedBox;
+        protected final AtomicFilter<A, F> filter;
+
+        private AtomicFilterRow(AtomicFilter filter) {
+            super();
+            this.filter = filter;
+            FXMLConstructor.construct(this, "FilterRow.fxml");
+        }
+
+        public ReadOnlyBooleanProperty getSelectedProperty() {
+            return selectedBox.selectedProperty();
+        }
+
+        public ReadOnlyObjectProperty<AtomicFilter.FilterComparison> getComparisonProperty() {
+            return comparisonBox.getSelectionModel().selectedItemProperty();
+        }
+
+        @FXML
+        void initialize() {
+            assert comparisonBox != null : "fx:id=\"comparisonBox\" was not injected: check your FXML file 'FilterRow.fxml'.";
+            assert filterLabel != null : "fx:id=\"filterLabel\" was not injected: check your FXML file 'FilterRow.fxml'.";
+            assert selectedBox != null : "fx:id=\"selectedBox\" was not injected: check your FXML file 'FilterRow.fxml'.";
+            comparisonBox.getItems().setAll(AtomicFilter.EQUALS, AtomicFilter.EQUALS_IGNORECASE);
+            switch (filter.filterAttribute.attrName) {
+                case MAKE:
+                case MODEL:
+                    comparisonBox.getSelectionModel().select(AtomicFilter.EQUALS_IGNORECASE);
+                    break;
+                default:
+                    comparisonBox.getSelectionModel().select(AtomicFilter.EQUALS);
+            }
+            final F filterValue = filter.getFilterValue();
+
+            if (filterValue == null || "".equals(filterValue)) {
+                filterLabel.setText("unknown");
+            } else {
+                filterLabel.setText(filterValue.toString());
+            }
+            selectedBox.selectedProperty().bindBidirectional(filter.active);
+        }
+
+        @Override
+        public AtomicFilter<A, F> getFilter() {
+            return filter;
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7dff206d9d98a891e6eba99df1761f11da2458ca
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java
@@ -0,0 +1,19 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+
+public class AtomicSqlFilter extends AtomicFilter implements SqlFilter {
+
+    public AtomicSqlFilter(DrawableAttribute filterAttribute, FilterComparison filterComparisson, Object filterValue) {
+        super(filterAttribute, filterComparisson, filterValue);
+    }
+
+    @Override
+    public String getFilterQueryString() {
+        return getFilterAttribute().attrName.name() + " " + getFilterComparisson().getSqlOperator() + " " + getFilterValue().toString();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..17534ea55b000420ae4ca930b673dcdda46d055e
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java
@@ -0,0 +1,43 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+
+/**
+ *
+ * @author jonathan
+ */
+public class AttributeFilter<AVT, FVT> extends UnionFilter<AtomicFilter<AVT, FVT>> {
+
+    DrawableAttribute filterAttribute;
+
+    public AttributeFilter(DrawableAttribute filterAttribute) {
+        super();
+        this.filterAttribute = filterAttribute;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return filterAttribute.getDisplayName();
+    }
+
+    public DrawableAttribute getAttribute() {
+        return filterAttribute;
+    }
+
+    public boolean containsSubFilterForValue(FVT val) {
+        try {
+            for (AtomicFilter sf : subFilters) {
+                if (val.equals(sf.getFilterValue())) {
+                    return true;
+                }
+            }
+        } catch (Exception e) {
+        }
+
+        return false;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c1700747637f021bf72282e67d598d2b78af2fd
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java
@@ -0,0 +1,28 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ *
+ */
+public abstract class CompoundFilter<T extends AbstractFilter> extends AbstractFilter {
+
+    public ObservableList<T> subFilters;
+
+    public CompoundFilter(ObservableList< T> subFilters) {
+        this.subFilters = FXCollections.synchronizedObservableList(subFilters);
+    }
+
+    public void clear() {
+        for (T filter : subFilters) {
+            filter.clear();
+        }
+        active.set(true);
+        subFilters.clear();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..9d84e288f3380030223dad9645e0e4f6a06e93d2
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.util.*?>
+<?import javafx.collections.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0" style="" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <children>
+    <HBox alignment="CENTER_LEFT" fillHeight="true" prefHeight="-1.0" prefWidth="-1.0" spacing="5.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <children>
+        <CheckBox fx:id="selectedBox" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" selected="true" text="" HBox.hgrow="NEVER" />
+        <ChoiceBox fx:id="comparisonBox" minHeight="-1.0" minWidth="0.0" prefHeight="-1.0" prefWidth="0.0" style=" -fx-background-color:&#10;        linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)),&#10;        linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%);" visible="false" HBox.hgrow="NEVER">
+          <items>
+            <FXCollections fx:factory="observableArrayList">
+              <String fx:value="Item 1" />
+              <String fx:value="Item 2" />
+              <String fx:value="Item 3" />
+            </FXCollections>
+          </items>
+        </ChoiceBox>
+        <Label fx:id="filterLabel" minWidth="50.0" text="Label" wrapText="true" HBox.hgrow="NEVER" />
+      </children>
+      <padding>
+        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+      </padding>
+    </HBox>
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java
new file mode 100644
index 0000000000000000000000000000000000000000..3eb13cd0ce83de90d58f364f5e97f650d8549edf
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java
@@ -0,0 +1,17 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AbstractFilter;
+import javafx.scene.layout.AnchorPane;
+
+/**
+ *
+ * @author jmillman
+ */
+public abstract class FilterRow<T extends AbstractFilter> extends AnchorPane {
+
+    public abstract T getFilter();
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..88c19e4056665f888cdd6f33d627060f225fde11
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java
@@ -0,0 +1,17 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+/**
+ *
+ * @author jonathan
+ */
+public class FilterSet extends IntersectionFilter<AttributeFilter> {
+
+    @Override
+    public String getDisplayName() {
+        return "Filters";
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e4b457885d43b0a36a9d72e9e6fcd486fc4f786
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java
@@ -0,0 +1,38 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ *
+ * Intersection(And) filter
+ */
+public abstract class IntersectionFilter<T extends AbstractFilter> extends CompoundFilter<T> {
+
+    public IntersectionFilter(ObservableList< T> subFilters) {
+        super(subFilters);
+    }
+
+    public IntersectionFilter() {
+        super(FXCollections.<T>observableArrayList());
+    }
+
+    @Override
+    public Boolean accept(DrawableFile df) {
+        if (isActive()) {
+            for (T f : subFilters) {
+                if (f.isActive() && f.accept(df) == false) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return true;
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..08b0f4305efe05d485c15517e7614c6ea375429b
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java
@@ -0,0 +1,70 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.fxml.FXML;
+import javafx.scene.control.TextField;
+
+/**
+ *
+ */
+public class NameFilter extends AtomicFilter<String, String> {
+
+    public NameFilter() {
+        super(DrawableAttribute.NAME, SUBSTRING, "");
+    }
+
+    @Override
+    public FilterRow getUI() {
+        return new NameRow(this);
+    }
+
+    private void setValue(String text) {
+        filterValue.set(text);
+    }
+
+    private static class NameRow extends FilterRow<NameFilter> {
+
+        @FXML
+        private ResourceBundle resources;
+        @FXML
+        private URL location;
+        @FXML
+        private TextField textField;
+        private final NameFilter filter;
+
+        @FXML
+        void initialize() {
+            assert textField != null : "fx:id=\"textField\" was not injected: check your FXML file 'NameFilterRow.fxml'.";
+
+            textField.textProperty().addListener(new InvalidationListener() {
+                @Override
+                public void invalidated(Observable observable) {
+                    filter.setValue(textField.getText());
+                }
+            });
+
+//            //hack to make listener hear that this filter has changed
+//            filter.active.set(!filter.isActive());
+//            filter.active.set(!filter.isActive());
+        }
+
+        private NameRow(NameFilter filter) {
+            this.filter = filter;
+            FXMLConstructor.construct(this, "NameFilterRow.fxml");
+        }
+
+        @Override
+        public NameFilter getFilter() {
+            return filter;
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..a51b6c5081591960ca1390a868206e55033dcac9
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.util.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="200.0" prefHeight="-1.0" prefWidth="-1.0" style="" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <children>
+    <TextField fx:id="textField" disable="false" editable="true" focusTraversable="false" maxHeight="-Infinity" maxWidth="-Infinity" minWidth="-1.0" prefColumnCount="12" prefWidth="-1.0" promptText="enter search term" style="" text="" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..75d530a5e587e37c4c131f9bdf8eae3a9ee6fd3e
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java
@@ -0,0 +1,18 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+
+/**
+ *
+ *
+ */
+public interface SqlFilter {
+
+    public Boolean accept(DrawableFile df);
+
+    public String getFilterQueryString();
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..898aff3947136ef5d4c109668b88d0e8dddf1a1c
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java
@@ -0,0 +1,39 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.sleuthkit.autopsy.imageanalyzer.filtering.filters;
+
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ *
+ * Union(or) filter
+ */
+abstract public class UnionFilter<T extends AbstractFilter> extends CompoundFilter<T> {
+
+    public UnionFilter(ObservableList<T> subFilters) {
+        super(subFilters);
+    }
+
+    public UnionFilter() {
+        super(FXCollections.<T>observableArrayList());
+    }
+
+    @Override
+    public Boolean accept(DrawableFile df) {
+        if (isActive()) {
+            for (T f : subFilters) {
+                if (f.isActive() && f.accept(df) == true) {
+                    return true;
+                }
+            }
+            return false;
+        } else {
+            return true;
+        }
+
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java
new file mode 100644
index 0000000000000000000000000000000000000000..47d367cc985e03829263809536dc4b9e7835126b
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java
@@ -0,0 +1,98 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+import java.util.Map;
+import java.util.Objects;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.datamodel.TagName;
+
+/**
+ * key identifying information of a {@link Grouping}. Used to look up groups in
+ * {@link Map}s and from the db.
+ */
+public class GroupKey<T> implements Comparable<GroupKey<Comparable<T>>> {
+
+    private final Comparable<T> val;
+
+    public Comparable<T> getValue() {
+        return val;
+    }
+
+    public DrawableAttribute<T> getAttribute() {
+        return attr;
+    }
+
+    private final DrawableAttribute<T> attr;
+
+    public GroupKey(DrawableAttribute<T> attr, Comparable<T> val) {
+        this.attr = attr;
+        this.val = val;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 29 * hash + Objects.hashCode(this.val);
+        hash = 29 * hash + Objects.hashCode(this.attr);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final GroupKey<T> other = (GroupKey<T>) obj;
+        if (!Objects.equals(this.val, other.val)) {
+            return false;
+        }
+        if (this.attr != other.attr) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int compareTo(GroupKey<Comparable<T>> o) {
+        if (val instanceof Comparable) {
+            return ((Comparable) val).compareTo(o.val);
+        } else {
+            return Integer.compare(val.hashCode(), o.val.hashCode());
+        }
+
+    }
+
+    public String getValueDisplayName() {
+        if (attr == DrawableAttribute.TAGS) {
+            return ((TagName) getValue()).getDisplayName();
+        } else {
+
+            return getValue().toString();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "GroupKey: " + getAttribute() + " = " + getValue();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ce9e048d6180b8c2b220fc91b8dceae9daef696
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java
@@ -0,0 +1,646 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
+import javax.swing.SortOrder;
+import org.apache.commons.lang3.StringUtils;
+import org.netbeans.api.progress.ProgressHandle;
+import org.netbeans.api.progress.ProgressHandleFactory;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaModule;
+import org.sleuthkit.autopsy.imageanalyzer.LoggedTask;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Provides an abstraction layer on top of {@link  DrawableDB} ( and to some
+ * extent {@link SleuthkitCase} ) to facilitate creation, retrieval, updating,
+ * and sorting of {@link Grouping}s.
+ */
+public class GroupManager {
+
+    private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName());
+
+    private DrawableDB db;
+
+    private final EurekaController controller;
+
+    /** map from {@link GroupKey}s to {@link  Grouping}s. All groups (even not
+     * fully analyzed or not visible groups could be in this map */
+    private final Map<GroupKey, Grouping> groupMap = new HashMap<>();
+
+    /** list of all analyzed groups */
+    @ThreadConfined(type = ThreadType.JFX)
+    private final ObservableList<Grouping> analyzedGroups = FXCollections.observableArrayList();
+
+    /** list of unseen groups */
+    @ThreadConfined(type = ThreadType.JFX)
+    private final ObservableList<Grouping> unSeenGroups = FXCollections.observableArrayList();
+
+    /** sorted list of unseen groups */
+    @ThreadConfined(type = ThreadType.JFX)
+    private final SortedList<Grouping> sortedUnSeenGroups = unSeenGroups.sorted();
+
+    private ReGroupTask groupByTask;
+
+    /* --- current grouping/sorting attributes --- */
+    private volatile GroupSortBy sortBy = GroupSortBy.NONE;
+
+    private volatile DrawableAttribute groupBy = DrawableAttribute.PATH;
+
+    private volatile SortOrder sortOrder = SortOrder.ASCENDING;
+
+    public void setDB(DrawableDB db) {
+        this.db = db;
+        regroup(groupBy, sortBy, sortOrder, Boolean.TRUE);
+    }
+
+    public ObservableList<Grouping> getAnalyzedGroups() {
+        return analyzedGroups;
+    }
+
+    @ThreadConfined(type = ThreadType.JFX)
+    public SortedList<Grouping> getUnSeenGroups() {
+        return sortedUnSeenGroups;
+    }
+
+    /**
+     * construct a group manager hooked up to the given db and controller
+     *
+     * @param db
+     * @param controller
+     */
+    public GroupManager(EurekaController controller) {
+        this.controller = controller;
+    }
+
+    /**
+     * using the current groupBy set for this manager, find groupkeys for all
+     * the groups the given file is a part of
+     *
+     * @param file
+     *
+     * @returna a set of {@link GroupKey}s representing the group(s) the given
+     * file is a part of
+     */
+    public Set<GroupKey> getGroupKeysForFile(DrawableFile file) {
+        Set<GroupKey> resultSet = new HashSet();
+        final Object valueOfAttribute = file.getValueOfAttribute(groupBy);
+        List<? extends Comparable> vals;
+        if (valueOfAttribute instanceof List<?>) {
+            vals = (List<? extends Comparable>) valueOfAttribute;
+        } else {
+            vals = Collections.singletonList((Comparable) valueOfAttribute);
+        }
+        for (Comparable val : vals) {
+            final GroupKey groupKey = new GroupKey(groupBy, val);
+            resultSet.add(groupKey);
+        }
+        return resultSet;
+    }
+
+    /**
+     * using the current groupBy set for this manager, find groupkeys for all
+     * the groups the given file is a part of
+     *
+     * @param file
+     *
+     * @returna a set of {@link GroupKey}s representing the group(s) the given
+     * file is a part of
+     */
+    public Set<GroupKey> getGroupKeysForFileID(Long fileID) {
+        Set<GroupKey> resultSet = new HashSet();
+        try {
+            DrawableFile file = db.getFileFromID(fileID);
+            final Object valueOfAttribute = file.getValueOfAttribute(groupBy);
+
+            List<? extends Comparable> vals;
+            if (valueOfAttribute instanceof List<?>) {
+                vals = (List<? extends Comparable>) valueOfAttribute;
+            } else {
+                vals = Collections.singletonList((Comparable) valueOfAttribute);
+            }
+
+            for (Comparable val : vals) {
+                final GroupKey groupKey = new GroupKey(groupBy, val);
+                resultSet.add(groupKey);
+            }
+        } catch (TskCoreException ex) {
+            Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex);
+        }
+        return resultSet;
+    }
+
+    /**
+     * @param groupKey
+     *
+     * @return return the Grouping (if it exists) for the given GroupKey, or
+     *         null if no group exists for that key.
+     */
+    public Grouping getGroupForKey(GroupKey groupKey) {
+        synchronized (groupMap) {
+            return groupMap.get(groupKey);
+        }
+    }
+
+    synchronized public void clear() {
+
+        if (groupByTask != null) {
+            groupByTask.cancel(true);
+        }
+        sortBy = GroupSortBy.GROUP_BY_VALUE;
+        groupBy = DrawableAttribute.PATH;
+        sortOrder = SortOrder.ASCENDING;
+        Platform.runLater(() -> {
+            unSeenGroups.clear();
+            analyzedGroups.clear();
+        });
+        synchronized (groupMap) {
+            groupMap.clear();
+        }
+        db = null;
+    }
+
+    public boolean isRegrouping() {
+        if (groupByTask == null) {
+            return false;
+        }
+
+        switch (groupByTask.getState()) {
+            case READY:
+            case RUNNING:
+            case SCHEDULED:
+                return true;
+            case CANCELLED:
+            case FAILED:
+
+            case SUCCEEDED:
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * make and return a group with the given key and files. If a group already
+     * existed for that key, it will be replaced.
+     *
+     * @param groupKey
+     * @param files
+     *
+     * @return
+     *
+     * TODO: check if a group already exists for that key and ... (do what?add
+     * files to it?) -jm
+     */
+    public Grouping makeGroup(GroupKey groupKey, List<Long> files) {
+        List<Long> newFiles = files == null ? new ArrayList<>() : files;
+
+        Grouping g = new Grouping(groupKey, newFiles);
+        synchronized (groupMap) {
+            groupMap.put(groupKey, g);
+        }
+        return g;
+    }
+
+    public void markGroupSeen(Grouping group) {
+        unSeenGroups.remove(group);
+        db.markGroupSeen(group.groupKey);
+    }
+
+    /**
+     * remove the given file from the group with the given key. If the group
+     * doesn't exist or doesn't already contain this file, this method is a
+     * no-op
+     *
+     * @param groupKey the value of groupKey
+     * @param fileID   the value of file
+     */
+    public synchronized void removeFromGroup(GroupKey groupKey, final Long fileID) {
+        //get grouping this file would be in
+        final Grouping group = getGroupForKey(groupKey);
+        if (group != null) {
+            group.removeFile(fileID);
+        }
+    }
+
+    public synchronized void populateAnalyzedGroup(final GroupKey groupKey, List<Long> filesInGroup) {
+        populateAnalyzedGroup(groupKey, filesInGroup, null);
+    }
+
+    /**
+     * create a group with the given GroupKey and file ids and add it to the
+     * analyzed group list.
+     *
+     * @param groupKey
+     * @param filesInGroup
+     */
+    private synchronized void populateAnalyzedGroup(final GroupKey groupKey, List<Long> filesInGroup, ReGroupTask task) {
+
+        /* if this is not part of a regroup task or it is but the task is not
+         * cancelled...
+         *
+         * this allows us to stop if a regroup task has been cancelled (e.g. the
+         * user picked a different group by attribute, while the current task
+         * was still running) */
+        if (task == null || (task.isCancelled() == false)) {
+            Grouping g = makeGroup(groupKey, filesInGroup);
+
+            final boolean groupSeen = db.isGroupSeen(groupKey);
+            Platform.runLater(() -> {
+                analyzedGroups.add(g);
+
+                if (groupSeen == false) {
+                    unSeenGroups.add(g);
+                }
+            });
+        }
+    }
+
+    /**
+     * check if the group for the given groupkey is analyzed
+     *
+     * @param groupKey
+     *
+     * @return null if this group is not analyzed or a list of file ids in this
+     *         group if they are all analyzed
+     */
+    public List<Long> checkAnalyzed(final GroupKey groupKey) {
+        try {
+            /* for attributes other than path we can't be sure a group is fully
+             * analyzed because we don't know all the files that will be a part
+             * of that group */
+            if ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey)) {
+                return getFileIDsInGroup(groupKey);
+            } else {
+                return null;
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.name() + " = " + groupKey.getValue(), ex);
+            return null;
+        }
+    }
+
+    /**
+     * the implementation of this should be moved to DrawableDB
+     *
+     * @param hashDbName
+     *
+     * @return
+     *
+     * @deprecated
+     */
+    @Deprecated
+    private List<Long> getFileIDsWithHashSetName(String hashDbName) {
+        List<Long> files = new ArrayList<>();
+        try {
+
+            final SleuthkitCase sleuthkitCase = EurekaController.getDefault().getSleuthKitCase();
+            String query = "SELECT obj_id FROM blackboard_attributes,blackboard_artifacts WHERE "
+                    + "attribute_type_id=" + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()
+                    + " AND blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id"
+                    + " AND blackboard_attributes.value_text='" + hashDbName + "'"
+                    + " AND blackboard_artifacts.artifact_type_id=" + BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID();
+
+            ResultSet rs = null;
+            try {
+                rs = sleuthkitCase.runQuery(query);
+                while (rs.next()) {
+                    long id = rs.getLong("obj_id");
+                    try {
+                        if (EurekaModule.isSupportedAndNotKnown(Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id))) {
+                            files.add(id);
+                        }
+                    } catch (TskCoreException ex) {
+                        Exceptions.printStackTrace(ex);
+                    }
+                }
+            } catch (SQLException ex) {
+                Exceptions.printStackTrace(ex);
+            } finally {
+                if (rs != null) {
+                    try {
+                        Case.getCurrentCase().getSleuthkitCase().closeRunQuery(rs);
+                    } catch (SQLException ex) {
+                        LOGGER.log(Level.WARNING, "Error closing result set after getting hashset hits", ex);
+                    }
+                }
+            }
+
+            return files;
+        } catch (IllegalStateException ex) {
+            LOGGER.log(Level.SEVERE, "Can't get the current case; there is no case open!", ex);
+            return files;
+        }
+    }
+
+    /**
+     * find the distinct values for the given column (DrawableAttribute) in the
+     * order given by sortBy and sortOrder.
+     *
+     * These values represent the groups of files.
+     *
+     * @param regroup
+     * @param sortBy
+     * @param sortOrder
+     *
+     * @return
+     */
+    public <A extends Comparable> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
+        try {
+            List<A> values;
+            switch (groupBy.attrName) {
+                //these cases get special treatment
+                case CATEGORY:
+                    values = (List<A>) Category.valuesList();
+                    break;
+                case TAGS:
+                    values = (List<A>) Case.getCurrentCase().getServices().getTagsManager().getTagNamesInUse();
+                    break;
+                case ANALYZED:
+                    values = (List<A>) Arrays.asList(false, true);
+                    break;
+                case HASHSET:
+                    TreeSet<A> names = new TreeSet<>((Set<A>) db.getHashSetNames());
+                    values = new ArrayList<>(names);
+                    break;
+                default:
+                    //otherwise do straight db query 
+                    return db.findValuesForAttribute(groupBy, sortBy, sortOrder);
+            }
+
+            //sort in memory
+            Collections.sort(values, sortBy.getValueComparator(groupBy, sortOrder));
+
+            return values;
+        } catch (TskCoreException ex) {
+            Exceptions.printStackTrace(ex);
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * find the distinct values of the regroup attribute in the order given by
+     * sortBy with a ascending order
+     *
+     * @param regroup
+     * @param sortBy
+     *
+     * @return
+     */
+    public <A extends Comparable> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy) {
+        return findValuesForAttribute(groupBy, sortBy, SortOrder.ASCENDING);
+    }
+
+    public List<Long> getFileIDsInGroup(GroupKey groupKey) throws TskCoreException {
+        switch (groupKey.getAttribute().attrName) {
+            //these cases get special treatment
+            case CATEGORY:
+                return getFileIDsWithCategory((Category) groupKey.getValue());
+            case TAGS:
+                return getFileIDsWithTag((TagName) groupKey.getValue());
+            case HASHSET: //comment out this case to use db functionality for hashsets
+                return getFileIDsWithHashSetName((String) groupKey.getValue());
+            default:
+                //straight db query
+                return db.getFileIDsInGroup(groupKey);
+        }
+    }
+
+    // @@@ This was kind of slow in the profiler.  Maybe we should cache it.
+    public List<Long> getFileIDsWithCategory(Category category) throws TskCoreException {
+
+        try {
+            if (category == Category.ZERO) {
+
+                List<Long> files = new ArrayList<>();
+                TagName[] tns = {Category.FOUR.getTagName(), Category.THREE.getTagName(), Category.TWO.getTagName(), Category.ONE.getTagName(), Category.FIVE.getTagName()};
+                for (TagName tn : tns) {
+                    List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tn);
+                    for (ContentTag ct : contentTags) {
+                        if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) {
+                            files.add(ct.getContent().getId());
+                        }
+                    }
+                }
+
+                return db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")");
+            } else {
+
+                List<Long> files = new ArrayList<>();
+                List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(category.getTagName());
+                for (ContentTag ct : contentTags) {
+                    if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) {
+                        files.add(ct.getContent().getId());
+                    }
+                }
+
+                return files;
+            }
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex);
+            throw ex;
+        }
+    }
+
+    public List<Long> getFileIDsWithTag(TagName tagName) throws TskCoreException {
+        try {
+            List<Long> files = new ArrayList<>();
+            List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tagName);
+            for (ContentTag ct : contentTags) {
+                if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) {
+                    files.add(ct.getContent().getId());
+                }
+            }
+
+            return files;
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex);
+            throw ex;
+        }
+    }
+
+    public GroupSortBy getSortBy() {
+        return sortBy;
+    }
+
+    public void setSortBy(GroupSortBy sortBy) {
+        this.sortBy = sortBy;
+    }
+
+    public DrawableAttribute getGroupBy() {
+        return groupBy;
+    }
+
+    public void setGroupBy(DrawableAttribute groupBy) {
+        this.groupBy = groupBy;
+    }
+
+    public SortOrder getSortOrder() {
+        return sortOrder;
+    }
+
+    public void setSortOrder(SortOrder sortOrder) {
+        this.sortOrder = sortOrder;
+    }
+
+    /**
+     * regroup all files in the database using given {@link  DrawableAttribute}
+     * see
+     * {@link ReGroupTask} for more details.
+     *
+     * @param groupBy
+     * @param sortBy
+     * @param sortOrder
+     * @param force     true to force a full db query regroup
+     */
+    public void regroup(final DrawableAttribute groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) {
+        //only re-query the db if the group by attribute changed or it is forced
+        if (groupBy != getGroupBy() || force == true) {
+            setGroupBy(groupBy);
+            setSortBy(sortBy);
+            setSortOrder(sortOrder);
+            if (groupByTask != null) {
+                groupByTask.cancel(true);
+            }
+            Platform.runLater(() -> {
+                sortedUnSeenGroups.setComparator(sortBy.getGrpComparator(groupBy, sortOrder));
+            });
+
+            groupByTask = new ReGroupTask(groupBy, sortBy, sortOrder);
+            controller.submitBGTask(groupByTask);
+        } else {
+            // just resort the list of groups
+            setSortBy(sortBy);
+            setSortOrder(sortOrder);
+            Platform.runLater(() -> {
+                sortedUnSeenGroups.setComparator(sortBy.getGrpComparator(groupBy, sortOrder));
+            });
+        }
+    }
+
+    /**
+     * Task to query database for files in sorted groups and build
+     * {@link Groupings} for them
+     */
+    private class ReGroupTask extends LoggedTask<Void> {
+
+        private ProgressHandle groupProgress;
+
+        private final DrawableAttribute groupBy;
+
+        private final GroupSortBy sortBy;
+
+        private final SortOrder sortOrder;
+
+        public ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
+            super("regrouping files by " + groupBy.attrName.name() + " sorted by " + sortBy.name() + " in " + sortOrder.name() + " order", true);
+
+            this.groupBy = groupBy;
+            this.sortBy = sortBy;
+            this.sortOrder = sortOrder;
+        }
+
+        @Override
+        public boolean isCancelled() {
+            return super.isCancelled() || groupBy != getGroupBy() || sortBy != getSortBy() || sortOrder != getSortOrder();
+        }
+
+        @Override
+        protected Void call() throws Exception {
+
+            if (isCancelled()) {
+                return null;
+            }
+
+            groupProgress = ProgressHandleFactory.createHandle(getTaskName(), this);
+            Platform.runLater(() -> {
+                analyzedGroups.clear();
+                unSeenGroups.clear();
+            });
+            groupMap.clear();
+
+            //get a list of group key vals
+            final List<Comparable> vals = findValuesForAttribute(groupBy, sortBy, sortOrder);
+
+            groupProgress.start(vals.size());
+
+            int p = 0;
+            //for each key value
+            for (final Comparable val : vals) {
+                if (isCancelled()) {
+                    return null;//abort
+                }
+                p++;
+                updateMessage("regrouping files by " + groupBy.attrName.name() + " : " + val);
+                updateProgress(p, vals.size());
+                groupProgress.progress("regrouping files by " + groupBy.attrName.name() + " : " + val, p);
+                //check if this group is analyzed
+                final GroupKey groupKey = new GroupKey(groupBy, val);
+
+                List<Long> checkAnalyzed = checkAnalyzed(groupKey);
+                if (checkAnalyzed != null) { // != null => the group is analyzed, so add it to the ui
+                    populateAnalyzedGroup(groupKey, checkAnalyzed, ReGroupTask.this);
+                }
+            }
+            updateProgress(1, 1);
+            return null;
+        }
+
+        @Override
+        protected void done() {
+            super.done();
+            if (groupProgress != null) {
+                groupProgress.finish();
+                groupProgress = null;
+            }
+
+//            controller.checkForGroups();
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca3a3ef792c3bb8ac5641658bb523762b58732f9
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java
@@ -0,0 +1,202 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.image.Image;
+import javax.swing.SortOrder;
+import static javax.swing.SortOrder.ASCENDING;
+import static javax.swing.SortOrder.DESCENDING;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.TAGS;
+import org.sleuthkit.datamodel.TagName;
+
+/** enum of possible properties to sort groups by. This is the model for the
+ * drop down in {@link  EurekaToolbar} as well as each enum value having the
+ * stategy ({@link  Comparator}) for sorting the groups */
+public enum GroupSortBy implements ComparatorProvider {
+
+    /** sort the groups by the number of files in each
+     * sort the groups by the number of files in each */
+    FILE_COUNT("Group Size", true, "folder-open-image.png") {
+                @Override
+        public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, final SortOrder sortOrder) {
+                    Comparator<Grouping> comparingInt = Comparator.comparingInt(Grouping::getSize);
+            return sortOrder == ASCENDING ? comparingInt : comparingInt.reversed();
+        }
+
+                @Override
+                public <A extends Comparable> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) {
+                    return (A v1, A v2) -> {
+                        Grouping g1 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v1));
+                        Grouping g2 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v2));
+                        return getGrpComparator(attr, sortOrder).compare(g1, g2);
+                    };
+                }
+            },
+    /** sort
+     * the groups by the natural order of the grouping value ( eg group them by
+     * path alphabetically ) */
+    GROUP_BY_VALUE("Group Name", true, "folder-rename.png") {
+                @Override
+        public Comparator<Grouping> getGrpComparator(final DrawableAttribute attr, final SortOrder sortOrder) {
+            return (Grouping o1, Grouping o2) -> {
+
+                        final Comparable c1 = o1.groupKey.getValue();
+                        final Comparable c2 = o2.groupKey.getValue();
+                        final boolean isTags = attr.attrName == TAGS;
+
+                switch (sortOrder) {
+                    case ASCENDING:
+                        return c1.compareTo(c2);
+                    case DESCENDING:
+                        return c1.compareTo(c2) * -1;
+                    default: //unsorted
+                        return 0;
+                }
+            };
+        }
+
+        @Override
+        public <A extends Comparable> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) {
+            return (A o1, A o2) -> {
+                Comparable c1;
+                Comparable c2;
+                switch (attr.attrName) {
+                    case TAGS:
+                        c1 = ((TagName) o1).getDisplayName();
+                        c2 = ((TagName) o2).getDisplayName();
+                        break;
+                    default:
+                        c1 = (Comparable) o1;
+                        c2 = (Comparable) o2;
+                }
+
+                        switch (sortOrder) {
+                            case ASCENDING:
+                                return c1.compareTo(c2);
+                            case DESCENDING:
+                                return c1.compareTo(c2) * -1;
+                            default: //unsorted
+                                return 0;
+                        }
+                    };
+                }
+            },
+    /** don't sort the groups just use what ever
+     * order they come in (ingest
+     * order) */
+    /** don't sort the groups just use what ever order they come
+     * in (ingest
+     * order) */
+    NONE("None", false, "prohibition.png") {
+                @Override
+        public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, SortOrder sortOrder) {
+            return new NoOpComparator<>();
+        }
+
+                @Override
+                public <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, final SortOrder sortOrder) {
+                    return new NoOpComparator<>();
+                }
+            },
+    /** sort
+     * the groups by some priority metric to be determined and implemented */
+    PRIORITY("Priority", false, "hashset_hits.png") {
+                @Override
+        public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, SortOrder sortOrder) {
+            return Comparator.nullsLast(Comparator.comparingDouble(Grouping::getHashHitDensity).thenComparing(Grouping::getSize).reversed());
+        }
+
+        @Override
+        public <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder) {
+            return (A v1, A v2) -> {
+                Grouping g1 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v1));
+                Grouping g2 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v2));
+
+                        return getGrpComparator(attr, sortOrder).compare(g1, g2);
+                    };
+                }
+            };
+
+    /**
+     * get a list of the values of this enum
+     *
+     * @return
+     */
+    public static ObservableList<GroupSortBy> getValues() {
+        return FXCollections.observableArrayList(Arrays.asList(values()));
+
+    }
+
+    final private String displayName;
+
+    private Image icon;
+
+    private final String imageName;
+
+    private final Boolean sortOrderEnabled;
+
+    private GroupSortBy(String displayName, Boolean sortOrderEnabled, String imagePath) {
+        this.displayName = displayName;
+        this.sortOrderEnabled = sortOrderEnabled;
+        this.imageName = imagePath;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public Image getIcon() {
+        if (icon == null) {
+            if (StringUtils.isBlank(imageName) == false) {
+                this.icon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/" + imageName, true);
+            }
+        }
+        return icon;
+    }
+
+    public Boolean isSortOrderEnabled() {
+        return sortOrderEnabled;
+    }
+
+    private static class NoOpComparator<A> implements Comparator<A> {
+
+        @Override
+        public int compare(A o1, A o2) {
+            return 0;
+        }
+    }
+}
+
+/** * implementers of this interface must provide a method to compare
+ * ({@link  Comparable}) values and Groupings based on an
+ * {@link DrawableAttribute} and a {@link SortOrder}
+ */
+interface ComparatorProvider {
+
+    <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder);
+
+    <A> Comparator<Grouping> getGrpComparator(DrawableAttribute<A> attr, SortOrder sortOrder);
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java
new file mode 100644
index 0000000000000000000000000000000000000000..01e985679b477673c8f5688cc496956ac0e2865b
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java
@@ -0,0 +1,7 @@
+package org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+public enum GroupViewMode {
+
+    TILE, SLIDE_SHOW
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java
new file mode 100644
index 0000000000000000000000000000000000000000..15666e795072cbab7461193d03b17d7650b70958
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java
@@ -0,0 +1,91 @@
+
+/* Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ *
+ */
+public class GroupViewState {
+
+    private final Grouping group;
+
+    private final GroupViewMode mode;
+
+    private final Optional<Long> slideShowfileID;
+
+    public Grouping getGroup() {
+        return group;
+    }
+
+    public GroupViewMode getMode() {
+        return mode;
+    }
+
+    public Optional<Long> getSlideShowfileID() {
+        return slideShowfileID;
+    }
+
+    private GroupViewState(Grouping g, GroupViewMode mode, Long slideShowfileID) {
+        this.group = g;
+        this.mode = mode;
+        this.slideShowfileID = Optional.ofNullable(slideShowfileID);
+    }
+
+    public static GroupViewState tile(Grouping g) {
+        return new GroupViewState(g, GroupViewMode.TILE, null);
+    }
+
+    public static GroupViewState slideShow(Grouping g, Long fileID) {
+        return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 17 * hash + Objects.hashCode(this.group);
+        hash = 17 * hash + Objects.hashCode(this.mode);
+        hash = 17 * hash + Objects.hashCode(this.slideShowfileID);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final GroupViewState other = (GroupViewState) obj;
+        if (!Objects.equals(this.group, other.group)) {
+            return false;
+        }
+        if (this.mode != other.mode) {
+            return false;
+        }
+        if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java
new file mode 100644
index 0000000000000000000000000000000000000000..d65e73c9de376e5dd5320242619d343d3eedce8c
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java
@@ -0,0 +1,129 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.grouping;
+
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import java.util.List;
+import java.util.Objects;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Represents a set of files in a group. The UI listens to changes to the group
+ * and updates itself accordingly.
+ *
+ * This class is named Grouping and not Group to avoid confusion with
+ * {@link javafx.scene.Group} and others.
+ */
+public class Grouping {
+
+    private static final Logger LOGGER = Logger.getLogger(Grouping.class.getName());
+
+    public static final String UNKNOWN = "unknown";
+
+    final private ObservableList<Long> fileIDs = FXCollections.observableArrayList();
+
+    //cache the number of files in this groups with hashset hits
+    private int filesWithHashSetHitsCount = -1;
+
+    synchronized public ObservableList<Long> fileIds() {
+        return fileIDs;
+    }
+
+    final public GroupKey groupKey;
+
+    public Grouping(GroupKey groupKey, List<Long> filesInGroup) {
+        this.groupKey = groupKey;
+        fileIDs.setAll(filesInGroup);
+    }
+
+    synchronized public Integer getSize() {
+        return fileIDs.size();
+    }
+
+    public double getHashHitDensity() {
+        return getFilesWithHashSetHitsCount() / getSize().doubleValue();
+    }
+
+    synchronized public Integer getFilesWithHashSetHitsCount() {
+
+        if (filesWithHashSetHitsCount < 0) {
+            filesWithHashSetHitsCount = 0;
+            for (Long fileID : fileIds()) {
+                try {
+                    long artcount = EurekaController.getDefault().getSleuthKitCase().getBlackboardArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, fileID);
+                    if (artcount > 0) {
+                        filesWithHashSetHitsCount++;
+                    }
+                } catch (IllegalStateException | TskCoreException ex) {
+                    LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()", ex);
+                    break;
+                }
+            }
+        }
+        return filesWithHashSetHitsCount;
+    }
+
+    @Override
+    public String toString() {
+        return "Grouping{ keyProp=" + groupKey + '}';
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 53 * hash + Objects.hashCode(this.groupKey);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Grouping other = (Grouping) obj;
+        if (!Objects.equals(this.groupKey, other.groupKey)) {
+            return false;
+        }
+        return true;
+    }
+
+    synchronized public void addFile(Long f) {
+        Platform.runLater(() -> {
+            if (fileIDs.contains(f) == false) {
+                fileIDs.add(f);
+            }
+        });
+
+    }
+
+    synchronized public void removeFile(Long f) {
+        Platform.runLater(() -> {
+            fileIDs.removeAll(f);
+        });
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java
new file mode 100644
index 0000000000000000000000000000000000000000..edc9f1b03f93522b2ec94112e3fe87cb38438fd5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java
@@ -0,0 +1,40 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import javafx.scene.control.ListCell;
+import javafx.scene.image.ImageView;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+
+/**
+ */
+class AttributeListCell extends ListCell<DrawableAttribute<?>> {
+
+    @Override
+    protected void updateItem(DrawableAttribute<?> item, boolean empty) {
+        super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates.
+        if (item != null) {
+            setText(item.getDisplayName());
+            setGraphic(new ImageView(item.getIcon()));
+        } else {
+            setGraphic(null);
+            setText(null);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..8bfa03862af828ea2e182cad81920da44efa6d74
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" opacity="1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)),        linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%); -fx-background-radius: 2;" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+  <children>
+    <BorderPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" snapToPixel="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <bottom>
+        <BorderPane maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER">
+          <center>
+            <Label id="pathLabel" fx:id="nameLabel" alignment="CENTER" contentDisplay="TEXT_ONLY" maxHeight="16.0" minHeight="16.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" text="file name" textAlignment="CENTER" wrapText="true">
+              <labelFor>
+                <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="200.0" opacity="1.0" pickOnBounds="true" preserveRatio="true" style="-fx-border-radius : 5;&#10;-fx-border-width : 5;&#10;-fx-border-color : blue;" BorderPane.alignment="CENTER" />
+              </labelFor>
+            </Label>
+          </center>
+          <left>
+            <HBox maxHeight="-Infinity" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0" BorderPane.alignment="CENTER_LEFT">
+              <children>
+                <ImageView fx:id="fileTypeImageView" fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" scaleX="1.0" scaleY="1.0">
+                  <image>
+                    <Image url="@../images/video-file.png" />
+                  </image>
+                </ImageView>
+                <ImageView fx:id="hashHitImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" style="">
+                  <image>
+                    <Image url="@../images/hashset_hits.png" />
+                  </image>
+                  <HBox.margin>
+                    <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" fx:id="x1" />
+                  </HBox.margin>
+                </ImageView>
+              </children>
+              <padding>
+                <Insets bottom="2.0" right="2.0" top="2.0" />
+              </padding>
+            </HBox>
+          </left>
+          <right>
+            <ToggleButton fx:id="followUpToggle" minWidth="24.0" mnemonicParsing="false" prefWidth="24.0" selected="false" text="">
+              <graphic>
+                <ImageView id="followUpImageview" fx:id="followUpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                  <image>
+                    <Image url="@../images/flag_gray.png" />
+                  </image>
+                </ImageView>
+              </graphic>
+            </ToggleButton>
+          </right>
+        </BorderPane>
+      </bottom>
+      <center>
+        <BorderPane fx:id="imageBorder" center="$imageView" maxHeight="-1.0" maxWidth="-1.0" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER">
+<center><ImageView fx:id="imageView" fitHeight="100.0" fitWidth="100.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" />
+</center></BorderPane>
+      </center>
+    </BorderPane>
+  </children>
+  <padding>
+    <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
+  </padding>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java
new file mode 100644
index 0000000000000000000000000000000000000000..aab1928ca9e2b747faf359072461984695d021c0
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java
@@ -0,0 +1,122 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.net.URL;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import javafx.fxml.FXML;
+import javafx.scene.CacheHint;
+import javafx.scene.control.Control;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+
+/**
+ * GUI component that represents a single image as a tile with an icon, a label
+ * a color coded border and possibly other controls. Designed to be in a
+ * {@link GroupPane}'s TilePane or SlideShow.
+ *
+ *
+ * TODO: refactor this to extend from {@link Control}? -jm
+ */
+public class DrawableTile extends SingleDrawableViewBase implements Category.CategoryListener, TagUtils.TagListener {
+
+    private static final DropShadow LAST_SELECTED_EFFECT = new DropShadow(10, Color.BLUE);
+
+    private static final Logger LOGGER = Logger.getLogger(DrawableTile.class.getName());
+
+    /**
+     * the central ImageView that shows a thumbnail of the represented file
+     */
+    @FXML
+    @ThreadConfined(type = ThreadType.UI)
+    private ImageView imageView;
+
+    public ImageView getImageView() {
+        return imageView;
+    }
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @Override
+    protected void disposeContent() {
+        //no-op
+    }
+
+    @FXML
+    @Override
+    protected void initialize() {
+        super.initialize();
+        assert imageBorder != null : "fx:id=\"imageAnchor\" was not injected: check your FXML file 'DrawableTile.fxml'.";
+        assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'DrawableTile.fxml'.";
+        assert nameLabel != null : "fx:id=\"nameLabel\" was not injected: check your FXML file 'DrawableTile.fxml'.";
+
+        //set up properties and binding
+        setCache(true);
+        setCacheHint(CacheHint.SPEED);
+        nameLabel.prefWidthProperty().bind(imageView.fitWidthProperty());
+
+        imageView.fitHeightProperty().bind(EurekaToolbar.getDefault().sizeSliderValue());
+        imageView.fitWidthProperty().bind(EurekaToolbar.getDefault().sizeSliderValue());
+
+        globalSelectionModel.lastSelectedProperty().addListener((observable, oldValue, newValue) -> {
+            setEffect(Objects.equals(newValue, fileID) ? LAST_SELECTED_EFFECT : null);
+        });
+    }
+
+    public DrawableTile(GroupPane gp) {
+        super();
+        FXMLConstructor.construct(this, "DrawableTile.fxml");
+        groupPane = gp;
+    }
+
+    @Override
+    @ThreadConfined(type = ThreadType.UI)
+    protected void clearContent() {
+        imageView.setImage(null);
+    }
+
+    @Override
+    protected Runnable getContentUpdateRunnable() {
+        Image image = file.getIcon();
+
+        return () -> {
+            imageView.setImage(image);
+        };
+    }
+
+    @Override
+    @ThreadConfined(type = ThreadType.UI)
+    protected String getLabelText() {
+        return file.getName();
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java
new file mode 100644
index 0000000000000000000000000000000000000000..aec3246430474e05f0eca70bffa45b98c480c873
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java
@@ -0,0 +1,93 @@
+package org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.util.Collection;
+import javafx.application.Platform;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+
+/**
+ * TODO: extract common interface out of {@link SingleImageView} and
+ * {@link MetaDataPane}
+ */
+public interface DrawableView extends Category.CategoryListener, TagUtils.TagListener {
+
+    //TODO: do this all in css? -jm
+    static final int CAT_BORDER_WIDTH = 10;
+
+    static final BorderWidths CAT_BORDER_WIDTHS = new BorderWidths(CAT_BORDER_WIDTH);
+
+    static final CornerRadii CAT_CORNER_RADII = new CornerRadii(3);
+
+    static final Border HASH_BORDER = new Border(new BorderStroke(Color.PURPLE, BorderStrokeStyle.DASHED, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT1_BORDER = new Border(new BorderStroke(Category.ONE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT2_BORDER = new Border(new BorderStroke(Category.TWO.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT3_BORDER = new Border(new BorderStroke(Category.THREE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT4_BORDER = new Border(new BorderStroke(Category.FOUR.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT5_BORDER = new Border(new BorderStroke(Category.FIVE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    static final Border CAT0_BORDER = new Border(new BorderStroke(Category.ZERO.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS));
+
+    Region getBorderable();
+
+    DrawableFile getFile();
+
+    void setFile(final Long fileID);
+
+    Long getFileID();
+
+    @Override
+    void handleCategoryChanged(Collection<Long> ids);
+
+    @Override
+    void handleTagsChanged(Collection<Long> ids);
+
+    default boolean hasHashHit() {
+        return getFile().getHashHitSetNames().isEmpty() == false;
+    }
+
+    static Border getCategoryBorder(Category category) {
+        switch (category) {
+            case ZERO:
+                return CAT0_BORDER;
+            case ONE:
+                return CAT1_BORDER;
+            case TWO:
+                return CAT2_BORDER;
+            case THREE:
+                return CAT3_BORDER;
+            case FOUR:
+                return CAT4_BORDER;
+            case FIVE:
+            default:
+                return CAT5_BORDER;
+        }
+    }
+
+    @ThreadConfined(type = ThreadConfined.ThreadType.ANY)
+    default Category updateCategoryBorder() {
+        final Category category = getFile().getCategory();
+        final Border border = hasHashHit() && (category == Category.ZERO)
+                              ? HASH_BORDER
+                              : DrawableView.getCategoryBorder(category);
+
+        Platform.runLater(() -> {
+            getBorderable().setBorder(border);
+        });
+        return category;
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..0cee5051111f085f7b252de0eb770dec663442d5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+  <items>
+    <Label text="Group By:">
+      <labelFor>
+        <ComboBox fx:id="groupByBox" editable="false" />
+      </labelFor>
+    </Label>
+    <fx:reference source="groupByBox" />
+    <Region prefHeight="-1.0" prefWidth="20.0" />
+    <Label text="Sort By:">
+      <labelFor>
+        <ComboBox fx:id="sortByBox" />
+      </labelFor>
+    </Label>
+    <HBox id="HBox" fx:id="sortControlGroup" alignment="CENTER" spacing="5.0">
+      <children>
+        <fx:reference source="sortByBox" />
+        <VBox alignment="CENTER_LEFT" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0">
+          <children>
+            <RadioButton fx:id="ascRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="true" text="Ascending">
+              <graphic>
+                <ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+                  <image>
+                    <Image url="@../images/arrow_up.png" />
+                  </image>
+                </ImageView>
+              </graphic>
+              <toggleGroup>
+                <ToggleGroup fx:id="orderGroup" />
+              </toggleGroup>
+            </RadioButton>
+            <RadioButton fx:id="descRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="false" text="Descending" toggleGroup="$orderGroup">
+              <graphic>
+                <ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+                  <image>
+                    <Image url="@../images/arrow_down.png" />
+                  </image>
+                </ImageView>
+              </graphic>
+            </RadioButton>
+          </children>
+        </VBox>
+      </children>
+    </HBox>
+    <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
+    <CheckBox fx:id="onlyAnalyzedCheckBox" allowIndeterminate="false" indeterminate="false" mnemonicParsing="false" prefWidth="16.0" selected="true" text="only analyzed groups" underline="false" visible="false" />
+    <Label contentDisplay="RIGHT" text="Apply to Selected Files:" textOverrun="ELLIPSIS" />
+    <SplitMenuButton id="tagSplitMenu" fx:id="tagSelectedMenuButton" disable="true" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS">
+      <items>
+        <MenuItem mnemonicParsing="false" text="Action 1" />
+        <MenuItem mnemonicParsing="false" text="Action 2" />
+      </items>
+    </SplitMenuButton>
+    <SplitMenuButton id="catSplitMenu" fx:id="catSelectedMenuButton" disable="true" mnemonicParsing="false" text="Cat-0">
+      <items>
+        <MenuItem mnemonicParsing="false" text="Action 1" />
+        <MenuItem mnemonicParsing="false" text="Action 2" />
+      </items>
+    </SplitMenuButton>
+    <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
+    <Label text="Thumbnail Size (px):">
+      <labelFor>
+        <Slider fx:id="sizeSlider" blockIncrement="100.0" majorTickUnit="100.0" max="300.0" min="100.0" minorTickCount="0" orientation="HORIZONTAL" prefHeight="-1.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="100.0" />
+      </labelFor>
+    </Label>
+    <fx:reference source="sizeSlider" />
+  </items>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c2ac9913c8e7c79fca1dfd5d865f899b01e594f
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java
@@ -0,0 +1,227 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.Slider;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javax.swing.SortOrder;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.IconCache;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadUtils;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Controller for the Eureka ToolBar */
+public class EurekaToolbar extends ToolBar {
+
+    private static final int SIZE_SLIDER_DEFAULT = 100;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private ComboBox<DrawableAttribute<?>> groupByBox;
+
+    @FXML
+    private CheckBox onlyAnalyzedCheckBox;
+
+    @FXML
+    private Slider sizeSlider;
+
+    @FXML
+    private ComboBox<GroupSortBy> sortByBox;
+
+    @FXML
+    private RadioButton ascRadio;
+
+    @FXML
+    private RadioButton descRadio;
+
+    @FXML
+    private ToggleGroup orderGroup;
+
+//    @FXML
+//    private ToggleButton metaDataToggle;
+    @FXML
+    private HBox sortControlGroup;
+
+    @FXML
+    private SplitMenuButton catSelectedMenuButton;
+
+    @FXML
+    private SplitMenuButton tagSelectedMenuButton;
+
+    private static EurekaToolbar instance;
+
+    private final SimpleObjectProperty<SortOrder> orderProperty = new SimpleObjectProperty<>(SortOrder.ASCENDING);
+
+    private final InvalidationListener queryInvalidationListener = (Observable o) -> {
+        if (orderGroup.getSelectedToggle() == ascRadio) {
+            orderProperty.set(SortOrder.ASCENDING);
+        } else {
+            orderProperty.set(SortOrder.DESCENDING);
+        }
+
+        EurekaController.getDefault().getGroupManager().regroup(groupByBox.getSelectionModel().getSelectedItem(), sortByBox.getSelectionModel().getSelectedItem(), getSortOrder(), false);
+    };
+
+    synchronized public SortOrder getSortOrder() {
+        return orderProperty.get();
+    }
+
+//    public ReadOnlyBooleanProperty showMetaDataProperty() {
+//        return metaDataToggle.selectedProperty();
+//    }
+    public DoubleProperty sizeSliderValue() {
+        return sizeSlider.valueProperty();
+    }
+
+    static synchronized public EurekaToolbar getDefault() {
+        if (instance == null) {
+            instance = new EurekaToolbar();
+        }
+        return instance;
+    }
+
+    @FXML
+    void initialize() {
+        assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert catSelectedMenuButton != null : "fx:id=\"catSelectedMenubutton\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert groupByBox != null : "fx:id=\"groupByBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert onlyAnalyzedCheckBox != null : "fx:id=\"onlyAnalyzedCheckBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert sizeSlider != null : "fx:id=\"sizeSlider\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert sortControlGroup != null : "fx:id=\"sortControlGroup\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+        assert tagSelectedMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'EurekaToolbar.fxml'.";
+
+        FileIDSelectionModel.getInstance().getSelected().addListener((Observable o) -> {
+            ThreadUtils.runNowOrLater(() -> {
+                tagSelectedMenuButton.setDisable(FileIDSelectionModel.getInstance().getSelected().isEmpty());
+                catSelectedMenuButton.setDisable(FileIDSelectionModel.getInstance().getSelected().isEmpty());
+            });
+        });
+
+        tagSelectedMenuButton.setOnAction((ActionEvent t) -> {
+            try {
+                TagUtils.createSelTagMenuItem(TagUtils.getFollowUpTagName(), tagSelectedMenuButton).getOnAction().handle(t);
+            } catch (TskCoreException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        });
+
+        tagSelectedMenuButton.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
+        tagSelectedMenuButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
+            if (t1) {
+                ArrayList<MenuItem> selTagMenues = new ArrayList<>();
+                for (final TagName tn : TagUtils.getNonCategoryTagNames()) {
+                    MenuItem menuItem = TagUtils.createSelTagMenuItem(tn, tagSelectedMenuButton);
+                    selTagMenues.add(menuItem);
+                }
+                tagSelectedMenuButton.getItems().setAll(selTagMenues);
+            }
+        });
+
+        catSelectedMenuButton.setOnAction(Category.FIVE.createSelCatMenuItem(catSelectedMenuButton).getOnAction());
+        catSelectedMenuButton.setText(Category.FIVE.getDisplayName());
+        catSelectedMenuButton.setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon()));
+        catSelectedMenuButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
+            if (t1) {
+                ArrayList<MenuItem> categoryMenues = new ArrayList<>();
+                for (final Category cat : Category.values()) {
+                    MenuItem menuItem = cat.createSelCatMenuItem(catSelectedMenuButton);
+                    categoryMenues.add(menuItem);
+                }
+                catSelectedMenuButton.getItems().setAll(categoryMenues);
+            }
+        });
+
+        groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs()));
+        groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
+        groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener);
+        groupByBox.disableProperty().bind(EurekaController.getDefault().regroupDisabled());
+        groupByBox.setCellFactory((listView) -> new AttributeListCell());
+        groupByBox.setButtonCell(new AttributeListCell());
+
+        sortByBox.setCellFactory((listView) -> new SortByListCell());
+        sortByBox.setButtonCell(new SortByListCell());
+        sortByBox.setItems(GroupSortBy.getValues());
+
+        sortByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener);
+
+        sortByBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
+            final boolean orderEnabled = newValue == GroupSortBy.NONE || newValue == GroupSortBy.PRIORITY;
+            ascRadio.setDisable(orderEnabled);
+            descRadio.setDisable(orderEnabled);
+
+        });
+        sortByBox.getSelectionModel().select(GroupSortBy.PRIORITY);
+//        ascRadio.disableProperty().bind(sortByBox.getSelectionModel().selectedItemProperty().isEqualTo(GroupSortBy.NONE));
+//        descRadio.disableProperty().bind(sortByBox.getSelectionModel().selectedItemProperty().isEqualTo(GroupSortBy.NONE));
+
+        orderGroup.selectedToggleProperty().addListener(queryInvalidationListener);
+
+        IconCache.getDefault().iconSize.bind(sizeSlider.valueProperty());
+
+    }
+
+    public void reset() {
+        Platform.runLater(() -> {
+            groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
+            sortByBox.getSelectionModel().select(GroupSortBy.NONE);
+            orderGroup.selectToggle(ascRadio);
+            sizeSlider.setValue(SIZE_SLIDER_DEFAULT);
+        });
+    }
+
+    private EurekaToolbar() {
+        FXMLConstructor.construct(this, "EurekaToolbar.fxml");
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java
new file mode 100644
index 0000000000000000000000000000000000000000..02e846588839f2a2da06ac0a7212126bdd3dafee
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java
@@ -0,0 +1,47 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+
+/**
+ *
+ */
+public interface Fitable {
+
+    BooleanProperty preserveRatioProperty();
+
+    DoubleProperty fitHeightProperty();
+
+    DoubleProperty fitWidthProperty();
+
+    boolean isPreserveRatio();
+
+    double getFitHeight();
+
+    double getFitWidth();
+
+    void setPreserveRatio(boolean b);
+
+    void setFitHeight(double d);
+
+    void setFitWidth(double d);
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..6eb799970d1ef1799510e3644d3bbce3b0c4d755
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+<?import org.controlsfx.control.*?>
+
+<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+   
+    <center>
+        <GridView fx:id="gridView" BorderPane.alignment="CENTER" />
+    </center>
+    <bottom>
+        <BorderPane BorderPane.alignment="CENTER">
+            <left>
+                <HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT">
+                    <children>
+                        <Label text="Group Viewing History: " />
+                        <Button fx:id="backButton" mnemonicParsing="false" text="back">
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/arrow-180.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                        <Button fx:id="forwardButton" mnemonicParsing="false" text="forward">
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/arrow.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                    </children>
+                    <BorderPane.margin>
+                        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                    </BorderPane.margin>
+                </HBox>
+            </left>
+            <right>
+                <HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT">
+                    <children>
+                        <Button fx:id="nextButton" contentDisplay="RIGHT" mnemonicParsing="false" text="next unseen group" BorderPane.alignment="CENTER_RIGHT">
+                            <BorderPane.margin>
+                                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                            </BorderPane.margin>
+                            <graphic>
+                                <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                    <image>
+                                        <Image url="@../images/control-double.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                    </children>
+                    <BorderPane.margin>
+                        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                    </BorderPane.margin>
+                </HBox>
+            </right>
+            <top>
+                <Separator prefWidth="200.0" BorderPane.alignment="CENTER">
+                    <BorderPane.margin>
+                        <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" />
+                    </BorderPane.margin>
+                </Separator>
+            </top>
+        </BorderPane>
+    </bottom>
+    <top>
+        <VBox>
+            <children>
+                <Label fx:id="groupLabel" wrapText="true">
+                    <font>
+                        <Font size="14.0" />
+                    </font>
+                    <VBox.margin>
+                        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+                    </VBox.margin>
+                </Label>
+     
+                <ToolBar id="headToolBar" fx:id="headerToolBar" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" BorderPane.alignment="CENTER" VBox.vgrow="NEVER">
+                    <items>
+                        <Label text="Apply to Group:" />
+                        <SplitMenuButton fx:id="grpTagSplitMenu" mnemonicParsing="false" text="Follow Up">
+                            <items>
+                                <MenuItem mnemonicParsing="false" text="Action 1" />
+                                <MenuItem mnemonicParsing="false" text="Action 2" />
+                            </items>
+                        </SplitMenuButton>
+                        <SplitMenuButton fx:id="grpCatSplitMenu" mnemonicParsing="false" text="Cat-0">
+                            <items>
+                                <MenuItem mnemonicParsing="false" text="Action 1" />
+                                <MenuItem mnemonicParsing="false" text="Action 2" />
+                            </items>
+                        </SplitMenuButton>
+                        <Region fx:id="spacer" prefHeight="-1.0" prefWidth="-1.0" />
+                        <Separator prefWidth="30.0" />
+                        <SegmentedButton fx:id="segButton">
+                            <buttons>
+                                <ToggleButton id="" fx:id="tileToggle" alignment="CENTER" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="32.0" mnemonicParsing="false" prefWidth="-1.0" scaleX="1.0" selected="true" text="" textAlignment="CENTER">
+                                    <graphic>
+                                        <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" translateY="1.0">
+                                            <image>
+                                                <Image url="@../images/application_view_tile.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                </ToggleButton>
+                                <ToggleButton id="filmStripToggle" fx:id="slideShowToggle" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="32.0" mnemonicParsing="false" prefWidth="-1.0" text="">
+                                    <graphic>
+                                        <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" rotate="90.0" translateY="1.0">
+                                            <image>
+                                                <Image url="@../images/film.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                </ToggleButton>        
+                            </buttons>       
+                        </SegmentedButton>
+
+                    </items>
+                </ToolBar>
+            </children>
+        </VBox>
+    </top>
+    <padding>
+        <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" />
+    </padding>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..04b1698c9816597d28f6534dc42eb57dcc1062f9
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java
@@ -0,0 +1,782 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import com.google.common.collect.ImmutableMap;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.stream.IntStream;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.Observable;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.geometry.Bounds;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToolBar;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.KeyCode;
+import static javafx.scene.input.KeyCode.DIGIT0;
+import static javafx.scene.input.KeyCode.DIGIT1;
+import static javafx.scene.input.KeyCode.DIGIT2;
+import static javafx.scene.input.KeyCode.DIGIT3;
+import static javafx.scene.input.KeyCode.DIGIT4;
+import static javafx.scene.input.KeyCode.DIGIT5;
+import static javafx.scene.input.KeyCode.DOWN;
+import static javafx.scene.input.KeyCode.LEFT;
+import static javafx.scene.input.KeyCode.NUMPAD0;
+import static javafx.scene.input.KeyCode.NUMPAD1;
+import static javafx.scene.input.KeyCode.NUMPAD2;
+import static javafx.scene.input.KeyCode.NUMPAD3;
+import static javafx.scene.input.KeyCode.NUMPAD4;
+import static javafx.scene.input.KeyCode.NUMPAD5;
+import static javafx.scene.input.KeyCode.RIGHT;
+import static javafx.scene.input.KeyCode.UP;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.TilePane;
+import javafx.scene.paint.Color;
+import javafx.util.Duration;
+import javax.swing.Action;
+import javax.swing.SwingUtilities;
+import org.apache.commons.lang3.StringUtils;
+import org.controlsfx.control.GridCell;
+import org.controlsfx.control.GridView;
+import org.controlsfx.control.SegmentedButton;
+import org.controlsfx.control.action.ActionUtils;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.actions.Presenter;
+import org.openide.windows.TopComponent;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.directorytree.ExtractAction;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaTopComponent;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType;
+import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction;
+import org.sleuthkit.autopsy.imageanalyzer.actions.Back;
+import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction;
+import org.sleuthkit.autopsy.imageanalyzer.actions.Forward;
+import org.sleuthkit.autopsy.imageanalyzer.actions.NextUnseenGroup;
+import org.sleuthkit.autopsy.imageanalyzer.actions.SwingMenuItemAdapter;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewMode;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** A GroupPane displays the contents of a {@link Grouping}. It support both a
+ * {@link  TilePane} based view and a {@link  SlideShowView} view by swapping out
+ * its internal components.
+ *
+ * TODO: review for synchronization issues.
+ * TODO: Extract the The TilePane instance to a separate class analogous to
+ * the SlideShow */
+public class GroupPane extends BorderPane implements GroupView {
+
+    private static final Logger LOGGER = Logger.getLogger(GroupPane.class.getName());
+
+    private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE);
+
+    private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)),
+                                                                new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR))
+    );
+
+    private static final FileIDSelectionModel globalSelectionModel = FileIDSelectionModel.getInstance();
+
+    private final Back backAction;
+
+    private final Forward forwardAction;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private SplitMenuButton grpCatSplitMenu;
+
+    @FXML
+    private SplitMenuButton grpTagSplitMenu;
+
+    @FXML
+    private ToolBar headerToolBar;
+
+    @FXML
+    private SegmentedButton segButton;
+
+    private SlideShowView slideShowPane;
+
+    @FXML
+    private ToggleButton slideShowToggle;
+
+    @FXML
+    private Region spacer;
+
+    @FXML
+    private GridView<Long> gridView;
+
+    @FXML
+    private ToggleButton tileToggle;
+
+    @FXML
+    private Button nextButton;
+
+    @FXML
+    private Button backButton;
+
+    @FXML
+    private Button forwardButton;
+
+    @FXML
+    private Label groupLabel;
+
+    private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler();
+
+    private final NextUnseenGroup nextGroupAction;
+
+    private final EurekaController controller;
+
+    private ContextMenu contextMenu;
+
+    private Integer selectionAnchorIndex;
+
+    /** the current GroupViewMode of this GroupPane */
+    private final SimpleObjectProperty<GroupViewMode> groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE);
+
+    /** the grouping this pane is currently the view for */
+    private final ReadOnlyObjectWrapper<Grouping> grouping = new ReadOnlyObjectWrapper<>();
+
+    /** map from fileIDs to their assigned cells in the tile view. This is used
+     * to determine whether fileIDs are visible or are offscreen. No entry
+     * indicates the given fileID is not displayed on screenDrawableCells
+     * responsible for adding and removing themselves from this map */
+    @ThreadConfined(type = ThreadType.UI)
+    private final Map<Long, DrawableCell> cellMap = new HashMap<>();
+
+    public GroupPane(EurekaController controller) {
+        this.controller = controller;
+        nextGroupAction = new NextUnseenGroup(controller);
+        backAction = new Back(controller);
+        forwardAction = new Forward(controller);
+        FXMLConstructor.construct(this, "GroupPane.fxml");
+    }
+
+    public void activateSlideShowViewer(Long slideShowFileId) {
+        groupViewMode.set(GroupViewMode.SLIDE_SHOW);
+        gridView.removeEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler);
+        //make a new slideShowPane if necessary
+        if (slideShowPane == null) {
+            slideShowPane = new SlideShowView(this);
+        }
+
+        //assign last selected file or if none first file in group
+        if (slideShowFileId == null || grouping.get().fileIds().contains(slideShowFileId) == false) {
+            slideShowFileId = grouping.get().fileIds().get(0);
+        }
+
+        slideShowPane.setFile(slideShowFileId);
+        setCenter(slideShowPane);
+    }
+
+    public void activateTileViewer() {
+        groupViewMode.set(GroupViewMode.TILE);
+        gridView.addEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler);
+        setCenter(gridView);
+        this.scrollToFileID(globalSelectionModel.lastSelectedProperty().get());
+    }
+
+    public Grouping getGrouping() {
+        return grouping.get();
+    }
+
+    /**
+     * @return the text to display as part of the header
+     */
+    public String getHeaderString() {
+        int size = grouping.get().getSize();
+        int hashHitCount = grouping.get().getFilesWithHashSetHitsCount();
+        String groupName;
+        if (grouping.get().groupKey.getAttribute() == DrawableAttribute.TAGS) {
+            groupName = ((TagName) grouping.get().groupKey.getValue()).getDisplayName();
+        } else {
+            groupName = grouping.get().groupKey.getValue().toString();
+        }
+        return StringUtils.defaultIfBlank(groupName, Grouping.UNKNOWN) + " -- " + hashHitCount + " hash set hits / " + size + " files";
+    }
+
+    private MenuItem createGrpCatMenuItem(final Category cat) {
+        final MenuItem menuItem = new MenuItem(cat.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon()));
+        menuItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent t) {
+                selectAllFiles();
+                new CategorizeAction().addTag(cat.getTagName(), "");
+
+                grpCatSplitMenu.setText(cat.getDisplayName());
+                grpCatSplitMenu.setOnAction(this);
+            }
+        });
+        return menuItem;
+    }
+
+    private MenuItem createGrpTagMenuItem(final TagName tn) {
+        final MenuItem menuItem = new MenuItem(tn.getDisplayName(), new ImageView(DrawableAttribute.TAGS.getIcon()));
+        menuItem.setOnAction(new EventHandler<ActionEvent>() {
+            @Override
+            public void handle(ActionEvent t) {
+                selectAllFiles();
+                AddDrawableTagAction.getInstance().addTag(tn, "");
+
+                grpTagSplitMenu.setText(tn.getDisplayName());
+                grpTagSplitMenu.setOnAction(this);
+            }
+        });
+        return menuItem;
+    }
+
+    private void selectAllFiles() {
+        globalSelectionModel.clearAndSelectAll(getGrouping().fileIds());
+    }
+
+    /** reset the text and icons to represent the currently filtered files */
+    protected void resetHeaderString() {
+        int size = grouping.get().getSize();
+        int hashHitCount = grouping.get().getFilesWithHashSetHitsCount();
+        String groupName;
+        if (grouping.get().groupKey.getAttribute() == DrawableAttribute.TAGS) {
+            groupName = ((TagName) grouping.get().groupKey.getValue()).getDisplayName();
+        } else {
+            groupName = grouping.get().groupKey.getValue().toString();
+        }
+        groupLabel.setText(StringUtils.defaultIfBlank(groupName, Grouping.UNKNOWN) + " -- " + hashHitCount + " hash set hits / " + size + " files");
+    }
+
+    ContextMenu getContextMenu() {
+        return contextMenu;
+    }
+
+    ReadOnlyObjectProperty<Grouping> grouping() {
+        return grouping.getReadOnlyProperty();
+    }
+
+    /** called automatically during constructor by FXMLConstructor.
+     *
+     * checks that FXML loading went ok and performs additional setup */
+    @FXML
+    void initialize() {
+        assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'.";
+        assert grpCatSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+        assert grpTagSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+        assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+        assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+        assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+        assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'.";
+
+        grouping.addListener((o) -> {
+            //when the assigned group changes, reset the scroll ar to the top
+            getScrollBar().ifPresent((scrollBar) -> {
+                scrollBar.setValue(0);
+            });
+            //and assign fileIDs to gridView
+            gridView.setItems(grouping.get().fileIds());
+        });
+
+        //configure flashing glow animation on next unseen group button
+        flashAnimation.setCycleCount(Timeline.INDEFINITE);
+        flashAnimation.setAutoReverse(true);
+
+        //configure gridView cell properties
+        gridView.cellHeightProperty().bind(EurekaToolbar.getDefault().sizeSliderValue().add(75));
+        gridView.cellWidthProperty().bind(EurekaToolbar.getDefault().sizeSliderValue().add(75));
+        gridView.setCellFactory((GridView<Long> param) -> new DrawableCell());
+
+        //configure toolbar properties
+        HBox.setHgrow(spacer, Priority.ALWAYS);
+        spacer.setMinWidth(Region.USE_PREF_SIZE);
+
+        ArrayList<MenuItem> grpTagMenues = new ArrayList<>();
+        for (final TagName tn : TagUtils.getNonCategoryTagNames()) {
+            MenuItem menuItem = createGrpTagMenuItem(tn);
+            grpTagMenues.add(menuItem);
+        }
+        try {
+            grpTagSplitMenu.setText(TagUtils.getFollowUpTagName().getDisplayName());
+            grpTagSplitMenu.setOnAction(createGrpTagMenuItem(TagUtils.getFollowUpTagName()).getOnAction());
+        } catch (TskCoreException tskCoreException) {
+            LOGGER.log(Level.WARNING, "failed to load FollowUpTagName", tskCoreException.getLocalizedMessage());
+            Exceptions.printStackTrace(tskCoreException);
+        }
+        grpTagSplitMenu.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
+        grpTagSplitMenu.getItems().setAll(grpTagMenues);
+
+        ArrayList<MenuItem> grpCategoryMenues = new ArrayList<>();
+        for (final Category cat : Category.values()) {
+            MenuItem menuItem = createGrpCatMenuItem(cat);
+            grpCategoryMenues.add(menuItem);
+        }
+        grpCatSplitMenu.setText(Category.FIVE.getDisplayName());
+        grpCatSplitMenu.setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon()));
+        grpCatSplitMenu.getItems().setAll(grpCategoryMenues);
+        grpCatSplitMenu.setOnAction(createGrpCatMenuItem(Category.FIVE).getOnAction());
+
+        Runnable syncMode = () -> {
+            switch (groupViewMode.get()) {
+                case SLIDE_SHOW:
+                    slideShowToggle.setSelected(true);
+                    break;
+                case TILE:
+                    tileToggle.setSelected(true);
+                    break;
+            }
+        };
+        syncMode.run();
+        //make togle states match view state
+        groupViewMode.addListener((o) -> {
+            syncMode.run();
+        });
+
+        slideShowToggle.toggleGroupProperty().addListener((o) -> {
+            slideShowToggle.getToggleGroup().selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> {
+                if (newToggle == null) {
+                    oldToggle.setSelected(true);
+                }
+            });
+        });
+
+        //listen to toggles and update view state
+        slideShowToggle.setOnAction((ActionEvent t) -> {
+            activateSlideShowViewer(globalSelectionModel.lastSelectedProperty().get());
+        });
+
+        tileToggle.setOnAction((ActionEvent t) -> {
+            activateTileViewer();
+        });
+
+        controller.viewState().addListener((ObservableValue<? extends GroupViewState> observable, GroupViewState oldValue, GroupViewState newValue) -> {
+            setViewState(newValue);
+        });
+
+        gridView.addEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler);
+        gridView.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
+
+            private ContextMenu buildContextMenu() {
+                ArrayList<MenuItem> menuItems = new ArrayList<>();
+
+                menuItems.add(CategorizeAction.getPopupMenu());
+
+                menuItems.add(AddDrawableTagAction.getInstance().getPopupMenu());
+
+                Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
+
+                for (ContextMenuActionsProvider provider : menuProviders) {
+
+                    for (final Action act : provider.getActions()) {
+
+                        if (act instanceof Presenter.Popup) {
+                            Presenter.Popup aact = (Presenter.Popup) act;
+
+                            menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter()));
+                        }
+                    }
+                }
+                final MenuItem extractMenuItem = new MenuItem("Extract File(s)");
+                extractMenuItem.setOnAction((ActionEvent t) -> {
+                    SwingUtilities.invokeLater(() -> {
+                        TopComponent etc = WindowManager.getDefault().findTopComponent(EurekaTopComponent.PREFERRED_ID);
+                        ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
+                    });
+                });
+                menuItems.add(extractMenuItem);
+
+                ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{}));
+                contextMenu.setAutoHide(true);
+                return contextMenu;
+            }
+
+            @Override
+            public void handle(MouseEvent t) {
+                switch (t.getButton()) {
+                    case PRIMARY:
+                        if (t.getClickCount() == 1) {
+                            globalSelectionModel.clearSelection();
+                            if (contextMenu != null) {
+                                contextMenu.hide();
+                            }
+                        }
+                        t.consume();
+                        break;
+                    case SECONDARY:
+                        if (t.getClickCount() == 1) {
+                            selectAllFiles();
+                        }
+                        if (contextMenu == null) {
+                            contextMenu = buildContextMenu();
+                        }
+
+                        contextMenu.hide();
+                        contextMenu.show(GroupPane.this, t.getScreenX(), t.getScreenY());
+                        t.consume();
+                        break;
+                }
+            }
+        });
+
+//        Platform.runLater(() -> {
+            ActionUtils.configureButton(nextGroupAction, nextButton);
+            final EventHandler<ActionEvent> onAction = nextButton.getOnAction();
+            nextButton.setOnAction((ActionEvent event) -> {
+                flashAnimation.stop();
+                nextButton.setEffect(null);
+                onAction.handle(event);
+            });
+
+            ActionUtils.configureButton(forwardAction, forwardButton);
+            ActionUtils.configureButton(backAction, backButton);
+//        });
+
+        nextGroupAction.disabledProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            nextButton.setEffect(newValue ? null : DROP_SHADOW);
+            if (newValue == false) {
+                flashAnimation.play();
+            } else {
+                flashAnimation.stop();
+            }
+        });
+
+        //listen to tile selection and make sure it is visible in scroll area
+        //TODO: make sure we are testing complete visability not just bounsd intersection
+        globalSelectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> {
+            if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW) {
+                slideShowPane.setFile(newFileId);
+            } else {
+
+                scrollToFileID(newFileId);
+            }
+        });
+
+        setViewState(controller.viewState().get());
+    }
+
+    @ThreadConfined(type = ThreadType.UI)
+    private void scrollToFileID(final Long newFileID) {
+        if (newFileID == null) {
+            //scrolling to no file doesn't make sense, so abort.
+            return;
+        }
+
+        int selectedIndex = grouping.get().fileIds().indexOf(newFileID);
+
+        if (selectedIndex == -1) {
+            //somehow we got passed a file id that isn't in the curent group.
+            //this should never happen, but if it does everything is going to fail, so abort.
+            return;
+        }
+
+        Optional<ScrollBar> scrollBarOptional = getScrollBar();
+        scrollBarOptional.ifPresent((ScrollBar scrollBar) -> {
+            DrawableCell cell = cellMap.get(newFileID);
+
+            //while there is no tile/cell for the given id, scroll based on index in group
+            while (cell == null) {
+                Integer minIndex = cellMap.keySet().stream()
+                        .map(grouping.get().fileIds()::indexOf)
+                        .min(Integer::compare).get();
+                Integer maxIndex = cellMap.keySet().stream()
+                        .map(grouping.get().fileIds()::indexOf)
+                        .max(Integer::compare).get();
+
+                if (selectedIndex < minIndex) {
+                    scrollBar.decrement();
+                } else if (selectedIndex > maxIndex) {
+                    scrollBar.increment();
+                } else {
+                    //sometimes the cellMap isn't up to date, so move the position arbitrarily to update the cellMap
+                    //TODO: this is clunky and slow, find a better way to do this
+                    scrollBar.adjustValue(.5);
+                }
+                cell = cellMap.get(newFileID);
+
+            }
+
+            final Bounds gridViewBounds = gridView.localToScene(gridView.getBoundsInLocal());
+
+            Bounds tileBounds = cell.localToScene(cell.getBoundsInLocal());
+
+            //while the cell is not within the visisble bounds of the gridview, scroll based on screen coordinates
+            int i = 0;
+
+            while (gridViewBounds.contains(tileBounds)
+                    == false && (i++ < 100)) {
+
+                if (tileBounds.getMinY() < gridViewBounds.getMinY()) {
+                    scrollBar.decrement();
+                } else if (tileBounds.getMaxY() > gridViewBounds.getMaxY()) {
+                    scrollBar.increment();
+                }
+                tileBounds = cell.localToScene(cell.getBoundsInLocal());
+            }
+        });
+    }
+
+    /**
+     * assigns a grouping for this pane to represent and initializes grouping
+     * specific properties and listeners
+     *
+     * @param grouping the new grouping assigned to this group
+     */
+    void setViewState(GroupViewState viewState) {
+        if (viewState == null) {
+            setCenter(null);
+            groupLabel.setText(null);
+        } else {
+            if (this.grouping.get() != viewState.getGroup()) {
+                this.grouping.set(viewState.getGroup());
+                //set the embeded header
+                resetHeaderString();
+                grouping.get().fileIds().addListener((Observable o) -> {
+                    resetHeaderString();
+                });
+
+            }
+
+            if (viewState.getMode() == GroupViewMode.TILE) {
+                activateTileViewer();
+            } else {
+                activateSlideShowViewer(viewState.getSlideShowfileID().orElse(null));
+
+            }
+        }
+    }
+
+    private class DrawableCell extends GridCell<Long> {
+
+        private final DrawableTile tile = new DrawableTile(GroupPane.this);
+
+        public DrawableCell() {
+            itemProperty().addListener((ObservableValue<? extends Long> observable, Long oldValue, Long newValue) -> {
+                if (oldValue != null) {
+                    cellMap.remove(oldValue, DrawableCell.this);
+                }
+                if (newValue != null) {
+                    cellMap.put(newValue, DrawableCell.this);
+                }
+            });
+
+            setGraphic(tile);
+        }
+
+        @Override
+        protected void updateItem(Long item, boolean empty) {
+            super.updateItem(item, empty);
+            tile.setFile(item);
+        }
+    }
+
+    /**
+     * implements the key handler for tile navigation ( up, down , left, right
+     * arrows)
+     */
+    private class KeyboardHandler implements EventHandler<KeyEvent> {
+
+        @Override
+        public void handle(KeyEvent t) {
+
+            if (t.getEventType() == KeyEvent.KEY_PRESSED) {
+                if (t.getCode() == KeyCode.SHIFT) {
+                    if (selectionAnchorIndex == null) {
+                        selectionAnchorIndex = grouping.get().fileIds().indexOf(globalSelectionModel.lastSelectedProperty().get());
+                    }
+                }
+                if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT).contains(t.getCode())) {
+                    handleArrows(t);
+                }
+                if (t.getCode() == KeyCode.PAGE_DOWN) {
+                    getScrollBar().ifPresent((scrollBar) -> {
+                        scrollBar.adjustValue(1);
+                    });
+                }
+
+                if (t.getCode() == KeyCode.PAGE_UP) {
+                    getScrollBar().ifPresent((scrollBar) -> {
+                        scrollBar.adjustValue(0);
+                    });
+                }
+                if (t.getCode() == KeyCode.ENTER) {
+                    nextGroupAction.handle(null);
+                }
+
+                if (t.isAltDown()) {
+                    switch (t.getCode()) {
+                        case NUMPAD0:
+                        case DIGIT0:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.FIVE.getTagName(), "");
+                            break;
+                        case NUMPAD1:
+                        case DIGIT1:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.ONE.getTagName(), "");
+                            break;
+                        case NUMPAD2:
+                        case DIGIT2:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.TWO.getTagName(), "");
+                            break;
+                        case NUMPAD3:
+                        case DIGIT3:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.THREE.getTagName(), "");
+                            break;
+                        case NUMPAD4:
+                        case DIGIT4:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.FOUR.getTagName(), "");
+                            break;
+                        case NUMPAD5:
+                        case DIGIT5:
+                            selectAllFiles();
+                            new CategorizeAction().addTag(Category.ZERO.getTagName(), "");
+                            break;
+                    }
+                } else {
+
+                    switch (t.getCode()) {
+                        case NUMPAD0:
+                        case DIGIT0:
+                            new CategorizeAction().addTag(Category.ZERO.getTagName(), "");
+                            break;
+                        case NUMPAD1:
+                        case DIGIT1:
+                            new CategorizeAction().addTag(Category.ONE.getTagName(), "");
+                            break;
+                        case NUMPAD2:
+                        case DIGIT2:
+                            new CategorizeAction().addTag(Category.TWO.getTagName(), "");
+                            break;
+                        case NUMPAD3:
+                        case DIGIT3:
+                            new CategorizeAction().addTag(Category.THREE.getTagName(), "");
+                            break;
+                        case NUMPAD4:
+                        case DIGIT4:
+                            new CategorizeAction().addTag(Category.FOUR.getTagName(), "");
+                            break;
+                        case NUMPAD5:
+                        case DIGIT5:
+                            new CategorizeAction().addTag(Category.FIVE.getTagName(), "");
+                            break;
+                    }
+                }
+            }
+
+            t.consume();
+        }
+
+        private Integer getPrefColumns() {
+            return Math.max((int) Math.floor((gridView.getWidth() - 18) / (gridView.getCellWidth() + gridView.getHorizontalCellSpacing() * 2)), 1);
+        }
+
+        private void handleArrows(KeyEvent t) {
+            Long lastSelectFileId = globalSelectionModel.lastSelectedProperty().get();
+
+            int lastSelectedIndex = lastSelectFileId != null
+                                    ? grouping.get().fileIds().indexOf(lastSelectFileId)
+                                    : Optional.ofNullable(selectionAnchorIndex).orElse(0);
+
+            final Integer columns = getPrefColumns();
+
+            final Map<KeyCode, Integer> tileIndexMap = ImmutableMap.of(UP, -columns, DOWN, columns, LEFT, -1, RIGHT, 1);
+
+            // implement proper keyboard based multiselect
+            int indexOfToBeSelectedTile = lastSelectedIndex + tileIndexMap.get(t.getCode());
+            final int size = grouping.get().fileIds().size();
+            if (0 > indexOfToBeSelectedTile) {
+                //don't select past begining of group
+            } else if (0 <= indexOfToBeSelectedTile && indexOfToBeSelectedTile < size) {
+                //normal selection within group
+                makeSelection(t.isShiftDown(), grouping.get().fileIds().get(indexOfToBeSelectedTile));
+            } else if (indexOfToBeSelectedTile <= size - 1 + columns - (size % columns)) {
+                //selection last item if selection is empty space at end of group
+                makeSelection(t.isShiftDown(), grouping.get().fileIds().get(size - 1));
+            } else {
+                //don't select past end of group
+            }
+        }
+    }
+
+    private Optional<ScrollBar> getScrollBar() {
+        if (gridView == null || gridView.getSkin() == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable((ScrollBar) gridView.getSkin().getNode().lookup(".scroll-bar"));
+    }
+
+    void makeSelection(Boolean shiftDown, Long newFileID) {
+
+        if (shiftDown) {
+            //TODO: do more hear to implement slicker multiselect
+            int endIndex = grouping.get().fileIds().indexOf(newFileID);
+            int startIndex = IntStream.of(grouping.get().fileIds().size(), selectionAnchorIndex, endIndex).min().getAsInt();
+            endIndex = IntStream.of(0, selectionAnchorIndex, endIndex).max().getAsInt();
+            List<Long> subList = grouping.get().fileIds().subList(startIndex, endIndex + 1);
+
+            globalSelectionModel.clearAndSelectAll(subList.toArray(new Long[subList.size()]));
+            globalSelectionModel.select(newFileID);
+        } else {
+            selectionAnchorIndex = null;
+            globalSelectionModel.clearAndSelect(newFileID);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java
new file mode 100644
index 0000000000000000000000000000000000000000..456d02970266e7ae269103059a046dfde1691ba4
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java
@@ -0,0 +1,8 @@
+
+package org.sleuthkit.autopsy.imageanalyzer.gui;
+
+
+
+public interface GroupView {
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..c872afc98ed1a3cbdb5588f773644cc3156698ca
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.util.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.layout.BorderPane" id="BorderPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <bottom>
+    <HBox fillHeight="true" prefHeight="-1.0" prefWidth="-1.0" style="-fx-background-color:&#10;        linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)),&#10;        linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%);&#10;">
+      <children>
+        <Button id="controlbutton" fx:id="controlButton" contentDisplay="TEXT_ONLY" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" text="&gt;" HBox.hgrow="NEVER" />
+        <Region prefHeight="16.0" prefWidth="16.0" HBox.hgrow="NEVER" />
+        <Label text="Time:" HBox.hgrow="NEVER" />
+        <Slider fx:id="timeSlider" prefHeight="21.0" prefWidth="315.0" HBox.hgrow="ALWAYS" />
+        <Label id="timeLable" fx:id="timeLabel" text="Label" HBox.hgrow="NEVER" />
+        <Region prefHeight="16.0" prefWidth="16.0" HBox.hgrow="NEVER" />
+        <Label text="Volume:" visible="false" HBox.hgrow="NEVER" />
+        <Slider fx:id="volumeSlider" prefHeight="21.0" prefWidth="100.0" visible="false" HBox.hgrow="SOMETIMES" />
+      </children>
+      <padding>
+        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+      </padding>
+    </HBox>
+  </bottom>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java
new file mode 100644
index 0000000000000000000000000000000000000000..a822acb00a2055b178f2c3462f623b8754648a75
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java
@@ -0,0 +1,300 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import java.net.URL;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.DoubleProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Slider;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Region;
+import javafx.scene.media.MediaPlayer;
+import javafx.scene.media.MediaPlayer.Status;
+import javafx.scene.media.MediaView;
+import javafx.util.Duration;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+public class MediaControl extends BorderPane implements Fitable {
+
+    private MediaPlayer mp;
+
+    private MediaView mediaView;
+
+    private final boolean repeat = false;
+
+    private boolean stopRequested = false;
+
+    private boolean atEndOfMedia = false;
+
+    private Duration duration;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private Button controlButton;
+
+    @FXML
+    private Label timeLabel;
+
+    @FXML
+    private Slider timeSlider;
+
+    @FXML
+    private Slider volumeSlider;
+
+    @FXML
+    void initialize() {
+        assert controlButton != null : "fx:id=\"controlButton\" was not injected: check your FXML file 'MediaControl.fxml'.";
+        assert timeLabel != null : "fx:id=\"timeLabel\" was not injected: check your FXML file 'MediaControl.fxml'.";
+        assert timeSlider != null : "fx:id=\"timeSlider\" was not injected: check your FXML file 'MediaControl.fxml'.";
+        assert volumeSlider != null : "fx:id=\"volumeSlider\" was not injected: check your FXML file 'MediaControl.fxml'.";
+
+        mediaView = new MediaView(mp);
+        mediaView.setPreserveRatio(true);
+        mediaView.fitHeightProperty().bind(this.heightProperty().subtract(50));
+        mediaView.fitWidthProperty().bind(this.widthProperty());
+        setCenter(mediaView);
+
+        controlButton.setOnAction(new EventHandler<ActionEvent>() {
+            public void handle(ActionEvent e) {
+                try {
+                    Status status = mp.getStatus();
+
+                    if (status == Status.UNKNOWN || status == Status.HALTED) {
+                        // don't do anything in these states
+                        return;
+                    }
+
+                    if (status == Status.PAUSED
+                            || status == Status.READY
+                            || status == Status.STOPPED) {
+                        // rewind the movie if we're sitting at the end
+                        if (atEndOfMedia) {
+                            mp.seek(mp.getStartTime());
+                            atEndOfMedia = false;
+                        }
+                        mp.play();
+                    } else {
+                        mp.pause();
+                    }
+                } catch (Exception ex) {
+                    Logger.getAnonymousLogger().log(Level.SEVERE, "message", ex);
+                }
+            }
+        });
+        mp.currentTimeProperty().addListener(new InvalidationListener() {
+            public void invalidated(Observable ov) {
+                updateValues();
+            }
+        });
+
+        mp.setOnPlaying(new Runnable() {
+            public void run() {
+                if (stopRequested) {
+                    mp.pause();
+                    stopRequested = false;
+                } else {
+                    controlButton.setText("||");
+                }
+            }
+        });
+
+        mp.setOnPaused(new Runnable() {
+            public void run() {
+                System.out.println("onPaused");
+                controlButton.setText(">");
+            }
+        });
+
+        mp.setOnReady(new Runnable() {
+            public void run() {
+                duration = mp.getMedia().getDuration();
+                updateValues();
+            }
+        });
+
+        mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1);
+        mp.setOnEndOfMedia(new Runnable() {
+            public void run() {
+                if (!repeat) {
+                    controlButton.setText(">");
+                    stopRequested = true;
+                    atEndOfMedia = true;
+                }
+            }
+        });
+
+        // Add time slider
+        timeSlider.setMinWidth(50);
+        timeSlider.setMaxWidth(Double.MAX_VALUE);
+        timeSlider.valueProperty().addListener(new InvalidationListener() {
+            public void invalidated(Observable ov) {
+                if (timeSlider.isValueChanging()) {
+                    // multiply duration by percentage calculated by slider position
+                    mp.seek(duration.multiply(timeSlider.getValue() / 100.0));
+                }
+            }
+        });
+
+        // Add Volume slider
+        volumeSlider.setPrefWidth(70);
+        volumeSlider.setMaxWidth(Region.USE_PREF_SIZE);
+        volumeSlider.setMinWidth(30);
+        volumeSlider.valueProperty().addListener(new InvalidationListener() {
+            public void invalidated(Observable ov) {
+                if (volumeSlider.isValueChanging()) {
+                    mp.setVolume(volumeSlider.getValue() / 100.0);
+                }
+            }
+        });
+
+    }
+
+    public MediaControl(final MediaPlayer mp) {
+        this.mp = mp;
+
+        FXMLConstructor.construct(this, "MediaControl.fxml");
+    }
+
+    protected void updateValues() {
+        if (timeLabel != null && timeSlider != null && volumeSlider != null) {
+            Platform.runLater(new Runnable() {
+                public void run() {
+                    Duration currentTime = mp.getCurrentTime();
+                    timeLabel.setText(formatTime(currentTime, duration));
+                    timeSlider.setDisable(duration.isUnknown());
+                    if (!timeSlider.isDisabled()
+                            && duration.greaterThan(Duration.ZERO)
+                            && !timeSlider.isValueChanging()) {
+                        timeSlider.setValue(currentTime.divide(duration).toMillis()
+                                * 100.0);
+                    }
+                    if (!volumeSlider.isValueChanging()) {
+                        volumeSlider.setValue((int) Math.round(mp.getVolume()
+                                * 100));
+                    }
+                }
+            });
+        }
+    }
+
+    private static String formatTime(Duration elapsed, Duration duration) {
+        int intElapsed = (int) Math.floor(elapsed.toSeconds());
+        int elapsedHours = intElapsed / (60 * 60);
+        if (elapsedHours > 0) {
+            intElapsed -= elapsedHours * 60 * 60;
+        }
+        int elapsedMinutes = intElapsed / 60;
+        int elapsedSeconds = intElapsed - elapsedHours * 60 * 60
+                - elapsedMinutes * 60;
+
+        if (duration.greaterThan(Duration.ZERO)) {
+            int intDuration = (int) Math.floor(duration.toSeconds());
+            int durationHours = intDuration / (60 * 60);
+            if (durationHours > 0) {
+                intDuration -= durationHours * 60 * 60;
+            }
+            int durationMinutes = intDuration / 60;
+            int durationSeconds = intDuration - durationHours * 60 * 60
+                    - durationMinutes * 60;
+            if (durationHours > 0) {
+                return String.format("%d:%02d:%02d/%d:%02d:%02d",
+                                     elapsedHours, elapsedMinutes, elapsedSeconds,
+                                     durationHours, durationMinutes, durationSeconds);
+            } else {
+                return String.format("%02d:%02d/%02d:%02d",
+                                     elapsedMinutes, elapsedSeconds, durationMinutes,
+                                     durationSeconds);
+            }
+        } else {
+            if (elapsedHours > 0) {
+                return String.format("%d:%02d:%02d", elapsedHours,
+                                     elapsedMinutes, elapsedSeconds);
+            } else {
+                return String.format("%02d:%02d", elapsedMinutes,
+                                     elapsedSeconds);
+            }
+        }
+    }
+
+    @Override
+    public final void setFitWidth(double d) {
+        mediaView.setFitWidth(d);
+    }
+
+    @Override
+    public final double getFitWidth() {
+        return mediaView.getFitWidth();
+    }
+
+    @Override
+    public final DoubleProperty fitWidthProperty() {
+        return mediaView.fitWidthProperty();
+    }
+
+    @Override
+    public final void setFitHeight(double d) {
+        mediaView.setFitHeight(d);
+    }
+
+    @Override
+    public final double getFitHeight() {
+        return mediaView.getFitHeight();
+    }
+
+    @Override
+    public final DoubleProperty fitHeightProperty() {
+        return mediaView.fitHeightProperty();
+    }
+
+    public void stopVideo() {
+        mp.stop();
+    }
+
+    @Override
+    public boolean isPreserveRatio() {
+        return mediaView.isPreserveRatio();
+    }
+
+    @Override
+    public void setPreserveRatio(boolean b) {
+        mediaView.setPreserveRatio(b);
+    }
+
+    @Override
+    public BooleanProperty preserveRatioProperty() {
+        return mediaView.preserveRatioProperty();
+    }
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..195600d74d320e49409dedf8e79a650b88be5a42
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root id="AnchorPane" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+  <children>
+    <TitledPane animated="false" collapsible="false" expanded="true" text="Details" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <content>
+        <AnchorPane id="Content">
+          <children>
+            <VBox alignment="TOP_CENTER" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+              <children>
+                <BorderPane id="imageAnchor" fx:id="imageBorder" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" VBox.vgrow="NEVER">
+                  <center>
+                    <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" />
+                  </center>
+                </BorderPane>
+                <TableView fx:id="tableView" editable="false" tableMenuButtonVisible="false" VBox.vgrow="ALWAYS">
+                  <columns>
+                    <TableColumn fx:id="attributeColumn" editable="false" prefWidth="100.0" text="Attribute" />
+                    <TableColumn fx:id="valueColumn" editable="false" prefWidth="100.0" text="Value" />
+                  </columns>
+                </TableView>
+              </children>
+            </VBox>
+          </children>
+        </AnchorPane>
+      </content>
+    </TitledPane>
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java
new file mode 100644
index 0000000000000000000000000000000000000000..e47468c42ef65dd3dc2dcd7677eb9d3ba9dc14c5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java
@@ -0,0 +1,260 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableCell;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Region;
+import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
+import javafx.scene.text.Text;
+import javafx.util.Callback;
+import javafx.util.Pair;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** */
+public class MetaDataPane extends AnchorPane implements Category.CategoryListener, TagUtils.TagListener, DrawableView {
+
+    private static final Logger LOGGER = Logger.getLogger(MetaDataPane.class.getName());
+
+    private final EurekaController controller;
+
+    private Long fileID;
+
+    @FXML
+    private ImageView imageView;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> attributeColumn;
+
+    @FXML
+    private TableView<Pair<DrawableAttribute, ? extends Object>> tableView;
+
+    @FXML
+    private TableColumn<Pair<DrawableAttribute, ? extends Object>, String> valueColumn;
+
+    @FXML
+    private BorderPane imageBorder;
+
+    private DrawableFile file;
+
+    @Override
+    public Long getFileID() {
+        return fileID;
+    }
+
+    @FXML
+    void initialize() {
+        assert attributeColumn != null : "fx:id=\"attributeColumn\" was not injected: check your FXML file 'MetaDataPane.fxml'.";
+        assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'MetaDataPane.fxml'.";
+        assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'MetaDataPane.fxml'.";
+        assert valueColumn != null : "fx:id=\"valueColumn\" was not injected: check your FXML file 'MetaDataPane.fxml'.";
+        TagUtils.registerListener(this);
+        Category.registerListener(this);
+
+        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        tableView.setPlaceholder(new Label("Select a file to show its details here."));
+
+        attributeColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DrawableAttribute, ?>, DrawableAttribute> param) -> new SimpleObjectProperty<>(param.getValue().getKey()));
+        attributeColumn.setCellFactory(new Callback<TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>, TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>>() {
+
+            @Override
+            public TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> call(TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> param) {
+                return new TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>() {
+
+                    @Override
+                    protected void updateItem(DrawableAttribute item, boolean empty) {
+                        super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates.
+                        if (item != null) {
+                            setText(item.getDisplayName());
+                            setGraphic(new ImageView(item.getIcon()));
+                        } else {
+                            setGraphic(null);
+                            setText(null);
+                        }
+                    }
+                };
+            }
+        });
+
+        attributeColumn.setPrefWidth(USE_COMPUTED_SIZE);
+
+        valueColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DrawableAttribute, ?>, String> p) -> {
+            if (p.getValue().getKey() == DrawableAttribute.TAGS) {
+                return new SimpleStringProperty(TagUtils.collectionToString((Collection<TagName>) p.getValue().getValue()));
+            } else {
+                return new SimpleStringProperty(p.getValue().getValue().toString());
+            }
+        });
+        valueColumn.setPrefWidth(USE_COMPUTED_SIZE);
+        valueColumn.setCellFactory(new Callback<TableColumn<Pair<DrawableAttribute, ? extends Object>, String>, TableCell<Pair<DrawableAttribute, ? extends Object>, String>>() {
+            @Override
+            public TableCell<Pair<DrawableAttribute, ? extends Object>, String> call(TableColumn<Pair<DrawableAttribute, ? extends Object>, String> p) {
+                return new TableCell<Pair<DrawableAttribute, ? extends Object>, String>() {
+
+                    @Override
+                    public void updateItem(String item, boolean empty) {
+                        super.updateItem(item, empty);
+
+                        if (!isEmpty()) {
+                            Text text = new Text(item);
+                            text.wrappingWidthProperty().bind(getTableColumn().widthProperty());
+                            setGraphic(text);
+                        } else {
+                            setGraphic(null);
+                        }
+                    }
+                };
+            }
+        });
+
+        tableView.getColumns().setAll(attributeColumn, valueColumn);
+
+        //listen for selection change
+        controller.getSelectionModel().lastSelectedProperty().addListener((observable, oldFileID, newFileID) -> {
+            setFile(newFileID);
+        });
+
+//        MetaDataPane.this.visibleProperty().bind(controller.getMetaDataCollapsed().not());
+//        MetaDataPane.this.managedProperty().bind(controller.getMetaDataCollapsed().not());
+    }
+
+    @Override
+    public DrawableFile getFile() {
+        if (fileID != null) {
+            if (file == null || file.getId() != fileID) {
+                try {
+                    file = controller.getFileFromId(fileID);
+                } catch (TskCoreException ex) {
+                    LOGGER.log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileID, ex);
+                    return null;
+                }
+            }
+        } else {
+            return null;
+        }
+        return file;
+    }
+
+    @Override
+    public void setFile(Long fileID) {
+        this.fileID = fileID;
+
+        if (fileID == null) {
+
+            Platform.runLater(() -> {
+                imageView.setImage(null);
+                tableView.getItems().clear();
+                getBorderable().setBorder(null);
+
+            });
+        } else {
+            try {
+                file = controller.getFileFromId(fileID);
+                updateUI();
+
+                file.categoryProperty().addListener(new ChangeListener<Category>() {
+                    @Override
+                    public void changed(ObservableValue<? extends Category> ov, Category t, final Category t1) {
+                        updateUI();
+                    }
+                });
+            } catch (TskCoreException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+
+    public MetaDataPane(EurekaController controller) {
+        this.controller = controller;
+
+        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MetaDataPane.fxml"));
+        fxmlLoader.setRoot(this);
+        fxmlLoader.setController(this);
+
+        try {
+            fxmlLoader.load();
+        } catch (IOException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    public void updateUI() {
+        final Image icon = getFile().getIcon();
+        final ObservableList attributesList = getFile().getAttributesList();
+
+        Platform.runLater(() -> {
+            imageView.setImage(icon);
+            tableView.getItems().setAll(attributesList);
+        });
+
+        updateCategoryBorder();
+    }
+
+    @Override
+    public Region getBorderable() {
+        return imageBorder;
+    }
+
+    @Override
+    public void handleCategoryChanged(Collection<Long> ids) {
+        if (getFile() != null && ids.contains(getFileID())) {
+            updateUI();
+        }
+    }
+
+    @Override
+    public void handleTagsChanged(Collection<Long> ids) {
+        if (getFile() != null && ids.contains(getFileID())) {
+            updateUI();
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..0f1e8beb113d9021b50a5d3a85c3146eb2ab39f0
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.effect.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.*?>
+
+<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" style="-fx-background-color: white;" type="GridPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+    <columnConstraints>
+        <ColumnConstraints halignment="CENTER" hgrow="NEVER" maxWidth="53.0" minWidth="10.0" prefWidth="53.0" />
+        <ColumnConstraints hgrow="NEVER" minWidth="10.0" />
+    </columnConstraints>
+    <rowConstraints>
+        <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
+    </rowConstraints>
+    <children>
+        <BorderPane fx:id="graphicBorder" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
+            <center>
+                <ImageView fitHeight="32.0" fitWidth="32.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER">
+                    <image>
+                        <Image url="@../images/information.png" />
+                    </image>
+                    <GridPane.margin>
+                        <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+                    </GridPane.margin>
+                </ImageView>
+            </center>
+        </BorderPane>
+        <Label fx:id="messageLabel" prefWidth="300.0" text="There are no events visible with the current zoom / filter settings.  Adjust the settings or:" wrapText="true" GridPane.columnIndex="1">
+            <GridPane.margin>
+                <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+            </GridPane.margin>
+            <font>
+                <Font size="14.0" />
+            </font>
+        </Label>
+    </children>
+    <effect>
+        <DropShadow />
+    </effect>
+<padding>
+<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+</padding>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..77bd00cc1f4ee940113203d9a7221a8b57ff1e5f
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java
@@ -0,0 +1,69 @@
+
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+
+/**
+ *
+ */
+public class NoGroupsDialog extends GridPane {
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private BorderPane graphicBorder;
+
+    @FXML
+    private Label messageLabel;
+
+    @FXML
+    void initialize() {
+        assert graphicBorder != null : "fx:id=\"graphicBorder\" was not injected: check your FXML file 'NoGroupsDialog.fxml'.";
+        assert messageLabel != null : "fx:id=\"messageLabel\" was not injected: check your FXML file 'NoGroupsDialog.fxml'.";
+
+    }
+
+    private NoGroupsDialog() {
+        FXMLConstructor.construct(this, "NoGroupsDialog.fxml");
+    }
+
+    public NoGroupsDialog(String message) {
+        this();
+        messageLabel.setText(message);
+    }
+
+    public NoGroupsDialog(String message, Node graphic) {
+        this();
+        messageLabel.setText(message);
+        graphicBorder.setCenter(graphic);
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..2dd100df7a58c401ce7be72b0b8631b4736f5005
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java
@@ -0,0 +1,399 @@
+
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javax.swing.Action;
+import javax.swing.SwingUtilities;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.actions.Presenter;
+import org.openide.windows.TopComponent;
+import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.FileNode;
+import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent;
+import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
+import org.sleuthkit.autopsy.directorytree.ExtractAction;
+import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaTopComponent;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType;
+import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction;
+import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction;
+import org.sleuthkit.autopsy.imageanalyzer.actions.SwingMenuItemAdapter;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** An abstract base class for {@link DrawableTile} and {@link SlideShowView},
+ * since they share a similar node tree and many behaviors, other implementers
+ * of {@link  DrawableView}s should implement the interface directly
+ * */
+public abstract class SingleDrawableViewBase extends AnchorPane implements DrawableView {
+
+    private static final Logger LOGGER = Logger.getLogger(SingleDrawableViewBase.class.getName());
+
+    private static final Border UNSELECTED_ORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
+
+    private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
+
+    //TODO: should this stuff be done in CSS? -jm
+    protected static final Image videoIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/video-file.png");
+
+    protected static final Image hashHitIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png");
+
+    protected static final Image followUpIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png");
+
+    protected static final Image followUpGray = new Image("org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png");
+
+    protected static final FileIDSelectionModel globalSelectionModel = FileIDSelectionModel.getInstance();
+
+    /**
+     * displays the icon representing video files
+     */
+    @FXML
+    protected ImageView fileTypeImageView;
+
+    /**
+     * displays the icon representing hash hits
+     */
+    @FXML
+    protected ImageView hashHitImageView;
+
+    /**
+     * displays the icon representing follow up tag
+     */
+    @FXML
+    protected ImageView followUpImageView;
+
+    @FXML
+    protected ToggleButton followUpToggle;
+
+    /**
+     * the label that shows the name of the represented file
+     */
+    @FXML
+    protected Label nameLabel;
+
+    @FXML
+    protected BorderPane imageBorder;
+
+    static private ContextMenu contextMenu;
+
+    protected DrawableFile file;
+
+    protected Long fileID;
+
+    /** the groupPane this {@link SingleDrawableViewBase} is embedded in */
+    protected GroupPane groupPane;
+
+    protected SingleDrawableViewBase() {
+
+        globalSelectionModel.getSelected().addListener((Observable observable) -> {
+            updateSelectionState();
+        });
+
+        //set up mouse listener
+        //TODO: split this between DrawableTile and SingleDrawableViewBase
+        addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
+
+            @Override
+            public void handle(MouseEvent t) {
+
+                switch (t.getButton()) {
+                    case PRIMARY:
+                        if (t.getClickCount() == 1) {
+                            if (t.isControlDown()) {
+                                globalSelectionModel.toggleSelection(fileID);
+                            } else {
+                                groupPane.makeSelection(t.isShiftDown(), fileID);
+                            }
+                        } else if (t.getClickCount() > 1) {
+                            groupPane.activateSlideShowViewer(fileID);
+                        }
+                        break;
+                    case SECONDARY:
+
+                        if (t.getClickCount() == 1) {
+                            if (globalSelectionModel.isSelected(fileID) == false) {
+                                groupPane.makeSelection(false, fileID);
+                            }
+                        }
+
+                        if (contextMenu != null) {
+                            contextMenu.hide();
+                        }
+                        final ContextMenu groupContextMenu = groupPane.getContextMenu();
+                        if (groupContextMenu != null) {
+                            groupContextMenu.hide();
+                        }
+                        contextMenu = buildContextMenu();
+                        contextMenu.show(SingleDrawableViewBase.this, t.getScreenX(), t.getScreenY());
+
+                        break;
+                }
+                t.consume();
+            }
+
+            private ContextMenu buildContextMenu() {
+                final ArrayList<MenuItem> menuItems = new ArrayList<>();
+
+                menuItems.add(CategorizeAction.getPopupMenu());
+
+                menuItems.add(AddDrawableTagAction.getInstance().getPopupMenu());
+
+                final MenuItem extractMenuItem = new MenuItem("Extract File(s)");
+                extractMenuItem.setOnAction((ActionEvent t) -> {
+                    SwingUtilities.invokeLater(() -> {
+                        TopComponent etc = WindowManager.getDefault().findTopComponent(EurekaTopComponent.PREFERRED_ID);
+                        ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
+                    });
+                });
+                menuItems.add(extractMenuItem);
+
+                MenuItem contentViewer = new MenuItem("Show Content Viewer");
+                contentViewer.setOnAction((ActionEvent t) -> {
+                    SwingUtilities.invokeLater(() -> {
+                        new NewWindowViewAction("Show Content Viewer", new FileNode(getFile().getAbstractFile())).actionPerformed(null);
+                    });
+                });
+                menuItems.add(contentViewer);
+
+                MenuItem externalViewer = new MenuItem("Open in External Viewer");
+                final ExternalViewerAction externalViewerAction = new ExternalViewerAction("Open in External Viewer", new FileNode(getFile().getAbstractFile()));
+
+                externalViewer.setDisable(externalViewerAction.isEnabled() == false);
+                externalViewer.setOnAction((ActionEvent t) -> {
+                    SwingUtilities.invokeLater(() -> {
+                        externalViewerAction.actionPerformed(null);
+                    });
+                });
+                menuItems.add(externalViewer);
+
+                Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
+
+                for (ContextMenuActionsProvider provider : menuProviders) {
+                    for (final Action act : provider.getActions()) {
+                        if (act instanceof Presenter.Popup) {
+                            Presenter.Popup aact = (Presenter.Popup) act;
+                            menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter()));
+                        }
+                    }
+                }
+
+                ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{}));
+                contextMenu.setAutoHide(true);
+                return contextMenu;
+            }
+        });
+    }
+
+    @ThreadConfined(type = ThreadType.UI)
+    protected abstract void clearContent();
+
+    protected abstract void disposeContent();
+
+    protected abstract Runnable getContentUpdateRunnable();
+
+    protected abstract String getLabelText();
+
+    protected void initialize() {
+        followUpToggle.setOnAction((ActionEvent t) -> {
+            if (followUpToggle.isSelected() == true) {
+                globalSelectionModel.clearAndSelect(fileID);
+                try {
+                    AddDrawableTagAction.getInstance().addTag(TagUtils.getFollowUpTagName(), "");
+                } catch (TskCoreException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            } else {
+
+                //TODO: convert this to an action!
+                List<ContentTag> contentTagsByContent;
+                try {
+                    contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(getFile());
+                    for (ContentTag ct : contentTagsByContent) {
+                        if (ct.getName().getDisplayName().equals(TagUtils.getFollowUpTagName().getDisplayName())) {
+                            Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct);
+                            SwingUtilities.invokeLater(() -> DirectoryTreeTopComponent.findInstance().refreshContentTreeSafe());
+                        }
+                    }
+                    EurekaController.getDefault().handleFileUpdate(new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.TAGS));
+                } catch (TskCoreException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            }
+        });
+    }
+
+    @Override
+    public DrawableFile getFile() {
+        if (fileID != null) {
+            if (file == null || file.getId() != fileID) {
+                try {
+                    file = EurekaController.getDefault().getFileFromId(fileID);
+                } catch (TskCoreException ex) {
+                    LOGGER.log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileID, ex);
+                    return null;
+                }
+            }
+        } else {
+            return null;
+        }
+        return file;
+    }
+
+    protected boolean hasFollowUp() throws TskCoreException {
+        String followUpTagName = TagUtils.getFollowUpTagName().getDisplayName();
+        Collection<TagName> tagNames = (Collection<TagName>) getFile().getValueOfAttribute(DrawableAttribute.TAGS);
+        return tagNames.stream().anyMatch((tn) -> tn.getDisplayName().equals(followUpTagName));
+    }
+
+    protected void updateUI(final boolean isVideo, final boolean hasHashSetHits, final String text) {
+        if (isVideo) {
+            fileTypeImageView.setImage(videoIcon);
+        } else {
+            fileTypeImageView.setImage(null);
+        }
+
+        if (hasHashSetHits) {
+            hashHitImageView.setImage(hashHitIcon);
+        } else {
+            hashHitImageView.setImage(null);
+        }
+
+        nameLabel.setText(text);
+        nameLabel.setTooltip(new Tooltip(text));
+    }
+
+    @Override
+    public Long getFileID() {
+        return fileID;
+    }
+
+    @Override
+    public void handleTagsChanged(Collection<Long> ids) {
+        if (fileID != null && ids.contains(fileID)) {
+            updateFollowUpIcon();
+        }
+    }
+
+    protected void updateFollowUpIcon() {
+        if (file != null) {
+            try {
+                boolean hasFollowUp = hasFollowUp();
+                Platform.runLater(() -> {
+                    followUpImageView.setImage(hasFollowUp ? followUpIcon : followUpGray);
+                    followUpToggle.setSelected(hasFollowUp);
+                });
+            } catch (TskCoreException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+
+    @Override
+    public void setFile(final Long fileID) {
+        if (Objects.equals(fileID, this.fileID) == false) {
+            this.fileID = fileID;
+            disposeContent();
+            if (this.fileID == null || Case.isCaseOpen() == false) {
+                Category.unregisterListener(this);
+                TagUtils.unregisterListener(this);
+                file = null;
+                Platform.runLater(() -> {
+                    clearContent();
+                });
+            } else {
+                Category.registerListener(this);
+                TagUtils.registerListener(this);
+
+                getFile();
+                updateSelectionState();
+                updateCategoryBorder();
+                updateFollowUpIcon();
+                final String text = getLabelText();
+                final boolean isVideo = file.isVideo();
+                final boolean hasHashSetHits = hasHashHit();
+                Platform.runLater(() -> {
+                    updateUI(isVideo, hasHashSetHits, text);
+                });
+
+                Platform.runLater(getContentUpdateRunnable());
+            }
+        }
+    }
+
+    private void updateSelectionState() {
+        final boolean selected = globalSelectionModel.isSelected(fileID);
+        Platform.runLater(() -> {
+            SingleDrawableViewBase.this.setBorder(selected ? SELECTED_BORDER : UNSELECTED_ORDER);
+        });
+    }
+
+    @Override
+    public Region getBorderable() {
+        return imageBorder;
+    }
+
+    @Override
+    public void handleCategoryChanged(Collection<Long> ids) {
+        if (ids.contains(fileID)) {
+            updateCategoryBorder();
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..2f2c783472d6ef125b8222b7f1d67220772372fd
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.net.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+<?import org.controlsfx.control.*?>
+<?scenebuilder-stylesheet GroupPane.css?>
+
+<fx:root type="AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+    <children>
+        <HBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+            <children>
+                <HBox maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER">
+                    <children>
+                        <Button fx:id="leftButton" contentDisplay="GRAPHIC_ONLY" maxHeight="1.7976931348623157E308" mnemonicParsing="false" text="" HBox.hgrow="NEVER">
+                            <graphic>
+                                <ImageView fitHeight="128.0" fitWidth="32.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="false" rotate="180.0" scaleX="1.0" translateX="0.0">
+                                    <image>
+                                        <Image url="@../images/right_arrow_128.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                    </children>
+                </HBox>
+                <BorderPane fx:id="internalBorderPane" maxWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER" HBox.hgrow="ALWAYS">
+                    <top>
+                        <ToolBar fx:id="toolBar" maxHeight="-Infinity">
+                            <items>
+                                <Region fx:id="spring" prefHeight="-1.0" prefWidth="-1.0" />
+                                <Label text="Apply to File:" />
+                                <SplitMenuButton id="tagSplitMenu" fx:id="tagSplitButton" disable="false" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS">
+                                    <items>
+                                        <MenuItem mnemonicParsing="false" text="Action 1" />
+                                        <MenuItem mnemonicParsing="false" text="Action 2" />
+                                    </items>
+                                </SplitMenuButton>
+                                <Region prefHeight="-1.0" prefWidth="10.0" />
+                                <Label text="Category:">
+                                    <graphic>
+                                        <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+                                            <image>
+                                                <Image url="@../images/category-icon.png" />
+                                            </image>
+                                        </ImageView>
+                                    </graphic>
+                                </Label>
+                                <HBox id="HBox" alignment="CENTER" spacing="0.0" styleClass="segmented-button-bar">
+                                    <children>
+                                       
+                                        <Region prefHeight="-1.0" prefWidth="5.0" />
+                                        <org.controlsfx.control.SegmentedButton>
+                                            <buttons>
+                                                <ToggleButton fx:id="cat0Toggle" mnemonicParsing="false" text="0" HBox.hgrow="ALWAYS" />
+                                                <ToggleButton fx:id="cat1Toggle" mnemonicParsing="false" style="" styleClass="button" text="1" HBox.hgrow="ALWAYS" />
+                                                <ToggleButton id="Cat2Toggle" fx:id="cat2Toggle" mnemonicParsing="false" styleClass="button" text="2" HBox.hgrow="ALWAYS" />
+                                                <ToggleButton fx:id="cat3Toggle" mnemonicParsing="false" styleClass="button" text="3" HBox.hgrow="ALWAYS" />
+                                                <ToggleButton fx:id="cat4Toggle" mnemonicParsing="false" styleClass="button" text="4" HBox.hgrow="ALWAYS" />
+                                                <ToggleButton fx:id="cat5Toggle" mnemonicParsing="false" text="5" toggleGroup="$cat" HBox.hgrow="ALWAYS" />                                          
+                                            </buttons>
+                                        
+                                        </org.controlsfx.control.SegmentedButton>
+                                        <Region prefHeight="-1.0" prefWidth="5.0" />
+                                      
+                                    </children>
+                                    <stylesheets>
+                                        <URL value="@GroupPane.css" />
+                                    </stylesheets>
+                                </HBox>
+                            </items>
+                        </ToolBar>
+                    </top>
+                    <center>
+                        <AnchorPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" opacity="1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-border-width: 1; -fx-border-color: darkgray; -fx-border-radius: 2; -fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)),        linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%); -fx-background-radius: 2;" BorderPane.alignment="CENTER">
+                            <children>
+                                <BorderPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" snapToPixel="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+                                    <bottom>
+                                        <BorderPane fx:id="footer" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER">
+                                            <center>
+                                                <Label id="pathLabel" fx:id="nameLabel" alignment="CENTER" contentDisplay="TEXT_ONLY" maxHeight="16.0" minHeight="16.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" text="file name" textAlignment="CENTER" wrapText="true">
+                                                    <labelFor>
+                                                        <ImageView fitHeight="200.0" fitWidth="200.0" opacity="1.0" pickOnBounds="true" preserveRatio="true" style="-fx-border-radius : 5;&#10;-fx-border-width : 5;&#10;-fx-border-color : blue;" BorderPane.alignment="CENTER" />
+                                                    </labelFor>
+                                                </Label>
+                                            </center>
+                                            <left>
+                                                <HBox maxHeight="-Infinity" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0" BorderPane.alignment="CENTER_LEFT">
+                                                    <children>
+                                                        <ImageView fx:id="fileTypeImageView" fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" scaleX="1.0" scaleY="1.0">
+                                                            <image>
+                                                                <Image url="@../images/video-file.png" />
+                                                            </image>
+                                                        </ImageView>
+                                                        <ImageView fx:id="hashHitImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" style="">
+                                                            <image>
+                                                                <Image url="@../images/hashset_hits.png" />
+                                                            </image>
+                                                            <HBox.margin>
+                                                                <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" />
+                                                            </HBox.margin>
+                                                        </ImageView>
+                                                    </children>
+                                                    <padding>
+                                                        <Insets bottom="2.0" right="2.0" top="2.0" />
+                                                    </padding>
+                                                </HBox>
+                                            </left>
+                                            <right>
+                                                <ToggleButton fx:id="followUpToggle" minWidth="24.0" mnemonicParsing="false" prefWidth="24.0" selected="false" text="">
+                                                    <graphic>
+                                                        <ImageView id="followUpImageview" fx:id="followUpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+                                                            <image>
+                                                                <Image url="@../images/flag_gray.png" />
+                                                            </image>
+                                                        </ImageView>
+                                                    </graphic>
+                                                </ToggleButton>
+                                            </right>
+                                        </BorderPane>
+                                    </bottom>
+                                    <center>
+                                        <BorderPane fx:id="imageBorder" center="$imageView" maxHeight="-1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-border-color: lightgray;&#10;-fx-border-width:10;&#10;-fx-border-radius:2;" BorderPane.alignment="CENTER" />
+                                    </center>
+                                </BorderPane>
+                            </children>
+                            <padding>
+                                <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
+                            </padding>
+                        </AnchorPane>
+                    </center>
+                </BorderPane>
+                <HBox maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER">
+                    <children>
+                        <Button fx:id="rightButton" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="1.7976931348623157E308" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" text="" HBox.hgrow="NEVER">
+                            <graphic>
+                                <ImageView fitHeight="128.0" fitWidth="32.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="false" scaleX="1.0" smooth="true" translateX="0.0">
+                                    <image>
+                                        <Image url="@../images/right_arrow_128.png" />
+                                    </image>
+                                </ImageView>
+                            </graphic>
+                        </Button>
+                    </children>
+                </HBox>
+            </children>
+        </HBox>
+    </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java
new file mode 100644
index 0000000000000000000000000000000000000000..635737a0d8e0b0a5fb652d36c141666100c9a25c
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java
@@ -0,0 +1,363 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.util.ArrayList;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.geometry.HorizontalDirection;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.ImageView;
+import static javafx.scene.input.KeyCode.LEFT;
+import static javafx.scene.input.KeyCode.RIGHT;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.BorderStroke;
+import javafx.scene.layout.BorderStrokeStyle;
+import javafx.scene.layout.BorderWidths;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel;
+import org.sleuthkit.autopsy.imageanalyzer.TagUtils;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType;
+import org.sleuthkit.autopsy.imageanalyzer.ThreadUtils;
+import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Displays the files of a group one at a time. Designed to be embedded in a
+ * GroupPane.
+ * TODO: Extract a subclass for video files in slideshow mode-jm
+ * TODO: reduce coupling to GroupPane */
+public class SlideShowView extends SingleDrawableViewBase implements TagUtils.TagListener, Category.CategoryListener {
+
+    private static final Logger LOGGER = Logger.getLogger(SlideShowView.class.getName());
+
+    @FXML
+    private ToggleButton cat0Toggle;
+
+    @FXML
+    private ToggleButton cat2Toggle;
+
+    @FXML
+    private SplitMenuButton tagSplitButton;
+
+    @FXML
+    private ToggleButton cat3Toggle;
+
+    @FXML
+    private Region spring;
+
+    @FXML
+    private Button leftButton;
+
+    @FXML
+    private ToggleButton cat4Toggle;
+
+    @FXML
+    private ToggleButton cat5Toggle;
+
+    @FXML
+    private ToggleButton cat1Toggle;
+
+    @FXML
+    private Button rightButton;
+
+    @FXML
+    private ToolBar toolBar;
+
+    @FXML
+    private BorderPane footer;
+
+    @FXML
+    @Override
+    protected void initialize() {
+        super.initialize();
+        assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert leftButton != null : "fx:id=\"leftButton\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert rightButton != null : "fx:id=\"rightButton\" was not injected: check your FXML file 'SlideShow.fxml'.";
+        assert tagSplitButton != null : "fx:id=\"tagSplitButton\" was not injected: check your FXML file 'SlideShow.fxml'.";
+
+        Platform.runLater(() -> {
+            HBox.setHgrow(spring, Priority.ALWAYS);
+            spring.setMinWidth(Region.USE_PREF_SIZE);
+        });
+
+        tagSplitButton.setOnAction((ActionEvent t) -> {
+            try {
+                TagUtils.createSelTagMenuItem(TagUtils.getFollowUpTagName(), tagSplitButton).getOnAction().handle(t);
+            } catch (TskCoreException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        });
+
+        tagSplitButton.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
+        tagSplitButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
+            if (t1) {
+                ArrayList<MenuItem> selTagMenues = new ArrayList<>();
+                for (final TagName tn : TagUtils.getNonCategoryTagNames()) {
+                    MenuItem menuItem = TagUtils.createSelTagMenuItem(tn, tagSplitButton);
+                    selTagMenues.add(menuItem);
+                }
+                tagSplitButton.getItems().setAll(selTagMenues);
+            }
+        });
+        cat0Toggle.setBorder(new Border(new BorderStroke(Category.ZERO.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+        cat1Toggle.setBorder(new Border(new BorderStroke(Category.ONE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+        cat2Toggle.setBorder(new Border(new BorderStroke(Category.TWO.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+        cat3Toggle.setBorder(new Border(new BorderStroke(Category.THREE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+        cat4Toggle.setBorder(new Border(new BorderStroke(Category.FOUR.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+        cat5Toggle.setBorder(new Border(new BorderStroke(Category.FIVE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1))));
+
+        cat0Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.ZERO));
+        cat1Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.ONE));
+        cat2Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.TWO));
+        cat3Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.THREE));
+        cat4Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.FOUR));
+        cat5Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.FIVE));
+
+        cat0Toggle.toggleGroupProperty().addListener((o, oldGroup, newGroup) -> {
+            newGroup.selectedToggleProperty().addListener((ov, oldToggle, newToggle) -> {
+                if (newToggle == null) {
+                    oldToggle.setSelected(true);
+                }
+            });
+        });
+
+        leftButton.setOnAction((ActionEvent t) -> {
+            cycleSlideShowImage(HorizontalDirection.LEFT);
+        });
+        rightButton.setOnAction((ActionEvent t) -> {
+            cycleSlideShowImage(HorizontalDirection.RIGHT);
+        });
+
+        //set up key listener equivalents of buttons
+        addEventFilter(KeyEvent.KEY_PRESSED, (KeyEvent t) -> {
+
+            if (t.getEventType() == KeyEvent.KEY_PRESSED) {
+                switch (t.getCode()) {
+                    case LEFT:
+                        cycleSlideShowImage(HorizontalDirection.LEFT);
+                        break;
+                    case RIGHT:
+                        cycleSlideShowImage(HorizontalDirection.RIGHT);
+                        break;
+
+                    //TODO: find a way to share these with grouppane/tileview... (ActionMap?)
+                    case NUMPAD0:
+                    case DIGIT0:
+                        new CategorizeAction().addTag(Category.ZERO.getTagName(), "");
+                        break;
+                    case NUMPAD1:
+                    case DIGIT1:
+                        new CategorizeAction().addTag(Category.ONE.getTagName(), "");
+                        break;
+                    case NUMPAD2:
+                    case DIGIT2:
+                        new CategorizeAction().addTag(Category.TWO.getTagName(), "");
+                        break;
+                    case NUMPAD3:
+                    case DIGIT3:
+                        new CategorizeAction().addTag(Category.THREE.getTagName(), "");
+                        break;
+                    case NUMPAD4:
+                    case DIGIT4:
+                        new CategorizeAction().addTag(Category.FOUR.getTagName(), "");
+                        break;
+                    case NUMPAD5:
+                    case DIGIT5:
+                        new CategorizeAction().addTag(Category.FIVE.getTagName(), "");
+                        break;
+                }
+                t.consume();
+            }
+        });
+
+        syncButtonVisibility();
+
+        groupPane.grouping()
+                .addListener((Observable observable) -> {
+                    syncButtonVisibility();
+                    groupPane.getGrouping().fileIds().addListener((Observable observable1) -> {
+                        syncButtonVisibility();
+                    });
+                }
+                );
+    }
+
+    @ThreadConfined(type = ThreadType.ANY)
+    private void syncButtonVisibility() {
+        final boolean hasMultipleFiles = groupPane.getGrouping().fileIds().size() > 1;
+        Platform.runLater(() -> {
+            rightButton.setVisible(hasMultipleFiles);
+            leftButton.setVisible(hasMultipleFiles);
+            rightButton.setManaged(hasMultipleFiles);
+            leftButton.setManaged(hasMultipleFiles);
+        });
+    }
+
+    SlideShowView(GroupPane gp) {
+        super();
+        groupPane = gp;
+        FXMLConstructor.construct(this, "SlideShow.fxml");
+
+    }
+
+    @ThreadConfined(type = ThreadType.UI)
+    public void stopVideo() {
+        if (imageBorder.getCenter() instanceof MediaControl) {
+            ((MediaControl) imageBorder.getCenter()).stopVideo();
+        }
+    }
+
+    @Override
+    public void setFile(final Long fileID) {
+        super.setFile(fileID);
+        if (this.fileID != null) {
+            groupPane.makeSelection(false, this.fileID);
+        }
+    }
+
+    @Override
+    protected void disposeContent() {
+        stopVideo();
+    }
+
+    @Override
+    @ThreadConfined(type = ThreadType.UI)
+    protected void clearContent() {
+        imageBorder.setCenter(null);
+    }
+
+    @Override
+    protected Runnable getContentUpdateRunnable() {
+        Node newCenterNode = file.getFullsizeDisplayNode();
+        if (newCenterNode instanceof Fitable) {
+            Fitable fitable = (Fitable) newCenterNode;
+            fitable.setPreserveRatio(true);
+            //JMTODO: this math is hack! fix it -jm
+            fitable.fitWidthProperty().bind(imageBorder.widthProperty().subtract(CAT_BORDER_WIDTH * 2));
+            fitable.fitHeightProperty().bind(this.heightProperty().subtract(CAT_BORDER_WIDTH * 4).subtract(footer.heightProperty()).subtract(toolBar.heightProperty()));
+        }
+        return () -> {
+            imageBorder.setCenter(newCenterNode);
+        };
+    }
+
+    @Override
+    protected String getLabelText() {
+        return file.getName() + " " + getSupplementalText();
+    }
+
+    private void cycleSlideShowImage(HorizontalDirection d) {
+
+        stopVideo();
+        if (fileID != null) {
+            int index = groupPane.getGrouping().fileIds().indexOf(fileID);
+            final int size = groupPane.getGrouping().fileIds().size();
+            index = (index + ((d == HorizontalDirection.LEFT) ? -1 : 1)) % size;
+            if (index < 0) {
+                index += size;
+            }
+            setFile(groupPane.getGrouping().fileIds().get(index));
+
+        } else {
+            setFile(groupPane.getGrouping().fileIds().get(0));
+        }
+
+    }
+
+    /** @return supplemental text to include in the label, specifically:
+     *          "image x of y" */
+    private String getSupplementalText() {
+        return " ( " + (groupPane.getGrouping().fileIds().indexOf(fileID) + 1) + " of " + groupPane.getGrouping().fileIds().size() + " in group )";
+    }
+
+    @Override
+    @ThreadConfined(type = ThreadType.ANY)
+    public Category updateCategoryBorder() {
+        final Category category = super.updateCategoryBorder();
+        ToggleButton toggleForCategory = getToggleForCategory(category);
+
+        ThreadUtils.runNowOrLater(() -> {
+            toggleForCategory.setSelected(true);
+        });
+        return category;
+    }
+
+    private ToggleButton getToggleForCategory(Category category) {
+        switch (category) {
+            case ZERO:
+                return cat0Toggle;
+            case ONE:
+                return cat1Toggle;
+            case TWO:
+                return cat2Toggle;
+            case THREE:
+                return cat3Toggle;
+            case FOUR:
+                return cat4Toggle;
+            case FIVE:
+                return cat5Toggle;
+            default:
+                throw new IllegalArgumentException(category.name());
+        }
+    }
+
+    private class CategorizeToggleHandler implements ChangeListener<Boolean> {
+
+        private final Category cat;
+
+        public CategorizeToggleHandler(Category cat) {
+            this.cat = cat;
+        }
+
+        @Override
+        public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
+
+            if (t1) {
+                FileIDSelectionModel.getInstance().clearAndSelect(fileID);
+                new CategorizeAction().addTag(cat.getTagName(), "");
+            }
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java
new file mode 100644
index 0000000000000000000000000000000000000000..5913fbae22b1f798781eb8aabc49e519c27d204d
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java
@@ -0,0 +1,38 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import javafx.scene.control.ListCell;
+import javafx.scene.image.ImageView;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy;
+
+public class SortByListCell extends ListCell<GroupSortBy> {
+    
+    @Override
+    protected void updateItem(GroupSortBy t, boolean bln) {
+        super.updateItem(t, bln);
+        if (t != null) {
+            setText(t.getDisplayName());
+            setGraphic(new ImageView(t.getIcon()));
+        } else {
+            setText(null);
+            setGraphic(null);
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..5f8a0615852e8f21b95d76129047143776a3b5c5
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.geometry.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+<fx:root id="AnchorPane" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+  <children>
+    <BorderPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <right>
+        <HBox alignment="CENTER_RIGHT" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER_RIGHT">
+          <children>
+            <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
+              <children>
+                <ProgressBar id="progBar" fx:id="fileTaskProgresBar" focusTraversable="false" maxHeight="-1.0" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-1.0" prefHeight="24.0" prefWidth="-1.0" progress="0.0" visible="true" />
+                <Label id="fileUpdateLabel" fx:id="fileUpdateTaskLabel" alignment="CENTER" contentDisplay="CENTER" graphicTextGap="0.0" labelFor="$fileTaskProgresBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="-1.0" text="0 File Update Tasks" StackPane.alignment="CENTER" />
+              </children>
+            </StackPane>
+            <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER">
+              <children>
+                <ProgressBar fx:id="bgTaskProgressBar" maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="24.0" prefWidth="-1.0" progress="0.0" StackPane.alignment="CENTER" />
+                <Label fx:id="bgTaskLabel" alignment="CENTER" cache="false" contentDisplay="CENTER" disable="false" focusTraversable="false" labelFor="$uiTaskProgressBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" text="0 BG Tasks" StackPane.alignment="CENTER" />
+              </children>
+<HBox.margin>
+<Insets right="5.0" />
+</HBox.margin>
+            </StackPane>
+          </children>
+<BorderPane.margin>
+<Insets left="10.0" />
+</BorderPane.margin>
+        </HBox>
+      </right>
+<center>
+<HBox>
+<children>
+        <Label fx:id="statusLabel" maxWidth="-Infinity" minWidth="-Infinity" wrapText="true" BorderPane.alignment="CENTER" HBox.hgrow="ALWAYS">
+<BorderPane.margin>
+<Insets left="10.0" right="10.0" />
+</BorderPane.margin>
+<HBox.margin>
+<Insets left="10.0" right="10.0" />
+</HBox.margin></Label>
+</children>
+</HBox>
+</center>
+<left><Label fx:id="staleLabel" text="Some data may be out of date.  Enable listening to ingest to update." BorderPane.alignment="CENTER">
+<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
+<image>
+<Image url="@../images/information.png" />
+</image></ImageView>
+</graphic>
+<BorderPane.margin>
+<Insets bottom="5.0" left="5.0" right="10.0" top="5.0" />
+</BorderPane.margin></Label>
+</left>
+    </BorderPane>
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca69651e7fa7d533013f44549d7996088d18d02f
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java
@@ -0,0 +1,115 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.layout.AnchorPane;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+
+/**
+ *
+ */
+public class StatusBar extends AnchorPane {
+
+    private final EurekaController controller;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private ProgressBar fileTaskProgresBar;
+
+    @FXML
+    private Label fileUpdateTaskLabel;
+
+    @FXML
+    private Label statusLabel;
+
+    @FXML
+    private Label bgTaskLabel;
+
+    @FXML
+    private Label staleLabel;
+
+    @FXML
+    private ProgressBar bgTaskProgressBar;
+
+    @FXML
+    void initialize() {
+        assert fileTaskProgresBar != null : "fx:id=\"fileTaskProgresBar\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert fileUpdateTaskLabel != null : "fx:id=\"fileUpdateTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert statusLabel != null : "fx:id=\"statusLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert bgTaskLabel != null : "fx:id=\"uiTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'.";
+        assert bgTaskProgressBar != null : "fx:id=\"uiTaskProgressBar\" was not injected: check your FXML file 'StatusBar.fxml'.";
+
+        controller.getFileUpdateQueueSizeProperty().addListener((ov, oldSize, newSize) -> {
+            Platform.runLater(() -> {
+                fileUpdateTaskLabel.setText(newSize.toString() + " File Update Tasks");
+                fileTaskProgresBar.setProgress((double) (newSize.intValue() > 0 ? -1 : 0));
+            });
+        });
+
+        controller.bgTaskQueueSizeProperty().addListener((ov, oldSize, newSize) -> {
+            Platform.runLater(() -> {
+                bgTaskLabel.setText(newSize.toString() + " BG Tasks");
+                bgTaskProgressBar.setProgress((double) (newSize.intValue() > 0 ? -1 : 0));
+            });
+        });
+
+        Platform.runLater(() -> {
+            staleLabel.setTooltip(new Tooltip("Some data may be out of date.  Enable listening to ingest in Tools | Options | Image /Video Analyzer , after ingest is complete to update."));
+        });
+        staleLabel.visibleProperty().bind(controller.stale());
+    }
+
+    public StatusBar(EurekaController controller) {
+        this.controller = controller;
+        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("StatusBar.fxml"));
+        fxmlLoader.setRoot(this);
+        fxmlLoader.setController(this);
+
+        try {
+            fxmlLoader.load();
+        } catch (IOException exception) {
+            throw new RuntimeException(exception);
+        }
+
+    }
+
+    public void setLabelText(final String newText) {
+        Platform.runLater(() -> {
+            statusLabel.setText(newText);
+        });
+    }
+
+    public String getLabeltext() {
+        return statusLabel.getText();
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..28f59bf9087d3f9d4f8f2dd73003d0153306973a
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import java.util.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.paint.*?>
+
+<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2">
+  <children>
+    <TableView id="summaryTable" fx:id="tableView" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+      <columns>
+        <TableColumn prefWidth="75.0" text="Category" fx:id="catColumn" />
+        <TableColumn prefWidth="75.0" text="# Files" fx:id="countColumn" />
+      </columns>
+    </TableView>
+  </children>
+</fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java
new file mode 100644
index 0000000000000000000000000000000000000000..b82c40385b96a194842034584253ca3cf2f04282
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java
@@ -0,0 +1,117 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.Priority;
+import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
+import javafx.scene.layout.VBox;
+import javafx.util.Pair;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Displays summary statistics (counts) for each group */
+public class SummaryTablePane extends AnchorPane implements Category.CategoryListener {
+
+    private static SummaryTablePane instance;
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    @FXML
+    private TableColumn<Pair<Category, Integer>, String> catColumn;
+
+    @FXML
+    private TableColumn<Pair<Category, Integer>, Integer> countColumn;
+
+    @FXML
+    private TableView<Pair<Category, Integer>> tableView;
+
+    @FXML
+    void initialize() {
+        assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
+        assert countColumn != null : "fx:id=\"countColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
+        assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
+
+        //set some properties related to layout/resizing
+        VBox.setVgrow(this, Priority.NEVER);
+        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        tableView.prefHeightProperty().set(7 * 25);
+
+        //set up columns
+        catColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<Category, Integer>, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName()));
+        catColumn.setPrefWidth(USE_COMPUTED_SIZE);
+
+        countColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<Category, Integer>, Integer> p) -> new SimpleObjectProperty<>(p.getValue().getValue()));
+        countColumn.setPrefWidth(USE_COMPUTED_SIZE);
+
+        tableView.getColumns().setAll(catColumn, countColumn);
+
+//        //register for category events
+        Category.registerListener(this);
+    }
+
+    private SummaryTablePane() {
+        FXMLConstructor.construct(this, "SummaryTablePane.fxml");
+    }
+
+    public static synchronized SummaryTablePane getDefault() {
+        if (instance == null) {
+            instance = new SummaryTablePane();
+        }
+
+        return instance;
+    }
+
+    /** listen to Category updates and rebuild the table */
+    @Override
+    public void handleCategoryChanged(Collection<Long> ids) {
+        if (Case.isCaseOpen()) {
+            final ObservableList<Pair<Category, Integer>> data = FXCollections.observableArrayList();
+
+            for (Category cat : Category.values()) {
+                try {
+                    data.add(new Pair<>(cat, EurekaController.getDefault().getGroupManager().getFileIDsWithCategory(cat).size()));
+                } catch (TskCoreException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            }
+            Platform.runLater(() -> {
+                tableView.setItems(data);
+            });
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java
new file mode 100644
index 0000000000000000000000000000000000000000..8114f6795de8f26f119493367f01dc61c54b55c2
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java
@@ -0,0 +1,87 @@
+package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel;
+
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.scene.Node;
+import javafx.scene.control.OverrunStyle;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.TreeCell;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+
+/** A {@link Node} in the tree that listens to its associated group. Manages
+ * visual representation of TreeNode in Tree. Listens to properties of group
+ * that don't impact hierarchy and updates ui to reflect them */
+class GroupTreeCell extends TreeCell<TreeNode> {
+
+    /** icon to use if this cell's TreeNode doesn't represent a group but just a
+     * folder(with no DrawableFiles) in the hierarchy. */
+    private static final Image EMPTY_FOLDER_ICON = new Image("org/sleuthkit/autopsy/imageanalyzer/images/folder.png");
+
+    public GroupTreeCell() {
+        //adjust indent, default is 10 which uses up a lot of space.
+        setStyle("-fx-indent:5;");
+        //since end of path is probably more interesting put ellipsis at front
+        setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
+    }
+
+    @Override
+    synchronized protected void updateItem(final TreeNode tNode, boolean empty) {
+        super.updateItem(tNode, empty);
+        prefWidthProperty().bind(getTreeView().widthProperty().subtract(15));
+
+        if (tNode != null) {
+            final String name = StringUtils.defaultIfBlank(tNode.getPath(), Grouping.UNKNOWN);
+            setTooltip(new Tooltip(name));
+
+            if (tNode.getGroup() == null) {
+                setText(name);
+                setGraphic(new ImageView(EMPTY_FOLDER_ICON));
+            } else {
+                //this TreeNode has a group so append counts to name ...
+                setText(name + " (" + getNumerator() + getDenominator() + ")");
+
+                //if number of files in this group changes (eg file is recategorized), update counts
+                tNode.getGroup().fileIds().addListener((Observable o) -> {
+                    Platform.runLater(() -> {
+                        setText(name + " (" + getNumerator() + getDenominator() + ")");
+                    });
+                });
+
+                //... and use icon corresponding to group type
+                setGraphic(new ImageView(tNode.getGroup().groupKey.getAttribute().getIcon()));
+            }
+        } else {
+            setTooltip(null);
+            setText(null);
+            setGraphic(null);
+        }
+    }
+
+    /** @return the Numerator of the count to append to the group name = number
+     *          of hashset hits + "/" */
+    synchronized private String getNumerator() {
+        try {
+            final String numerator = (getItem().getGroup().groupKey.getAttribute() != DrawableAttribute.HASHSET)
+                                     ? getItem().getGroup().getFilesWithHashSetHitsCount() + "/"
+                                     : "";
+            return numerator;
+        } catch (NullPointerException ex) {
+            return "";
+        }
+
+    }
+
+    /** @return the Denominator of the count to append to the group name =
+     *          number of files in group */
+    synchronized private Integer getDenominator() {
+        try {
+            return getItem().getGroup().getSize();
+        } catch (NullPointerException ex) {
+            return 0;
+        }
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..7dfcc5d47d63b01100988bbeb765efd068b3c882
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java
@@ -0,0 +1,252 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui.navpanel;
+
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.event.EventType;
+import javafx.scene.control.TreeItem;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/** A node in the nav/hash tree. Manages inserts and removals. Has parents
+ * and children. Does not have graphical properties these are configured in
+ * {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path
+ * segment and may or may not have a group */
+class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeItem> {
+
+    /** maps a path segment to the child item of this item with that path
+     * segment */
+    private Map<String, GroupTreeItem> childMap = new HashMap<>();
+    /** the comparator if any used to sort the children of this item */
+    private TreeNodeComparators comp;
+
+    public GroupTreeItem(String t, Grouping g, TreeNodeComparators comp) {
+        super(new TreeNode(t, g));
+        this.comp = comp;
+    }
+
+    /** Returns the full absolute path of this level in the tree
+     *
+     * @return the full absolute path of this level in the tree */
+    public String getAbsolutePath() {
+        if (getParent() != null) {
+            return ((GroupTreeItem) getParent()).getAbsolutePath() + getValue().getPath() + "/";
+        } else {
+            return getValue().getPath() + "/";
+        }
+    }
+
+    /** Recursive method to add a grouping at a given path.
+     *
+     * @param path Full path (or subset not yet added) to add
+     * @param g    Group to add
+     * @param tree True if it is part of a tree (versus a list)
+     */
+    void insert(String path, Grouping g, Boolean tree) {
+        if (tree) {
+            String cleanPath = StringUtils.stripStart(path, "/");
+
+            // get the first token
+            String prefix = StringUtils.substringBefore(cleanPath, "/");
+
+            // Are we at the end of the recursion?
+            if ("".equals(prefix)) {
+                getValue().setGroup(g);
+            } else {
+                GroupTreeItem prefixTreeItem = childMap.get(prefix);
+                if (prefixTreeItem == null) {
+                    final GroupTreeItem newTreeItem = new GroupTreeItem(prefix, null, comp);
+
+                    prefixTreeItem = newTreeItem;
+                    childMap.put(prefix, prefixTreeItem);
+                    Platform.runLater(new Runnable() {
+                        @Override
+                        public void run() {
+                            synchronized (getChildren()) {
+                                getChildren().add(newTreeItem);
+                            }
+                        }
+                    });
+
+                }
+
+                // recursively go into the path
+                prefixTreeItem.insert(StringUtils.stripStart(cleanPath, prefix), g, tree);
+            }
+        } else {
+            GroupTreeItem treeItem = childMap.get(path);
+            if (treeItem == null) {
+                final GroupTreeItem newTreeItem = new GroupTreeItem(path, g, comp);
+                newTreeItem.setExpanded(true);
+                childMap.put(path, newTreeItem);
+
+                Platform.runLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        synchronized (getChildren()) {
+                            getChildren().add(newTreeItem);
+                            if (comp != null) {
+                                FXCollections.sort(getChildren(), comp);
+                            }
+                        }
+                    }
+                });
+
+            }
+        }
+    }
+
+    /** Recursive method to add a grouping at a given path.
+     *
+     * @param path Full path (or subset not yet added) to add
+     * @param g    Group to add
+     * @param tree True if it is part of a tree (versus a list)
+     */
+    void insert(List<String> path, Grouping g, Boolean tree) {
+        if (tree) {
+            // Are we at the end of the recursion?
+            if (path.isEmpty()) {
+                getValue().setGroup(g);
+            } else {
+                String prefix = path.get(0);
+
+                GroupTreeItem prefixTreeItem = childMap.get(prefix);
+                if (prefixTreeItem == null) {
+                    final GroupTreeItem newTreeItem = new GroupTreeItem(prefix, null, comp);
+
+                    prefixTreeItem = newTreeItem;
+                    childMap.put(prefix, prefixTreeItem);
+
+                    Platform.runLater(new Runnable() {
+                        @Override
+                        public void run() {
+                            synchronized (getChildren()) {
+                                getChildren().add(newTreeItem);
+                            }
+                        }
+                    });
+
+                }
+
+                // recursively go into the path
+                prefixTreeItem.insert(path.subList(1, path.size()), g, tree);
+            }
+        } else {
+            //flat list
+            GroupTreeItem treeItem = childMap.get(StringUtils.join(path, "/"));
+            if (treeItem == null) {
+                final GroupTreeItem newTreeItem = new GroupTreeItem(StringUtils.join(path, "/"), g, comp);
+                newTreeItem.setExpanded(true);
+                childMap.put(path.get(0), newTreeItem);
+
+                Platform.runLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        synchronized (getChildren()) {
+                            getChildren().add(newTreeItem);
+                            if (comp != null) {
+                                FXCollections.sort(getChildren(), comp);
+                            }
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    @Override
+    public int compareTo(GroupTreeItem o) {
+        return comp.compare(this, o);
+    }
+
+    static GroupTreeItem getTreeItemForGroup(GroupTreeItem root, Grouping grouping) {
+        if (root.getValue().getGroup() == grouping) {
+            return root;
+        } else {
+            synchronized (root.getChildren()) {
+                for (TreeItem<TreeNode> child : root.getChildren()) {
+                    final GroupTreeItem childGTI = (GroupTreeItem) child;
+
+                    GroupTreeItem val = getTreeItemForGroup(childGTI, grouping);
+                    if (val != null) {
+                        return val;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    GroupTreeItem getTreeItemForPath(List<String> path) {
+        // end of recursion
+        if (path.isEmpty()) {
+            return this;
+        } else {
+            synchronized (getChildren()) {
+                String prefix = path.get(0);
+
+                GroupTreeItem prefixTreeItem = childMap.get(prefix);
+                if (prefixTreeItem == null) {
+                    // @@@ ERROR;
+                    return null;
+                }
+
+                // recursively go into the path
+                return prefixTreeItem.getTreeItemForPath(path.subList(1, path.size()));
+            }
+        }
+    }
+
+    void removeFromParent() {
+        final GroupTreeItem parent = (GroupTreeItem) getParent();
+        if (parent != null) {
+            parent.childMap.remove(getValue().getPath());
+
+            Platform.runLater(new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (parent.getChildren()) {
+                        parent.getChildren().removeAll(GroupTreeItem.this);
+                    }
+                }
+            });
+
+            if (parent.childMap.isEmpty()) {
+                parent.removeFromParent();
+            }
+        }
+    }
+
+    void resortChildren(TreeNodeComparators newComp) {
+        this.comp = newComp;
+        synchronized (getChildren()) {
+            FXCollections.sort(getChildren(), comp);
+        }
+        for (GroupTreeItem ti : childMap.values()) {
+            ti.resortChildren(comp);
+        }
+    }
+}
\ No newline at end of file
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml
new file mode 100644
index 0000000000000000000000000000000000000000..27132629fa649f578f429b5c72bd135060750134
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import java.lang.*?>
+<?import javafx.collections.*?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.image.*?>
+<?import javafx.scene.layout.*?>
+
+
+    <fx:root fx:id="navTabPane" maxHeight="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" tabClosingPolicy="UNAVAILABLE" type="TabPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
+      <tabs>
+        <Tab fx:id="navTab" text="Contents">
+          <content>
+            <AnchorPane prefHeight="-1.0" prefWidth="-1.0">
+              <children>
+                <TreeView fx:id="navTree" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" style="" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
+              </children>
+            </AnchorPane>
+          </content>
+          <graphic>
+            <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+              <image>
+                <Image url="@../../images/folder-tree.png" />
+              </image>
+            </ImageView>
+          </graphic>
+        </Tab>
+        <Tab fx:id="hashTab" text="Hash Hits">
+          <content>
+            <AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0">
+              <children>
+                <BorderPane prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
+                  <center>
+                    <TreeView fx:id="hashTree" prefHeight="-1.0" prefWidth="-1.0" />
+                  </center>
+                  <top>
+                    <ToolBar>
+                      <items>
+                        <Label text="Sort By:" />
+                        <ComboBox fx:id="sortByBox">
+                          <items>
+                            <FXCollections fx:factory="observableArrayList">
+                              <String fx:value="Item 1" />
+                              <String fx:value="Item 2" />
+                              <String fx:value="Item 3" />
+                            </FXCollections>
+                          </items>
+                        </ComboBox>
+                      </items>
+                    </ToolBar>
+                  </top>
+                </BorderPane>
+              </children>
+            </AnchorPane>
+          </content>
+          <graphic>
+            <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
+              <image>
+                <Image url="@../../images/hashset_hits.png" />
+              </image>
+            </ImageView>
+          </graphic>
+        </Tab>
+      </tabs>
+    </fx:root>
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..d59257e1abef32a44e09668cf48452222bbb8f6d
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java
@@ -0,0 +1,358 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui.navpanel;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.ResourceBundle;
+import javafx.application.Platform;
+import javafx.beans.Observable;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ListView;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.Exceptions;
+import org.sleuthkit.autopsy.imageanalyzer.EurekaController;
+import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName;
+import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.PATH;
+import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.TAGS;
+import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState;
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+import org.sleuthkit.datamodel.TagName;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/** Display two trees. one shows all folders (groups) and calls out folders
+ * with images. the user can select folders with images to see them in the
+ * main GroupPane The other shows folders with hash set hits.
+ */
+public class NavPanel extends TabPane {
+
+    @FXML
+    private ResourceBundle resources;
+
+    @FXML
+    private URL location;
+
+    /** TreeView for folders with hash hits */
+    @FXML
+    private TreeView<TreeNode> hashTree;
+
+    @FXML
+    private TabPane navTabPane;
+
+    /** TreeView for all folders */
+    @FXML
+    private TreeView<TreeNode> navTree;
+
+    @FXML
+    private Tab hashTab;
+
+    @FXML
+    private Tab navTab;
+
+    @FXML
+    private ComboBox<TreeNodeComparators> sortByBox;
+
+    /** contains the 'active tree' */
+    private final SimpleObjectProperty<TreeView<TreeNode>> activeTreeProperty = new SimpleObjectProperty<>();
+
+    private GroupTreeItem navTreeRoot;
+
+    private GroupTreeItem hashTreeRoot;
+
+    private final EurekaController controller;
+
+    public NavPanel(EurekaController controller) {
+        this.controller = controller;
+        FXMLConstructor.construct(this, "NavPanel.fxml");
+    }
+
+    @FXML
+    void initialize() {
+        assert hashTab != null : "fx:id=\"hashTab\" was not injected: check your FXML file 'NavPanel.fxml'.";
+        assert hashTree != null : "fx:id=\"hashTree\" was not injected: check your FXML file 'NavPanel.fxml'.";
+        assert navTab != null : "fx:id=\"navTab\" was not injected: check your FXML file 'NavPanel.fxml'.";
+        assert navTabPane != null : "fx:id=\"navTabPane\" was not injected: check your FXML file 'NavPanel.fxml'.";
+        assert navTree != null : "fx:id=\"navTree\" was not injected: check your FXML file 'NavPanel.fxml'.";
+        assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'.";
+
+        VBox.setVgrow(this, Priority.ALWAYS);
+
+        navTree.setShowRoot(false);
+        hashTree.setShowRoot(false);
+
+        navTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+        hashTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+
+        sortByBox.setCellFactory((ListView<TreeNodeComparators> p) -> new TreeNodeComparators.ComparatorListCell());
+
+        sortByBox.setButtonCell(new TreeNodeComparators.ComparatorListCell());
+        sortByBox.setItems(FXCollections.observableArrayList(FXCollections.observableArrayList(TreeNodeComparators.values())));
+        sortByBox.getSelectionModel().select(TreeNodeComparators.HIT_COUNT);
+        sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> {
+            resortHashTree();
+        });
+
+        navTree.setCellFactory((TreeView<TreeNode> p) -> new GroupTreeCell());
+
+        hashTree.setCellFactory((TreeView<TreeNode> p) -> new GroupTreeCell());
+
+        activeTreeProperty.addListener((Observable o) -> {
+            updateControllersGroup();
+            activeTreeProperty.get().getSelectionModel().selectedItemProperty().addListener((Observable o1) -> {
+                updateControllersGroup();
+            });
+        });
+
+        this.activeTreeProperty.set(navTree);
+
+        navTabPane.getSelectionModel().selectedItemProperty().addListener((ObservableValue<? extends Tab> ov, Tab t, Tab t1) -> {
+            if (t1 == hashTab) {
+                activeTreeProperty.set(hashTree);
+            } else if (t1 == navTab) {
+                activeTreeProperty.set(navTree);
+            }
+        });
+
+        initHashTree();
+        initNavTree();
+
+        controller.getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends Grouping> change) -> {
+            while (change.next()) {
+                for (Grouping g : change.getAddedSubList()) {
+                    insertIntoNavTree(g);
+                    if (g.getFilesWithHashSetHitsCount() > 0) {
+                        insertIntoHashTree(g);
+                    }
+                }
+                for (Grouping g : change.getRemoved()) {
+                    removeFromNavTree(g);
+                    removeFromHashTree(g);
+                }
+            }
+        });
+
+        for (Grouping g : controller.getGroupManager().getAnalyzedGroups()) {
+            insertIntoNavTree(g);
+            if (g.getFilesWithHashSetHitsCount() > 0) {
+                insertIntoHashTree(g);
+            }
+        }
+
+        controller.viewState().addListener((ObservableValue<? extends GroupViewState> observable, GroupViewState oldValue, GroupViewState newValue) -> {
+            if (newValue != null && newValue.getGroup() != null) {
+                setFocusedGroup(newValue.getGroup());
+            }
+        });
+    }
+
+    private void updateControllersGroup() {
+        final TreeItem<TreeNode> selectedItem = activeTreeProperty.get().getSelectionModel().getSelectedItem();
+        if (selectedItem != null && selectedItem.getValue() != null && selectedItem.getValue().getGroup() != null) {
+            controller.pushGroup(GroupViewState.tile(selectedItem.getValue().getGroup()), false);
+        }
+    }
+
+    private void resortHashTree() {
+        hashTreeRoot.resortChildren(sortByBox.getSelectionModel().getSelectedItem());
+    }
+
+    private void insertIntoHashTree(Grouping g) {
+        initHashTree();
+        hashTreeRoot.insert(g.groupKey.getValueDisplayName(), g, false);
+    }
+
+    /** Set the tree to the passed in group
+     *
+     * @param grouping */
+    public void setFocusedGroup(Grouping grouping) {
+
+        List<String> path = groupingToPath(grouping);
+
+        final GroupTreeItem treeItemForGroup = ((GroupTreeItem) activeTreeProperty.get().getRoot()).getTreeItemForPath(path);
+
+        if (treeItemForGroup != null) {
+            Platform.runLater(() -> {
+                TreeItem ti = treeItemForGroup;
+                while (ti != null) {
+                    ti.setExpanded(true);
+                    ti = ti.getParent();
+                }
+                int row = activeTreeProperty.get().getRow(treeItemForGroup);
+                if (row != -1) {
+                    activeTreeProperty.get().getSelectionModel().select(treeItemForGroup);
+                    activeTreeProperty.get().scrollTo(row);
+                }
+            });
+        }
+    }
+
+    private static List<String> groupingToPath(Grouping g) {
+
+        AttributeName attrName = g.groupKey.getAttribute().attrName;
+        String path = null;
+        switch (attrName) {
+            case PATH:
+                path = ((String) g.groupKey.getValue());
+                break;
+            case TAGS:
+                path = ((TagName) g.groupKey.getValue()).getDisplayName();
+            default:
+                if (path == null) {
+                    path = g.groupKey.getValue().toString();
+                }
+        }
+
+        String cleanPath = StringUtils.stripStart(path, "/");
+        String[] tokens = cleanPath.split("/");
+
+        return Arrays.asList(tokens);
+    }
+
+    private void insertIntoNavTree(Grouping g) {
+        initNavTree();
+        List<String> path = groupingToPath(g);
+
+        AttributeName attrName = g.groupKey.getAttribute().attrName;
+        switch (attrName) {
+            case PATH:
+                navTreeRoot.insert(path, g, true);
+                break;
+            default:
+                navTreeRoot.insert(path, g, false);
+                break;
+        }
+    }
+
+    private void removeFromNavTree(Grouping g) {
+        initNavTree();
+        final GroupTreeItem treeItemForGroup = GroupTreeItem.getTreeItemForGroup(navTreeRoot, g);
+        if (treeItemForGroup != null) {
+            treeItemForGroup.removeFromParent();
+        }
+    }
+
+    private void removeFromHashTree(Grouping g) {
+        initHashTree();
+        final GroupTreeItem treeItemForGroup = GroupTreeItem.getTreeItemForGroup(hashTreeRoot, g);
+        if (treeItemForGroup != null) {
+            treeItemForGroup.removeFromParent();
+        }
+    }
+
+    private void initNavTree() {
+        if (navTreeRoot == null) {
+            navTreeRoot = new GroupTreeItem("", null, null);
+
+            Platform.runLater(() -> {
+                navTree.setRoot(navTreeRoot);
+                navTreeRoot.setExpanded(true);
+            });
+        }
+    }
+
+    private void initHashTree() {
+        if (hashTreeRoot == null) {
+            hashTreeRoot = new GroupTreeItem("", null, null);
+
+            Platform.runLater(() -> {
+                hashTree.setRoot(hashTreeRoot);
+                hashTreeRoot.setExpanded(true);
+            });
+        }
+    }
+
+    //these are not used anymore, but could be usefull at some point
+    //TODO: remove them or find a use and undeprecate
+    @Deprecated
+    private void rebuildNavTree() {
+        navTreeRoot = new GroupTreeItem("", null, sortByBox.getSelectionModel().selectedItemProperty().get());
+
+        ObservableList<Grouping> groups = controller.getGroupManager().getAnalyzedGroups();
+
+        for (Grouping g : groups) {
+            insertIntoNavTree(g);
+        }
+
+        Platform.runLater(() -> {
+            navTree.setRoot(navTreeRoot);
+            navTreeRoot.setExpanded(true);
+        });
+    }
+
+    @Deprecated
+    private void rebuildHashTree() {
+        hashTreeRoot = new GroupTreeItem("", null, sortByBox.getSelectionModel().getSelectedItem());
+        //TODO: can we do this as db query?
+        List<String> hashSetNames = controller.getGroupManager().findValuesForAttribute(DrawableAttribute.HASHSET, GroupSortBy.NONE);
+        for (String name : hashSetNames) {
+            try {
+                List<Long> fileIDsInGroup = controller.getGroupManager().getFileIDsInGroup(new GroupKey(DrawableAttribute.HASHSET, name));
+
+                for (Long fileId : fileIDsInGroup) {
+
+                    DrawableFile file = controller.getFileFromId(fileId);
+                    Collection<GroupKey> groupKeysForFile;
+                    if (controller.getGroupManager().getGroupBy() == DrawableAttribute.TAGS) {
+                        Collection<TagName> tagNames = (Collection<TagName>) file.getValueOfAttribute(DrawableAttribute.TAGS);
+                        groupKeysForFile = new ArrayList<>();
+                        for (TagName tn : tagNames) {
+                            groupKeysForFile.add(new GroupKey(DrawableAttribute.TAGS, tn));
+                        }
+                    } else {
+                        groupKeysForFile = controller.getGroupManager().getGroupKeysForFile(file);
+                    }
+
+                    for (GroupKey k : groupKeysForFile) {
+                        final Grouping groupForKey = controller.getGroupManager().getGroupForKey(k);
+                        if (groupForKey != null) {
+                            insertIntoHashTree(groupForKey);
+                        }
+                    }
+                }
+            } catch (TskCoreException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+        Platform.runLater(() -> {
+            hashTree.setRoot(hashTreeRoot);
+            hashTreeRoot.setExpanded(true);
+        });
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b9919dc84ca475e5f8ad537f31aa3a0fe421075
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java
@@ -0,0 +1,47 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui.navpanel;
+
+import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping;
+
+/**
+ *
+ */
+ class TreeNode {
+
+   private  String path;
+   private Grouping group;
+
+    public String getPath() {
+        return path;
+    }
+
+    public Grouping getGroup() {
+        return group;
+    }
+
+    public TreeNode(String path, Grouping group) {
+        this.path = path;
+        this.group = group;
+    }
+
+    void setGroup(Grouping g) {
+    group = g;
+    }
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java
new file mode 100644
index 0000000000000000000000000000000000000000..bab4f5c6691e14c30db39ae74afc0b7ee15c7937
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java
@@ -0,0 +1,94 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.gui.navpanel;
+
+import java.util.Comparator;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.TreeItem;
+
+/**
+ *
+ */
+enum TreeNodeComparators implements Comparator<TreeItem<TreeNode>>, NonNullCompareable {
+
+    ALPHABETICAL("Group Name") {
+        @Override
+        public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) {
+
+            return o1.getValue().getGroup().groupKey.getValue().toString().compareTo(o2.getValue().getGroup().groupKey.getValue().toString());
+        }
+    }, HIT_COUNT("Hit Count") {
+        @Override
+        public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) {
+
+            return -Integer.compare(o1.getValue().getGroup().getFilesWithHashSetHitsCount(), o2.getValue().getGroup().getFilesWithHashSetHitsCount());
+        }
+    }, FILE_COUNT("Group Size") {
+        @Override
+        public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) {
+
+            return -Integer.compare(o1.getValue().getGroup().getSize(), o2.getValue().getGroup().getSize());
+        }
+    }, HIT_FILE_RATIO("Hit Density") {
+        @Override
+        public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) {
+
+            return -Double.compare(o1.getValue().getGroup().getFilesWithHashSetHitsCount() / o1.getValue().getGroup().getSize().doubleValue(),
+                                   o2.getValue().getGroup().getFilesWithHashSetHitsCount() / o2.getValue().getGroup().getSize().doubleValue());
+        }
+    };
+
+    @Override
+    public int compare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) {
+        if (o1.getValue().getGroup() == null) {
+            if (o1.getValue().getGroup() == null) {
+                return 0;
+            }
+            return 1;
+        } else if (o2.getValue().getGroup() == null) {
+            return -1;
+        }
+        return nonNullCompare(o1, o2);
+    }
+    final private String displayName;
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private TreeNodeComparators(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public static class ComparatorListCell extends ListCell<TreeNodeComparators> {
+
+        @Override
+        protected void updateItem(TreeNodeComparators t, boolean bln) {
+            super.updateItem(t, bln);
+            if (t != null) {
+                setText(t.getDisplayName());
+            }
+        }
+    }
+}
+
+interface NonNullCompareable {
+
+    int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2);
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..134ec3828be1031442406547f035af1e2c781538
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..77fb7695deb151621129a32a9d1b377851a65359
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea290a20c72e077e8e5e39770c2da518ef26206e
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png
new file mode 100644
index 0000000000000000000000000000000000000000..3bc0bd32fceb21d70368f7842a00a53d6369ba48
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png
new file mode 100644
index 0000000000000000000000000000000000000000..f62345e491b0af46cf10f59d46467c257d5be8d9
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d2aa3ccb2d59ecf6cc1db2006bc330a7b32423c
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png
new file mode 100644
index 0000000000000000000000000000000000000000..56e9a63d6233853be9c14ec522652611d7b9056c
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png
new file mode 100644
index 0000000000000000000000000000000000000000..06f1ee390a69aede315c33ab6ba8c085bcdd003e
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c5605506772be9ca53f69e424c4a37a98375c02
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png
new file mode 100644
index 0000000000000000000000000000000000000000..dcfa5ab5364f6b4d0093a20d453df6e06bb8f06c
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png
new file mode 100644
index 0000000000000000000000000000000000000000..a41414154515ed02b66ec3508436849ea927427b
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..12077d33242acb0933fb8a1eb1e9b283173bf3b6
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c4e279377bf348f9cf53894e76bb673ccf067bd
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ebb193243780b8eb1919a51ef27c2a0d36ccec2
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png
new file mode 100644
index 0000000000000000000000000000000000000000..367ab664eb9283f66f5e33c83dff15411a02c29b
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cdfad4ae58a8b48ff96a5a26099fa03bbfec344
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b23c06d7b4f4689dc8c9fd4e9d4d1f199fe376f
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..8536d1a795888d8d396ac4211b639c6395dd08e6
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1a3f6a24aebe4cb403d45887605ed82a17fbfa19
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a07b14b24d23b8b7443802447655722bd376325
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b89ab946efb92f31946a7c1f5edd7fd3ccdd674
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png
new file mode 100644
index 0000000000000000000000000000000000000000..b3e5e329950eb2caaf33eb8cc39eab41980df824
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png
new file mode 100644
index 0000000000000000000000000000000000000000..826e3d7f03cfa0c62cf7c121b53e86ee2438d5b1
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png
new file mode 100644
index 0000000000000000000000000000000000000000..da6e413da1aaf7468f6dfca3ac1fa5e04a5ba7a0
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png
new file mode 100644
index 0000000000000000000000000000000000000000..f4db3cfd2592f9d0408792510132945b86978b6e
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4e0830f67db1b0bc85b2e6768aea911e1dc9ae9
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png
new file mode 100644
index 0000000000000000000000000000000000000000..6cb4ed8cb59ea4c3ece677295f99a105c98fb9dd
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab4753fbfd58cb3d7adafefbdde15d516826d9f1
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png
new file mode 100644
index 0000000000000000000000000000000000000000..8232606b0fbcac9ae9314f0502db46238a2a2a92
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png
new file mode 100644
index 0000000000000000000000000000000000000000..4197fb468badbb0a39a578739a99ab817d3bb9e8
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png
new file mode 100644
index 0000000000000000000000000000000000000000..31fa687ecd6474f13007b3b0a903ed5ee937a6b6
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png
new file mode 100644
index 0000000000000000000000000000000000000000..1888f8da96ea4b145de240a906f985540bcf8037
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8a602da7b17a323b2c9afe3d8aac62cb717a0b8
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b21e1abbb7ba2fc1e13cea8ab799f3beedc3ac0
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ae261d4d171cdfa71687b24e85c54129e5c187f
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png
new file mode 100644
index 0000000000000000000000000000000000000000..6812f52669d508ffcc1623b1e027f8463bd00d69
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png
new file mode 100644
index 0000000000000000000000000000000000000000..784e8fa48234f4f64b6922a6758f254ee0ca08ec
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png
new file mode 100644
index 0000000000000000000000000000000000000000..052b33638eaa0f870a255bfdd5df5b79fb01a89e
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea9cefe849c36a569ba9891c9e207e4d7e521d94
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f69604528f29ca95e3b124de2849067797d839f
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png
new file mode 100644
index 0000000000000000000000000000000000000000..7fb4e1f1e1cd6ee67d33ffd24f09ddd5c3478bec
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1caff1f30333a7d3dc5ab278472b433a2e6f165
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d353a191098dee1e1e8f4bdf99c7f4bccb0f292
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png
new file mode 100644
index 0000000000000000000000000000000000000000..d22fde8ba46eabd4335e4fa88077e80f96b92d62
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png
new file mode 100644
index 0000000000000000000000000000000000000000..44084add79b9a0fc3354d16bbd4b4b5ff8095da7
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_48_silhouette.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_48_silhouette.png
new file mode 100644
index 0000000000000000000000000000000000000000..170ad4c8b83fd5dd4f5ef95bdeb143d2b50132bb
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_48_silhouette.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48.png
new file mode 100644
index 0000000000000000000000000000000000000000..83477c8a163b8715c43cda1a08b5bf7a46f77123
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48_silhouette.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48_silhouette.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcc34dee53a364dc97de0d5621e831044da83c35
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/polaroid_green_48_silhouette.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png
new file mode 100644
index 0000000000000000000000000000000000000000..18f151071ad328b247966fe9d743b19e6827e430
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e304ad84c772659762b15ee5b02e08254d482ca
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c88990a852fb5a54087c28ae2f5b9285912dce5
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb2ff516d35dc9a92ed6ffdc79595d61513e65e3
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f1ae4496e4f874742133d42941b2301d23fb95c
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ebb37d25f58c68246d8ad6a015295dfa5367870
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png
new file mode 100644
index 0000000000000000000000000000000000000000..c290609f59048e61cb28af42c4500709e342f437
Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png differ
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fb3e1b684a3502c1045b0aa4b372b145a785f4a1
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt
@@ -0,0 +1,18 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-14 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.
+ */
\ No newline at end of file
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..80ccece647f086ca664cf58e9921daaedec783fa
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java
@@ -0,0 +1,43 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.progress;
+
+import javafx.beans.property.ReadOnlyDoubleProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.concurrent.Worker;
+
+/**
+ *
+ */
+public interface ProgressAdapter {
+
+    public double getProgress();
+
+    public ReadOnlyDoubleProperty progressProperty();
+
+    public String getMessage();
+
+    public ReadOnlyStringProperty messageProperty();
+
+    public Worker.State getState();
+
+    public ReadOnlyObjectProperty<Worker.State> stateProperty();
+
+}
diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0fe1be6e2606d9b053db982e9ef072ae1b9da23
--- /dev/null
+++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java
@@ -0,0 +1,83 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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 org.sleuthkit.autopsy.imageanalyzer.progress;
+
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.concurrent.Worker;
+
+/**
+ *
+ */
+public abstract class ProgressAdapterBase implements ProgressAdapter {
+
+    @Override
+    public double getProgress() {
+        return progress.get();
+    }
+
+    public final void updateProgress(Double workDone) {
+        this.progress.set(workDone);
+    }
+
+    @Override
+    public String getMessage() {
+        return message.get();
+    }
+
+    public final void updateMessage(String Status) {
+        this.message.set(Status);
+    }
+
+    SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty(Worker.State.READY);
+
+    SimpleDoubleProperty progress = new SimpleDoubleProperty(this, "pregress");
+
+    SimpleStringProperty message = new SimpleStringProperty(this, "status");
+
+    @Override
+    public SimpleDoubleProperty progressProperty() {
+        return progress;
+    }
+
+    @Override
+    public SimpleStringProperty messageProperty() {
+        return message;
+    }
+
+    @Override
+    public Worker.State getState() {
+        return state.get();
+    }
+
+    protected void updateState(Worker.State newState) {
+        state.set(newState);
+    }
+
+    @Override
+    public ReadOnlyObjectProperty<Worker.State> stateProperty() {
+        return new ReadOnlyObjectWrapper<>(state.get());
+    }
+
+    public ProgressAdapterBase() {
+    }
+}
diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
index a4b426a229a459f217d0f132d4c440156b548114..92f657c0b37ed56045a468ec3da39fc9bad4166f 100644
--- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
+++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
@@ -1,5 +1,5 @@
 #Updated by build script
-#Mon, 28 Apr 2014 01:45:18 -0400
+#Tue, 05 Aug 2014 14:10:24 -0400
 LBL_splash_window_title=Starting Autopsy
 SPLASH_HEIGHT=288
 SPLASH_WIDTH=538
diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
index 24255ffb3fb7370366c868ce4d1fb711b290348d..c9b7f9be63e51c9b1b23b7d62be081c323302f70 100644
--- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
+++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
@@ -1,5 +1,5 @@
 #Updated by build script
-#Mon, 28 Apr 2014 01:45:18 -0400
+#Tue, 05 Aug 2014 14:10:24 -0400
 
 CTL_MainWindow_Title=Autopsy 3.1.0_Beta
 CTL_MainWindow_Title_No_Project=Autopsy 3.1.0_Beta
diff --git a/docs/doxygen-user/Doxyfile b/docs/doxygen-user/Doxyfile
new file mode 100644
index 0000000000000000000000000000000000000000..83d3d73cbab7caa06afc87da20208f2845b04e02
--- /dev/null
+++ b/docs/doxygen-user/Doxyfile
@@ -0,0 +1,1811 @@
+# Doxyfile 1.8.2
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a hash (#) is considered a comment and will be ignored.
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or sequence of words) that should
+# identify the project. Note that if you do not use Doxywizard you need
+# to put quotes around the project name if it contains spaces.
+
+PROJECT_NAME           = "Autopsy User Documentation"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER         = 3.1
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer
+# a quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "Graphical digital forensics platform for The Sleuth Kit and other tools."
+
+# With the PROJECT_LOGO tag one can specify an logo or icon that is
+# included in the documentation. The maximum height of the logo should not
+# exceed 55 pixels and the maximum width should not exceed 200 pixels.
+# Doxygen will copy the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = user-docs
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create
+# 4096 sub-directories (in 2 levels) under the output directory of each output
+# format and will distribute the generated files over these directories.
+# Enabling this option can be useful when feeding doxygen a huge amount of
+# source files, where putting all generated files in the same directory would
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
+# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
+# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
+# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
+# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak,
+# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator
+# that is used to form the text in various listings. Each string
+# in this list, if found as the leading text of the brief description, will be
+# stripped from the text and the result after processing the whole list, is
+# used as the annotated text. Otherwise, the brief description is used as-is.
+# If left blank, the following values are used ("$name" is automatically
+# replaced with the name of the entity): "The $name class" "The $name widget"
+# "The $name file" "is" "provides" "specifies" "contains"
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = YES
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the
+# path to strip. Note that you specify absolute paths here, but also
+# relative paths, which will be relative from the directory where doxygen is
+# started.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of
+# the path mentioned in the documentation of a class, which tells
+# the reader which header file to include in order to use a class.
+# If left blank only the name of the header file containing the class
+# definition is used. Otherwise one should specify the include paths that
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful if your file system
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like regular Qt-style comments
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will
+# interpret the first line (until the first dot) of a Qt-style
+# comment as the brief description. If set to NO, the comments
+# will behave just like regular Qt-style comments (thus requiring
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce
+# a new page for each member. If set to NO, the documentation of a member will
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 8
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                =
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name=value". For example adding
+# "class=itcl::class" will allow you to use the command class in the
+# itcl::class meaning.
+
+TCL_SUBST              =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
+# sources only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java
+# sources only. Doxygen will then generate output that is more tailored for
+# Java. For instance, namespaces will be presented as packages, qualified
+# scopes will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = YES
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources only. Doxygen will then generate output that is more tailored for
+# Fortran.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for
+# VHDL.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension,
+# and language is one of the parsers supported by doxygen: IDL, Java,
+# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C,
+# C++. For instance to make doxygen treat .inc files as Fortran files (default
+# is PHP), and .f files as C (default is Fortran), use: inc=Fortran f=C. Note
+# that for custom extensions you also need to set FILE_PATTERNS otherwise the
+# files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all
+# comments according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you
+# can mix doxygen, HTML, and XML commands with Markdown formatting.
+# Disable only in case of backward compatibilities issues.
+
+MARKDOWN_SUPPORT       = YES
+
+# When enabled doxygen tries to link words that correspond to documented classes,
+# or namespaces to their corresponding documentation. Such a link can be
+# prevented in individual cases by by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should
+# set this tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s.
+# func(std::string) {}). This also makes the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only.
+# Doxygen will parse them like normal C++ but will assume all classes use public
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate getter and setter methods for a property. Setting this option to YES (the default) will make doxygen replace the get and set methods by a property in the documentation. This will only work if the methods are indeed getting or setting a simple type. If this is not the case, or you want to show the methods anyway, you should set this option to NO.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and
+# unions are shown inside the group in which they are included (e.g. using
+# @ingroup) instead of on a separate page (for HTML and Man pages) or
+# section (for LaTeX and RTF).
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and
+# unions with only public data fields will be shown inline in the documentation
+# of the scope in which they are defined (i.e. file, namespace, or group
+# documentation), provided this scope is documented. If set to NO (the default),
+# structs, classes, and unions are shown on a separate page (for HTML and Man
+# pages) or section (for LaTeX and RTF).
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum
+# is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically
+# be useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to
+# determine which symbols to keep in memory and which to flush to disk.
+# When the cache is full, less often used symbols will be written to disk.
+# For small to medium size projects (<1000 input files) the default value is
+# probably good enough. For larger projects a too small cache size can cause
+# doxygen to be busy swapping symbols to and from disk most of the time
+# causing a significant performance penalty.
+# If the system has enough physical memory increasing the cache will improve the
+# performance by keeping more symbols in memory. Note that the value works on
+# a logarithmic scale so increasing the size by one will roughly double the
+# memory usage. The cache size is given by this formula:
+# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+SYMBOL_CACHE_SIZE      = 0
+
+# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be
+# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given
+# their name and scope. Since this can be an expensive process and often the
+# same symbol appear multiple times in the code, doxygen keeps a cache of
+# pre-resolved symbols. If the cache is too small doxygen will become slower.
+# If the cache is too large, memory is wasted. The cache size is given by this
+# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = YES
+
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal
+# scope will be included in the documentation.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC         = YES
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. When set to YES local
+# methods, which are defined in the implementation section but not in
+# the interface are included in the documentation.
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base
+# name of the file that contains the anonymous namespace. By default
+# anonymous namespaces are hidden.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen
+# will list include files with double quotes in the documentation
+# rather than with sharp brackets.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the
+# brief documentation of file, namespace and class members alphabetically
+# by member name. If set to NO (the default) the members will appear in
+# declaration order.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen
+# will sort the (brief and detailed) documentation of class members so that
+# constructors and destructors are listed first. If set to NO (the default)
+# the constructors will appear in the respective orders defined by
+# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS.
+# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO
+# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
+# hierarchy of group names into alphabetical order. If set to NO (the default)
+# the group names will appear in their defined order.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be
+# sorted by fully-qualified names, including namespaces. If set to
+# NO (the default), the class list will be sorted only by class name,
+# not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to
+# do proper type resolution of all parameters of a function it will reject a
+# match between the prototype and the implementation of a member function even
+# if there is only one candidate or it is obvious which candidate to choose
+# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen
+# will still accept a match between prototype and implementation in such cases.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or macro consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and macros in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page.
+# This will remove the Files entry from the Quick Index and from the
+# Folder Tree View (if specified). The default is YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the
+# Namespaces page.
+# This will remove the Namespaces entry from the Quick Index
+# and from the Folder Tree View (if specified). The default is YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command <command> <input-file>, where <command> is the value of
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file
+# provided by doxygen. Whatever the program writes to standard output
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option.
+# You can optionally specify a file name after the option, if omitted
+# DoxygenLayout.xml will be used as the name of the layout file.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files
+# containing the references data. This must be a list of .bib files. The
+# .bib extension is automatically appended if omitted. Using this command
+# requires the bibtex tool to be installed. See also
+# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style
+# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this
+# feature you need bibtex and perl available in the search path.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some
+# parameters in a documented function, or documenting parameters that
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# The WARN_NO_PARAMDOC option can be enabled to get warnings for
+# functions that are documented, but have no documentation for their parameters
+# or return value. If set to NO (the default) doxygen will only warn about
+# wrong or incomplete parameter documentation, but not about the absence of
+# documentation.
+
+WARN_NO_PARAMDOC       = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text. Optionally the format may contain
+# $version, which will be replaced by the version of the file (if it could
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT            = "$file:$line: $text "
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT                  = main.dox \
+                         installation.dox \
+                         quick_start.dox \
+                         image_viewer.dox
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
+# also the default input encoding. Doxygen uses libiconv (or the iconv built
+# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for
+# the list of possible encodings.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh
+# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py
+# *.f90 *.f *.for *.vhd *.vhdl
+
+FILE_PATTERNS          = *.dox
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories. Note that the wildcards are matched
+# against the file with absolute path, so to exclude all test directories
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH             = .
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output.
+# If FILTER_PATTERNS is specified, this tag will be
+# ignored.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis.
+# Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match.
+# The filters are a list of the form:
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further
+# info on how filters are used. If FILTER_PATTERNS is empty or if
+# non of the patterns match the file name, INPUT_FILTER is applied.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any)
+# and it is also possible to disable source filtering for a specific pattern
+# using *.ext= (so without naming a filter). This option only has effect when
+# FILTER_SOURCE_FILES is enabled.
+
+FILTER_SOURCE_PATTERNS =
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+# Note: To get rid of all source code in the generated output, make sure also
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER         = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C, C++ and Fortran comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = YES
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code.
+# Otherwise they will link to the documentation.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code
+# will point to the HTML generated by the htags(1) tool instead of doxygen
+# built-in source browser. The htags tool is part of GNU's global source
+# tagging system (see http://www.gnu.org/software/global/global.html). You
+# will need version 4.8.6 or higher.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = YES
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = 3.1
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header. Note that when using a custom header you are responsible
+#  for the proper inclusion of any scripts and style sheets that doxygen
+# needs, which is dependent on the configuration options used.
+# It is advised to generate a default header using "doxygen -w html
+# header.html footer.html stylesheet.css YourConfigFile" and then modify
+# that header. Note that the header is subject to change so you typically
+# have to redo this when upgrading to a newer version of doxygen or when
+# changing the value of configuration settings such as GENERATE_TREEVIEW!
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If left blank doxygen will
+# generate a default style sheet. Note that it is recommended to use
+# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this
+# tag will in the future become obsolete.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional
+# user-defined cascading style sheet that is included after the standard
+# style sheets created by doxygen. Using this option one can overrule
+# certain style aspects. This is preferred over using HTML_STYLESHEET
+# since it does not replace the standard style sheet and is therefor more
+# robust against future updates. Doxygen will copy the style sheet file to
+# the output directory.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that
+# the files will be copied as-is; there are no commands or markers available.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output.
+# Doxygen will adjust the colors in the style sheet and background images
+# according to this color. Hue is specified as an angle on a colorwheel,
+# see http://en.wikipedia.org/wiki/Hue for more information.
+# For instance the value 0 represents red, 60 is yellow, 120 is green,
+# 180 is cyan, 240 is blue, 300 purple, and 360 is red again.
+# The allowed range is 0 to 359.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of
+# the colors in the HTML output. For a value of 0 the output will use
+# grayscales only. A value of 255 will produce the most vivid colors.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to
+# the luminance component of the colors in the HTML output. Values below
+# 100 gradually make the output lighter, whereas values above 100 make
+# the output darker. The value divided by 100 is the actual gamma applied,
+# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2,
+# and 100 does not change the gamma.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting
+# this to NO can help when comparing the output of multiple runs.
+
+HTML_TIMESTAMP         = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+
+HTML_DYNAMIC_SECTIONS  = YES
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of
+# entries shown in the various tree structured indices initially; the user
+# can expand and collapse entries dynamically later on. Doxygen will expand
+# the tree to such a level that at most the specified number of entries are
+# visible (unless a fully collapsed tree already exceeds this amount).
+# So setting the number of entries 1 will produce a full collapsed tree by
+# default. 0 is a special value representing an infinite number of entries
+# and will result in a full expanded tree by default.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files
+# will be generated that can be used as input for Apple's Xcode 3
+# integrated development environment, introduced with OSX 10.5 (Leopard).
+# To create a documentation set, doxygen will generate a Makefile in the
+# HTML output directory. Running make will produce the docset in that
+# directory and running "make install" will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find
+# it at startup.
+# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+
+GENERATE_DOCSET        = YES
+
+# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the
+# feed. A documentation feed provides an umbrella under which multiple
+# documentation sets from a single provider (such as a company or product suite)
+# can be grouped.
+
+DOCSET_FEEDNAME        = "Doxygen docs"
+
+# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that
+# should uniquely identify the documentation set bundle. This should be a
+# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
+# will append .docset to the name.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Doxygen
+
+# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely
+# identify the documentation publisher. This should be a reverse domain-name
+# style string, e.g. com.mycompany.MyDocSet.documentation.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compiled HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can
+# be used to specify the file name of the resulting .chm file. You
+# can add a path in front of the file if the result should not be
+# written to the html output directory.
+
+CHM_FILE               =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can
+# be used to specify the location (absolute path including file name) of
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING
+# is used to encode HtmlHelp index (hhk), content (hhc) and project file
+# content.
+
+CHM_INDEX_ENCODING     =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated
+# that can be used as input for Qt's qhelpgenerator to generate a
+# Qt Compressed Help (.qch) of the generated HTML documentation.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can
+# be used to specify the file name of the resulting .qch file.
+# The path specified is relative to the HTML output folder.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#namespace
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#virtual-folders
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to
+# add. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#custom-filters
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see
+# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">
+# Qt Help Project / Custom Filters</a>.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's
+# filter section matches.
+# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">
+# Qt Help Project / Filter Attributes</a>.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
+# be used to specify the location of Qt's qhelpgenerator.
+# If non-empty doxygen will try to run qhelpgenerator on the generated
+# .qhp file.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files
+#  will be generated, which together with the HTML files, form an Eclipse help
+# plugin. To install this plugin and make it available under the help contents
+# menu in Eclipse, the contents of the directory containing the HTML and XML
+# files needs to be copied into the plugins directory of eclipse. The name of
+# the directory within the plugins directory should be the same as
+# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before
+# the help appears.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have
+# this name.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs)
+# at top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it. Since the tabs have the same information as the
+# navigation tree you can set this option to NO if you already set
+# GENERATE_TREEVIEW to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information.
+# If the tag value is set to YES, a side panel will be generated
+# containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
+# Windows users are probably better off using the HTML help feature.
+# Since the tree basically has the same information as the tab index you
+# could consider to set DISABLE_INDEX to NO when enabling this option.
+
+GENERATE_TREEVIEW      = YES
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values
+# (range [0,1..20]) that doxygen will group on one line in the generated HTML
+# documentation. Note that a value of 0 will completely suppress the enum
+# values from appearing in the overview section.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open
+# links to external symbols imported via tag files in a separate window.
+
+EXT_LINKS_IN_WINDOW    = YES
+
+# Use this tag to change the font size of Latex formulas included
+# as images in the HTML documentation. The default is 10. Note that
+# when you change the font size after a successful doxygen run you need
+# to manually remove any form_*.png images from the HTML output directory
+# to force them to be regenerated.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are
+# not supported properly for IE 6.0, but are supported on all modern browsers.
+# Note that when changing this option you need to delete any form_*.png files
+# in the HTML output before the changes have effect.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax
+# (see http://www.mathjax.org) which uses client side Javascript for the
+# rendering instead of using prerendered bitmaps. Use this if you do not
+# have LaTeX installed or if you want to formulas look prettier in the HTML
+# output. When enabled you may also need to install MathJax separately and
+# configure the path to it using the MATHJAX_RELPATH option.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you need to specify the location relative to the
+# HTML output directory using the MATHJAX_RELPATH option. The destination
+# directory should contain the MathJax.js script. For instance, if the mathjax
+# directory is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to
+# the MathJax Content Delivery Network so you can quickly see the result without
+# installing MathJax.
+# However, it is strongly recommended to install a local
+# copy of MathJax from http://www.mathjax.org before deployment.
+
+MATHJAX_RELPATH        = http://cdn.mathjax.org/mathjax/latest
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension
+# names that should be enabled during MathJax rendering.
+
+MATHJAX_EXTENSIONS     =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box
+# for the HTML output. The underlying search engine uses javascript
+# and DHTML and should work on any modern browser. Note that when using
+# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets
+# (GENERATE_DOCSET) there is already a search function so this one should
+# typically be disabled. For large projects the javascript based search engine
+# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a PHP enabled web server instead of at the web client
+# using Javascript. Doxygen will generate the search PHP script and index
+# file to put on the web server. The advantage of the server
+# based approach is that it scales better to large projects and allows
+# full text search. The disadvantages are that it is more difficult to setup
+# and does not have live searching capabilities.
+
+SERVER_BASED_SEARCH    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           =
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked. If left blank `latex' will be used as the default command name.
+# Note that when enabling USE_PDFLATEX this option is only used for
+# generating bitmaps for formulas in the HTML output, but not in the
+# Makefile that is written to the output directory.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for
+# the generated latex document. The footer should contain everything after
+# the last chapter. If it is left blank doxygen will generate a
+# standard footer. Notice: only use this tag if you know what you are doing!
+
+LATEX_FOOTER           =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not
+# include the index chapters (such as File Index, Compound Index, etc.)
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+# If LATEX_SOURCE_CODE is set to YES then doxygen will include
+# source code with syntax highlighting in the LaTeX output.
+# Note that which sources are shown also depends on other settings
+# such as SOURCE_BROWSER.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See
+# http://en.wikipedia.org/wiki/BibTeX for more info.
+
+LATEX_BIB_STYLE        = plain
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimized for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             =
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load style sheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             =
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_SCHEMA             =
+
+# The XML_DTD tag can be used to specify an XML DTD,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_DTD                =
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will
+# dump the program listings (including syntax highlighting
+# and cross-referencing information) to the XML output. Note that
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will
+# generate a Perl module file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
+# nicely formatted so it can be parsed by a human reader.
+# This is useful
+# if you want to understand what is going on.
+# On the other hand, if this
+# tag is set to NO the size of the Perl module output will be much smaller
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX.
+# This is useful so different doxyrules.make files included by the same
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_DEFINED tags.
+
+EXPAND_ONLY_PREDEF     = YES
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# pointed to by INCLUDE_PATH will be searched when a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed. To prevent a macro definition from being
+# undefined via #undef or recursively expanded use the := operator
+# instead of the = operator.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition that
+# overrules the definition found in the source code.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all references to function-like macros
+# that are alone on a line, have an all uppercase name, and do not end with a
+# semicolon, because these will confuse the parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. For each
+# tag file the location of the external documentation should be added. The
+# format of a tag file without this location is as follows:
+#
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+#
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where "loc1" and "loc2" can be relative or absolute paths
+# or URLs. Note that each tag file must have a unique name (where the name does
+# NOT include the path). If a tag file is not located in the directory in which
+# doxygen is run, you must also specify the path to the tagfile here.
+
+TAGFILES               = 
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base
+# or super classes. Setting the tag to NO turns the diagrams off. Note that
+# this option also works with HAVE_DOT disabled, but it is recommended to
+# install and use dot, since it yields more powerful graphs.
+
+CLASS_DIAGRAMS         = NO
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see
+# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH            =
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is
+# allowed to run in parallel. When set to 0 (the default) doxygen will
+# base this on the number of processors available in the system. You can set it
+# explicitly to a value larger than 0 to get control over the balance
+# between CPU load and processing speed.
+
+DOT_NUM_THREADS        = 0
+
+# By default doxygen will use the Helvetica font for all dot files that
+# doxygen generates. When you want a differently looking font you can specify
+# the font name using DOT_FONTNAME. You need to make sure dot is able to find
+# the font, which can be done by putting it in a standard location or by setting
+# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
+# directory containing the font.
+
+DOT_FONTNAME           = FreeSans
+
+# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs.
+# The default size is 10pt.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the Helvetica font.
+# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to
+# set the path where dot can find it.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside
+# the class node. If there are many fields or methods and many nodes the
+# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS
+# threshold limits the number of items for each type to make the size more
+# managable. Set this to 0 for no limit. Note that the threshold may be
+# exceeded by 50% before the limit is enforced.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT options are set to YES then
+# doxygen will generate a call dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable call graphs
+# for selected functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then
+# doxygen will generate a caller dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable caller
+# graphs for selected functions only using the \callergraph command.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will generate a graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES
+# then doxygen will show the dependencies a directory has on other directories
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are svg, png, jpg, or gif.
+# If left blank png will be used. If you choose svg you need to set
+# HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible in IE 9+ (other browsers do not have this requirement).
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+# Note that this requires a modern browser other than Internet Explorer.
+# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you
+# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files
+# visible. Older versions of IE do not have SVG support.
+
+INTERACTIVE_SVG        = NO
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the
+# \mscfile command).
+
+MSCFILE_DIRS           =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of
+# nodes that will be shown in the graph. If the number of nodes in a graph
+# becomes larger than this value, doxygen will truncate the graph, which is
+# visualized by representing a node as a red box. Note that doxygen if the
+# number of direct children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note
+# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the
+# graphs generated by dot. A depth value of 3 means that only nodes reachable
+# from the root by following a path via at most 3 edges will be shown. Nodes
+# that lay further from the root node will be omitted. Note that setting this
+# option to 1 or 2 may greatly reduce the computation time needed for large
+# code bases. Also note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not
+# seem to support this out of the box. Warning: Depending on the platform used,
+# enabling this option may lead to badly anti-aliased labels on the edges of
+# a graph (i.e. they become hard to read).
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10)
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermediate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP            = YES
diff --git a/docs/doxygen-user/ImageAnalyzer/application_view_tile.png b/docs/doxygen-user/ImageAnalyzer/application_view_tile.png
new file mode 100644
index 0000000000000000000000000000000000000000..3bc0bd32fceb21d70368f7842a00a53d6369ba48
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/application_view_tile.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/bisque.png b/docs/doxygen-user/ImageAnalyzer/bisque.png
new file mode 100644
index 0000000000000000000000000000000000000000..f99cc0d1e57e27b28b0ff80e790861fc3132be4e
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/bisque.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/drawabletile.png b/docs/doxygen-user/ImageAnalyzer/drawabletile.png
new file mode 100644
index 0000000000000000000000000000000000000000..d6b97547bcad5f9d077b45de11b4b5c3c3b2172f
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/drawabletile.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/flag_gray.png b/docs/doxygen-user/ImageAnalyzer/flag_gray.png
new file mode 100644
index 0000000000000000000000000000000000000000..1888f8da96ea4b145de240a906f985540bcf8037
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/flag_gray.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/flag_red.png b/docs/doxygen-user/ImageAnalyzer/flag_red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8a602da7b17a323b2c9afe3d8aac62cb717a0b8
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/flag_red.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/folder_picture.png b/docs/doxygen-user/ImageAnalyzer/folder_picture.png
new file mode 100644
index 0000000000000000000000000000000000000000..052b33638eaa0f870a255bfdd5df5b79fb01a89e
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/folder_picture.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/gray.png b/docs/doxygen-user/ImageAnalyzer/gray.png
new file mode 100644
index 0000000000000000000000000000000000000000..538aaacd4601c0ba9fa7cc15c40f0629aa38b9cf
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/gray.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/green.png b/docs/doxygen-user/ImageAnalyzer/green.png
new file mode 100644
index 0000000000000000000000000000000000000000..6aa2f632112a87d53a1f2b787a1f621600a4f5b5
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/green.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/hashset_hits.png b/docs/doxygen-user/ImageAnalyzer/hashset_hits.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1caff1f30333a7d3dc5ab278472b433a2e6f165
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/hashset_hits.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/orange.png b/docs/doxygen-user/ImageAnalyzer/orange.png
new file mode 100644
index 0000000000000000000000000000000000000000..97ef5d24b63076bfc20c42de1bf828d325e7606d
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/orange.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/purpledash.png b/docs/doxygen-user/ImageAnalyzer/purpledash.png
new file mode 100644
index 0000000000000000000000000000000000000000..edd815d641076d28220f2c1e41a1b7d8953537ce
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/purpledash.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/red.png b/docs/doxygen-user/ImageAnalyzer/red.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e5d76720d9aa032a0e972ea753e4b49a550622e
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/red.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/slide.png b/docs/doxygen-user/ImageAnalyzer/slide.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f1ae4496e4f874742133d42941b2301d23fb95c
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/slide.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/video-file.png b/docs/doxygen-user/ImageAnalyzer/video-file.png
new file mode 100644
index 0000000000000000000000000000000000000000..c290609f59048e61cb28af42c4500709e342f437
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/video-file.png differ
diff --git a/docs/doxygen-user/ImageAnalyzer/yellow.png b/docs/doxygen-user/ImageAnalyzer/yellow.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e593b22d30ac923bd36f3dfea0e92121da94699
Binary files /dev/null and b/docs/doxygen-user/ImageAnalyzer/yellow.png differ
diff --git a/docs/doxygen-user/image_viewer.dox b/docs/doxygen-user/image_viewer.dox
new file mode 100644
index 0000000000000000000000000000000000000000..e7d9666f77a6910ee042b3e4b26bf18dc34f007d
--- /dev/null
+++ b/docs/doxygen-user/image_viewer.dox
@@ -0,0 +1,118 @@
+/*! \page image_viewer_page Image and Video Viewer
+Overview
+========
+This document outlines the use of the new Image Analyzer feature of Autopsy.  This feature was funded by DHS S&T to help provide free and open source digital forensics tools to law enforcement. 
+
+The new image analyzer feature has been designed specifically with child-exploitation cases in mind, but can be used for a variety of other investigation types that involve images and videos. It offers the following features beyond the traditional long list of thumbnails that Autopsy and other tools currently provide. 
+- Groups images by folder (and other attributes) to help examiner break the large set of images into smaller groups and to help focus on areas with images of interest.
+- Allows examiner to start viewing images immediately upon adding them to the case.  As images are hashed, they are updated in the interface.  You do not need to wait until the entire image is ingested.
+
+This document assumes basic familiarity with Autopsy. 
+
+Quick Start 
+===========
+1. The Image Analysis tool can be configured to collect data about images/videos as ingest runs or all at once after ingest.  To change this setting go to Tools->Options->Image /Video Analyzer.  This setting is saved per case, but can not be changed during ingest.
+2. Create a case as normal and add a disk image (or folder of files) as a data source.  Ensure that you have the hash lookup module enabled with NSRL and known bad hashsets, the EXIF module enabled, and the File Type module enabled. 
+3. Click Tools->Analyze Images/Videos in the menu.  This will open the  Autopsy Image/Video Analysis tool in a new window.
+4. Groups of images will be presented as they are analyzed by the background ingest modules.  You can later resort and regroup, but it is required to keep it grouped by folder while ingest is still ongoing. 
+5. As each group is reviewed, the next highest priority group is presented, according to a sorting criteria (the default is the density of hash set hits).
+6. Images that were hits from hashsets, will have a dashed border around them.
+7. You can use the menu bar on the top of the group to categorize the entire group.
+8. You can right click on an image to categorize or tag the individual image.
+9. Tag files with customizable tags. A ‘Follow Up’ tag is already built into the tool and integrated into the filter options.  Tags can be applied in addition to categorization.  An image can only have one categorization, but can have many tags to support your work-flow.
+10. Create a report containing the details of every tagged and/or categorized file, via the standard Autopsy report generation feature.
+
+Use Case Details
+===============
+In addition to the basic ideas presented in the previous section, here are some hints on use cases that were designed into the tool.
+- When you are viewing the groups, they are presented in an order based on density of hash hits(by default).  If you find a group that has lots of interesting files and you want to see what is in the parent folder or nearby folders, use the navigation tree on the left. 
+- At any time, you can use the list on the left-hand side to see the groups with the largest hashset hits. 
+- To see which folders have the most images in them, sort the groups by group size (descending). 
+- Files that have hashset hits are not automatically tagged or categorized.  You need to do that after reviewing them.  The easiest way to do that is to wait until ingest is over and then group by hashsets.  You can then review each group and categorize the entire group at a time using the group header. 
+
+Categories
+==========
+The tool has been designed specifically with child-exploitation cases in mind and has a notion of categorizes.  We will be changing this in the future to be more flexible with custom category names, but currently it is hard coded to use the names that Project Vic  (and other international groups) use.  We have assigned colors to each category to highlight each image. 
+
+
+Name|Description|Color
+----|-----------------|------
+CAT-0|Uncategorized|![gray](ImageAnalyzer/gray.png)
+CAT-1|Child Abuse Material  |![red](ImageAnalyzer/red.png)
+CAT-2|Child Exploitative / Age Difficult|![orange](ImageAnalyzer/orange.png)
+CAT-3|CGI / Animation|![yellow](ImageAnalyzer/yellow.png)
+CAT-4|Comparison Images |![bisque](ImageAnalyzer/bisque.png)
+CAT-5|Non-pertinent|![green](ImageAnalyzer/green.png)
+
+GUI controls
+=================
+You can do your entire investigation using the mouse, but many examiners like to use keyboard shortcuts to quickly process large amounts of images. 
+
+Keyboard Shortcuts
+-----------------
+shortcut | action
+-----------|------
+digits 0-5 | assign the correspondingly numbered category to the selected file(s)
+alt + 0-5  | assign the correspondingly numbered category to all files in the focused group
+arrows | select the next file in the direction pressed
+page up/down | scroll the list of files
+
+Additional Mouse Controls
+-------------------------
+mouse gesture| action
+----------|----------
+ctrl + left click|toggle selection of clicked file, select multiple files
+right click on file|bring up context menu allowing per file actions (tag, categorize, extract to local file, view in external viewer, view in Autopsy content viewer, add file to HashDB)
+right click empty space of group|bring up context menu allowing per group actions (tag, categorize, extract to local file(s), add file(s) to HashDB)
+double click on file|open selected file in slide show mode
+
+UI Details
+==========
+Group Display Area
+-------------------
+The central display area contains the list of files in the current group.  Images in the group can be displayed in either thumbnail mode or slide show mode.  Slide show mode provides larger images and playback of video files. At the right of the group header is a toggle for changing the viewing mode  of the group (tiles  vs slide-show ).
+
+Image/Video Tiles
+-----------------
+
+Each file is represented in the main display area via a small tile.  The tile shows:
+- Thumbnail of the image/video
+- Name of the file
+- Indicators of other important details:
+
+| image | description | meaning|
+|----|----|-----|
+| | solid colored border | file’s assigned category.|
+| ![](ImageAnalyzer/purpledash.png) "" | purple dashed border |   file  has a known bad hashset hit, but has not yet been categorized. |
+| ![](ImageAnalyzer/hashset_hits.png) ""|pushpin |  file  has a known bad hashset hit|
+| ![](ImageAnalyzer/video-file.png) ""| clapboard on document | video file|
+| ![](ImageAnalyzer/flag_red.png) ""| a red flag | file has been 'flagged' as with the follow up tag|
+
+
+Slide Show Mode
+---------------
+In slide show mode a group shows only one file at a time at an increased size.  Per file tag/category controls above the top right corner of the image, and large left and right buttons allow cycling through the files in the group.  If the active file is an Autopsy supported video format, video playback controls appear below the video. 
+
+Table/Tree of contents
+----------------------
+The section in the top left with tabs labeled “Contents” and “Hash Hits” provides an overview of the groups of files in the case.  It changes to reflect the current Group By setting: for hierarchical groupings (path) it shows a tree of folders (folders containing images/videos (groups) are marked with a distinctive icon ), and for other groupings it shows only a flat list.  
+
+Each group shows the number of files that hit against configured Hash DBs during ingest (hash hits) and the total number of image/video files as a ratio (hash hits / total) after its name.  By selecting groups in the tree/list you can navigate directly to them in the main display area.  If the Hash Hits tab is selected only groups containing files that have hash hits are shown.    
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+*/
diff --git a/docs/doxygen-user/installation.dox b/docs/doxygen-user/installation.dox
new file mode 100644
index 0000000000000000000000000000000000000000..125f0e27bc9573f4014a52a4a85e981d6fe064a3
--- /dev/null
+++ b/docs/doxygen-user/installation.dox
@@ -0,0 +1,32 @@
+/*! \page installation_page Installation 
+
+Installation
+-----
+
+Download Autopsy from the website:
+
+http://sleuthkit.org/autopsy/download.php
+
+The current version of Autopsy 3 runs only on Microsoft Windows.
+We have gotten it to run on other platforms, such as Linux and OS X, but we do not have it in a state that makes it easy to distribute and find the needed libraries.
+
+The Windows installer is self contained and will place everything in the needed places. Simply follow the standard prompts for target installation directory.
+
+
+Optimizing Performance
+-----
+
+After installing Autopsy, there are several hardware-based things that we suggest you do to optimize performance:
+
+1) Change the number of parallel pipelines that can be run at a time. The default is 2 pipelines, but this can be increased if you are running on a system with several cores. To do this:
+- Run Autopsy from the start menu or desktop
+- When presented with the case creation screen, cancel/close the menu window
+- Select tools > options 
+- On the first tab, there is a drop down for "number of ingest" threads. We recommend that you set this value to be smaller than the number of cores minus two.  If you set this number too high, performance can degrade because the pipelines are fighting for the same resources.  Testing should be done to find an optimal setting. 
+- After each change, restart Autopsy to let this setting take effect.
+
+2) When making a case, use different drives to store the case and the images. The case directory is where the SQLite database and keyword search index is stored.  This allows the maximum amount of data to be read and written at the same time. 
+
+3) We have had best performance using either local solid state drives or fibre channel-attached SAN storage. 
+
+*/
diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox
new file mode 100644
index 0000000000000000000000000000000000000000..4367b75fa7951a08f6e32797059d8548b33ce5e7
--- /dev/null
+++ b/docs/doxygen-user/main.dox
@@ -0,0 +1,24 @@
+/*! \mainpage Autopsy User's Guide 
+
+
+Overview
+-----
+
+This is the User's Guide for the open source Autopsy platform (http://www.sleuthkit.org/autopsy/). 
+It is a work in progress. We are moving documentation from the JavaHelp system to this document. 
+
+Note that Autopsy 3 is a complete rewrite from Autopsy 2 and none of this document is relevant to Autopsy 2.
+
+Help Topics
+-------
+The following topics are available here:
+
+- \subpage installation_page
+- \subpage quick_start_page
+- \subpage image_viewer_page
+
+If the topic you need is not listed, refer to the Help system in the tool or the wiki (http://wiki.sleuthkit.org/index.php?title=Autopsy_User%27s_Guide). 
+
+*/
+
+
diff --git a/docs/QuickStartGuide/index.html b/docs/doxygen-user/quick_start.dox
similarity index 90%
rename from docs/QuickStartGuide/index.html
rename to docs/doxygen-user/quick_start.dox
index 7bafa3b45209aad7d8ad59d134a455e17500cf28..9f326a271b2e2caf459cabc3557004a3db011324 100644
--- a/docs/QuickStartGuide/index.html
+++ b/docs/doxygen-user/quick_start.dox
@@ -1,30 +1,5 @@
-<html>
+/*! \page quick_start_page Quick Start Guide
 
-    <head>
-        <link rel="stylesheet" href="nbdocs:/org/sleuthkit/autopsy/core/docs/ide.css" type="text/css">
-        <style>
-            h1 { font-size: 145%; color: #666666; }
-            h2 { font-size: 120%; color: #666666; }
-        </style>
-        <title>Autopsy 3 Quick Start Guide</title>
-    </head>
-    <body>
-
-        <p align="center" style="font-size: 145%;"><strong>Autopsy 3 Quick Start Guide</strong></p>
-        <p align="center" style="font-size: 120%;">June 2013</p>
-        <p align="center"><a href="http://www.sleuthkit.org/autopsy/">www.sleuthkit.org/autopsy/</a></p>
-
-
-        <h1>Installation</h1>
-        <p>
-            The current version of Autopsy 3 runs only on Microsoft Windows.  
-            We have gotten it to run on other platforms, such as Linux and OS X, but we do not have it in a state that makes it easy to distribute and find the needed libraries.
-        </p>
-        <p>
-            The Windows installer will make a directory for Autopsy and place all of the needed files inside of it. 
-            The installer includes all dependencies, including Sleuth Kit and Java.
-        </p>
-        <p>Note that Autopsy 3 is a complete rewrite from Autopsy 2 and none of this document is relevant to Autopsy 2.</p> 
 
         <h1>Adding a Data Source (image, local disk, logical files)</h1>
         <p>
@@ -114,6 +89,8 @@ <h2>Ingest Modules</h2>
 
 
         <h1>Analysis Basics</h1>
+
+
         <img src="screenshot.png" alt="Autopsy Screenshot" />
         <p>You will start all of your analysis techniques from the tree on the left.</p>
         <ul>
@@ -217,5 +194,7 @@ <h1>Reporting</h1>
             This work is licensed under a
             <a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/us/">Creative Commons Attribution-Share Alike 3.0 United States License</a>.
         </i></p>
-    </body>
-</html>
+
+
+
+*/
diff --git a/docs/QuickStartGuide/screenshot.png b/docs/doxygen-user/screenshot.png
similarity index 100%
rename from docs/QuickStartGuide/screenshot.png
rename to docs/doxygen-user/screenshot.png
diff --git a/nbproject/project.properties b/nbproject/project.properties
index 5bff9cbf3fc89bedfaeee0a6af130df736940c87..55eff9f604bf9f88edff15c28f2699b181efdd95 100644
--- a/nbproject/project.properties
+++ b/nbproject/project.properties
@@ -9,6 +9,7 @@ app.version=3.1.0
 ### Must be one of: DEVELOPMENT, RELEASE
 #build.type=RELEASE
 build.type=DEVELOPMENT
+project.org.sleuthkit.autopsy.imageanalyzer=ImageAnalyzer
 update_versions=false
 #custom JVM options
 #Note: can be higher on 64 bit systems, should be in sync with build.xml
@@ -27,7 +28,8 @@ modules=\
     ${project.org.sleuthkit.autopsy.thunderbirdparser}:\
     ${project.org.sleuthkit.autopsy.core}:\
     ${project.org.sleuthkit.autopsy.corelibs}:\
-    ${project.org.sleuthkit.autopsy.scalpel}
+    ${project.org.sleuthkit.autopsy.scalpel}:\
+    ${project.org.sleuthkit.autopsy.imageanalyzer}
 project.org.sleuthkit.autopsy.core=Core
 project.org.sleuthkit.autopsy.corelibs=CoreLibs
 project.org.sleuthkit.autopsy.keywordsearch=KeywordSearch