-
-
Save daveray/1441520 to your computer and use it in GitHub Desktop.
; A REPL-based, annotated Seesaw tutorial | |
; Please visit https://github.com/daveray/seesaw for more info | |
; | |
; This is a very basic intro to Seesaw, a Clojure UI toolkit. It covers | |
; Seesaw's basic features and philosophy, but only scratches the surface | |
; of what's available. It only assumes knowledge of Clojure. No Swing or | |
; Java experience is needed. | |
; | |
; This material was first presented in a talk at @CraftsmanGuild in | |
; Ann Arbor, MI. | |
; | |
; Dave Ray, December 2011 | |
; First install Leiningen (https://github.com/technomancy/leiningen) and | |
; create a new project. In a terminal: | |
; | |
; $ lein new hello-seesaw | |
; $ cd hello-seesaw | |
; $ edit project.clj and add [seesaw "1.4.2"] to :dependencies | |
; $ lein deps | |
; Now let's start up the REPL and do some Seesaw stuff. Repl results are | |
; marked with ;=> in the usual way. | |
$ lein repl | |
; Here we go. First will use Seesaw and stuff | |
(use 'clojure.repl) | |
;=> nil | |
(use 'seesaw.core) | |
;=> nil | |
; Now before we create any UI stuff, tell Seesaw to try to make things look as | |
; native as possible. This puts the menubar in the right place on Mac, etc. | |
(native!) | |
;=> nil | |
; Now we'll start by making a frame to put stuff in. Most Seesaw widgets are | |
; constructed with simple functions that take :keyword/value pairs. | |
; (seesaw.core/frame) creates a new frame. | |
(def f (frame :title "Get to know Seesaw")) | |
;=> #'user/f | |
; So now we have a frame, but we haven't displayed it yet. Usually, we | |
; want to pack and show a frame. pack! just auto-sizes the frame for its | |
; contents | |
(-> f pack! show!) | |
;=> #<JFrame ... > | |
; Note that pack! and show! both return their argument so they can be chained. | |
; This is true of most Seesaw functions with side-effects. | |
; | |
; At this point you should have a very small, very boring window on your screen. | |
; | |
; | |
; +-----------------------------+ | |
; | Get to know Seesaw x| | |
; +-----------------------------+ | |
; | | | |
; +-----------------------------+ | |
; | |
; The properties of a widget can be queried ... | |
(config f :title) | |
;=> "Get to know Seesaw" | |
; ... and modified | |
(config! f :title "No RLY, get to know Seesaw!") | |
;=> #<JFrame ...> | |
; Note that the title of the frame changed! | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; | | | |
; +------------------------------+ | |
; So we have an empty frame. Let's give it some content | |
(config! f :content "This is some content") | |
;=> #<JFrame ... > | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; | This is some content | | |
; +------------------------------+ | |
; Now we have a frame with a label in it. When Seesaw sees something like a | |
; string, it will create a label, mostly out of habit. Of course, we could | |
; create a label ourselves ... | |
(def lbl (label "I'm another label")) | |
;=> #'user/lbl | |
; and show it in the frame | |
(config! f :content lbl) | |
;=> #<JFrame ... > | |
; You know, we're going to be doing that a lot. Let's make a function | |
(defn display [content] | |
(config! f :content content) | |
content) | |
;=> #'user/display | |
(display lbl) | |
; Like the frame, a label can be manipulated with (config!). We can set some | |
; colors | |
(config! lbl :background :pink :foreground "#00f") | |
;=> #<JLabel ... > | |
; Seesaw knows about CSS color names and codes. Notice how we can set as many | |
; properties as we want with one (config!) call | |
; We can change the font too | |
(config! lbl :font "ARIAL-BOLD-21") | |
;=> #<JLabel ... > | |
; "FAMILY-STYLE-SIZE" is conventient, but using (seesaw.font/font) we get a | |
; little more control | |
(use 'seesaw.font) | |
;=> nil | |
(config! lbl :font (font :name :monospaced | |
:style #{:bold :italic} | |
:size 18)) | |
;=> #<JLabel ... > | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; So we know a little about labels now. How about a button? | |
(def b (button :text "Click Me")) | |
;=> #'user/b | |
; Sometimes we might want to show a message ... | |
(alert "I'm an alert") | |
;=> nil | |
; or get input from the user ... | |
(input "What's your favorite color?") | |
;=> "Blue" | |
; Notice that both functions block until the popup is dismissed. They both | |
; take an additional, initial argument which indicates which frame they're | |
; associated with. Otherwise, they'll just pop up in the middle of your screen | |
; Let's get back to our button and get him doing something... | |
(display b) | |
;=> #<JButton> | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; |+----------------------------+| | |
; || || | |
; || Click Me || | |
; || || | |
; |+----------------------------+| | |
; +------------------------------+ | |
; | |
; Click it. Nothing happens. | |
; Seesaw's (listen) function let's us register handler functions for events on | |
; a widget. All buttons support an :action event. So, | |
(listen b :action (fn [e] (alert e "Thanks!"))) | |
;=> #<core$juxt$fn__3775 clojure.core$juxt$fn__3775@2e46638f> | |
; Now we can click the button and see something happen. The 'e' parameter | |
; is an event object. Most of the time in Seesaw, an event object can be used | |
; as a proxy for the widget that triggered the event. In this case, the button | |
; becomes the "parent" of the alert, so it's placed nicely over our frame. | |
; Also note that (listen) returned a function. Calling this function will | |
; undo the effects of the (listen) call, i.e. unregister the listener | |
(*1) | |
;=> [...] | |
; Now clicking does nothing again. | |
; (listen) can register multiple event handlers at once | |
(listen b :mouse-entered #(config! % :foreground :blue) | |
:mouse-exited #(config! % :foreground :red)) | |
;=> #<core$juxt$fn__3775 clojure.core$juxt$fn__3775@2e46638f> | |
; Move the mouse over the buttons to see the text color change. Again note | |
; that we're using the event parameter as a proxy for the button. This | |
; time with (config!). | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; Now let's talk about a slightly more interesting widget, a listbox. | |
(def lb (listbox :model (-> 'seesaw.core ns-publics keys sort))) | |
;=> #<JList ...> | |
(display lb) | |
;=> #<JList ...> | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; |#abtract-panel################| | |
; | action | | |
; | add! | | |
; | add!* | | |
; | alert | | |
; +------------------------------+ | |
; (listbox)'s most important option is :model which can take any (finite) | |
; sequence and display it. Items are displayed using (str) unless a custom | |
; :renderer is used. | |
; This listbox is ok, but our list doesn't fit in the frame and there's no | |
; scrollbars. You can add scrolling to most widgets with (scrollable) ... | |
(display (scrollable lb)) | |
;=> #<JScrollPane ... > | |
; Now that we have a list, it'd be nice to know what its selection is. Seesaw | |
; has a unified nice selection interface. If there's no selection we get nil | |
(selection lb) | |
;=> nil | |
; Otherwise, we get the value: | |
(selection lb) | |
;=> abstract-panel | |
(type *1) | |
;=> clojure.lang.Symbol | |
; Note we get the same objects out that we put in the :model above. | |
; If we want multi-selection (shift/ctrl-click) | |
(selection lb {:multi? true}) | |
;=> (action add! add!* alert) | |
; Similarly, we can set the selection | |
(selection! lb 'all-frames) | |
;=> #<JList ... > | |
; was all-frames selected for you too? | |
; Finally, we might want to know when the selection changes. Everybody | |
; supports the :selection event | |
(listen lb :selection (fn [e] (println "Selection is " (selection e)))) | |
;=> #<core$juxt$fn__3775 clojure.core$juxt$fn__3775@2e46638f> | |
; click around on the listbox and watch the selection print out. | |
; and unregister as usual | |
(*1) | |
;=> [...] | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; Now we come to editable text widgets. It's easy to make one | |
(def field (display (text "This is a text field."))) | |
;=> #<JTextField ... > | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; | | | |
; | | | |
; | This is a text field| | | |
; | | | |
; | | | |
; +------------------------------+ | |
; A text field is a single line of text. You can query the text... | |
(text field) | |
;=> "This is a text field." | |
; ... and change it programmatically | |
(text! field "A new value") | |
;=> #<JTextField ... > | |
; ... and you can set the font, etc as usual with config | |
(config! field :font "MONOSPACED-PLAIN-12" :background "#f88") | |
;=> #<JTextField ... > | |
; If you've got more than one line, you can make it multi-line... | |
(def area (text :multi-line? true :font "MONOSPACED-PLAIN-14" | |
:text "This | |
is | |
multi | |
line | |
text")) | |
;=> #<JTextArea ... > | |
(display area) | |
;=> #<JTextArea ... > | |
; | |
; +------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +------------------------------+ | |
; | This | | |
; | is | | |
; | multi | | |
; | line| | | |
; +------------------------------+ | |
; If you've got a reader, URL, or anything else slurp-able, you can fill up the | |
; text area with it | |
(text! area (java.net.URL. "http://clojure.com")) | |
;=> #<JTextArea ... > | |
; Of course, we need scrollbars now | |
(display (scrollable area)) | |
;=> #<JTextArea ... > | |
; Like selection, Seesaw has a unified scrolling API. You can scroll to the top ... | |
(scroll! area :to :top) | |
;=> #<JTextArea ... > | |
; ... or the bottom ... | |
(scroll! area :to :bottom) | |
;=> #<JTextArea ... > | |
; ... or to a particular line ... | |
(scroll! area :to [:line 50]) | |
;=> #<JTextArea ... > | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; Let's try showing two widgets at once. One option is a splitter: | |
(def split (left-right-split (scrollable lb) (scrollable area) :divider-location 1/3)) | |
;=> #'user/split | |
(display split) | |
;=> #<JSplitPane ... > | |
; | |
; | |
; +-------------------------------------+ | |
; | NO RLY, get to know Seesaw x| | |
; +-----------+-------------------------+ | |
; |abstract-pa| This | | |
; |action | is | | |
; |add!#######| multi | | |
; |add!* | line| | | |
; +-----------+-------------------------+ | |
; | |
; This shows both our listbox on the left and text area on the right with a | |
; movable splitter. If you're familiar with Swing, you'll recognize what an | |
; achievement :divider-location is. | |
; Let's hook them together. First a function to grab a doc string | |
(defn doc-str [s] (-> (symbol "seesaw.core" (name s)) resolve meta :doc)) | |
;=> #'user/doc-str | |
; ... and now the usual selection handler | |
(listen lb :selection | |
(fn [e] | |
(when-let [s (selection e)] | |
(-> area | |
(text! (doc-str s)) | |
(scroll! :to :top))))) | |
;=> #<JList ... > | |
; note how we have to be sure to check for nil selection | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; Ok. We're pushing on the limits of what we can comfortably type in a REPL, | |
; so just a couple more things ... | |
; Imagine we wanted to show source as well as docs. A set of radio button might be | |
; a nice way to switch betwen them ... | |
(def rbs (for [i [:source :doc]] | |
(radio :id i :class :type :text (name i)))) | |
;=> #'user/rbs | |
; we have a couple radio buttons. Let's add them to our frame. We'll use a | |
; border-panel to lay things out. This is what a border-panel layout looks like: | |
; | |
; +------------------------------------------+ | |
; | :north | | |
; | | | |
; +----------+-------------------+-----------+ | |
; | | | | | |
; | | | | | |
; | :west | :center | :east | | |
; | | | | | |
; | | | | | |
; +----------+-------------------+-----------+ | |
; | | | |
; | :south | | |
; +------------------------------------------+ | |
; | |
(display (border-panel | |
:north (horizontal-panel :items rbs) | |
:center split | |
:vgap 5 :hgap 5 :border 5)) | |
;=> #<JPanel ...> | |
; The :vgap, :hgap, and :border options just make things look a little nicer. | |
; If you click the radio buttons, you'll notice a little problem. They're not | |
; mutually exclusive. We'll put them in a button group to fix that, but first | |
; a small detour... | |
; How can we get to the radio buttons without using the rbs var? How about | |
; with a selector: | |
(select f [:JRadioButton]) | |
;=> (#<JRadioButton ... > #<JRadioButton ... >) | |
; or, since we gave them a :class option above ... | |
(select f [:.type]) | |
;=> (#<JRadioButton ... > #<JRadioButton ... >) | |
; or, we can get them individually by :id | |
(select f [:#source]) | |
;=> #<JRadioButton ... > | |
; Selectors are always enclosed in a vector. | |
; Now, about those buttons. We'll need a button group... | |
(def group (button-group)) | |
;=> #'user/group | |
; It happens that (config!) can take a sequence as its first argument in addition | |
; to a single widget ... | |
(config! (select f [:.type]) :group group) | |
;=> (#<JRadioButton ... > #<JRadioButton ... >) | |
; Now our buttons are nice and mutex-y. A button group, like most things, has a | |
; selection too, whichever button is currently selected: | |
(selection group) | |
;=> #<JRadioButton ...> | |
; Combine this with (id-of) and we have our original keywords back: | |
(-> group selection id-of) | |
;=> :source | |
(-> group selection id-of) | |
;=> :doc | |
; and, of course, you can register a listener for group selection changes | |
(listen group :selection | |
(fn [e] | |
(when-let [s (selection group)] | |
(println "Selection is " (id-of s))))) | |
; Now open hello-seesaw.core.clj and see if you can put this all together into | |
; an app. | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
; For more info see the Seesaw wiki: | |
; https://github.com/daveray/seesaw/wiki | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
Thanks for the tutorial. Easy to follow.
I couldn't get this to work by precisely following the instructions. To get it to work I had to add a :main statement to project.clj. and add (:use seesaw.core) to the ns definition in core.clj. Running (use 'seesaw.core) from the repl didn't do it, it had to be in the ns definition. I have no idea why this is... Great demo though. Thanks..
Just in case anyone else hits the same problem, here is my project.clj:
(defproject junk "0.1.0-SNAPSHOT"
: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.7.0"]
[seesaw "1.4.5"]]
:main junk.core)
and my ns defn in core.clj
(ns junk.core
(:use seesaw.core))
EDIT: This problem only occurs when the cider-nrepl plugin is in use.
This tutorial is perfect, thanks for this.
I currently get this error: NullPointerException seesaw.core/pack! (core.clj:291)
This is when I'm executing (-> f pack! show!)
. I'm using Clojure 1.8.0 with seesaw version 1.4.5.
I've used seesaw before. Now in 2019 I'm about to use again. Thank you for such a nice library.
Tested today. Works well ❤️ @daveray Many thanks for this tutorial!
Only just discovered this, what a cracking lib and tutorial. So productive from the start
really awesome. you rock