Skip to content

Commit

Permalink
Unifying (action) with the rest of the framework. Refactoring some pi…
Browse files Browse the repository at this point in the history
…eces for reuse.
  • Loading branch information
daveray committed Apr 22, 2011
1 parent b43f4ed commit 1afeec5
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 76 deletions.
21 changes: 15 additions & 6 deletions README.md
Expand Up @@ -4,6 +4,7 @@ Seesaw's a *primordial* experiment to see what I can do to make Swing funner in

## TODO

* Menus
* GridBagLayout needs more work
* JTree
* Cell renderers
Expand Down Expand Up @@ -56,7 +57,7 @@ Most of Seesaw's container functions (`flow-panel`, `grid-panel`, etc) take an `
(flow-panel
:items ["File" [:fill-h 5]
(text (System/getProperty "user.dir")) [:fill-h 5]
(action choose :name "...")]))
(action :handler choose :name "...")]))

creates a panel with a "File" label, a text entry field initialized to the current working directory and a button that doesn't do much. Each component is separated by 5 pixel padding.

Expand Down Expand Up @@ -106,7 +107,7 @@ A `FlowLayout` with some items:
(flow-panel
:align :left
:hgap 20
:items ["Label" (action alert "Button") "Another label"])
:items ["Label" (action :handler alert "Button") "Another label"])

A `GridLayout` with 2 columns and a titled border:

Expand Down Expand Up @@ -139,13 +140,15 @@ Note that these same arguments can be given to the `:listen` property when the w
See `seesaw.events/add-listener` for more details.

### Actions
It's typical in Swing apps to use actions for menus, buttons, etc. An action needs an event handler function and some properties. Here's an example of creating an action and adding it to a toolbar:
It's typical in Swing apps to use actions for menus, buttons, etc. An action needs a handler function and some properties. Here's an example of creating an action and adding it to a toolbar:

(use 'seesaw.core)
(let [open-action (action (fn [e] (alert "I should open a new something."))
(let [open-action (action
:handler (fn [e] (alert "I should open a new something."))
:name "Open"
:tip "Open a new something something.")
exit-action (action (fn [e] (.dispose (to-frame e)))
exit-action (action
:handler (fn [e] (.dispose (to-frame e)))
:name "Exit"
:tip "Close this window")]
(frame
Expand All @@ -154,7 +157,13 @@ It's typical in Swing apps to use actions for menus, buttons, etc. An action nee
:north (toolbar :items [open-action exit-action])
:center "Insert content here")))

`(action)` also supports an `:icon` property which can be a `javax.swing.Icon`, a `java.net.URL` or something that looks like a file or URL after `(str)` has been applied to it.
`(action)` also supports an `:icon` property which can be a `javax.swing.Icon`, a `java.net.URL` or something that looks like a file or URL after `(str)` has been applied to it. See `seesaw/action.clj` for an accurate list of options.

Like widgets, actions can be modified with the `(config)` function:

(def a (action :name "Fire Missiles" :enabled? false))

(config a :name "Fire Missiles!!!" :enabled? true :handler (fn [e] (println "FIRE")))

### Selection Handling
The `(selection)` function handles the details of selection management for listboxes, checkboxes, toggle buttons, combo boxes, etc. To get the current selection, just pass a widget (or something convertible to a widget). It will always return a seq of values, or `nil` if there is no selection. For single-selection cases, just use `(first)`. Note that you can apply `(selection)` to event objects as well:
Expand Down
34 changes: 34 additions & 0 deletions src/seesaw/action.clj
@@ -0,0 +1,34 @@
; Copyright (c) Dave Ray, 2011. All rights reserved.

; The use and distribution terms for this software are covered by the
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
; which can be found in the file epl-v10.html at the root of this
; distribution.
; By using this software in any fashion, you are agreeing to be bound by
; the terms of this license.
; You must not remove this notice, or any other, from this software.

(ns seesaw.action
(:use [seesaw util icon])
(:import [javax.swing Action AbstractAction]))

;*******************************************************************************
; Actions

; store the handler function in a property on the action.
(def ^{:private true} action-handler-property "seesaw-action-handler")
(def ^{:private true} action-options {
:enabled? #(.setEnabled %1 (boolean %2))
:selected? #(.putValue %1 Action/SELECTED_KEY (boolean %2))
:name #(.putValue %1 Action/NAME (str %2))
:command #(.putValue %1 Action/ACTION_COMMAND_KEY (str %2))
:tip #(.putValue %1 Action/SHORT_DESCRIPTION (str %2))
:icon #(.putValue %1 Action/SMALL_ICON (icon %2))
:handler #(.putValue %1 action-handler-property %2)
})

(defn action [& opts]
(let [a (proxy [AbstractAction] []
(actionPerformed [e]
(if-let [f (.getValue this action-handler-property)] (f e))))]
(apply-options a opts action-options)))
58 changes: 15 additions & 43 deletions src/seesaw/core.clj
Expand Up @@ -9,16 +9,15 @@
; You must not remove this notice, or any other, from this software.

(ns seesaw.core
(:use seesaw.util)
(:use seesaw.font)
(:use seesaw.border)
(:use seesaw.color)
(:use [seesaw util font border color])
(:require [seesaw.event :as sse]
[seesaw.selection :as sss])
[seesaw.selection :as sss]
[seesaw.icon :as ssi]
[seesaw.action :as ssa])
(:import [java.util EventObject]
[javax.swing
SwingUtilities SwingConstants
Icon Action AbstractAction ImageIcon
Action
BoxLayout
JFrame JComponent Box JPanel JScrollPane JSplitPane JToolBar JTabbedPane
JLabel JTextField JTextArea
Expand All @@ -27,8 +26,7 @@
[javax.swing.text JTextComponent]
[javax.swing.event ChangeListener DocumentListener]
[java.awt Component FlowLayout BorderLayout GridLayout GridBagLayout GridBagConstraints
Dimension ItemSelectable Image]
[java.awt.event MouseAdapter ActionListener]))
Dimension ItemSelectable Image]))

(declare to-widget)

Expand All @@ -39,6 +37,9 @@
; alias event/add-listener for convenience
(def listen sse/add-listener)

; alias action/action for convenience
(def action ssa/action)

; to-widget wrapper and stuff for (seesaw.selection/selection)
(defn selection
"Gets/sets the selection on a widget. target is passed through (to-widget)
Expand All @@ -62,28 +63,9 @@
[target & args]
(apply sss/selection (to-widget target) args))

;*******************************************************************************
; Icons

(defn icon [p]
(cond
(nil? p) nil
(instance? javax.swing.Icon p) p
(instance? java.awt.Image p) (ImageIcon. p)
(instance? java.net.URL p) (ImageIcon. p)
:else (ImageIcon. (to-url p))))

(def icon ssi/icon)
(def ^{:private true} make-icon icon)

;*******************************************************************************
; Actions

(defn action [f & {:keys [name tip icon] :or { name "" }}]
(doto (proxy [AbstractAction] [] (actionPerformed [e] (f e)))
(.putValue Action/NAME (str name))
(.putValue Action/SHORT_DESCRIPTION tip)
(.putValue Action/SMALL_ICON (make-icon icon))))

;*******************************************************************************
; Widget coercion prototcol

Expand Down Expand Up @@ -224,19 +206,6 @@
:model #(.setModel %1 %2)
})

