I'm trying to listen to the :key-pressed and :key-released events on my Seesaw frame, but the events aren't firing. I've narrowed the problem down to a listbox -- when the listbox is present, the frame no longer captures the key events. Here's a simplified version of my code that shows the behavior:
(ns ainur.example
(:use seesaw.core))
(let [lst (listbox :model ["Chiptune" "Sinewave"])
f (frame :title "Ainur"
:on-close :exit
:size [1024 :by 768]
:content (border-panel :hgap 10 :vgap 10
:center (label "Center")
:north (label "North")
:south (label "South")
:west lst))]
(listen lst :selection (fn [e]
(let [active-inst (selection e)]
(println active-inst))))
(listen f
:key-pressed (fn [e]
(println "Key pressed"))
:key-released (fn [e]
(println "Key released")))
(invoke-later
(native!)
(show! f)))
Can anyone help me figure out why the key events aren't triggered? Any help would be really appreciated. Thanks in advance!
I posted this question in seesaw's Google Group, and received an excellent answer from Seesaw's creator, Dave Ray, himself. I'm posting it here in case anyone else runs into this issue:
"Hi. Once there's another widget in the hierarchy like a listbox, it grabs keyboard focus so the events never get to the frame. I think the best bet would be to put the key listener on a nested widget like a panel and then give it keyboard focus. A kind of similar example can be seen here:
https://github.com/daveray/regenemies/blob/master/src/regenemies/ui.clj#L163
The :key-typed event is bound to the canvas and then .requestFocusInWindow is used to give it keyboard focus."
Many thanks, Dave!
Related
What is a simple way to delay :on-click event to see first if :on-double-click event is triggered?
[:div {:on-click (fn [e]
;; listen to double-click event, within 500ms,
;; if so on-double-click-fn,
;; if not, on-click-fn
)
:on-double-click (fn [e]
;; on-click-fn
)}]
Thanks!
First attempt:
(defn sleep [timeout]
(let [maxtime (+ (.getTime (js/Date.)) timeout)]
(while (< (.getTime (js/Date.)) maxtime))))
[:div {:on-click (fn [e] (sleep 500) (print "single-clicked"))
:on-double-click (fn [e] (print "double-clicked"))}]
Second attempt:
(def state (atom {:click-count 0}))
(defn handle-click [e click-fns-map]
(swap! state update :click-count inc)
(sleep 500)
(let [click-count (get #state :click-count)]
(swap! state assoc :click-count 0)
(cond
(= click-count 1) ((:on-single-click click-fns-map) e)
(> click-count 1) ((:on-double-click click-fns-map) e)))))
[:div
{:on-mouse-down
(fn [e]
(handle-click e {:on-single-click #(print "single-click")
:on-double-click #(print "double-click")}))}]
;;=> "single-click"
;;=> "single-click"
EDIT:
Based on Taylor Wood's answer, here is an abstraction that wraps html element args and overwrites :on-click and :on-double-click for you.
(defn ensure-single-double-click
[{:keys [on-click on-double-click] :as args}]
(let [waiting? (atom false)]
(merge
args
{:on-click (fn [e]
(when (compare-and-set! waiting? false true)
(js/setTimeout
(fn [] (when #waiting?
(on-click %)
(reset! waiting? false)))
300)))
:on-double-click (fn [e]
(reset! waiting? false)
(on-double-click %))})))
[:a (ensure-single-double-click
{:style {:color "blue"} ;; this works
:on-click #(print "single-click")
:on-double-click #(print "double-click")})
"test"]
Here's one way to do it:
(defn slow-link [text single-click-fn double-click-fn]
(let [waiting? (atom false)]
[:a {:on-click #(when (compare-and-set! waiting? false true)
(js/setTimeout (fn [] (when #waiting?
(single-click-fn %)
(reset! waiting? false)))
500))
:on-double-click #(do (reset! waiting? false)
(double-click-fn %))}
text]))
[slow-link "Test" #(prn "single-click") #(prn "double-click")]
This starts a JS timer that will execute a given function after 500ms. The function checks to see if we're still waiting? on another click when the timeout elapses, and if so it executes single-click-fn. If we're not waiting? that means the double-click event has already happened, reset waiting? to false, and called double-click-fn.
The :on-click handler uses compare-and-set to only take action if we're not already in a waiting? state, avoiding some racy behavior for triple/quadruple clicks, etc.
Taylor Wood's answer comes close, but compare-and-set! did not protect me from triple clicks (or even more clicks!), because if, say, three clicks happen within 500ms, waiting? will be set to false again the third time around and a second timeout is scheduled. I think this means a new timeout is technically scheduled upon every odd numbered click.
Luckily, the click event comes with a property called detail, which is set to the number of consecutive clicks. I found it here. The following should solve the OP's problem without allowing for triple clicks:
:on-click
(fn [e]
; This prevents the click handler from running
; a second, third, or other time.
(when (-> e .-detail (= 1))
(reset! waiting? true))
; Wait an appropriate time for the double click
; to happen...
(js/setTimeout
(fn []
; If we are still waiting for the double click
; to happen, it didn't happen!
(when #waiting?
(single-click-fn %)
(reset! waiting? false)))
500)))
:on-double-click #(do (reset! waiting? false)
(double-click-fn %))
As exotic as triple clicks may sound, they do serve a purpose: to select an entire line of text, so I wouldn't want the user to miss out on that ability.
The rest is an addendum for those interested in making text selection work on elements that are listening for a single click. I came here from Google looking for how to do this, so maybe it helps someone.
A challenge I ran into is that my application dictated that the user can perform a double click without releasing the second click at first; a bit like "one-and-a-half-clicks". Why? Because I am listening for clicks on a span, and the user may well perform a double click to select a whole word, but then keep the second click pressed and drag his mouse in order to select additional words next to the original one. The problem is that the double click event handler only fires after the user releases the second click, and so waiting? is not set to false on time.
I solved this using an :on-mouse-down handler:
:on-click
(fn [e]
; This prevents the click handler from running
; a second, third, or other time.
(when (-> e .-detail (= 1))
(reset! waiting? true))
; Wait an appropriate time for the double click
; to happen...
(js/setTimeout
(fn []
; If we are still waiting for the double click
; to happen, it didn't happen!
(when #waiting?
(single-click-fn %)
(reset! waiting? false)))
500)))
:on-double-click #(double-click-fn %)
:on-mouse-down #(reset! waiting? false)
Remember that the :on-click and :on-double-click handlers only fire upon release (and that the handlers fire in the order of mouse-down, click, double-click), which gives the :on-mouse-down handler an opportunity to set waiting? to false, which is needed if the user hasn't released the mouse yet, because he won't have triggered the :on-double-click event handler.
Note that now you don't even need to set waiting? to false in your double click handler anymore, because the mouse down handler has already done that by the time the double click handler is run.
Lastly, in my particular application, it so happens that a user may want to select text without triggering the click handler. In order to do this, he will click on a piece of text, and, without releasing the mouse, drag the cursor to select more text. When the cursor is released, that should not trigger the click event. So I had to additionally track whether the user had made a selection at any time before he let go of the mouse (a sort of "half-click"). For this case, I had to add a couple more things to the component's state (a boolean atom called selection-made? and an event handler function called selection-handler). This case relies on detection of selection, and, since a selection is made on double click, does not need to check the event's detail property anymore to protect against triple or more clicks.
The whole solution looks like this (but keep in mind that this is specifically for text elements, so just an addition to what the OP asked for):
(defn component
[]
(let [waiting? (r/atom false)
selection-made? (r/atom false)
selection-handler
(fn []
(println "selection-handler running")
(when (seq (.. js/document getSelection toString))
(reset! selection-made? true)))]
(fn []
[:div
; For debugging
[:pre {} "waiting? " (str #waiting?)]
[:pre {} "selection-made? " (str #selection-made?)]
; Your clickable element
[:span
{:on-click
(fn [e]
(println "click handler triggered")
; Remove the selection handler in any case because
; there is a small chance that the selection handler
; was triggered without selecting any text (by
; holding down the mouse on the text for a little
; while without moving it).
(.removeEventListener js/document "selectionchange" selection-handler)
(if #selection-made?
; If a selection was made, only perform cleanup.
(reset! selection-made? false)
; If no selection was made, treat it as a
; simple click for now...
(do
(reset! waiting? true)
; Wait an appropriate amount of time for the
; double click to happen...
(js/setTimeout
(fn []
; If we are still waiting for the double click
; to happen, it didn't happen! The mouse-down
; handler would have set waiting? to false
; by now if it had been clicked a second time.
; (Remember that the mouse down handler runs
; before the click handler since the click handler
; runs only once the mouse is released.
(when #waiting?
(single-click-fn e)
(reset! waiting? false)))
500))))
:on-mouse-down
(fn [e]
; Set this for the click handler in case a double
; click is happening.
(reset! waiting? false)
; Only run this if it is a left click, or the event
; listener is not removed until a single click on this
; segment is performed again, and will listen on
; every click everywhere in the window.
(when (-> e .-button zero?)
(js/console.log "mouse down handler running")
(.addEventListener js/document "selectionchange" selection-handler)))
:on-double-click #(double-click-fn %)}
some content here]])))
Okay, so this may be a silly one, but I am at such a loss that I created a SOF account for that.
Here's a thing that does almost what I want:
(let [lb (listbox :model ["a" "b" "c"])]
(listen lb :selection
(fn [e] (alert (selection lb))))
(-> (frame :content lb)
pack! show!))
If you run this code you'll see a listbox with three entries (a, b, c). If you click on any one of them an alert pops up with that entry in it.
What I want to do is make the listbox react in this way to DOUBLE-clicks, not single-clicks. How should I go about it?
Extra kudos to those who tell me how to make the number of the double-clicked item appear in the popup (0 for a, 1 for b, 2 for c).
Seesaw's listbox function returns a JList. A JList's ListSelectionModel does not provide a way to determine whether the ListSelectionEvent was the result of a double-click. So a :selection listener won't help here.
On the other-hand, MouseEvent does provide getClickCount, which can be used to detect a double-click. So you can use a :mouse-clicked listener instead, and filter for double-clicks. Then all you need to do is find the ListItem that corresponds with the click location. Fortunately, JList provides a locationToIndex method that can be used for this purpose. This answer to "Double-click event on JList element" puts those pieces together for Java. A translation to Clojure/Seesaw would look something like this:
(listen lb :mouse-clicked
(fn [ev]
(when (= 2 (. ev getClickCount))
(let [index (. list locationToIndex (. ev getPoint))]
<... do something with the index / list item ...>))))
a new to Clojure here.
I would like to share a behaviour which seems strange to me, but may be it's totally ok.
I followed the tutorial on github https://gist.github.com/daveray/1441520#file-seesaw-repl-tutorial-clj-L381 , and more precisely the part where I am supposed to add a Listener to a Label. Let's make a constructor and display helper:
(defn make-lb [s]
(listbox :model (-> (symbol s) ns-publics keys sort)))
(defn display [content frame]
(config! frame :content content)
content)
This works perfectly:
(def lb (make-lb "clojure.core"))
(display (scrollable lb) f)
(listen lb :selection (fn [e] (println "Selection is " (selection e))))
Howevever, this doesn't:
(def lb (scrollable (make-lb "clojure.core")))
(display lb f)
(listen lb :selection (fn [e] (println "Selection is " (selection e))))
Notice the different "Scrollable" emplacement.
In the second case, compilier tells me "Unknown event type :selection seesaw.util/illegal-Argument (utils.clj:19)"
I don't see any reason why the first snippet works, and the second doesn't. I don't have any knowledge of Swing and/or other Java libraries
Why doesn't this work? (implied)
tl;dr
listbox and scrollable return different things
Details
Check out the return values of the different calls (original make-lb included for clarity):
(defn make-lb [s]
(listbox :model (-> (symbol s) ns-publics keys sort)))
(class (make-lb "clojure.core"))
;;=> seesaw.core.proxy$javax.swing.JList$Tag$fd407141
(class (scrollable (make-lb "clojure.core")))
;;=> seesaw.core.proxy$javax.swing.JScrollPane$Tag$fd407141
For our purposes, we'll just say that listbox returns a JList and scrollable returns a JScrollPane
Given that, the calls to display are equivalent
However, the calls to listen are not equivalent
In the first case, lb resolves to a JList, and in the second case, lb resolves to a JScrollPane
More details
If we look in the source for seesaw.event, we'd see the following:
; :selection is an artificial event handled specially for each class
; of widget, so we hack...
What I'll call a "real selection type" is resolved in resolve-event-aliases
You'll notice that there's a case for JList, but not for JScrollPane
In the JScrollPane case, the artificial :selection is simply handed back from the call to resolve-event-aliases
Since this isn't a "real selection type", it's only a matter of time before things go pear-shaped
And sure enough, get-or-install-handlers attempts to look up :selection, gets nothing back, and calls (illegal-argument "Unknown event type %s" event-name) where event-name is bound to :selection, which matches the exception that you were receiving
I have the following ClojureScript code and am trying to detect the coordinates of a a click. So far, I can't even get Javascript alerts to recognize a click, let alone give me the coords.
I know I will have to write a function to have it give me the exact cells being clicked, but as a start, need to know how to get the coordinates of any area clicked on a page.
Thanks!
(defn header [color text]
[:h1
{:style
{:color color
:background-color "blue"}}
text])
(defn Cell []
[:div
{:style
{:width "40px"
:height "40px"
:float "right"
:margin-bottom "1px"
:margin-right "1px"
:background-color "grey"
:border "1px" "solid" "white"}}])
(defn home-page []
[:div
[header "red" "Minesweeper"]
[:div
{:style
{:width "440px"
:height "440px"}}
(repeat 100 [Cell])]])
Put an :onClick key at the same map indentation level as the :style. Its value should be a function, which will get an event e. Then:
(let [coords (.getClientPosition e)
coords' {:x (aget coords "x")
:y (aget coords "y")}])
Here's an example of a hashmap that has an :onClick event and its function:
{ :href "#"
:onClick (fn [e]
(.preventDefault e)
(swap! counter inc))}
The only thing that matters in the above is getting the e and using it. That was taken from the flappybird example program, which is how I got started.
How to evaluate foo when mouse is down and moving using core/async?
Whilst attempting to learn the concepts behind core/async I have worked through the ClojureScript 101 tutorial (but I suspect this question applies to clojure to).
I create a channel where mouse movement events are placed using the following:
;; helper to get a channel where a dom event type will be put
(defn listen [el type]
(let [out (chan)]
(events/listen el type
(fn [e] (put! out e)))
out))
;; create a channel for mouse moves, take the values
;; and pass them to the console
(let [moves (listen (dom/getElement "canvas") "mousemove")]
(go (while true
(foo (<! moves)))))
This works, foo is evaluated when the mouse moves. But how can this be done only when the mouse is down?
My first guess would be to use an atom and two new channels for mousedown and mouseup. Then update atom with the mouse state and test against this in the go block. But I suspect this is wrong due to the use of an atom; hence the question.
Answering my own question, here is the closest I have got. Appears to work.
;; Util for create DOM channels
(defn listen [el type]
"Takes a DOM element and an event type. Returns a channel for the event"
;; out is a new channel
(let [out (chan (sliding-buffer 1))]
;; attach an event listener
(events/listen el type
;; the handler/callback of the listener takes the
;; event and put! in on the channel. We are using
;; put because we are not in a go block
(fn [e] (put! out e)))
;; return the channel
out))
(def canvas-el (dom/getElement "canvas"))
(def mouse-up (listen canvas-el "mouseup"))
(def mouse-down (listen canvas-el "mousedown"))
(def mouse-move (listen canvas-el "mousemove"))
(go (while true
(<! mouse-down)
(loop []
(let [[v ch] (alts! [mouse-move mouse-up])]
(when (= ch mouse-move)
(do
(.log js/console "move" (.-clientX v) (.-clientY v))
(recur)))))))