diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc325a56..1af147e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ This change log follows the conventions of
## [Unreleased][unreleased]
+Nothing so far.
+
+## [0.1.1] - 2016-05-20
+
### Added
- A status summary of the selected player is shown after the menu.
@@ -17,6 +21,9 @@ This change log follows the conventions of
- Support for offline operation when no DJ Link device can be found.
- The list of triggers is saved when the application exits and
restored when it starts.
+- You can save or load the configuration to a text file of your choice.
+- Triggers can be deleted by right-clicking on them as long as there
+ is more than one in the list.
- A first-stage loading process which checks the Java version and
presents an error dialog if it is too old to successfully load the
rest of the application, offering to open the download page.
@@ -48,4 +55,5 @@ This change log follows the conventions of
- Set up initial project structure.
- Selector to choose MIDI output as trigger destination.
-[unreleased]: https://github.com/brunchboy/beat-link/compare/v0.1.0...HEAD
+[unreleased]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.1...HEAD
+[0.1.1]: https://github.com/brunchboy/beat-link-trigger/compare/v0.1.0...v0.1.1
diff --git a/project.clj b/project.clj
index 8caaac82..2b17624b 100644
--- a/project.clj
+++ b/project.clj
@@ -1,10 +1,11 @@
-(defproject beat-link-trigger "0.1.1-SNAPSHOT"
+(defproject beat-link-trigger "0.1.1"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]
[environ "1.0.3"]
+ [fipp "0.6.5"]
[org.deepsymmetry/beat-link "0.1.4"]
[overtone/midi-clj "0.5.0"]
[overtone/osc-clj "0.9.0"]
diff --git a/src/beat_link_trigger/core.clj b/src/beat_link_trigger/core.clj
index 82220002..76b86c7b 100644
--- a/src/beat_link_trigger/core.clj
+++ b/src/beat_link_trigger/core.clj
@@ -1,7 +1,7 @@
(ns beat-link-trigger.core
"Send MIDI or OSC events when a CDJ starts playing."
(:require [overtone.midi :as midi]
- [seesaw.cells]
+ [seesaw.chooser :as chooser]
[seesaw.core :as seesaw]
[seesaw.mig :as mig]
[beat-link-trigger.about :as about]
@@ -179,6 +179,19 @@
(when-let [frame @trigger-frame]
(seesaw/config (seesaw/select frame [:#triggers]) :items)))
+(defn- adjust-to-new-trigger
+ "Called when a trigger is added or removed to restore the proper
+ alternation of background colors, expand the window if it still fits
+ the screen, and update any other user interface elements that might
+ be affected."
+ []
+ (doall (map (fn [trigger color]
+ (seesaw/config! trigger :background color))
+ (get-triggers) (cycle ["#eee" "#ddd"])))
+ (when (< 100 (- (.height (.getBounds (.getGraphicsConfiguration @trigger-frame)))
+ (.height (.getBounds @trigger-frame))))
+ (.pack @trigger-frame)))
+
(defn- create-trigger-row
"Create a row for watching a player in the trigger window. If `m` is
supplied, it is a map containing values to recreate the row from a
@@ -209,7 +222,15 @@
[(seesaw/label :id :enabled-label :text "Enabled:") "gap unrelated"]
[(seesaw/checkbox :id :enabled) "wrap"]]
- :user-data (atom {:playing false}))]
+
+ :user-data (atom {:playing false}))
+ delete-action (seesaw/action :handler (fn [e]
+ (seesaw/config! (seesaw/select @trigger-frame [:#triggers])
+ :items (remove #(= % panel) (get-triggers)))
+ (adjust-to-new-trigger)
+ (.pack @trigger-frame))
+ :name "Delete Trigger")]
+ (seesaw/config! panel :popup (fn [e] (when (> (count (get-triggers)) 1) [delete-action])))
(seesaw/listen (seesaw/select panel [:#players])
:item-state-changed (fn [e] ; Update player status when selection changes
(show-device-status panel)))
@@ -221,24 +242,12 @@
(show-device-status panel)
panel)))
-(defn- adjust-to-new-trigger
- "Called when a trigger is added or removed to restore the proper
- alternation of background colors, and update any other user
- interface elements that might be affected."
- []
- (doall (map (fn [trigger color]
- (seesaw/config! trigger :background color))
- (get-triggers) (cycle ["#eee" "#ddd"]))))
-
(def ^:private new-trigger-action
- "The menu action which adds a new Trigger to the end of the list"
+ "The menu action which adds a new Trigger to the end of the list."
(seesaw/action :handler (fn [e]
(seesaw/config! (seesaw/select @trigger-frame [:#triggers])
:items (concat (get-triggers) [(create-trigger-row)]))
- (adjust-to-new-trigger)
- (when (< 100 (- (.height (.getBounds (.getGraphicsConfiguration @trigger-frame)))
- (.height (.getBounds @trigger-frame))))1
- (.pack @trigger-frame)))
+ (adjust-to-new-trigger))
:name "New Trigger"
:key "menu T"))
@@ -259,6 +268,38 @@
(prefs/add-reader 'beat_link_trigger.core.PlayerChoice map->PlayerChoice)
(prefs/add-reader 'beat_link_trigger.core.MidiChoice map->MidiChoice)
+(def ^:private save-action
+ "The menu action which saves the configuration to a user-specified file."
+ (seesaw/action :handler (fn [e]
+ (save-triggers-to-preferences)
+ (when-let [file (chooser/choose-file :type :save)]
+ (try
+ (prefs/save-to-file file)
+ (catch Exception e
+ (seesaw/alert (str "Unable to Save.
" e)
+ :title "Problem Writing File" :type :error)))))
+ :name "Save"
+ :key "menu S"))
+
+(declare recreate-trigger-rows)
+
+(def ^:private load-action
+ "The menu action which loads the configuration from a user-specified file."
+ (seesaw/action :handler (fn [e]
+ (when-let [file (chooser/choose-file
+ :filters [(chooser/file-filter "BeatLinkTrigger Files"
+ prefs/valid-file?)])]
+ (try
+ (prefs/load-from-file file)
+ (seesaw/config! (seesaw/select @trigger-frame [:#triggers])
+ :items (recreate-trigger-rows))
+ (adjust-to-new-trigger)
+ (catch Exception e
+ (seesaw/alert (str "Unable to Load.
" e)
+ :title "Problem Reading File" :type :error)))))
+ :name "Load"
+ :key "menu L"))
+
(defn- midi-environment-changed
"Called when CoreMidi4J reports a change to the MIDI environment, so we can update the menu of
available MIDI outputs."
@@ -327,15 +368,15 @@
"Create and show the trigger window."
[]
(let [root (seesaw/frame :title "Beat Link Triggers" :on-close :exit
- :menubar (seesaw/menubar :items [(seesaw/menu :text "Window" :items [new-trigger-action])]))
+ :menubar (seesaw/menubar :items [(seesaw/menu :text "File" :items [load-action save-action])
+ (seesaw/menu :text "Window" :items [new-trigger-action])]))
panel (seesaw/scrollable (seesaw/vertical-panel
:id :triggers
:items (recreate-trigger-rows)))]
(seesaw/config! root :content panel)
- (seesaw/pack! root)
- (seesaw/show! root)
(reset! trigger-frame root)
- (adjust-to-new-trigger)))
+ (adjust-to-new-trigger)
+ (seesaw/show! root)))
(defn- install-mac-about-handler
"If we are running on a Mac, load the namespace that only works
diff --git a/src/beat_link_trigger/prefs.clj b/src/beat_link_trigger/prefs.clj
index b0e3881e..0fff4e73 100644
--- a/src/beat_link_trigger/prefs.clj
+++ b/src/beat_link_trigger/prefs.clj
@@ -1,6 +1,7 @@
(ns beat-link-trigger.prefs
"Functions for managing application preferences"
(:require [clojure.edn :as edn]
+ [fipp.edn :as fipp]
[beat-link-trigger.about :as about])
(:import java.util.prefs.Preferences))
@@ -34,3 +35,30 @@
(let [prefs (prefs-node)]
(.put prefs "prefs" (prn-str m))
(.flush prefs)))
+
+(defn save-to-file
+ "Saves the preferences to a text file."
+ [file]
+ (spit file (with-out-str (fipp/pprint (get-preferences)))))
+
+(defn valid-file?
+ "Checks whether the specified file seems to be a valid save file. If
+ so, returns it; otherwiser returns nil."
+ [file]
+ (try
+ (with-open [in (java.io.PushbackReader. (clojure.java.io/reader file))]
+ (let [m (edn/read {:readers @prefs-readers} in)]
+ (when (some? (:beat-link-trigger-version m))
+ m)))
+ (catch Exception e
+ nil)))
+
+(defn load-from-file
+ "Read the preferences from a text file."
+ [file]
+ (if (valid-file? file)
+ (with-open [in (java.io.PushbackReader. (clojure.java.io/reader file))]
+ (let [m (edn/read {:readers @prefs-readers} in)]
+ (put-preferences m)
+ m))
+ (throw (IllegalArgumentException. (str "Unreadable file: " file)))))