(def ^{:private true} options-property "seesaw-creation-options")

(defn apply-options
[target opts handler-map]
(check-args (or (map? opts) (even? (count opts)))
"opts must be a map or have an even number of entries")
(doseq [[k v] (if (map? opts) opts (partition 2 opts))]
(if-let [f (get handler-map k)]
(f target v)
(throw (IllegalArgumentException. (str "Unknown option " k)))))
(cond-doto target
(instance? JComponent target) (.putClientProperty options-property handler-map)))

(defn apply-default-opts
"only used in tests!"
([p] (apply-default-opts p {}))
Expand All @@ -253,8 +222,11 @@

(extend-type javax.swing.JComponent ConfigureWidget
(config* [target args]
(let [options (or (.getClientProperty target options-property) default-options)]
(apply-options target args options))))
(reapply-options target args default-options)))

(extend-type Action ConfigureWidget
(config* [target args]
(reapply-options target args default-options)))

(defn config
"Applies properties in the argument list to one or more targets. For example:
Expand Down
5 changes: 3 additions & 2 deletions src/seesaw/examples/hotpotatoes.clj
Expand Up @@ -24,7 +24,7 @@
(defn app
[exit?]
(let [exit-action (action (fn [e] (if exit? (System/exit 0) (.dispose (to-frame e)))) :name "Exit")
(let [exit-action (action :handler (fn [e] (if exit? (System/exit 0) (.dispose (to-frame e)))) :name "Exit")
url-text (text "http://google.com")
status (label "Ready")
result-text (text :multi-line? true :editable? false :font "MONOSPACED-14")
Expand All @@ -50,7 +50,7 @@
(horizontal-panel
:border [5 "Configure Request"]
:items ["URL" url-text
(action go-handler :name "Go")])
(action :handler go-handler :name "Go")])
:center
(horizontal-panel
:border [5 "Request Result"]
Expand All @@ -60,3 +60,4 @@
(defn -main [& args]
(invoke-later #(app true)))
;(app false)
10 changes: 5 additions & 5 deletions src/seesaw/examples/kitchensink.clj
Expand Up @@ -24,7 +24,7 @@
:north (toolbar
:floatable? false
:items [(button :id :button :text "This") :separator "is a toolbar" :separator
(action #(.dispose (to-frame %)) :name "Close this frame")
(action :handler #(.dispose (to-frame %)) :name "Close this frame")
(combobox :id :combo :model ["First" "Second" "Third"])])
:center (top-bottom-split
(left-right-split
Expand All @@ -34,7 +34,7 @@
:border [10 "This is a border layout" (empty-border :thickness 15)]
:north (horizontal-panel
:items [(action
#(println "FOO" %)
:handler #(println "FOO" %)
:name "Click Me"
:icon rss-url
:tip "Yum!")
Expand All @@ -52,7 +52,7 @@
:id :link)
(text
:text "HI"
:listen [:action (fn [e] (println (.. (to-widget e) (getText))))])
:listen [:action :handler (fn [e] (println (.. (to-widget e) (getText))))])
(scrollable
(text
:text (apply str (interpose "\n" (range 0 20)))
Expand All @@ -79,7 +79,7 @@
:vgap 10
:columns 3
:items (map #(action
(fn [e] (alert (str "Clicked " %)))
:handler (fn [e] (alert (str "Clicked " %)))
:name %)
(range 0 12))))
(tabbed-panel
Expand Down Expand Up @@ -133,5 +133,5 @@
(invoke-later app))
;(doseq [f (JFrame/getFrames)]
;(.dispose f))
;(-main)
(-main)

24 changes: 24 additions & 0 deletions src/seesaw/icon.clj
@@ -0,0 +1,24 @@
; Copyright (c) Dave Ray, 2011. All rights reserved.

; The use and distribution terms for this software are covered by the
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
; which can be found in the file epl-v10.html at the root of this
; distribution.
; By using this software in any fashion, you are agreeing to be bound by
; the terms of this license.
; You must not remove this notice, or any other, from this software.

(ns seesaw.icon
(:use [seesaw util])
(:import [javax.swing ImageIcon]))

;*******************************************************************************
; Icons

(defn icon [p]
(cond
(nil? p) nil
(instance? javax.swing.Icon p) p
(instance? java.awt.Image p) (ImageIcon. p)
(instance? java.net.URL p) (ImageIcon. p)
:else (ImageIcon. (to-url p))))
6 changes: 6 additions & 0 deletions src/seesaw/selection.clj
Expand Up @@ -15,6 +15,12 @@
(get-selection [target])
(set-selection [target args]))

(extend-protocol Selection
javax.swing.Action
(get-selection [target]
(when-let [s (.getValue target javax.swing.Action/SELECTED_KEY)] [true]))
(set-selection [target [v]] (.putValue target javax.swing.Action/SELECTED_KEY (boolean v))))

(extend-protocol Selection
javax.swing.AbstractButton
(get-selection [target] (seq (.getSelectedObjects target)))
Expand Down
31 changes: 31 additions & 0 deletions src/seesaw/util.clj
Expand Up @@ -93,3 +93,34 @@
(URL. (str s))
(catch MalformedURLException e nil)))

(def ^{:private true} options-property "seesaw-creation-options")

; TODO custom metadata storage like this should be a protocol so apply-options
; can be used on anybody.
(defn- store-option-handlers
[target handler-map]
(cond-doto target
(instance? javax.swing.JComponent target) (.putClientProperty options-property handler-map)
(instance? javax.swing.Action target) (.putValue options-property handler-map)))

(defn- get-option-handlers
[target]
(cond
(instance? javax.swing.JComponent target) (.getClientProperty target options-property)
(instance? javax.swing.Action target) (.getValue target options-property)))

(defn apply-options
[target opts handler-map]
(check-args (or (map? opts) (even? (count opts)))
"opts must be a map or have an even number of entries")
(doseq [[k v] (if (map? opts) opts (partition 2 opts))]
(if-let [f (get handler-map k)]
(f target v)
(throw (IllegalArgumentException. (str "Unknown option " k)))))
(store-option-handlers target handler-map))

(defn reapply-options
[target args default-options]
(let [options (or (get-option-handlers target) default-options)]
(apply-options target args options)))

41 changes: 41 additions & 0 deletions test/seesaw/test/action.clj
@@ -0,0 +1,41 @@
; Copyright (c) Dave Ray, 2011. All rights reserved.

; The use and distribution terms for this software are covered by the
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
; which can be found in the file epl-v10.html at the root of this
; distribution.
; By using this software in any fashion, you are agreeing to be bound by
; the terms of this license.
; You must not remove this notice, or any other, from this software.

(ns seesaw.test.action
(:use seesaw.action)
(:use [lazytest.describe :only (describe it testing)]
[lazytest.expect :only (expect)])
(:import [javax.swing Action]))

(describe action
(it "sets the name and tooltip"
(let [a (action :name "Test" :tip "This is a tip" :command "Go!")]
(expect (instance? Action a))
(expect (.isEnabled a))
(expect (= "Test" (.getValue a Action/NAME)))
(expect (= "Go!" (.getValue a Action/ACTION_COMMAND_KEY)))
(expect (not (.getValue a Action/SELECTED_KEY)))
(expect (= "This is a tip" (.getValue a Action/SHORT_DESCRIPTION)))))
(it "calls the handler when actionPerformed is called"
(let [called (atom false)
f (fn [e] (reset! called true))
a (action :handler f)]
(.actionPerformed a nil)
(expect @called)))
(it "does nothing when actionPerformed is called and no handler is installed"
(let [a (action)]
(.actionPerformed a nil)
; Just making sure no exception was thrown
true))
(it "handles the :enabled? option"
(not (.isEnabled (action :enabled? false))))
(it "handles the :selected? option"
(.getValue (action :selected? true) Action/SELECTED_KEY)))

0 comments on commit 1afeec5

Please sign in to comment.