initial commit master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 27 Sep 2026 15:57:17 +0000 (18:57 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 27 Sep 2026 15:57:17 +0000 (18:57 +0300)
26 files changed:
.gitignore [new file with mode: 0755]
COPYING [new file with mode: 0644]
Documentation/index.org [new file with mode: 0644]
Documentation/screenshot.png [new file with mode: 0644]
Tools/Open with IntelliJ IDEA [new file with mode: 0755]
Tools/Update web site [new file with mode: 0755]
install [new file with mode: 0755]
instantlauncher [new file with mode: 0755]
pom.xml [new file with mode: 0644]
src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/Main.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/Utils.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java [new file with mode: 0755]
src/test/resources/exampleMenu/Moon.jpeg [new file with mode: 0644]
src/test/resources/exampleMenu/another directory/sample.txt [new file with mode: 0644]
src/test/resources/exampleMenu/broken link [new symlink]
src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg [new file with mode: 0644]
src/test/resources/exampleMenu/chosen subdirectory/sample.txt [new file with mode: 0644]
src/test/resources/exampleMenu/executable.sh [new file with mode: 0755]
src/test/resources/exampleMenu/sample link [new symlink]

diff --git a/.gitignore b/.gitignore
new file mode 100755 (executable)
index 0000000..46bbcca
--- /dev/null
@@ -0,0 +1,7 @@
+/.classpath
+/target/
+/bin
+/.settings
+/.idea/
+/*.iml
+*.html
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..0e259d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/Documentation/index.org b/Documentation/index.org
new file mode 100644 (file)
index 0000000..141ef0d
--- /dev/null
@@ -0,0 +1,180 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Instantlauncher - navigate to your files and programs quickly!
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+* Introduction
+
+Quick filesystem based document/application launcher. It starts by
+showing content of [[id:5ced6bf5-165a-49a0-8de4-3755d810d987][predefined directory]]. Sub directories can be chosen
+by simply moving mouse pointer over corresponding directory.
+
+[[file:screenshot.png]]
+
+Files types are color-coded.
+
+** File type colors
+:PROPERTIES:
+:ID:       ab6a1982-b856-403c-9960-43680720399a
+:END:
+
+Colors come from the file type (selected entry = brighter shade):
+
++ blue :: Directory - including a symlink that points to a directory
++ red :: Executable file
++ green :: Non-executable file
++ olive/yellow :: Anything that is neither directory nor regular file,
+  e.g. a broken symlink
+
+Note: a symlink pointing to a file takes the *file* colors
+(red/green), not blue. The Usage table's "Directory/symlink (blue)"
+holds only for symlinks pointing to directories.
+
+** Usage
+Simply hovering mouse pointer over directory expands its contents in
+the next right column.
+
+
++ Executable file (red):
+  + Left mouse click:
+    + Run executable file.
+  + Right mouse click:
+    + Toggle executable state.
+
++ Non-executable file (green):
+  + Left mouse click:
+    + Open file using corresponding application.
+  + Right mouse click:
+    + Toggle executable state.
+
++ Directory/symlink (blue):
+  + Left mouse click:
+    + Open directory using configured filesystem browser.
+  + Right mouse click:
+    + Open directory using configured text based terminal emulator.
+
+* Installation
+:PROPERTIES:
+:ID:       aea98e61-17aa-4c69-acd3-a11e043088a6
+:END:
+
+Run the install script from the project root (asks for sudo):
+#+BEGIN_SRC sh
+./install
+#+END_SRC
+
+The script does everything: builds the jar (=mvn clean package=,
+requires JDK 21 and Maven), copies =instantlauncher.jar= and the
+=instantlauncher= launcher script to =/opt/instantlauncher/=, and
+symlinks the launcher into =/usr/bin/=.
+
+After that the program starts by typing =instantlauncher= in a terminal.
+Only a Java runtime is needed at launch; the launcher script runs
+=java -jar /opt/instantlauncher/instantlauncher.jar=.
+
+The jar name carries no version number, so these scripts never change
+when the project version does. Re-running =install= is safe: it
+rebuilds and updates the installed copy.
+
+** Configuration
+When instantlauncher starts, it looks for configuration file under
+: ~/.instantlauncher
+
+If file is not found, it creates new default configuration file in
+YAML format:
+#+BEGIN_SRC
+!configuration
+directoryOpenCommand: nautilus -w {file}
+directoryTerminalOpenCommand: gnome-terminal --working-directory={file}
+fileAssociations:
+-  command: vlc {file}
+   fileRegex: .*\.mkv$
+-  command: emacs {file}
+   fileRegex: .*\.txt$
+-  command: eog {file}
+   fileRegex: .*\.jpeg$
+-  command: eog {file}
+   fileRegex: .*\.png$
+-  command: vlc {file}
+   fileRegex: .*\.avi$
+-  command: vlc {file}
+   fileRegex: .*\.mp4$
+-  command: eog {file}
+   fileRegex: .*\.jpg$
+-  command: emacs {file}
+   fileRegex: .*\.org$
+navigationRootPath: /home/svjatoslav/
+#+END_SRC
+
+*** navigationRootPath
+    :PROPERTIES:
+    :ID:       5ced6bf5-165a-49a0-8de4-3755d810d987
+    :END:
+Root directory from where to start navigation.
+
+This parameter is user specific! In my case instantlauncher by default
+pointed this to my home directory. But it can be any directory user
+chooses.
+
+I think good use-case is to create special directory to be used for
+instantlauncher root. This special root directory can in turn contain
+symlinks to various places user wants to visit often. Instantlauncher
+will follow symlinks as if they were directories.
+
+: navigationRootPath: /home/svjatoslav/
+
+*** fileAssociations
+Allows to map file name/extension using regular expression pattern
+matching to corresponding application.
+
+Example:
+#+BEGIN_SRC conf
+-  command: vlc {file}
+   fileRegex: .*\.mkv$
+#+END_SRC
+
+Where:
+- command :: Command to execute to open given file. Special variable
+             {file} will point to user chosen file using absolute file
+             path.
+
+- fileRegex :: Regular expression that must match the file.
+
+*** directoryOpenCommand
+Command to use when opening directory.
+
+By default instantlauncher assumes that Gnome desktop environment is
+installed and uses Nautilus file manager:
+: directoryOpenCommand: nautilus -w {file}
+
+*** directoryTerminalOpenCommand
+Command to use when opening directory in text terminal based console.
+
+By default instantlauncher assumes that Gnome desktop environment is
+installed and uses Gnome Terminal:
+
+: directoryTerminalOpenCommand: gnome-terminal --working-directory={file}
+* Source code
+- This program is free software: released under Creative Commons Zero
+  (CC0) license
+
+- Program author:
+  - Svjatoslav Agejenko
+  - Homepage: https://svjatoslav.eu
+  - Email: mailto://svjatoslav@svjatoslav.eu
+
+- [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]]
+
+** Source code
+- [[https://www2.svjatoslav.eu/gitweb/?p=instantlauncher.git;a=snapshot;h=HEAD;sf=tgz][Download latest snapshot in TAR GZ format]]
+
+- [[https://www2.svjatoslav.eu/gitweb/?p=instantlauncher.git;a=summary][Browse Git repository online]]
+
+- Clone Git repository using command:
+  : git clone https://www2.svjatoslav.eu/git/instantlauncher.git
+
diff --git a/Documentation/screenshot.png b/Documentation/screenshot.png
new file mode 100644 (file)
index 0000000..5b840af
Binary files /dev/null and b/Documentation/screenshot.png differ
diff --git a/Tools/Open with IntelliJ IDEA b/Tools/Open with IntelliJ IDEA
new file mode 100755 (executable)
index 0000000..de9bae5
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+#
+# This is a helper bash script that starts IntelliJ with the current project.
+# Script is written is such a way that you can simply click on it in file
+# navigator to run it.
+#
+#
+# Script assumes:
+#
+#    + GNU operating system
+#    + IntelliJ is installed and commandline launcher "idea" is enabled.
+#
+
+cd "${0%/*}"
+cd ..
+
+setsid idea . &>/dev/null
diff --git a/Tools/Update web site b/Tools/Update web site
new file mode 100755 (executable)
index 0000000..3d35617
--- /dev/null
@@ -0,0 +1,89 @@
+#!/bin/bash
+cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi;
+
+cd ..
+
+# Function to export org to html using emacs in batch mode
+export_org_to_html() {
+    local org_file=$1
+    local dir=$(dirname "$org_file")
+    local base=$(basename "$org_file" .org)
+    (
+        cd "$dir" || return 1
+        local html_file="${base}.html"
+        if [ -f "$html_file" ]; then
+            rm -f "$html_file"
+        fi
+        echo "Exporting: $org_file → $dir/$html_file"
+        emacs --batch -l ~/.emacs --visit="${base}.org" --funcall=org-html-export-to-html --kill
+        if [ $? -eq 0 ]; then
+            echo "✓ Successfully exported $org_file"
+        else
+            echo "✗ Failed to export $org_file"
+            return 1
+        fi
+    )
+}
+
+export_org_files_to_html() {
+    echo "🔍 Searching for .org files in Documentation/ ..."
+    echo "======================================="
+
+    mapfile -t ORG_FILES < <(find Documentation -type f -name "*.org" | sort)
+
+    if [ ${#ORG_FILES[@]} -eq 0 ]; then
+        echo "❌ No .org files found!"
+        return 1
+    fi
+
+    echo "Found ${#ORG_FILES[@]} .org file(s):"
+    printf '%s\n' "${ORG_FILES[@]}"
+    echo "======================================="
+
+    SUCCESS_COUNT=0
+    FAILED_COUNT=0
+
+    for org_file in "${ORG_FILES[@]}"; do
+        export_org_to_html "$org_file"
+        if [ $? -eq 0 ]; then
+            ((SUCCESS_COUNT++))
+        else
+            ((FAILED_COUNT++))
+        fi
+    done
+
+    echo "======================================="
+    echo "📊 SUMMARY:"
+    echo "   ✓ Successful: $SUCCESS_COUNT"
+    echo "   ✗ Failed:     $FAILED_COUNT"
+    echo "   Total:        $((SUCCESS_COUNT + FAILED_COUNT))"
+    echo ""
+
+    return $FAILED_COUNT
+}
+
+# Compile Emacs org-mode files into HTML format
+export_org_files_to_html
+
+if [ $? -ne 0 ]; then
+    echo "✗ HTML export failed - NOT publishing."
+    echo ""
+    echo "Press ENTER to close this window."
+    read
+    exit 1
+fi
+
+# Upload assembled documentation to server
+echo "📤 Uploading to server..."
+rsync -avz --delete  -e 'ssh -p 10006' Documentation/ \
+      n0@www3.svjatoslav.eu:/mnt/big/projects/instantlauncher/
+
+if [ $? -eq 0 ]; then
+    echo "✓ Upload completed successfully!"
+else
+    echo "✗ Upload failed!"
+fi
+
+echo ""
+echo "Press ENTER to close this window."
+read
diff --git a/install b/install
new file mode 100755 (executable)
index 0000000..770fe25
--- /dev/null
+++ b/install
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# Requires: JDK 21 and Maven to build, sudo to install.
+# Idempotent: safe to re-run, updates the installed copy.
+
+mvn clean package
+
+sudo mkdir -p /opt/instantlauncher
+sudo cp target/instantlauncher.jar /opt/instantlauncher/
+sudo cp instantlauncher /opt/instantlauncher/
+sudo ln -sf /opt/instantlauncher/instantlauncher /usr/bin/
diff --git a/instantlauncher b/instantlauncher
new file mode 100755 (executable)
index 0000000..f56287b
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -f
+
+java -jar /opt/instantlauncher/instantlauncher.jar "$@"
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..f752dc9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,117 @@
+<project
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xmlns="http://maven.apache.org/POM/4.0.0"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>eu.svjatoslav</groupId>
+    <artifactId>instantlauncher</artifactId>
+    <version>1.0</version>
+    <packaging>jar</packaging>
+    <name>Instant Launcher</name>
+    <description>popup panel with shortcuts for quick application launching</description>
+
+    <properties>
+        <java.version>21</java.version>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>eu.svjatoslav</groupId>
+            <artifactId>svjatoslavcommons</artifactId>
+            <version>1.9</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.esotericsoftware.yamlbeans</groupId>
+            <artifactId>yamlbeans</artifactId>
+            <version>1.12</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>instantlauncher</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>21</source>
+                    <target>21</target>
+                    <optimize>true</optimize>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>eu.svjatoslav.instantlauncher.Main</mainClass>
+                        </manifest>
+                    </archive>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+
+                <executions>
+
+                    <execution>
+                        <id>package-jar-with-dependencies</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <appendAssemblyId>false</appendAssemblyId>
+                            <descriptorRefs>
+                                <descriptorRef>jar-with-dependencies</descriptorRef>
+                            </descriptorRefs>
+                            <archive>
+                                <manifest>
+                                    <mainClass>eu.svjatoslav.instantlauncher.Main</mainClass>
+                                </manifest>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-release-plugin</artifactId>
+                <version>2.5.2</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.apache.maven.scm</groupId>
+                        <artifactId>maven-scm-provider-gitexe</artifactId>
+                        <version>1.9.4</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+
+        </plugins>
+    </build>
+
+
+    <repositories>
+        <repository>
+            <id>svjatoslav.eu</id>
+            <name>Svjatoslav repository</name>
+            <url>https://www3.svjatoslav.eu/maven/</url>
+        </repository>
+    </repositories>
+
+    <organization>
+        <name>svjatoslav.eu</name>
+        <url>https://svjatoslav.eu</url>
+    </organization>
+
+
+</project>
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java b/src/main/java/eu/svjatoslav/instantlauncher/InstantLauncher.java
new file mode 100755 (executable)
index 0000000..02fc7a4
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher;
+
+import eu.svjatoslav.instantlauncher.configuration.ConfigurationManager;
+import eu.svjatoslav.instantlauncher.configuration.FileAssociation;
+
+import java.io.File;
+import java.io.IOException;
+
+import static eu.svjatoslav.instantlauncher.Utils.runOpeningApplication;
+
+public class InstantLauncher {
+
+    private final MainFrame mainFrame;
+    public ConfigurationManager configurationManager;
+
+    public InstantLauncher() throws IOException {
+        configurationManager = new ConfigurationManager();
+        mainFrame = new MainFrame(this);
+    }
+
+    public static void exitProgram() {
+        System.exit(0);
+    }
+
+    /**
+     * @return <code>true</code> if file was opened. <code>false</code> if
+     * unknown file type.
+     */
+    public boolean openFile(final File file) {
+        FileAssociation fileAssociation = configurationManager.getConfiguration().findFileAssociation(file);
+        if (fileAssociation == null) return false;
+
+        runOpeningApplication(fileAssociation.command, file);
+        return true;
+    }
+
+    public void run() {
+        mainFrame.show();
+    }
+
+    public void openDirectory(File chosenFile) {
+        runOpeningApplication(configurationManager.getConfiguration().directoryOpenCommand, chosenFile);
+    }
+
+    public void openDirectoryInTerminal(File chosenFile) {
+        runOpeningApplication(configurationManager.getConfiguration().directoryTerminalOpenCommand, chosenFile);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/Main.java b/src/main/java/eu/svjatoslav/instantlauncher/Main.java
new file mode 100755 (executable)
index 0000000..a849cc7
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher;
+
+import java.io.IOException;
+
+class Main {
+
+    public static void main(final String[] args) throws IOException {
+        new InstantLauncher().run();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java b/src/main/java/eu/svjatoslav/instantlauncher/MainFrame.java
new file mode 100755 (executable)
index 0000000..3977104
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher;
+
+import eu.svjatoslav.instantlauncher.menu.MultiLevelMenu;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+class MainFrame {
+
+    private final InstantLauncher instantLauncher;
+    private JFrame frame;
+    private boolean isShowing;
+
+    public MainFrame(final InstantLauncher instantLauncher) {
+        this.instantLauncher = instantLauncher;
+    }
+
+    private void buildContentPane() {
+
+        final MultiLevelMenu multiLevelMenu = new MultiLevelMenu(instantLauncher);
+        frame.getContentPane().add(multiLevelMenu.buildContentPanel(), BorderLayout.CENTER);
+
+    }
+
+    public void show() {
+
+        if (!isShowing) {
+            // initialize frame
+            frame = new JFrame("InstantLauncher");
+
+            buildContentPane();
+
+            // registor window listener
+            final FrameWindowListener windowListener = new FrameWindowListener();
+            frame.addWindowListener(windowListener);
+
+            // maximize window
+            frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
+            frame.setVisible(true);
+
+            isShowing = true;
+        }
+
+    }
+
+    private static class FrameWindowListener implements WindowListener {
+
+        @Override
+        public void windowActivated(final WindowEvent e) {
+        }
+
+        @Override
+        public void windowClosed(final WindowEvent e) {
+        }
+
+        @Override
+        public void windowClosing(final WindowEvent e) {
+            InstantLauncher.exitProgram();
+        }
+
+        @Override
+        public void windowDeactivated(final WindowEvent e) {
+        }
+
+        @Override
+        public void windowDeiconified(final WindowEvent e) {
+        }
+
+        @Override
+        public void windowIconified(final WindowEvent e) {
+        }
+
+        @Override
+        public void windowOpened(final WindowEvent e) {
+        }
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/Utils.java b/src/main/java/eu/svjatoslav/instantlauncher/Utils.java
new file mode 100755 (executable)
index 0000000..7410bab
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher;
+
+import eu.svjatoslav.commons.gui.dialog.ExceptionDialog;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+
+public class Utils {
+
+    private static final String FILE_INDICATOR = "{file}";
+
+    public static void setComponentSize(JComponent component, Dimension size) {
+        component.setMinimumSize(size);
+        component.setMaximumSize(size);
+        component.setSize(size);
+        component.setPreferredSize(size);
+    }
+
+    public static void runOpeningApplication(String commands, final File file) {
+        try {
+            String[] commandsArray = commands.split("\\s+");
+            for (int i=0; i< commandsArray.length; i++)
+                commandsArray[i] = commandsArray[i].replaceAll("\\{file\\}", file.getAbsolutePath());
+
+            Runtime.getRuntime().exec(commandsArray);
+        } catch (final IOException e) {
+            new ExceptionDialog(e);
+        }
+    }
+
+    public static void executeCommand(String... c) {
+        try {
+            Runtime.getRuntime().exec(c);
+        } catch (final IOException e) {
+            new ExceptionDialog(e);
+        }
+    }
+
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/Configuration.java
new file mode 100644 (file)
index 0000000..7dac14c
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.configuration;
+
+import java.io.File;
+import java.util.Set;
+
+public class Configuration {
+    public Set<FileAssociation> fileAssociations;
+    public String navigationRootPath;
+    public String directoryOpenCommand;
+    public String directoryTerminalOpenCommand;
+
+    public FileAssociation findFileAssociation(File file) {
+        for (FileAssociation association : fileAssociations)
+            if (association.matchesFile(file)) return association;
+
+        return null;
+    }
+
+    public File getNavigationRootDirectory() {
+        return new File(navigationRootPath);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/ConfigurationManager.java
new file mode 100755 (executable)
index 0000000..5a2e512
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.configuration;
+
+import com.esotericsoftware.yamlbeans.YamlConfig;
+import com.esotericsoftware.yamlbeans.YamlReader;
+import com.esotericsoftware.yamlbeans.YamlWriter;
+
+import java.io.*;
+import java.util.HashSet;
+
+public class ConfigurationManager {
+
+    private static final String CONFIG_FILE_NAME = ".instantlauncher";
+    private Configuration configuration;
+
+    public ConfigurationManager() throws IOException {
+        initConfig();
+    }
+
+    public Configuration getConfiguration() {
+        return configuration;
+    }
+
+    private File getPropertiesFile() {
+        return new File(System.getProperty("user.home") + "/" + CONFIG_FILE_NAME);
+    }
+
+
+    private void initConfig() throws IOException {
+        final File propertiesFile = getPropertiesFile();
+        if (!propertiesFile.exists()) {
+            initDefaultConfiguration();
+        } else
+            loadConfigFile(propertiesFile);
+    }
+
+    private void loadConfigFile(File propertiesFile) throws IOException {
+        YamlReader reader = new YamlReader(new FileReader(propertiesFile));
+        configureYaml(reader.getConfig());
+        configuration = reader.read(Configuration.class);
+        if (configuration == null)
+            initDefaultConfiguration();
+    }
+
+    private void saveConfigFile() throws IOException {
+        YamlWriter writer = new YamlWriter(new FileWriter(getPropertiesFile()));
+        configureYaml(writer.getConfig());
+        writer.write(configuration);
+        writer.close();
+    }
+
+    private void configureYaml(YamlConfig config) {
+        config.setClassTag("configuration",  Configuration.class);
+        config.setPropertyElementType(Configuration.class, "fileAssociations", FileAssociation.class);
+    }
+
+    private void registerFileAssociation(String fileRegex, String command) {
+        FileAssociation association = new FileAssociation(fileRegex, command);
+        configuration.fileAssociations.add(association);
+    }
+
+    private void initDefaultConfiguration() throws IOException {
+        configuration = new Configuration();
+        configuration.fileAssociations = new HashSet<>();
+
+        configuration.navigationRootPath = System.getProperty("user.home") + "/";
+        configuration.directoryOpenCommand = "nautilus -w {file}";
+        configuration.directoryTerminalOpenCommand = "gnome-terminal --working-directory={file}";
+
+        registerFileAssociation(".*\\.jpeg$", "eog {file}");
+        registerFileAssociation(".*\\.jpg$", "eog {file}");
+        registerFileAssociation(".*\\.png$", "eog {file}");
+        registerFileAssociation(".*\\.txt$", "emacs {file}");
+        registerFileAssociation(".*\\.org$", "emacs {file}");
+        registerFileAssociation(".*\\.avi$", "vlc {file}");
+        registerFileAssociation(".*\\.mp4$", "vlc {file}");
+        registerFileAssociation(".*\\.mkv$", "vlc {file}");
+
+        saveConfigFile();
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java b/src/main/java/eu/svjatoslav/instantlauncher/configuration/FileAssociation.java
new file mode 100644 (file)
index 0000000..9f3a3be
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.configuration;
+
+import java.io.File;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FileAssociation implements Comparable<FileAssociation> {
+    public String command;
+    public String fileRegex;
+
+    public FileAssociation() {
+    }
+
+    public FileAssociation(String fileRegex, String command) {
+        this.fileRegex = fileRegex;
+        this.command = command;
+    }
+
+    @Override
+    public int compareTo(FileAssociation o) {
+        return fileRegex.compareTo(o.fileRegex);
+    }
+
+    public boolean matchesFile(File file) {
+        String absolutePath = file.getAbsolutePath();
+
+        Pattern pattern = Pattern.compile(fileRegex);
+        Matcher matcher = pattern.matcher(absolutePath);
+        return matcher.matches();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/Menu.java
new file mode 100755 (executable)
index 0000000..3c50d37
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.menu;
+
+import eu.svjatoslav.commons.gui.dialog.ExceptionDialog;
+import eu.svjatoslav.instantlauncher.Utils;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+class Menu {
+
+    public static final Dimension SIZE_MENU_PANEL = new Dimension(210, 900);
+    private static final Color BUTTON_BG_DIRECTORY = new Color(0, 0, 60);
+
+    private static final Color BUTTON_BG_DIRECTORY_SELECTED = new Color(0, 0, 100);
+    private static final Color BUTTON_BG_LINK = new Color(60, 60, 0);
+
+    private static final Color BUTTON_BG_LINK_SELECTED = new Color(100, 100, 0);
+    private static final Color BUTTON_BG_EXECUTABLE_FILE = new Color(60, 0, 0);
+
+    private static final Color BUTTON_BG_EXECUTABLE_FILE_SELECTED = new Color(100, 0, 0);
+    private static final Color BUTTON_BG_NONEXECUTABLE_FILE = new Color(0, 60, 0);
+
+    private static final Color BUTTON_BG_NONEXECUTABLE_FILE_SELECTED = new Color(0, 100, 0);
+    private static final Dimension SIZE_BUTTON = new Dimension(200, 30);
+    private final Set<MenuListener> menuListeners = new HashSet<>();
+    private JPanel menuPanel;
+    private JButton selectedButton;
+    private File selectedFile;
+
+    public Menu(final File directory) {
+        buildContentPanel(directory);
+    }
+
+    /**
+     * <pre>
+     * Assign specific colors to:
+     *    Directory
+     *    Executable file
+     *    Non executable file
+     * </pre>
+     */
+    private static Color getFileColor(final File file, final boolean selected) {
+        if (file.isDirectory())
+            if (selected)
+                return BUTTON_BG_DIRECTORY_SELECTED;
+            else
+                return BUTTON_BG_DIRECTORY;
+
+        if (file.isFile())
+            if (file.canExecute()) {
+                if (selected)
+                    return BUTTON_BG_EXECUTABLE_FILE_SELECTED;
+                else
+                    return BUTTON_BG_EXECUTABLE_FILE;
+            } else if (selected)
+                return BUTTON_BG_NONEXECUTABLE_FILE_SELECTED;
+            else
+                return BUTTON_BG_NONEXECUTABLE_FILE;
+
+        if (selected)
+            return BUTTON_BG_LINK_SELECTED;
+        else
+            return BUTTON_BG_LINK;
+    }
+
+    public void addMenuListener(final MenuListener menuListener) {
+        menuListeners.add(menuListener);
+    }
+
+    private JButton buildButton(final File file) {
+        JButton button;
+        if (file.isDirectory())
+            button = new JButton("[ " + file.getName() + " ]");
+        else
+            button = new JButton(file.getName());
+
+        button.setBackground(getFileColor(file, false));
+        button.setForeground(new Color(255, 255, 255));
+
+        button.setBorder(new EmptyBorder(5, 10, 5, 10));
+
+        final Menu.ButtonMouseListener mouseListener = new ButtonMouseListener(button, this, file);
+        button.addMouseListener(mouseListener);
+
+        button.setHorizontalTextPosition(SwingConstants.CENTER);
+
+        // set button size
+        Utils.setComponentSize(button, SIZE_BUTTON);
+
+        return button;
+    }
+
+    private void buildContentPanel(final File directory) {
+        menuPanel = new JPanel();
+
+        Utils.setComponentSize(menuPanel, SIZE_MENU_PANEL);
+
+        final BoxLayout layout = new BoxLayout(menuPanel, BoxLayout.Y_AXIS);
+        menuPanel.setLayout(layout);
+        menuPanel.setBackground(Color.BLACK);
+
+        final File[] listFiles = directory.listFiles();
+
+        // Sort files by name
+        Arrays.sort(listFiles, (f1, f2) -> {
+
+            // directories come before files
+            if (f1.isFile() && f2.isDirectory())
+                return 1;
+            if (f2.isFile() && f1.isDirectory())
+                return -1;
+
+            // order by file name
+            return f1.getName().toUpperCase().compareTo(f2.getName().toUpperCase());
+        });
+
+        // add files to the panel
+        for (final File file : listFiles)
+            // ignore hidden files
+            if (!file.getName().startsWith(".")) {
+                menuPanel.add(Box.createVerticalStrut(10));
+                menuPanel.add(buildButton(file));
+            }
+    }
+
+    public JPanel getMenuPanel() {
+        return menuPanel;
+    }
+
+    private JButton getSelectedButton() {
+        return selectedButton;
+    }
+
+    private File getSelectedFile() {
+        return selectedFile;
+    }
+
+    private void menuItemHighlighted(final File chosenFile) {
+        for (final MenuListener menuListener : menuListeners)
+            menuListener.menuItemHighlighted(this, chosenFile);
+    }
+
+    private void menuItemSelected(final File chosenFile) {
+        for (final MenuListener menuListener : menuListeners)
+            menuListener.menuItemSelected(chosenFile);
+    }
+
+    private void menuItemSelectedAlternative(final File chosenFile) {
+        for (final MenuListener menuListener : menuListeners)
+            menuListener.menuItemSelectedAlternative(chosenFile);
+    }
+
+    private void menuItemSelectedAlternative2(final File chosenFile) {
+        for (final MenuListener menuListener : menuListeners)
+            menuListener.menuItemSelectedAlternative2(chosenFile);
+    }
+
+    private void setSelected(final JButton selectedButton, final File selectedFile) {
+        this.selectedButton = selectedButton;
+        this.selectedFile = selectedFile;
+    }
+
+    public static class ButtonMouseListener implements MouseListener {
+
+        final JButton parentButton;
+        final Menu parentMenu;
+        final File file;
+
+        public ButtonMouseListener(final JButton button, final Menu parentMenu, final File file) {
+            parentButton = button;
+            this.parentMenu = parentMenu;
+            this.file = file;
+        }
+
+        @Override
+        public void mouseClicked(final MouseEvent e) {
+            try {
+
+                if (e.getButton() == 1) // left mouse click
+                    parentMenu.menuItemSelected(file);
+                if (e.getButton() == 2) // middle mouse click
+                    parentMenu.menuItemSelectedAlternative2(file);
+                if (e.getButton() == 3) //right mouse click
+                    parentMenu.menuItemSelectedAlternative(file);
+            } catch (final Exception ex) {
+                new ExceptionDialog(ex);
+            }
+        }
+
+        @Override
+        public void mouseEntered(final MouseEvent e) {
+            try {
+                if (parentButton != parentMenu.getSelectedButton()) {
+
+                    if (parentMenu.getSelectedButton() != null)
+                        parentMenu.getSelectedButton().setBackground(getFileColor(parentMenu.getSelectedFile(), false));
+
+                    parentMenu.setSelected(parentButton, file);
+                    parentButton.setBackground(getFileColor(file, true));
+
+                    parentMenu.menuItemHighlighted(file);
+                }
+            } catch (final Exception ex) {
+                new ExceptionDialog(ex);
+            }
+        }
+
+        @Override
+        public void mouseExited(final MouseEvent e) {
+        }
+
+        @Override
+        public void mousePressed(final MouseEvent e) {
+        }
+
+        @Override
+        public void mouseReleased(final MouseEvent e) {
+        }
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/MenuListener.java
new file mode 100755 (executable)
index 0000000..d19317d
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.menu;
+
+import java.io.File;
+
+interface MenuListener {
+
+    /**
+     * mouse over
+     */
+    void menuItemHighlighted(Menu menu, java.io.File chosenFile);
+
+    /**
+     * left click
+     */
+    void menuItemSelected(File chosenFile);
+
+    /**
+     * right click
+     */
+    void menuItemSelectedAlternative(File chosenFile);
+
+    /**
+     * middle click
+     */
+    void menuItemSelectedAlternative2(File chosenFile);
+
+}
diff --git a/src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java b/src/main/java/eu/svjatoslav/instantlauncher/menu/MultiLevelMenu.java
new file mode 100755 (executable)
index 0000000..d47b05c
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * Instantlauncher. Author: Svjatoslav Agejenko, svjatoslav@svjatoslav.eu
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.instantlauncher.menu;
+
+import eu.svjatoslav.instantlauncher.InstantLauncher;
+import eu.svjatoslav.instantlauncher.Utils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import static eu.svjatoslav.instantlauncher.Utils.executeCommand;
+
+public class MultiLevelMenu implements MenuListener {
+
+    private static final int VERTICAL_MENUS_COUNT = 7;
+    private static final Dimension CONTENT_PANEL_SIZE = new Dimension(1024, 900);
+    private final InstantLauncher instantLauncher;
+    private final ArrayList<Menu> menus = new ArrayList<>();
+
+    private final ArrayList<JPanel> panels = new ArrayList<>();
+    private JPanel contentPane;
+
+    public MultiLevelMenu(final InstantLauncher instantLauncher) {
+        this.instantLauncher = instantLauncher;
+    }
+
+    /**
+     * Adds new vertical menu.
+     *
+     * @return <code>true</code> if operation succeeded, or <code>false</code>
+     * if there was not enough space left for a new menu.
+     */
+    private boolean addMenu(final File directory) {
+        final int newMenuHorizontalIndex = menus.size();
+
+        if (newMenuHorizontalIndex >= VERTICAL_MENUS_COUNT) {
+            return false;
+        }
+
+        final Menu menu = new Menu(directory);
+        menu.addMenuListener(this);
+
+        final JPanel placeholderPanel = panels.get(newMenuHorizontalIndex);
+        final JPanel menuPanel = menu.getMenuPanel();
+        placeholderPanel.add(menuPanel, BorderLayout.CENTER);
+
+        menus.add(menu);
+        return true;
+    }
+
+    public JPanel buildContentPanel() {
+
+        contentPane = new JPanel();
+        contentPane.setBackground(Color.BLACK);
+        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS));
+        Utils.setComponentSize(contentPane, CONTENT_PANEL_SIZE);
+
+        // initialize panels
+        for (int i = 0; i < VERTICAL_MENUS_COUNT; i++) {
+            contentPane.add(Box.createHorizontalStrut(10));
+
+            final JPanel panel = new JPanel(new BorderLayout());
+            panel.setBackground(Color.BLACK);
+
+            Utils.setComponentSize(panel, Menu.SIZE_MENU_PANEL);
+
+            panels.add(panel);
+            contentPane.add(panel);
+        }
+
+        final File directory = instantLauncher.configurationManager.getConfiguration().getNavigationRootDirectory();
+        addMenu(directory);
+
+        return contentPane;
+    }
+
+    private int getMenuIndex(final Menu menu) {
+        int i = 0;
+        for (final Menu m : menus) {
+            if (m == menu) {
+                return i;
+            }
+            i++;
+        }
+        return -1;
+    }
+
+    @Override
+    public void menuItemHighlighted(final Menu menu, final File chosenFile) {
+
+        if (chosenFile.isDirectory()) {
+            final int menuIndex = getMenuIndex(menu);
+
+            if (menuIndex >= 0) {
+                removeMenus(menuIndex + 1);
+                if (addMenu(chosenFile)) {
+                    contentPane.validate();
+                    contentPane.repaint();
+                }
+            }
+        }
+
+    }
+
+    @Override
+    public void menuItemSelected(final File chosenFile) {
+        if (chosenFile.isFile()) {
+            if (chosenFile.canExecute()) {
+                executeCommand(chosenFile.getAbsolutePath());
+                InstantLauncher.exitProgram();
+            } else {
+                boolean fileOpened;
+                try {
+                    fileOpened = instantLauncher.openFile(chosenFile.getCanonicalFile());
+                    if (fileOpened)
+                        InstantLauncher.exitProgram();
+
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+
+        if (chosenFile.isDirectory()) {
+            try {
+                instantLauncher.openDirectory(chosenFile.getCanonicalFile());
+                InstantLauncher.exitProgram();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    @Override
+    public void menuItemSelectedAlternative(final File chosenFile) {
+        if (chosenFile.isFile())
+            chosenFile.setExecutable(!chosenFile.canExecute());
+        else {
+            instantLauncher.openDirectoryInTerminal(chosenFile);
+            InstantLauncher.exitProgram();
+        }
+    }
+
+    private void removeMenus(final int fromIndex) {
+
+        for (int i = fromIndex; i < menus.size(); i++) {
+            final JPanel jPanel = panels.get(i);
+            jPanel.removeAll();
+        }
+
+        while (menus.size() > fromIndex) {
+            menus.remove(fromIndex);
+        }
+    }
+
+    @Override
+    public void menuItemSelectedAlternative2(File chosenFile) {
+        // TODO: define some middle mouse click functions
+    }
+
+
+}
diff --git a/src/test/resources/exampleMenu/Moon.jpeg b/src/test/resources/exampleMenu/Moon.jpeg
new file mode 100644 (file)
index 0000000..c3beeac
Binary files /dev/null and b/src/test/resources/exampleMenu/Moon.jpeg differ
diff --git a/src/test/resources/exampleMenu/another directory/sample.txt b/src/test/resources/exampleMenu/another directory/sample.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/test/resources/exampleMenu/broken link b/src/test/resources/exampleMenu/broken link
new file mode 120000 (symlink)
index 0000000..86a410d
--- /dev/null
@@ -0,0 +1 @@
+broken
\ No newline at end of file
diff --git a/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg b/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg
new file mode 100644 (file)
index 0000000..33d6eef
Binary files /dev/null and b/src/test/resources/exampleMenu/chosen subdirectory/Sunrise over Tallinn.jpeg differ
diff --git a/src/test/resources/exampleMenu/chosen subdirectory/sample.txt b/src/test/resources/exampleMenu/chosen subdirectory/sample.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/test/resources/exampleMenu/executable.sh b/src/test/resources/exampleMenu/executable.sh
new file mode 100755 (executable)
index 0000000..8bb9f02
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+xclock
+
+
+
diff --git a/src/test/resources/exampleMenu/sample link b/src/test/resources/exampleMenu/sample link
new file mode 120000 (symlink)
index 0000000..454bc28
--- /dev/null
@@ -0,0 +1 @@
+another directory/
\ No newline at end of file