zilti / clojurefx Goto Github PK
View Code? Open in Web Editor NEWA Clojure JavaFX wrapper.
License: Eclipse Public License 1.0
A Clojure JavaFX wrapper.
License: Eclipse Public License 1.0
This isn't an issue, but since GitHub disabled private messaging, I was mainly just wondering if anything new was planned for ClojureFX. I myself haven't been able to work on it since early December until today. Let me know what I can do to contribute.
so I'm of the camp that I want to build my UIs as I have been accustomed to do with enterprise tools i.e. a UI builder and specifically SceneBuilder.
So I'm hoping to have Clojure load the FXML and then in a clojure idiomatic manner interact with the UI components e.g. add more elements to a list, setup action handlers to actions specified in the FXML etc.
Is this possible now with ClojureFX ?
thanks
Charles
I made some changes to your code(btw thanks for this) and I wondered what you thought about them.
First , extremely arbitrary change, I renamed fx to make(I think that the names should be more generic reflecting what they do).
I changed the construct node so that we could use a :cons key. :cons stands for constructor, I thought it might be nice to do things like this...
(make image :cons ["http://static.giantbomb.com/uploads/original/0/26/9780-itsahim.JPG" true] :height 100 :width 1000)
It can be passed a single argument or an array of args.
I would like to be able to contribute, I am a neophyte but I will be trying to use javafx a good bit...
Title says it all.
I experimented with creating the first JavaFX getting started tutorials using clojurefx, and it runs fine within the context of the REPL. But when I try to create a launchable application using a -main function, it runs and then immediately quits without an error. It seems that the event loop immediately stops after showing the stage. How would I go about creating a launchable clojurefx app?
Hey! It's been a long time since I've contributed/posted here.
I'm trying to add a ComboBox control to the scene. So far I've successfully done it like so:
(def my-obs-list
(FXCollections/observableList
[(Rectangle. 10 10 (. Color RED ))
(Rectangle. 10 10 (. Color GREEN))
(Rectangle. 10 10 (. Color BLUE ))]))
(def-fx combo-box-1 combo-box
:items my-obs-list)
(conj-fx! rt combo-box-1)
However, there are problems with this implementation that I won't detail here. Sure enough, I went to the JavaFX API and apparently it says that _"Putting nodes into the items list is strongly not recommended. This is because the default cell factory simply inserts Node items directly into the cell, including in the ComboBox 'button' area too. Because the scenegraph only allows for Nodes to be in one place at a time, this means that when an item is selected it becomes removed from the ComboBox list, and becomes visible in the button area. ... [T]he recommended approach, rather than inserting Node instances into the items list, is to put the relevant information into the ComboBox, and then provide a custom cell factory[:]"_
ComboBox<Color> cmb = new ComboBox<Color>();
cmb.getItems().addAll(
Color.RED,
Color.GREEN,
Color.BLUE);
cmb.setCellFactory(new Callback<ListView<Color>, ListCell<Color>>() {
@Override public ListCell<Color> call(ListView<Color> p) {
return new ListCell<Color>() {
private final Rectangle rectangle;
{
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
rectangle = new Rectangle(10, 10);
}
@Override protected void updateItem(Color item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setGraphic(null);
} else {
rectangle.setFill(item);
setGraphic(rectangle);
}
}
};
}
});
Now here's where it gets tricky for me. I'm not exactly sure how to implement this in Clojure. I have a vague idea that I need to start with something like:
(set-fx! combo-box-1 :cell-factory
(proxy [Callback] []
(call [p]
...
But that's where I'm not sure what to do. There's an override method within the return statement which is part of the ListCell class ("updateItem"), and I'm not sure how to go about implementing it in Clojure. Honestly, my Java experience is extremely limited to the point that, as of today, I barely knew what an interface meant or what a callback was, but I've at least managed to figure out things like those somewhat. Even so, I'll need some more help to get this ComboBox up and running. Who knew it would be so difficult (for me)?
Thanks for any help you can give!
_EDIT:_*
Maybe I don't even need to use a CellFactory and I can just use normal Clojure functions (working on JavaFX controls) instead?
I just wanted to make you aware of one thing.
This:
(bind-property! rt :gridLinesVisible (atom true))
nil
works perfectly fine. But
(bind-property! rt :grid-lines-visible (atom true))
#<IllegalArgumentException java.lang.IllegalArgumentException: No matching method found: bind for class java.lang.IllegalArgumentException>
doesn't work; I don't think it camelcases properly using the "camel" private function in clojurefx.core, so Java treats it like it's not a valid property.
That said, this works just fine (grid-pane -> GridPane):
(deffx rt grid-pane
:hgap 10
:vgap 10
:padding (Insets. 25 25 25 25)) ; :alignment CENTER
I am unable to invoke the scene constructor,
also "lein test" is failing the scene test:
FAIL "Constructor detection" at (core_test.clj:25)
Expected: javafx.scene.Scene
Actual: java.lang.IllegalArgumentException
The problem is here:
(defmethod preprocess-event javafx.scene.input.MouseEvent [e]
(-> (prep-event-map e
:button (-> (.getButton e) .valueOf str/lower-case keyword)
.valueOf returns enum constant from the string, while we are looking for the string from the enum, so probably .toString should be used instead.
_---Problem---_
After doing all the necessary require
and import
statements, I try to set up the root, scene, and stage like this. It worked great before the latest commit. Now there are a few errors with (.show stg) like so:
(deffx rt grid-pane
:hgap 10
:vgap 10
:padding (Insets. 25 25 25 25))
-> 'user/rt
(deffx scn scene
:width 800
:height 600
:root rt)
-> 'user/scn
(deffx stg stage
:title "Testing"
:scene scn)
-> 'user/stg
(run-now (.show stg))
-> IllegalArgumentException No matching field found: show for class java.lang.ClassCastException clojure.lang.Reflector.getInstanceField (Reflector.java:271)
(.show stg)
-> IllegalArgumentException No matching field found: show for class java.lang.ClassCastException clojure.lang.Reflector.getInstanceField (Reflector.java:271)
_---Source of problem---_
There are two blocks of code in the latest commits that cause this problem. The first is the Constructor Tools (clojurefx.core, line 320) block (specifically the construct
and construct-node
macros, methods, and multimethods). The second is the fx
macro (clojurefx.core, line 377). I'm not sure how to fix this.
Based on the issue we talked about recently - fx
vs. Java's new
operator - I'd like to eliminate new
altogether from my code using fx
. But to do that, there need to be ClojureFX constructors defined for each object, right? Or is there an easier way, just like you do programmatically for methods using method-fetcher
? Could there possibly be a constructor-fetcher
that programmatically gets the constructors for a given object type, which fx
then uses in place of a bunch of construct-node
methods?
Also, the construct-node
method for LinearGradient isn't working for me. I input the following:
(fx linear-gradient
:start-x 0.0 :start-y 1.0
:end-x 1.0 :end-y 0.0
:proportional true
:cycle-method (. CycleMethod NO_CYCLE)
:stops [(new Stop 0 (. Color (web "#f8bd55")))
(new Stop 0.14 (. Color (web "#c0fe56")))
(new Stop 0.28 (. Color (web "#5dfbc1")))
(new Stop 0.43 (. Color (web "#64c2f8")))
(new Stop 0.57 (. Color (web "#be4af7")))
(new Stop 0.71 (. Color (web "#ed5fc2")))
(new Stop 0.85 (. Color (web "#ef504c")))
(new Stop 1 (. Color (web "#f2660f")))])
And I just get the following error:
-> #<CompilerException java.lang.IllegalArgumentException: No matching ctor found for class javafx.scene.paint.LinearGradient>
I tried creating a construct-node
method for Stop based on the same code base:
(defmethod construct-node javafx.scene.paint.Stop [c {:keys [offset color]}]
(constructor-helper c [offset color]))
-> (fx stop :offset 0.0 :color (. Color (web "#f8bd55")))
But this yields the same error.
In order for us to know how to wrap an argument, we need to know the property name and the class that contains it.
(defmulti wrap-arg "Autoboxing-like behaviour for arguments for ClojureFX nodes."
(fn [arg# clazz] [(key arg#) clazz]))
(defmethod wrap-arg :default [arg# clazz] (val arg#))
And this would be an example:
(defn list->observable [l]
(javafx.collections.FXCollections/observableArrayList l))
(defmethod wrap-arg [:data javafx.scene.chart.PieChart] [arg# clazz]
(list->observable (val arg#)))
So that, when we call fx to build the component:
(fx pie-chart :data [(javafx.scene.chart.PieChart$Data. "Pie1" 3)
(javafx.scene.chart.PieChart$Data. "Pie2" 9)])
;by the way, PieChart$Data could also be added to the pkgs to make it nicer
fx* is going to be called it in a generic way as:
...
(doseq [arg# args#] ;; Apply arguments
(if (contains? methods# (key arg#))
(((key arg#) methods#) obj# (wrap-arg arg# (type obj#)))))
...
_This relates somewhat to the creating construct-node
methods issue._
As in the LinearGradient IllegalArgumentException issue, this works fine:
_(See note about get-prop-fx
at the end of the comment for info)_
(def kv-x
(new KeyValue (get-prop-fx stpn scale-x) 2))
But this doesn't:
(deffx kv-x key-value
:target (get-prop-fx stpn scale-x)
:end-value 2.0)
But the weird thing is that I already followed the resolution to the NullPointerException issue and added the KeyValue
class in core/pkgs
and also created a construct-node
multimethod implementation for it like so:
(defmethod construct-node javafx.animation.KeyValue [c {:keys [target end-value]}]
(constructor-helper c [target end-value]))
But no luck. I just get this error just like the LinearGradient IllegalArgumentException issue:
java.lang.IllegalArgumentException: No matching ctor found for class javafx.animation.KeyValue
This is an issue for the KeyValue
, KeyFrame
, and AnimationTimer
classes, and I'm sure will be an issue for other classes in the future, too, as I explore them all.
Also, just a question - if there is no construct-node
multimethod implementation for a given class/object, then is the object by default instantiated without any arguments, and then the key-value pairs are progressively added in via Java set
methods (setXValue
, setColor
, etc.)? If so, that would explain the IllegalArgumentException
s with only certain classes/objects and not others. The ones that have IllegalArgumentException
s are the classes/objects that must have arguments in the constructor.
_Note about get-prop-fx
:
get-prop-fx
is a function I created to get the property of something. (getfx circle1 :radius)
-> circle1.getRadius()
but (get-prop-fx circle1 :radius)
-> circle1.radiusProperty()
_
I'm currently using a function add!
that does (obviously) the opposite of what remove!
would do:
(defn add! [parent child]
(swap-content! parent #(conj % child)))
-> #'core/add!
(add! rt big-circle) ; an already-defined big circle appears on the scene
-> true
remove!
would remove a child node from its parent node. It could work like so:
(defn remove! [parent child]
; working code here
)
->#'core/remove!
(remove! rt big-circle) ; the big circle is removed and disappears
-> true
And finally, remove-all!
would remove all children nodes from their parent node.
(defn remove-all! [parent]
; working code here, maybe some looping constructs
)
->#'core/remove-all!
(remove-all! rt) ; the scene is cleared
-> true
The issue is this:
(deffx hide-button button
:text "Hide this window")
-> 'user/hide-button
(set-listener! hide-button
:onAction [_] (core/run-now (.hide stg)))
-> <IllegalArgumentException java.lang.IllegalArgumentException: No matching method found: setOnaction for class javafx.scene.control.Button>
I think this may have something to do with the camel
function again. It should say setOnAction
, not setOnaction
.
Currently I'm trying to set up an EventHandler for a ComboBox object named combo-box-1
. I've done it before with a Button object (login-btn
) like so:
(set-listener! login-btn
:on-action [_] (set-fx! login-btn :text-fill (. Color FIREBRICK))
For the ComboBox, I want a more specific type of event, specifically, a DragOver event (or perhaps something else - I'm just experimenting). I'm trying to do it like so, but it's not working:
(set-listener! combo-box-1
:on-drag-over [_] (set-fx! combo-box-1
:items my-obs-list-2))
How can I set this up? I'm not too familiar with this. Thanks!
_EDIT:_
I decided to play around with some things and discovered that the following works quite nicely, even though it's not idiomatic to ClojureFX. I did create a function for wrapping the EventHandler setup:
(defn event-handling [function]
(proxy [EventHandler] []
(handle [event] function)))
(defn my-event-handler-function [event]
(if (= (get-fx event :code) (. KeyCode ENTER))
(do (if (= (get-fx stage-title :fill) (. Color RED))
(set-fx! stage-title :fill (. Color BLACK))
(set-fx! stage-title :fill (. Color RED)))
(.consume event))))
(set-fx! combo-box-1 :on-key-pressed
(event-handling my-event-handler-function))
Maybe this can somehow be used like so into the EventHandler wrapper for ClojureFX:
(set-fx! combo-box-1 :on-key-pressed
(fx event-handler :handler my-event-handler-function))
When I try to construct a KeyValue using Java interop, it works great:
_(By the way, get-prop-fx
is a function I created to get the property of something. (getfx circle1 :radius) -> circle1.getRadius()
but (get-prop-fx circle1 :radius) -> circle1.radiusProperty()
...)_
(new KeyValue
(get-prop-fx (eval (symbol (str "circle" 1))) :translate-x)
(* (. Math random) 800))
-> #<KeyValue KeyValue [target=DoubleProperty [bean: Circle[centerX=0.0, centerY=0.0, radius=150.0, fill=0xffffff0d, ...
But when I try to use the deffx
function, it throws a NullPointerException
:
(deffx key-value1 key-value
:target (get-prop-fx (eval (symbol (str "circle" 1))) :translate-x)
:end-value (* (. Math random) 800))
-> CompilerException java.lang.NullPointerException
Maybe it's because I'm trying to do get-prop-fx
inside of deffx
, which creates problems with the JavaFX threads?
I find myself having to do a lot of (new ObjectType arguments)
constructors:
(deffx path1 path)
(. add (getfx path1 elements)
(new MoveTo 20 20)
(new CubicCurveTo 380 0 380 120 200 120)
(new CubicCurveTo 0 120 0 240 380 240))
However, I think it would be more idiomatic to ClojureFX and much easier to read to use a deffx
anonymously (with no associated symbol) like so:
(deffx path1 path)
(. add (getfx path1 elements)
(deffx move-to ; no symbol associated, just the JavaFX object type
:x 20 :y 20)
(deffx cubic-curve-to
:control-x-1 380 :control-y-1 0
:control-x-2 380 :control-y-2 120
:x 200 :y 120)
(deffx cubic-curve-to
:control-x-1 0 :control-y-1 120
:control-x-2 0 :control-y-2 240
:x 380 :y 240))
In the other Clojure wrapper for JavaFX I was using, I used Java interop directly to get properties of JavaFX objects. Is there a function in ClojureFX similar to deffx
that uses key-value pairs to get properties? Like so:
(getfx btn :text)
-> "This is the button's text"
(getfx scn :width)
-> 800
Alternatively - and I don't know if this is possible - perhaps one could call the object directly like so:
(btn :text)
-> "This is the button's text"
Is there anything that does this without having to call Java directly (which is the whole point of a wrapper)?
Before a recent commit, the GridPane
method of the swap-content!
multimethod didn't exist specifically. I guess (def-simple-swapper javafx.scene.layout.Pane .getChildren .setAll)
might have been used in place of it, but I'm not sure. In either case, the new GridPane
method of the swap-content!
multimethod doesn't work correctly, even though most of the others (for instance, for Stage
, Scene
, etc.) do. I'm using a function I created to add a child node to a parent node, which uses swap-content!
like so:
(defn add! [parent child]
(swap-content! parent #(conj % child)))
I most commonly use it to add JavaFX objects to the root
like so:
(add! rt my-text)
But for some reason, when rt
is a GridPane
and I don't comment out the new GridPane
method of swap-content!
, I get the following error:
llegalStateException Not on FX application thread; currentThread = nREPL-worker-0 com.sun.javafx.tk.Toolkit.checkFxUserThread (Toolkit.java:209)
I'm not sure why this is. Maybe the GridPane
method of swap-content!
isn't using run-now
or run-later
?
Also, just a random question. If the def-simple-swapper
s work with swap-content!
, then why worry about the complexity of creating specific methods for each object? I've used the def-simple-swapper
s indirectly with all kinds of objects and I've commented out most of the new methods of swap-content!
, and it all still works great. Is there something I'm missing here?
P.S. - I love the page you created here: http://zilti.github.io/clojurefx/ . Very informative!
This works fine:
(new LinearGradient
0.0 1.0 1.0 0.0 true
(. CycleMethod NO_CYCLE)
(into-array Stop
[(new Stop 0 (. Color (web "#f8bd55")))
(new Stop 0.14 (. Color (web "#c0fe56")))
(new Stop 0.28 (. Color (web "#5dfbc1")))
(new Stop 0.43 (. Color (web "#64c2f8")))
(new Stop 0.57 (. Color (web "#be4af7")))
(new Stop 0.71 (. Color (web "#ed5fc2")))
(new Stop 0.85 (. Color (web "#ef504c")))
(new Stop 1 (. Color (web "#f2660f")))]))
-> #<LinearGradient linear-gradient(from 0.0% 100.0% to 100.0% 0.0%, 0xf8bd55ff 0.0%, 0xc0fe56ff 14.000000000000002%, ...
But using the deffx equivalent does not:
(deffx background linear-gradient
:start-x 0.0
:start-y 1.0
:end-x 1.0
:end-y 0.0
:proportional true
:cycle-method (. CycleMethod NO_CYCLE)
:stops (into-array Stop
[(new Stop 0 (. Color (web "#f8bd55")))
(new Stop 0.14 (. Color (web "#c0fe56")))
(new Stop 0.28 (. Color (web "#5dfbc1")))
(new Stop 0.43 (. Color (web "#64c2f8")))
(new Stop 0.57 (. Color (web "#be4af7")))
(new Stop 0.71 (. Color (web "#ed5fc2")))
(new Stop 0.85 (. Color (web "#ef504c")))
(new Stop 1 (. Color (web "#f2660f")))]))
-> <CompilerException java.lang.IllegalArgumentException: No matching ctor found for class javafx.scene.paint.LinearGradient
Any solutions?
This is a minor addition.
I was trying to work with the Group class but realized that there was no method in swap-content
for it. So I added this to the list of def-simple-swapper
:
(def-simple-swapper javafx.scene.Group .getChildren .setAll)
This seems to work well so far.
Static and non-static methods have to be invoked in different ways; find a way to distinguish them.
I've been using the :listener
key in deffx
for a few simple things - for instance, changing the text of a button on a given event, etc. But the :listener
key only associates an EventListener
to a given object, for instance, a Button
. I've been working through some JavaFX exercises and one of them calls for the creation of an AnimationTimer
like so:
timer = new AnimationTimer() {
@Override
public void handle(long n) {
txt.setText(i.toString());
}
};
But there is currently no idiomatic ClojureFX way to do this. It would be nice if this were possible:
(defn my-handle-func [] (bind-property! txt :text (str i)))
(deffx timer animation-timer
:handle my-handle-func)
I've also been needing to use an EventHandler
instantiated like so:
EventHandler onFinished = new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
stack.setTranslateX(java.lang.Math.random()*200-100);
}
};
It would be nice if this were possible in ClojureFX:
(deffx event-h event-handler
:handle (fn [] (bind-property! stackpn :translate-x (* (. Math random) (- 200 100)))))
Note that I have standardized some of the function names for my own use (deffx
-> def-fx
; fx-remove!
-> remove-fx!
; setfx!
-> set-fx!
, etc.).
_fx-remove!
works very well when I comment out the GridPane swap-content_!
multimethod.*
;-----Root-----
(def-fx rt grid-pane
:hgap 10
:vgap 10
:padding (Insets. 25 25 25 25)
:alignment (. Pos CENTER))
;-----Scene/Window-----
(def-fx scn scene
:width 800
:height 600
:root rt)
;-----Stage/Window-surround-----
(def-fx stg stage
:title "Testing"
:scene scn)
(do-fx (.show stg))
;-----Scene title-----
(def-fx stage-title text
:text "Welcome to the tester."
:font (Font. "Myriad Pro Light" 30))
(set-grid-index! stage-title 1 0)
(conj-fx! rt stage-title)
Note that the last return value is:
#<GridPane Grid hgap=10.0, vgap=10.0, alignment=CENTER>
Then, as a test, I do the following:
(remove-fx! rt stage-title)
Instead of the Text object disappearing, I get the following return value, the same one as before:
#<GridPane Grid hgap=10.0, vgap=10.0, alignment=CENTER>
remove-all-fx!
, however, works very well, even with the GridPane swap-content*!
multimethod.
This currently doesn't work anymore:
(fx button {:text "Buttontext"})
Just an enhancement suggestion/question.
Currently I'm doing this:
(bind-property! scn :width (atom 300))
(bind-property! scn :width (atom 600))
Would it be possible to make the function capable of taking in multiple properties as arguments to bind-property!
kind of like deffx
?:
(deffx scn scene
:width 800
:height 600
:root rt)
It would work like this:
(bind-property! scn :width (atom 300) :height (atom 600))
A cursory look tells me that you can just change the code in bind-property!
from [obj prop at]
to include an ampersand &
to account for more arguments, then do some sort of loop construct over the atoms.
For some reason, I can't seem to set the layoutX or layoutY properties of a ListView object, which means I can't reposition it once I conj it to the root node. I've tried this:
(set-fx! my-list-view :layout-y 200)
The first time I do this, an exception is thrown (IllegalStateException Not on FX application thread
), and the second time, nil
is returned but nothing is actually changed (at least visually, because get-fx
returns the updated value of layoutX or layoutY).
I've also tried:
(core/run-later (.setLayoutY my-list-view 200))
and:
(core/run-now (.setLayoutY my-list-view 200))
but no luck.
The only thing that seems to work is just using remove-fx!
to remove the ListView from the root and creating a new one with an updated layout value, but that seems unnecessary.
Even so, perhaps this issue is a force for good, because perhaps it can be an impetus towards developing a more functional approach. Although it could potentially be slower, one alternative is, instead of changing the value of a given property, to instead traverse the properties of a node/object and return a new node with a different value associated with that key, like so:
(update-fx my-list-view :pref-width 220)
It would have no side effects and would instead simply return the same node/object but with a different value associated with pref-width
. Then it could be conj!
ed on to its parent node as necessary.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.