├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.clj ├── demo.gif ├── demo.mp4 ├── deps.edn ├── help-ui.png ├── inspector.png ├── src └── cljfx │ ├── dev.clj │ └── dev │ ├── definitions.clj │ ├── extensions.clj │ ├── help.clj │ ├── ui.clj │ └── validation.clj └── test └── cljfx └── dev_test.clj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: vlaaad 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cljfx-dev.iml 2 | /.cpcache 3 | /target 4 | /.idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 cljfx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Clojars Project](https://img.shields.io/clojars/v/io.github.cljfx/dev.svg)](https://clojars.org/io.github.cljfx/dev) 2 | 3 | ![Demo](demo.gif) 4 | 5 | # Cljfx dev tools 6 | 7 | [Cljfx](https://github.com/cljfx/cljfx) is a declarative, functional and extensible wrapper of JavaFX inspired by better parts of react and re-frame. Cljfx dev tools are a set of tools that help with developing cljfx applications but should not be included into the production distribution of the cljfx app. 8 | 9 | ## Rationale 10 | 11 | The default developer experience of cljfx has some issues: 12 | - what are the allowed props for different JavaFX types is not clear and requires looking it up in the source code; 13 | - what are the allowed JavaFX type keywords requires looking it up in the source code; 14 | - errors when using non-existent props are unhelpful; 15 | - generally, errors that happen during cljfx lifecycle are unhelpful because the stack traces have mostly cljfx internals instead of user code. 16 | 17 | Cljfx dev tools solve these issues by providing: 18 | - reference for cljfx types and props; 19 | - specs for cljfx descriptions, so they can be validated; 20 | - dev-time lifecycles that perform validation and add cljfx component stacks to exceptions to help with debugging; 21 | 22 | ## Installation 23 | 24 | See latest version on [Clojars](https://clojars.org/io.github.cljfx/dev). 25 | 26 | ## Requirements 27 | 28 | Cljfx dev tools require Java 11 or later. 29 | 30 | ## Tools 31 | 32 | ### Props and types reference 33 | 34 | If you don't remember the props required by some cljfx type, or if you don't know what are the available types, you can use `cljfx.dev/help` to look up this information: 35 | 36 | ```clojure 37 | (require 'cljfx.dev) 38 | 39 | ;; look up available types: 40 | (cljfx.dev/help) 41 | ;; Available cljfx types: 42 | ;; Cljfx type Instance class 43 | ;; :accordion javafx.scene.control.Accordion 44 | ;; :affine javafx.scene.transform.Affine 45 | ;; ...etc 46 | 47 | 48 | 49 | ;; look up information about fx type: 50 | (cljfx.dev/help :label) 51 | ;; Cljfx type: 52 | ;; :label 53 | ;; 54 | ;; Instance class: 55 | ;; javafx.scene.control.Label 56 | ;; 57 | ;; Props Value type 58 | ;; :accessible-help string 59 | ;; :accessible-role either of: :button, :check-box, :check-menu-item, :combo-box, :context-menu, :date-picker, :decrement-button, :hyperlink, :image-view, :increment-button, :list-item, :list-view, :menu, :menu-bar, :menu-button, :menu-item, :node, :page-item, :pagination, :parent, :password-field, :progress-indicator, :radio-button, :radio-menu-item, :scroll-bar, :scroll-pane, :slider, :spinner, :split-menu-button, :tab-item, :tab-pane, :table-cell, :table-column, :table-row, :table-view, :text, :text-area, :text-field, :thumb, :titled-pane, :toggle-button, :tool-bar, :tooltip, :tree-item, :tree-table-cell, :tree-table-row, :tree-table-view, :tree-view 60 | ;; :accessible-role-description string 61 | ;; ...etc 62 | 63 | 64 | 65 | ;; look up information about a prop: 66 | (cljfx.dev/help :label :graphic) 67 | ;; Prop of :label - :graphic 68 | ;; 69 | ;; Cljfx desc, a map with :fx/type key 70 | ;; 71 | ;; Required instance class: 72 | ;; javafx.scene.Node¹ 73 | ;; 74 | ;; --- 75 | ;; ¹javafx.scene.Node - Fitting cljfx types: 76 | ;; Cljfx type Class 77 | ;; :accordion javafx.scene.control.Accordion 78 | ;; :ambient-light javafx.scene.AmbientLight 79 | ;; :anchor-pane javafx.scene.layout.AnchorPane 80 | ;; :arc javafx.scene.shape.Arc 81 | ;; ...etc 82 | ``` 83 | 84 | You can also use help in a UI form that shows that same information, but is easier to search for: 85 | ```clojure 86 | (cljfx.dev/help-ui) 87 | ``` 88 | Invoking this fn will open a window with props and types reference: 89 | ![Help UI screenshot](help-ui.png) 90 | 91 | ### Improved error messages with spec 92 | 93 | You can set validating type->lifecycle opt that will validate all cljfx component descriptions using spec and properly describe the errors: 94 | 95 | ```clojure 96 | ;; suppose you have a simple app: 97 | (require '[cljfx.api :as fx]) 98 | 99 | (defn message-view [{:keys [text]}] 100 | {:fx/type :label 101 | :text text}) 102 | 103 | (defn root-view [{:keys [text]}] 104 | {:fx/type :stage 105 | :showing true 106 | :scene {:fx/type :scene 107 | :root {:fx/type message-view 108 | :text text}}}) 109 | 110 | (def state (atom {:text "Hello world"})) 111 | 112 | ;; you will use custom logic here to determine if it's a prod or dev, 113 | ;; e.g. by using a system property: (Boolean/getBoolean "my-app.dev") 114 | (def in-development? true) 115 | 116 | (def renderer 117 | (fx/create-renderer 118 | :middleware (fx/wrap-map-desc #(assoc % :fx/type root-view)) 119 | ;; optional: print errors in REPL's *err* output 120 | :error-handler (bound-fn [^Throwable ex] 121 | (.printStackTrace ^Throwable ex *err*)) 122 | :opts (cond-> {} 123 | ;; Validate descriptions in dev 124 | in-development? 125 | (assoc :fx.opt/type->lifecycle @(requiring-resolve 'cljfx.dev/type->lifecycle))))) 126 | 127 | (defn -main [] 128 | (fx/mount-renderer state renderer)) 129 | 130 | ;; then start the app: 131 | (-main) 132 | ;; invalid state change - making text prop of a label not a string: 133 | (swap! state assoc :text :not-a-string) 134 | ;; prints to *err*: 135 | ;; clojure.lang.ExceptionInfo: Invalid cljfx description of :label type: 136 | ;; :not-a-string - failed: string? in [:text] 137 | ;; 138 | ;; Cljfx component stack: 139 | ;; :label 140 | ;; user/message-view 141 | ;; :scene 142 | ;; :stage 143 | ;; user/root-view 144 | ;; 145 | ;; at cljfx.dev$ensure_valid_desc.invokeStatic(validation.clj:62) 146 | ;; at cljfx.dev$ensure_valid_desc.invoke(validation.clj:58) 147 | ;; at cljfx.dev$wrap_lifecycle$reify__22150.advance(validation.clj:80) 148 | ;; at ... 149 | ``` 150 | If you already use custom type->lifecycle opt, instead of using `cljfx.dev/type->lifecycle` you can use `cljfx.dev/wrap-type->lifecycle` to wrap your type->lifecycle with validations. 151 | 152 | Additionally, you can validate individual descriptions while developing: 153 | ```clojure 154 | (cljfx.dev/explain-desc 155 | {:fx/type :stage 156 | :showing true 157 | :scene {:fx/type :scene 158 | :root {:fx/type :text-formatter 159 | :value-converter :int}}}) 160 | ;; :int - failed: #{:local-date-time :long :double :short :date-time :number :local-time :default :float :integer :byte :local-date :big-integer :boolean :character :big-decimal} in [:scene :root :value-converter] 161 | ;; :int - failed: (instance-of javafx.util.StringConverter) in [:scene :root :value-converter] 162 | 163 | (cljfx.dev/explain-desc 164 | {:fx/type :stage 165 | :showing true 166 | :scene {:fx/type :scene 167 | :root {:fx/type :text-formatter 168 | :value-converter :integer}}}) 169 | ;; {:fx/type :text-formatter, :value-converter :integer} - failed: (desc-of (quote javafx.scene.Parent)) in [:scene :root] 170 | 171 | (cljfx.dev/explain-desc 172 | {:fx/type :stage 173 | :showing true 174 | :scene {:fx/type :scene 175 | :root {:fx/type :text-field 176 | :text-formatter {:fx/type :text-formatter 177 | :value-converter :integer}}}}) 178 | ;; Success! 179 | ``` 180 | 181 | ### Cljfx component inspector 182 | 183 | Using the same dev type->lifecycle opt, you also get cljfx component tree inspector that can be opened by pressing F12: 184 | 185 | ![Inspector screenshot](inspector.png) 186 | 187 | Inspector shows a live tree of components and their props. Open shortcut can be configured using `:inspector-shortcut` argument to `wrap-type->lifecycle` fn. 188 | 189 | ## Acknowledgments 190 | 191 | Many thanks to [Clojurists Together](https://www.clojuriststogether.org/) for funding this work! -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [clojure.tools.build.api :as b] 3 | [cemerick.pomegranate.aether :as aether])) 4 | 5 | (def lib 'io.github.cljfx/dev) 6 | (def version (format "1.0.%s" (b/git-count-revs nil))) 7 | (def class-dir "target/classes") 8 | (def basis (b/create-basis {:project "deps.edn"})) 9 | (def jar-file (format "target/%s-%s.jar" (name lib) version)) 10 | (def pom-path (format "%s/META-INF/maven/%s/pom.xml" class-dir lib)) 11 | 12 | (defn deploy [_] 13 | (b/delete {:path "target"}) 14 | (b/write-pom 15 | {:basis basis 16 | :class-dir class-dir 17 | :lib lib 18 | :version version 19 | :src-dirs ["src"] 20 | :scm {:url "https://github.com/cljfx/dev" 21 | :tag (b/git-process {:git-args ["rev-parse" "HEAD"]})} 22 | :pom-data [[:licenses 23 | [:license 24 | [:name "MIT"] 25 | [:url "https://opensource.org/license/mit"]]]]}) 26 | (b/copy-dir 27 | {:src-dirs ["src"] 28 | :target-dir class-dir}) 29 | (b/jar {:class-dir class-dir 30 | :jar-file jar-file}) 31 | (b/git-process {:git-args ["tag" (str "v" version)]}) 32 | (aether/deploy 33 | :coordinates [lib version] 34 | :jar-file jar-file 35 | :pom-file pom-path 36 | :repository (-> basis 37 | :mvn/repos 38 | (select-keys ["clojars"]) 39 | (update "clojars" assoc 40 | :username "vlaaad" 41 | :password (if-let [console (System/console)] 42 | (-> console 43 | (.readPassword "Clojars token:" (object-array 0)) 44 | String/valueOf) 45 | (do (print "Clojars token:") 46 | (flush) 47 | (read-line)))))) 48 | (b/git-process {:git-args ["push" "origin" (str "v" version)]})) 49 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cljfx/dev/4dcbfb2528939ff4fe7dea0667c8a94f8fccf615/demo.gif -------------------------------------------------------------------------------- /demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cljfx/dev/4dcbfb2528939ff4fe7dea0667c8a94f8fccf615/demo.mp4 -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {cljfx/cljfx {:mvn/version "1.9.1"} 2 | cljfx/css {:mvn/version "1.1.0"}} 3 | :aliases {;; clj -T:build deploy 4 | :build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.5" :git/sha "2a21b7a"} 5 | clj-commons/pomegranate {:mvn/version "1.2.0"}} 6 | :ns-default build} 7 | :dev {:extra-deps {enlive/enlive {:mvn/version "1.1.6"}}}}} 8 | -------------------------------------------------------------------------------- /help-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cljfx/dev/4dcbfb2528939ff4fe7dea0667c8a94f8fccf615/help-ui.png -------------------------------------------------------------------------------- /inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cljfx/dev/4dcbfb2528939ff4fe7dea0667c8a94f8fccf615/inspector.png -------------------------------------------------------------------------------- /src/cljfx/dev.clj: -------------------------------------------------------------------------------- 1 | (ns cljfx.dev 2 | "Helpers for cljfx app development that shouldn't be included into production 3 | 4 | You can get help for existing lifecycles and props by using help fn: 5 | 6 | (cljfx.dev/help) 7 | ;; prints information about all cljfx lifecycles including :label 8 | (cljfx.dev/help :label) 9 | ;; prints information about label and its props including :graphic 10 | (cljfx.dev/help :label :graphic) 11 | ;; prints information about :graphic prop of a label 12 | 13 | You can also add cljfx component validation that greatly improves error 14 | messages using cljfx.dev/type->lifecycle (or cljfx.dev/wrap-type->lifecycle): 15 | 16 | (fx/create-component 17 | {:fx/type :stage 18 | :scene {:fx/type :scene 19 | :root {:fx/type :label 20 | :text true}}} 21 | {:fx.opt/type->lifecycle cljfx.dev/type->lifecycle}) 22 | ;; Execution error (ExceptionInfo) at cljfx.dev/ensure-valid-desc (validation.clj:62). 23 | ;; Invalid cljfx description of :stage type: 24 | ;; true - failed: string? in [:scene :root :text] 25 | ;; 26 | ;; Cljfx component stack: 27 | ;; :stage" 28 | (:require [cljfx.lifecycle :as lifecycle] 29 | [cljfx.api :as fx] 30 | [clojure.spec.alpha :as s] 31 | [clojure.set :as set])) 32 | 33 | (def ^:private registry 34 | ;; types is a map: 35 | ;; id -> lifecycle config (lc) 36 | ;; id is either: 37 | ;; - keyword (assume invalid if validated id not registered) 38 | ;; - symbol (assume valid if validated id not registered) 39 | ;; lifecycle config is a map with keys: 40 | ;; - :id 41 | ;; - :spec (optional, the spec) 42 | ;; - :of (symbol of a class this lifecycle produces, or maybe fn from desc to symbol) 43 | ;; props is a map of id -> prop kw -> prop type maps (i.e. {:type ...}) 44 | (atom {:types {} 45 | :props {}})) 46 | 47 | (defn any->id [x] 48 | (:cljfx/id (meta x))) 49 | 50 | (defn keyword->id [x] 51 | (when (keyword? x) x)) 52 | 53 | (defn fn->id [x] 54 | (when (fn? x) 55 | (when-let [[_ str] (->> x 56 | class 57 | .getName 58 | Compiler/demunge 59 | (re-matches #"^([^/]*?/([^/]*?|/))(--\d\d\d\d)?$"))] 60 | (symbol str)))) 61 | 62 | (def ^:dynamic *type->id* 63 | (some-fn any->id keyword->id fn->id)) 64 | 65 | (def ^:dynamic *type->lifecycle* 66 | (some-fn fx/keyword->lifecycle fx/fn->lifecycle)) 67 | 68 | (defn- valid-fx-type? [m] 69 | (let [type (:fx/type m) 70 | lifecycle (or (*type->lifecycle* type) type)] 71 | (or (contains? (meta lifecycle) `lifecycle/create) 72 | (satisfies? lifecycle/Lifecycle lifecycle)))) 73 | 74 | (defmacro defdynaspec [name & fn-tail] 75 | (let [multi-name (symbol (str name "-multi-fn"))] 76 | `(do 77 | (defmulti ~multi-name (constantly :cljfx/desc)) 78 | (defmethod ~multi-name :cljfx/desc ~@fn-tail) 79 | (def ~name (s/multi-spec ~multi-name :cljfx/desc))))) 80 | 81 | (defn- keyword-id-should-be-registered [_] false) 82 | (def ^:private keyword-id-should-be-registered-spec (s/spec keyword-id-should-be-registered)) 83 | (def ^:private any-spec (s/spec any?)) 84 | 85 | (defdynaspec desc->spec [m] 86 | (let [type (:fx/type m) 87 | id (*type->id* type)] 88 | (if (nil? id) 89 | any-spec 90 | (if-let [lc (-> @registry :types id)] 91 | (or (some->> (:spec lc) (s/and (s/conformer #(dissoc % :fx/type)))) 92 | any-spec) 93 | (if (keyword? id) 94 | keyword-id-should-be-registered-spec ;; assume typo 95 | any-spec))))) 96 | 97 | (s/def :cljfx/desc 98 | (s/and map? valid-fx-type? desc->spec)) 99 | 100 | (defn register-props! 101 | "Associate props description with some id 102 | 103 | Args: 104 | id prop identifier, either keyword or symbol 105 | parent semantical parent id of the prop map, meaning props with the id 106 | should also accept props with parent id 107 | props a map from keyword to prop description, which is a map with 108 | a :type key that can be either: 109 | - symbol of a class name 110 | - keyword that defines a corresponding spec form by extending 111 | keyword-prop->spec-form multi-method" 112 | ([id props] 113 | (register-props! id nil props)) 114 | ([id parent props] 115 | {:pre [(ident? id) 116 | (or (nil? parent) (ident? parent)) 117 | (or (nil? props) 118 | (and (map? props) 119 | (every? (fn [[k v]] 120 | (and (keyword? k) 121 | (contains? v :type))) 122 | props)))]} 123 | (swap! 124 | registry 125 | (fn [registry] 126 | (update 127 | registry 128 | :props (fn [id->props] 129 | (let [props 130 | (cond->> props 131 | parent 132 | (merge 133 | (or (id->props parent) 134 | (throw 135 | (ex-info 136 | (str "parent " parent " not registered") 137 | {:parent parent 138 | :ids (set (keys id->props))})))))] 139 | (assoc id->props id props)))))) 140 | id)) 141 | 142 | (defn register-type! 143 | "Associate cljfx type description with some id 144 | 145 | Optional kv-args: 146 | :spec a spec to use when validating props of components with the id 147 | :of component instance class identifier, either: 148 | - symbol of a class name, e.g. javafx.scene.Node 149 | - keyword of a prop that hold another cljfx description that 150 | defines component instance class, e.g. :desc" 151 | [id & {:keys [spec of] :as opts}] 152 | {:pre [(ident? id) 153 | (or (nil? of) 154 | (ident? of))]} 155 | (swap! registry update :types assoc id (assoc opts :id id)) 156 | id) 157 | 158 | (defn only-keys [ks] 159 | (fn [m] 160 | (every? #(contains? ks %) (keys m)))) 161 | 162 | (defn instance-of [c] 163 | (fn [x] 164 | (instance? c x))) 165 | 166 | (defmulti keyword-prop->spec-form :type) 167 | 168 | (defn prop->spec-form 169 | "Convert prop type config to spec form (i.e. clojure form that evals to spec) 170 | 171 | You can extend prop type configs by adding more implementations to 172 | keyword-prop->spec-form multimethod" 173 | [prop] 174 | (let [{:keys [type]} prop] 175 | (if (symbol? type) 176 | `(instance-of ~type) 177 | (keyword-prop->spec-form prop)))) 178 | 179 | (defn make-composite-spec [id & {:keys [req]}] 180 | (let [props (-> @registry :props (get id)) 181 | ks (set (keys props)) 182 | spec-ns (str "cljfx.composite." (name id)) 183 | k->spec-kw #(keyword spec-ns (name %))] 184 | (eval 185 | `(do 186 | ~@(for [k ks] 187 | `(s/def ~(k->spec-kw k) ~(prop->spec-form (get props k)))) 188 | (s/and (s/keys 189 | ~@(when req 190 | [:req-un (if (set? req) 191 | [(list* 'or (mapv #(list* 'and (mapv k->spec-kw %)) req))] 192 | (mapv k->spec-kw req))]) 193 | :opt-un ~(into [] (map k->spec-kw) (sort ks))) 194 | (only-keys ~ks)))))) 195 | 196 | (defn register-composite! 197 | "Associate a composite lifecycle type description with some id 198 | 199 | Required kv-args: 200 | :of symbol of a component instance class 201 | 202 | Optional kv-args: 203 | :parent semantic parent id of a lifecycle, meaning lifecycle with the id 204 | should also accept all props of parent id 205 | :props a map from keyword to prop description, which is a map with 206 | a :type key that can be either: 207 | - symbol of a class name 208 | - keyword that defines a corresponding spec form by extending 209 | keyword-prop->spec-form multi-method 210 | :req required props on the component, either: 211 | - a vector of prop keywords (all are required) 212 | - a set of vectors of prop keywords (either vector is required)" 213 | [id & {:keys [parent props of req]}] 214 | {:pre [(symbol? of) 215 | (every? simple-keyword? (keys props))]} 216 | (register-props! id parent props) 217 | (apply register-type! id :spec (make-composite-spec id :req req) :of of (when req 218 | [:req req]))) 219 | 220 | (load "dev/definitions") 221 | 222 | (load "dev/extensions") 223 | 224 | (defmulti short-keyword-prop-help-string :type) 225 | (defmethod short-keyword-prop-help-string :default [{:keys [type]}] 226 | (name type)) 227 | (defn- short-prop-help-string [{:keys [type] :as prop-desc}] 228 | (if (symbol? type) 229 | (str "instance of " type) 230 | (short-keyword-prop-help-string prop-desc))) 231 | 232 | (defmulti long-keyword-prop-help-syntax :type) 233 | (defmethod long-keyword-prop-help-syntax :default [prop] 234 | (short-keyword-prop-help-string prop)) 235 | (defn long-prop-help-syntax [{:keys [type] :as prop}] 236 | (if (symbol? type) 237 | (str "Instance of:\n" type) 238 | (long-keyword-prop-help-syntax prop))) 239 | 240 | (load "dev/help") 241 | 242 | (defn help 243 | "Print help about cljfx types and props 244 | 245 | All the information provided by this window can also be retrieved using 246 | cjlfx.dev/help-ui window." 247 | ([] 248 | (let [ts (->> @registry :types)] 249 | (println "Available cljfx types:") 250 | (println 251 | (str-table 252 | (->> ts keys (sort-by str)) 253 | {:label "Cljfx type" :fn identity} 254 | {:label "Instance class" :fn #(let [of (:of (get ts %))] 255 | (if (symbol? of) (str of) ""))})))) 256 | ([fx-type] 257 | (cond 258 | (or (keyword? fx-type) (qualified-symbol? fx-type)) 259 | (let [r @registry 260 | props (get-in r [:props fx-type]) 261 | type (get-in r [:types fx-type]) 262 | javadoc (get-in r [:javadoc fx-type])] 263 | (when (or type props) 264 | (println "Cljfx type:") 265 | (println fx-type) 266 | (println)) 267 | (when (symbol? (:of type)) 268 | (println "Instance class:") 269 | (println (:of type)) 270 | (println)) 271 | (when javadoc 272 | (println "Javadoc URL:") 273 | (println (str javadoc-prefix javadoc)) 274 | (println)) 275 | (when (:req type) 276 | (if (set? (:req type)) 277 | (do (println "Required props, either:") 278 | (doseq [req (:req type)] 279 | (println "-" (str/join ", " (sort req))))) 280 | (do (println "Required props:") 281 | (println (str/join ", " (sort (:req type)))))) 282 | (println)) 283 | (when props 284 | (println 285 | (str-table 286 | (sort (keys props)) 287 | {:label "Props" :fn identity} 288 | {:label "Value type" :fn #(-> % props short-prop-help-string)}))) 289 | (when (and (not props) (:spec type)) 290 | (println "Spec:") 291 | (println (s/describe (:spec type)))) 292 | (when (and (not type) (not props)) 293 | (println '???))) 294 | 295 | (or (simple-symbol? fx-type) (class? fx-type)) 296 | (let [ts (known-types-of fx-type)] 297 | (println "Class:") 298 | (println fx-type) 299 | (println) 300 | (when (seq ts) 301 | (println "Fitting cljfx types:") 302 | (println 303 | (str-table 304 | ts 305 | {:label "Cljfx type" :fn :id} 306 | {:label "Class" :fn :of})))) 307 | 308 | (*type->id* fx-type) 309 | (recur (*type->id* fx-type)) 310 | 311 | :else 312 | (println '???))) 313 | ([fx-type prop-kw] 314 | (cond 315 | (or (keyword? fx-type) (qualified-symbol? fx-type)) 316 | (let [r @registry 317 | prop (get-in r [:props fx-type prop-kw])] 318 | (if prop 319 | (do 320 | (println (str "Prop of " fx-type " - " prop-kw)) 321 | (println) 322 | (println (convert-help-syntax-to-string (long-prop-help-syntax prop)))) 323 | (println '???))) 324 | 325 | :else 326 | (println '???)))) 327 | 328 | (load "dev/validation") 329 | 330 | (defn wrap-type->lifecycle 331 | "Wrap type->lifecycle used in the cljfx UI app with improved error messages 332 | 333 | Wrapped lifecycle performs spec validation of cljfx descriptions that results 334 | in better error messages shown when cljfx descriptions are invalid. 335 | 336 | Additionally, exceptions thrown during cljfx lifecycle show a cljfx component 337 | stack to help with debugging. 338 | 339 | Optional kv-args: 340 | type->lifecycle the type->lifecycle fn used in opts of your app 341 | type->id custom type->id if you need a way to get id from your 342 | custom lifecycles 343 | inspector-shortcut shortcut to open cljfx component inspector view, key 344 | combination, defaults to [:f12]" 345 | [& {:keys [type->lifecycle type->id inspector-shortcut] 346 | :or {type->lifecycle *type->lifecycle* 347 | type->id *type->id* 348 | inspector-shortcut [:f12]}}] 349 | (let [f (memoize wrap-lifecycle)] 350 | (fn [type] 351 | (f type type->lifecycle type->id inspector-shortcut)))) 352 | 353 | (def type->lifecycle 354 | "Default type->lifecycle that can be used in the cljfx UI app to improve error 355 | messages" 356 | (wrap-type->lifecycle)) 357 | 358 | (defn explain-desc 359 | "Validate cljfx description and report any issues 360 | 361 | Args: 362 | type->lifecycle the type->lifecycle fn used in opts of your app 363 | type->id custom type->id if you need a way to get id from your 364 | custom lifecycles" 365 | [desc & {:keys [type->lifecycle type->id] 366 | :or {type->lifecycle *type->lifecycle* 367 | type->id *type->id*}}] 368 | (binding [*type->lifecycle* type->lifecycle 369 | *type->id* type->id] 370 | (if-let [explain-data (s/explain-data :cljfx/desc desc)] 371 | (println (explain-str explain-data)) 372 | (println "Success!")))) 373 | 374 | (load "dev/ui") 375 | 376 | (defn help-ui 377 | "Open a window with cljfx type and prop reference 378 | 379 | All the information provided by this window can also be retrieved using 380 | cjlfx.dev/help fn. 381 | 382 | Controls in the window: 383 | - type when focused on type/prop list views - filter the list view 384 | - Escape when focused on type/prop list views - clear the filter 385 | - Ctrl+Tab/Ctrl+Shift+Tab when focuesed on prop/javadoc tab pane - switch tabs" 386 | [] 387 | (launch-help-ui!) 388 | nil) -------------------------------------------------------------------------------- /src/cljfx/dev/extensions.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'cljfx.dev) 2 | (import '[clojure.lang IRef]) 3 | 4 | (s/def :cljfx.ext-on-instance-lifecycle/on-created ifn?) 5 | (s/def :cljfx.ext-on-instance-lifecycle/on-advanced ifn?) 6 | (s/def :cljfx.ext-on-instance-lifecycle/on-deleted ifn?) 7 | 8 | (register-type! `fx/ext-on-instance-lifecycle 9 | :spec (s/keys :opt-un [:cljfx.ext-on-instance-lifecycle/on-created 10 | :cljfx.ext-on-instance-lifecycle/on-advanced 11 | :cljfx.ext-on-instance-lifecycle/on-deleted] 12 | :req-un [:cljfx/desc]) 13 | :of :desc) 14 | 15 | (s/def :cljfx.ext-let-refs/refs (s/nilable (s/map-of any? :cljfx/desc))) 16 | 17 | (register-type! `fx/ext-let-refs 18 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-let-refs/refs]) 19 | :of :desc) 20 | 21 | (s/def :cljfx.ext-get-ref/ref any?) 22 | 23 | (register-type! `fx/ext-get-ref :spec (s/keys :req-un [:cljfx.ext-get-ref/ref])) 24 | 25 | (s/def :cljfx.ext-set-env/env map?) 26 | 27 | (register-type! `fx/ext-set-env 28 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-set-env/env]) 29 | :of :desc) 30 | 31 | (s/def :cljfx.ext-get-env/env coll?) 32 | 33 | (register-type! `fx/ext-get-env 34 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-get-env/env]) 35 | :of :desc) 36 | 37 | (s/def :cljfx.ext-many/desc (s/coll-of :cljfx/desc)) 38 | 39 | (register-type! `fx/ext-many 40 | :spec (s/keys :req-un [:cljfx.ext-many/desc]) 41 | :of 'java.util.Collection) 42 | 43 | (s/def :cljfx.make-ext-with-props/props (s/nilable map?)) 44 | 45 | (register-type! `fx/make-ext-with-props 46 | :spec (s/keys :req-un [:cljfx/desc :cljfx.make-ext-with-props/props]) 47 | :of :desc) 48 | 49 | (s/def :cljfx.ext-watcher/ref (instance-of IRef)) 50 | 51 | (register-type! `fx/ext-watcher 52 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-watcher/ref]) 53 | :of :desc) 54 | 55 | (s/def :cljfx.ext-state/initial-state any?) 56 | 57 | (register-type! `fx/ext-state 58 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-state/initial-state]) 59 | :of :desc) 60 | 61 | (s/def :cljfx.ext-effect/fn ifn?) 62 | (s/def :cljfx.ext-effect/args (s/nilable (s/coll-of any? :kind sequential?))) 63 | 64 | (register-type! `fx/ext-effect 65 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-effect/args :cljfx.ext-effect/fn]) 66 | :of :desc) 67 | 68 | (s/def :cljfx.ext-recreate-on-key-changed/key any?) 69 | 70 | (register-type! `fx/ext-recreate-on-key-changed 71 | :spec (s/keys :req-un [:cljfx/desc :cljfx.ext-recreate-on-key-changed/key]) 72 | :of :desc) -------------------------------------------------------------------------------- /src/cljfx/dev/help.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'cljfx.dev) 2 | 3 | (defmethod short-keyword-prop-help-string :desc [{:keys [of]}] 4 | (str "cljfx desc of " of)) 5 | (defmethod short-keyword-prop-help-string :enum [{:keys [of]}] 6 | (let [options (into (sorted-set) 7 | (map #(keyword (str/replace (str/lower-case (.name ^Enum %)) #"_" "-"))) 8 | (.getEnumConstants (resolve of)))] 9 | (str "either of: " (str/join ", " options)))) 10 | 11 | (defmethod short-keyword-prop-help-string :nilable [{:keys [of]}] 12 | (str "nil or " (short-keyword-prop-help-string of))) 13 | 14 | (defmethod short-keyword-prop-help-string :coll [{:keys [item]}] 15 | (str "coll of " (short-keyword-prop-help-string item))) 16 | 17 | (defmethod short-keyword-prop-help-string :add-props [{:keys [to props]}] 18 | (str (short-keyword-prop-help-string to) " with extra props (" (str/join ", "(sort (keys props))) ")")) 19 | 20 | (defmethod short-keyword-prop-help-string :pref-or-computed-size-double [_] 21 | (str "number, :use-computed-size or :use-pref-size")) 22 | (defmethod short-keyword-prop-help-string :computed-size-double [_] 23 | (str "number or :use-computed-size")) 24 | (defmethod short-keyword-prop-help-string :animation [_] 25 | (str "number or :indefinite")) 26 | (defmethod short-keyword-prop-help-string :animation-status [_] 27 | "either of: :running, :paused, :stopped") 28 | (defmethod short-keyword-prop-help-string :map [{:keys [key value]}] 29 | (str "map from " (short-keyword-prop-help-string key) " to " (short-keyword-prop-help-string value))) 30 | (defmethod short-keyword-prop-help-string :media-player-state [_] 31 | "either of: :playing, :paused, :stopped") 32 | (defmethod short-keyword-prop-help-string :pseudo-classes [_] 33 | "a set of keywords or instances of javafx.css.PseudoClass") 34 | 35 | (def ^:private superscript (zipmap "0123456789" "⁰¹²³⁴⁵⁶⁷⁸⁹")) 36 | 37 | (defn- indent 38 | [s & {:keys [by start] 39 | :or {start true 40 | by 2}}] 41 | (let [indent-str (str/join (repeat by \space))] 42 | (cond->> (str/join (str "\n" indent-str) 43 | (str/split-lines s)) 44 | start (str indent-str)))) 45 | 46 | (defn- convert-help-syntax-to-string [syntax] 47 | (letfn [(apply-syntax [footnotes syntax] 48 | (cond 49 | (string? syntax) 50 | syntax 51 | 52 | (vector? syntax) 53 | (reduce (fn [acc syntax] 54 | (cond 55 | (string? syntax) 56 | (str acc syntax) 57 | 58 | (vector? syntax) 59 | (let [[text] syntax] 60 | (let [existing-footnote (first 61 | (keep-indexed 62 | (fn [i v] 63 | (when (= syntax v) 64 | i)) 65 | @footnotes)) 66 | n (if existing-footnote 67 | (inc existing-footnote) 68 | (count (vswap! footnotes conj syntax)))] 69 | (str acc text (str/join (map superscript (str n)))))) 70 | 71 | :else 72 | (throw (ex-info "Invalid syntax" {:syntax syntax})))) 73 | "" 74 | syntax) 75 | 76 | :else 77 | (throw (ex-info "Invalid syntax" {:syntax syntax}))))] 78 | (let [footnotes (volatile! []) 79 | initial (cond-> (apply-syntax footnotes syntax) 80 | (pos? (count @footnotes)) (str "\n\n---"))] 81 | (loop [i 0 82 | acc initial] 83 | (if (= i (count @footnotes)) 84 | acc 85 | (let [[text syntax] (@footnotes i)] 86 | (recur 87 | (inc i) 88 | (str acc 89 | "\n" 90 | (str/join (map superscript (str (inc i)))) 91 | text 92 | " - " 93 | (indent (apply-syntax footnotes syntax) :by (count (str i)) :start false) 94 | "\n")))))))) 95 | 96 | (defmethod long-keyword-prop-help-syntax :enum [{:keys [of]}] 97 | (let [consts (.getEnumConstants (resolve of))] 98 | (str "Enum:\n" 99 | of 100 | "\n\nIdiomatic values:\n- " 101 | (->> consts 102 | (map #(keyword (str/replace (str/lower-case (.name ^Enum %)) #"_" "-"))) 103 | (into (sorted-set)) 104 | (str/join "\n- ")) 105 | "\n\nAlso works:\n- " 106 | (->> consts 107 | (map #(str of "/" (.name ^Enum %))) 108 | sort 109 | (str/join "\n- "))))) 110 | (defmethod long-keyword-prop-help-syntax :insets [_] 111 | "Insets, either: 112 | - number 113 | - map with optional keys - :top, :bottom, :left, :right - numbers 114 | - literal - :empty 115 | - instance of javafx.geometry.Insets") 116 | (defmethod long-keyword-prop-help-syntax :image [_] 117 | "Image, either: 118 | - instance of javafx.scene.image.Image 119 | - string: either a resource path or URL string pointing to image 120 | - map with required :url (url string) or :is (input stream) keys and optional :requested-width (number), :requested-height (number), :preserve-ratio (boolean), :smooth (boolean) and :background-loading (boolean) keys") 121 | (defmethod long-keyword-prop-help-syntax :duration [_] 122 | "Duration, either: 123 | - tuple of number and time unit (:ms, :s, :m, or :h), e.g. [10 :s] 124 | - string in the format [number][ms|s|m|h], e.g. 10s 125 | - number (ms) 126 | - literal - :zero, :one (ms), :indefinite or :unknown 127 | - instance of javafx.util.Duration") 128 | (defmethod long-keyword-prop-help-syntax :font [_] 129 | ["Font, either: 130 | - string, font family name 131 | - number, font size (of a default font) 132 | - literal - :default 133 | - map with required :family key (string) and optional " 134 | [":weight" (long-prop-help-syntax '{:type :enum :of javafx.scene.text.FontWeight})] 135 | ", " 136 | [":posture" (long-prop-help-syntax '{:type :enum :of javafx.scene.text.FontPosture})] 137 | " and :size (number) keys 138 | - instance of javafx.scene.text.Font"]) 139 | 140 | (defmethod long-keyword-prop-help-syntax :color [_] 141 | (str "A color, either: 142 | - string in either format: 143 | - 0x0000FF - 0x-prefixed hex web value 144 | - 0x00F - 0x-prefixed short hex web value 145 | - #0000FF - #-prefixed hex web value 146 | - #00F - #-prefixed short hex web value 147 | - 0000FF - hex web value 148 | - 00F - short hex web value 149 | - rgba(0,0,255,1.0) - rgb web value, explicit alpha 150 | - rgb(0,0,255) - rgb web value, implicit alpha 151 | - rgba(0,0,100%,1.0) - rgb percent web value, explicit alpha 152 | - rgb(0,0,100%) - rgb percent web value, implicit alpha 153 | - hsla(270,100%,100%,1.0) - hsl web value, explicit alpha 154 | - hsl(270,100%,100%) - hsl web value, implicit alpha 155 | - instance of javafx.scene.paint.Color 156 | - keyword of a named color: 157 | - " (-> Color 158 | .getDeclaredClasses 159 | ^Class first 160 | (.getDeclaredField "NAMED_COLORS") 161 | (doto (.setAccessible true)) 162 | (.get nil) 163 | (keys) 164 | (->> (map keyword) 165 | sort 166 | (str/join "\n - "))))) 167 | 168 | (defmethod long-keyword-prop-help-syntax :key-combination [_] 169 | ["Key combination, either: 170 | - a vector of modifier keywords + key identifier, where: 171 | - modifier keyword is either :shift, :ctrl, :alt, :meta or :shortcut 172 | - key identifier is either " 173 | ["key code" (long-prop-help-syntax '{:type :enum :of javafx.scene.input.KeyCode})] 174 | " (creates KeyCodeCombination) or 1-character string (creates KeyCharacterCombination) 175 | - string with +-interposed modifiers with key in the end, e.g. Shift+Ctrl+C 176 | - instance of javafx.scene.input.KeyCombination"]) 177 | 178 | (defmethod long-keyword-prop-help-syntax :map [{:keys [key value]}] 179 | ["Map from " 180 | [(short-prop-help-string key) 181 | (long-prop-help-syntax key)] 182 | " to " 183 | [(short-prop-help-string value) 184 | (long-prop-help-syntax value)]]) 185 | 186 | (defmethod long-keyword-prop-help-syntax :event-handler [{:keys [of]}] 187 | (str "Event handler, either: 188 | - a map event 189 | - fn 190 | - instance of " of)) 191 | 192 | (defmethod long-keyword-prop-help-syntax :float-map [_] 193 | "Float map, either: 194 | - a map with optional keys :width (int), :height (int) and :samples (a coll of maps with keys :x - int, :y - int and :s - tuple of number and number) 195 | - instance of javafx.scene.effect.FloatMap") 196 | 197 | (defn- known-types-of [sym-or-cls] 198 | (let [cls (cond-> sym-or-cls (symbol? sym-or-cls) resolve) 199 | r @registry 200 | ts (->> r 201 | :types 202 | vals 203 | (filter (fn [{:keys [of]}] 204 | (and (symbol? of) 205 | (isa? (resolve of) cls)))) 206 | (sort-by (comp str :id)) 207 | seq)] 208 | ts)) 209 | 210 | (defn- str-table [items & columns] 211 | (let [columns (vec columns) 212 | column-strs (mapv #(into [(:label %)] (map (comp str (:fn %))) items) columns) 213 | max-lengths (mapv #(transduce (map count) max 0 %) column-strs)] 214 | (reduce 215 | (fn [acc i] 216 | (let [row (mapv #(% i) column-strs)] 217 | (str acc 218 | (str/join 219 | " " 220 | (map (fn [max-length item] 221 | (str item (str/join (repeat (- max-length (count item)) \space)))) 222 | max-lengths row)) 223 | "\n"))) 224 | "" 225 | (range (inc (count items)))))) 226 | 227 | 228 | (defn- splice-syntax [& syntaxes] 229 | (reduce #(into %1 (if (string? %2) [%2] %2)) [] syntaxes)) 230 | 231 | (defmethod long-keyword-prop-help-syntax :nilable [{:keys [of]}] 232 | (splice-syntax 233 | "Nilable " 234 | (long-keyword-prop-help-syntax of))) 235 | 236 | (defmethod long-keyword-prop-help-syntax :desc [{:keys [of]}] 237 | ["Cljfx desc, a map with :fx/type key 238 | 239 | Required instance class:\n" 240 | (if-let [ts (known-types-of of)] 241 | [(str of) (str "Fitting cljfx types:\n" (str-table ts 242 | {:label "Cljfx type" :fn :id} 243 | {:label "Class" :fn :of}))] 244 | (str of))]) 245 | 246 | (defmethod long-keyword-prop-help-syntax :coll [{:keys [item]}] 247 | (splice-syntax 248 | (str "Coll of " (short-prop-help-string item) "\n\nItem:\n") 249 | (long-prop-help-syntax item))) 250 | 251 | (defmethod long-keyword-prop-help-syntax :add-props [{:keys [props to]}] 252 | (splice-syntax 253 | (long-prop-help-syntax to) 254 | "\n\nExtra props:\n" 255 | (str-table 256 | (sort-by key props) 257 | {:label "Prop" :fn key} 258 | {:label "Value type" :fn (comp short-prop-help-string val)}))) 259 | 260 | (defmethod long-keyword-prop-help-syntax :point-3d [_] 261 | "Point3D, either: 262 | - literal - :zero :x-axis :y-axis :z-axis 263 | - map with required keys :x, :y and :z (numbers) 264 | - instance of javafx.geometry.Point3D") 265 | 266 | (defmethod long-keyword-prop-help-syntax :style-class [_] 267 | "Style class, either: 268 | - a string 269 | - a coll of strings") 270 | 271 | (defmethod long-keyword-prop-help-syntax :style [_] 272 | "Style, either: 273 | - a css-like style string, e.g. \"-fx-background-color: red; -fx-text-fill: green\" 274 | - a map that will be converted to such string, where keys must by keywords and values can be either: 275 | - keywords (will use name) 276 | - vectors (will by interposed by spaces) 277 | - strings (will be used as is) 278 | Example: {:-fx-background-color :red :-fx-text-fill :green}") 279 | 280 | (defmethod long-keyword-prop-help-syntax :cursor [_] 281 | "Cursor, either: 282 | - string - either a resource path or URL string pointing to cursor image 283 | - keyword - either: 284 | - :closed-hand 285 | - :crosshair 286 | - :default 287 | - :disappear 288 | - :e-resize 289 | - :h-resize 290 | - :hand 291 | - :move 292 | - :n-resize 293 | - :name 294 | - :ne-resize 295 | - :none 296 | - :nw-resize 297 | - :open-hand 298 | - :s-resize 299 | - :se-resize 300 | - :sw-resize 301 | - :text 302 | - :v-resize 303 | - :w-resize 304 | - :wait 305 | - instance of javafx.scene.Cursor") 306 | 307 | (defmethod long-keyword-prop-help-syntax :rectangle [_] 308 | "Rectangle, either: 309 | - a map with required :min-x, :min-y, :width and :height keys (numbers) 310 | - instance of javafx.geometry.Rectangle2D") 311 | 312 | (defmethod long-keyword-prop-help-syntax :interpolator [_] 313 | ["Interpolator, either: 314 | - literal - :discrete, :ease-both, :ease-in, :ease-out or :linear 315 | - 3-element tuple of literal :tangent, " ["duration" (long-prop-help-syntax {:type :duration})]" and number 316 | - 5-element tuple of literal :tangent, " ["duration" (long-prop-help-syntax {:type :duration})]", number, " ["duration" (long-prop-help-syntax {:type :duration})]" and number 317 | - 5-element tuple of literal :spline and 4 numbers"]) 318 | 319 | (defmethod long-keyword-prop-help-syntax :chronology [_] 320 | "Chronology, either: 321 | - literal - :iso, :hijrah, :japanese, :minguo or :thai-buddhist 322 | - instance of java.time.chrono.Chronology") 323 | 324 | (defmethod long-keyword-prop-help-syntax :vertex-format [_] 325 | "Vertex format, either: 326 | - literal - :point-texcoord or :point-normal-texcoord 327 | - instance of javafx.scene.shape.VertexFormat") 328 | 329 | (defmethod long-keyword-prop-help-syntax :bounding-box [_] 330 | "Bounding box, either: 331 | - literal - 0 332 | - 4-element tuple of numbers (minX, minY, width, height) 333 | - 6-element tuple of numbers (minX, minY, minZ, width, height, depth) 334 | - instance of javafx.geometry.BoundingBox") 335 | 336 | (defmethod long-keyword-prop-help-syntax :button-type [_] 337 | ["Button type, either: 338 | - literal - :apply, :cancel, :close, :finish, :next, :no, :ok, :previous or :yes 339 | - map with optional keys - :text (string) and " 340 | [":button-data" (long-keyword-prop-help-syntax '{:type :enum :of javafx.scene.control.ButtonBar$ButtonData})] 341 | " 342 | - string - button type text 343 | - instance of javafx.scene.control.ButtonType"]) 344 | 345 | (defmethod long-keyword-prop-help-syntax :table-sort-policy [_] 346 | "Table sort policy, either: 347 | - literal - :default 348 | - instance of javafx.util.Callback") 349 | 350 | (defmethod long-keyword-prop-help-syntax :text-formatter-filter [_] 351 | "Text formatter filter, either: 352 | - literal - nil 353 | - fn from javafx.scene.control.TextFormatter$Change to javafx.scene.control.TextFormatter$Change 354 | - instance of java.util.function.UnaryOperator") 355 | 356 | (defmethod long-keyword-prop-help-syntax :text-formatter [_] 357 | ["Text formatter, either: 358 | - instance of javafx.scene.control.TextFormatter 359 | - " ["cljfx desc of javafx.scene.control.TextFormatter" (long-prop-help-syntax '{:type :desc :of javafx.scene.control.TextFormatter})] 360 | ""]) 361 | 362 | (defmethod long-keyword-prop-help-syntax :toggle-group [_] 363 | ["Toggle group, either: 364 | - instance of javafx.scene.control.ToggleGroup 365 | - " ["cljfx desc of javafx.scene.control.ToggleGroup" (long-prop-help-syntax '{:type :desc :of javafx.scene.control.ToggleGroup})]]) 366 | 367 | (defmethod long-keyword-prop-help-syntax :result-converter [_] 368 | "Result converter, either: 369 | - fn from javafx.scene.control.ButtonType to any object 370 | - instance of javafx.util.Callback") 371 | 372 | (defmethod long-keyword-prop-help-syntax :column-resize-policy [_] 373 | "Column resize policy, either: 374 | - literal - :unconstrained or :constrained 375 | - predicate fn of javafx.scene.control.TableView$ResizeFeatures 376 | - instance of javafx.util.Callback") 377 | 378 | (defmethod long-keyword-prop-help-syntax :string-converter [_] 379 | "String converter, either: 380 | - literal - :big-decimal, :big-integer, :boolean, :byte, :character, :date-time, :default, :double, :float, :integer, :local-date, :local-date-time, :local-time, :long, :number or :short 381 | - instance of javafx.util.StringConverter") 382 | 383 | (defmethod long-keyword-prop-help-syntax :page-factory [_] 384 | ["Page factory, either: 385 | - fn from page index to " 386 | ["cljfx desc of javafx.scene.Node" (long-keyword-prop-help-syntax '{:type :desc :of javafx.scene.Node})] 387 | " 388 | - instance of javafx.util.Callback"]) 389 | 390 | (defmethod long-keyword-prop-help-syntax :cell-value-factory [_] 391 | "Cell value factory, either: 392 | - fn that converts row value to cell value 393 | - instance of javafx.util.Callback") 394 | 395 | (defmethod long-keyword-prop-help-syntax :cell-factory [{:keys [of]}] 396 | (let [of-syntax (if-let [ts (known-types-of of)] 397 | [(str of) (str "Fitting cljfx types:\n" (str-table ts 398 | {:label "Cljfx type" :fn :id} 399 | {:label "Class" :fn :of}))] 400 | (str of))] 401 | ["Cell factory, either: 402 | - a map with required :fx/cell-type and :describe keys, where: 403 | - :fx/cell-type is either a keyword or a composite lifecycle instance of " of-syntax " 404 | - :describe is fn from item to prop map (without :fx/type) of the :fx/cell-type type 405 | - deprecated: fn from item to prop map (without :fx/type) of " of-syntax])) 406 | 407 | (defmethod long-keyword-prop-help-syntax :paint [_] 408 | ["Paint, either: 409 | - " ["color" (long-prop-help-syntax {:type :color})] " 410 | - 2-element tuple of :linear-gradient and a map with required keys: 411 | - :start-x - double 412 | - :start-y - double 413 | - :end-x - double 414 | - :end-y - double 415 | - :proportional - boolean 416 | - :cycle method - " [(short-prop-help-string '{:type :enum :of javafx.scene.paint.CycleMethod}) 417 | (long-prop-help-syntax '{:type :enum :of javafx.scene.paint.CycleMethod})] " 418 | - :stops - a coll of 2-element tuples of double and " ["color" (long-prop-help-syntax {:type :color})] " 419 | - 2-element tuple of :radial-gradient and a map with required keys: 420 | - :focus-angle - double 421 | - :focus-distance - double 422 | - :center-x - double 423 | - :center-y - double 424 | - :radius - double 425 | - :proportional - boolean 426 | - :cycle-method - "[(short-prop-help-string '{:type :enum :of javafx.scene.paint.CycleMethod}) 427 | (long-prop-help-syntax '{:type :enum :of javafx.scene.paint.CycleMethod})]" 428 | - 2-element tuple of :image-pattern and a map with keys: 429 | - :image - " ["image" (long-prop-help-syntax '{:type :image})]", required 430 | - :x - double 431 | - :y - double 432 | - :width - double 433 | - :height - double 434 | - :proportional - boolean 435 | - instance of javafx.scene.paint.Paint"]) 436 | 437 | (def ^:private radii-syntax 438 | ["Corner radii, either: 439 | - literal :empty 440 | - number - a corner radius 441 | - a map with required either :radius (number) or both :top-left, :top-right, :bottom-right and :bottom-left keys (numbers), and optional :as-percent boolean key 442 | - instance of javafx.scene.layout.CornerRadii"]) 443 | 444 | (def ^:private background-position-axis-syntax 445 | ["Background image position axis, a map with keys: 446 | - :position - a number, required 447 | - :side - " [(short-prop-help-string '{:type :enum :of javafx.geometry.Side}) (long-keyword-prop-help-syntax '{:type :enum :of javafx.geometry.Side})] " 448 | - :as-percentage - boolean"]) 449 | 450 | (def ^:private background-position-syntax 451 | ["Background image position, either: 452 | - literal - :center or :default 453 | - a map with required :horizontal and :vertical keys (" ["background position axes" background-position-axis-syntax] ") 454 | - instance of javafx.scene.layout.BackgroundPosition"]) 455 | 456 | (def ^:private background-size-syntax 457 | ["Background image size, either: 458 | - literal - :auto or :default 459 | - a map with keys: 460 | - :width - number, required 461 | - :height - number, required 462 | - :width-as-percentage - boolean 463 | - :height-as-percentage - boolean 464 | - :contain - boolean 465 | - :cover - boolean 466 | - instance of javafx.scene.layout.BackgroundSize"]) 467 | 468 | (defmethod long-keyword-prop-help-syntax :background [_] 469 | (let [background-fills ["background fills" ["Background fill, either: 470 | - map with optional " [":fill" (long-keyword-prop-help-syntax {:type :paint})] ", " [":radii" radii-syntax] " and " [":insets" (long-prop-help-syntax {:type :insets})] " keys 471 | - instance of javafx.scene.layout.BackgroundFill"]] 472 | background-images ["background images" ["Background image is either: 473 | - string: either a resource path or URL string pointing to image 474 | - map with keys: 475 | - :image - " ["image" (long-prop-help-syntax {:type :image})] ", required 476 | - :repeat-x - "[(short-prop-help-string '{:type :enum :of javafx.scene.layout.BackgroundRepeat}) (long-prop-help-syntax '{:type :enum :of javafx.scene.layout.BackgroundRepeat})]" 477 | - :repeat-y - "[(short-prop-help-string '{:type :enum :of javafx.scene.layout.BackgroundRepeat}) (long-prop-help-syntax '{:type :enum :of javafx.scene.layout.BackgroundRepeat})]" 478 | - :position - " ["background position" background-position-syntax] " 479 | - :size - " ["background size" background-size-syntax] " 480 | - instance of javafx.scene.layout.BackgroundImage"]]] 481 | ["Background, either: 482 | - a map with optional keys: 483 | - :fills - a coll of " background-fills " 484 | - :images - a coll of " background-images " 485 | - instance of javafx.scene.layout.Background"])) 486 | 487 | (def ^:private border-stroke-style-syntax 488 | ["Border stroke style, either: 489 | - literal - :dashed, :dotted, :none or :solid 490 | - map with optional keys: 491 | - :type - " [(short-prop-help-string '{:type :enum :of javafx.scene.shape.StrokeType}) (long-prop-help-syntax '{:type :enum :of javafx.scene.shape.StrokeType})] " 492 | - :line-join - " [(short-prop-help-string '{:type :enum :of javafx.scene.shape.StrokeLineJoin}) (long-prop-help-syntax '{:type :enum :of javafx.scene.shape.StrokeLineJoin})] " 493 | - :line-cap - " [(short-prop-help-string '{:type :enum :of javafx.scene.shape.StrokeLineCap}) (long-prop-help-syntax '{:type :enum :of javafx.scene.shape.StrokeLineCap})] " 494 | - :miter-limit - number 495 | - :dash-offset - number 496 | - :dash-array - coll of numbers 497 | - instance of javafx.scene.layout.BorderStrokeStyle"]) 498 | 499 | (def ^:private border-widths-syntax 500 | ["Border widths, either: 501 | - literal - :auto, :default, :empty or :full 502 | - number 503 | - map with keys: 504 | - :top - number, required 505 | - :right - number, required 506 | - :bottom - number, required 507 | - :left - number, required 508 | - :as-percentage - boolean 509 | - :top-as-percentage - boolean 510 | - :right-as-percentage - boolean 511 | - :bottom-as-percentage - boolean 512 | - :left-as-percentage - boolean 513 | - instance of javafx.scene.layout.BorderWidths"]) 514 | 515 | (def ^:private border-strokes-syntax 516 | ["A collection of border strokes, that are either: 517 | - map with optional keys: 518 | - :stroke - " ["paint" (long-prop-help-syntax {:type :paint})] " 519 | - :style - " ["border stroke style" border-stroke-style-syntax] " 520 | - :radii - " ["corner radii" radii-syntax] " 521 | - :widths - " ["border widths" border-widths-syntax] " 522 | - :insets - " ["insets" (long-prop-help-syntax {:type :insets})] " 523 | - instance of javafx.scene.layout.BorderStroke"]) 524 | 525 | (def ^:private border-images-syntax 526 | ["A collection of border images that are either: 527 | - map with keys: 528 | - :image - " ["image" (long-prop-help-syntax {:type :image})] ", required 529 | - :widths - " ["border widths" border-widths-syntax] " 530 | - :insets - " ["insets" (long-prop-help-syntax {:type :insets})] " 531 | - :slices - " ["border widths" border-widths-syntax] " 532 | - :filled - boolean 533 | - :repeat-x - " [(short-prop-help-string '{:type :enum :of javafx.scene.layout.BorderRepeat}) (long-prop-help-syntax '{:type :enum :of javafx.scene.layout.BorderRepeat})] " 534 | - :repeat-y - " [(short-prop-help-string '{:type :enum :of javafx.scene.layout.BorderRepeat}) (long-prop-help-syntax '{:type :enum :of javafx.scene.layout.BorderRepeat})] " 535 | - instance of javafx.scene.layout.BorderImage"]) 536 | 537 | (defmethod long-keyword-prop-help-syntax :border [_] 538 | ["Border, either: 539 | - literal :empty 540 | - a map with optional " [":strokes" border-strokes-syntax] " and " [":images" border-images-syntax] " keys 541 | - instance of javafx.scene.layout.Border"]) -------------------------------------------------------------------------------- /src/cljfx/dev/ui.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'cljfx.dev) 2 | 3 | (require '[cljfx.ext.list-view :as fx.ext.list-view]) 4 | 5 | (defn- set-help-ui-selection [state {:keys [key fx/event]}] 6 | (update state key assoc :selection event)) 7 | 8 | (defn- set-help-ui-filter-term [state {:keys [key ^KeyEvent fx/event]}] 9 | (let [ch (.getCharacter event)] 10 | (cond 11 | (and (= 1 (count ch)) (#{127 27} (int (first ch)))) 12 | (update state key assoc :filter-term "") 13 | 14 | (= ch "\b") 15 | (update-in state [key :filter-term] #(cond-> % (pos? (count %)) (subs 0 (dec (count %))))) 16 | 17 | (= ch "\t") 18 | state 19 | 20 | (re-matches #"^[a-zA-Z0-9:/\-.+!@#$%^&*()-={}\[\]<>?,/\\'\"]$" ch) 21 | (update-in state [key :filter-term] str (.getCharacter event)) 22 | 23 | :else 24 | state))) 25 | 26 | (defn- help-list-view [{:keys [filter-term selection items key]}] 27 | {:fx/type :stack-pane 28 | :min-width 150 29 | :max-width 150 30 | :children [{:fx/type fx.ext.list-view/with-selection-props 31 | :props {:selected-item selection 32 | :on-selected-item-changed {:fn #'set-help-ui-selection :key key}} 33 | :desc {:fx/type :list-view 34 | :on-key-typed {:fn #'set-help-ui-filter-term :key key} 35 | :items items}} 36 | {:fx/type :label 37 | :visible (boolean (seq filter-term)) 38 | :style-class ["label" "filter-term"] 39 | :stack-pane/margin 7 40 | :stack-pane/alignment :bottom-right 41 | :text filter-term}]}) 42 | 43 | (defn- process-filter-selection [{:keys [filter-term selection] :as m} items] 44 | (let [items (cond->> items 45 | (not (str/blank? filter-term)) 46 | (filter #(str/includes? % filter-term))) 47 | items (sort-by str items) 48 | selection (or (and selection (some #{selection} items)) 49 | (first items))] 50 | (assoc m :items items :selection selection))) 51 | 52 | (def ^:private ext-recreate-on-key-changed 53 | (reify lifecycle/Lifecycle 54 | (create [_ {:keys [key desc]} opts] 55 | (with-meta {:key key 56 | :child (lifecycle/create lifecycle/dynamic desc opts)} 57 | {`component/instance #(-> % :child component/instance)})) 58 | (advance [this component {:keys [key desc] :as this-desc} opts] 59 | (if (= (:key component) key) 60 | (update component :child #(lifecycle/advance lifecycle/dynamic % desc opts)) 61 | (do (lifecycle/delete this component opts) 62 | (lifecycle/create this this-desc opts)))) 63 | (delete [_ component opts] 64 | (lifecycle/delete lifecycle/dynamic (:child component) opts)))) 65 | 66 | (def ^:private ext-with-shown-on 67 | (fx/make-ext-with-props 68 | {:shown-on (prop/make 69 | (mutator/adder-remover 70 | (fn [^Popup popup ^Node node] 71 | (let [bounds (.getBoundsInLocal node) 72 | node-pos (.localToScreen node 73 | -8 74 | (- (.getHeight bounds) 6))] 75 | (.show popup node 76 | (.getX node-pos) 77 | (.getY node-pos)))) 78 | (fn [^Popup popup _] 79 | (.hide popup))) 80 | lifecycle/dynamic)})) 81 | 82 | (defn- hover-help-syntax-element [state {:keys [path key]}] 83 | (assoc state key path)) 84 | 85 | (defn- hide-hover-help-popup [state {:keys [key]}] 86 | (update state key pop)) 87 | 88 | (defn- help-ui-syntax-view [{:keys [syntax key hover] :as props}] 89 | (letfn [(apply-syntax [syntax path] 90 | (cond 91 | (string? syntax) 92 | [{:fx/type :text 93 | :style {:-fx-font-family "monospace" :-fx-font-size 13} 94 | :text syntax}] 95 | 96 | (vector? syntax) 97 | (map-indexed 98 | (fn [i x] 99 | (cond 100 | (string? x) 101 | {:fx/type :text 102 | :style {:-fx-font-family "monospace" :-fx-font-size 13} 103 | :text x} 104 | 105 | (vector? x) 106 | (let [[text syntax] x 107 | current-path (conj path i)] 108 | {:fx/type fx/ext-let-refs 109 | :refs 110 | {::view 111 | {:fx/type :label 112 | :underline true 113 | :style {:-fx-font-family "monospace" :-fx-font-size 13} 114 | :text-fill "#000e26" 115 | :on-mouse-entered 116 | {:fn #'hover-help-syntax-element 117 | :path current-path 118 | :key key} 119 | :text text}} 120 | :desc 121 | {:fx/type fx/ext-let-refs 122 | :refs 123 | {::popup 124 | {:fx/type ext-with-shown-on 125 | :props (if (= current-path (take (count current-path) hover)) 126 | {:shown-on 127 | {:fx/type fx/ext-get-ref 128 | :ref ::view}} 129 | {}) 130 | :desc {:fx/type :popup 131 | :anchor-location :window-top-left 132 | :auto-hide true 133 | :hide-on-escape true 134 | :on-auto-hide {:fn #'hide-hover-help-popup 135 | :key key} 136 | :content [{:fx/type :stack-pane 137 | :max-width 960 138 | :pref-height :use-computed-size 139 | :max-height 600 140 | :style-class "popup-root" 141 | :children 142 | [{:fx/type :scroll-pane 143 | :content {:fx/type :text-flow 144 | :padding 5 145 | :children (apply-syntax syntax current-path)}}]}]}}} 146 | :desc {:fx/type fx/ext-get-ref :ref ::view}}}) 147 | 148 | :else 149 | (throw (ex-info "Invalid syntax" {:syntax x})))) 150 | 151 | syntax) 152 | 153 | :else 154 | (throw (ex-info "Invalid syntax" {:syntax syntax}))))] 155 | (-> props 156 | (dissoc :syntax :key :hover) 157 | (assoc 158 | :fx/type :scroll-pane 159 | :content 160 | {:fx/type :text-flow 161 | :padding 5 162 | :children (apply-syntax syntax [])})))) 163 | 164 | (defn- help-ui-view [{:keys [registry type prop type-hover prop-hover]}] 165 | (let [filtered-type-map (process-filter-selection type (keys (:types registry))) 166 | selected-type (:selection filtered-type-map) 167 | selected-props (-> registry :props (get selected-type)) 168 | filtered-prop-map (process-filter-selection prop (keys selected-props)) 169 | selected-prop-id (:selection filtered-prop-map) 170 | selected-prop (get selected-props selected-prop-id) 171 | javadoc (get-in registry [:javadoc selected-type])] 172 | {:fx/type :stage 173 | :showing true 174 | :title "cljfx/dev" 175 | :width 900 176 | :scene 177 | {:fx/type :scene 178 | :stylesheets [(::css/url help-ui-css)] 179 | :root {:fx/type :grid-pane 180 | :style {:-fx-background-color "#ccc"} 181 | :column-constraints [{:fx/type :column-constraints 182 | :min-width 150 183 | :max-width 150} 184 | {:fx/type :column-constraints 185 | :hgrow :always}] 186 | :row-constraints [{:fx/type :row-constraints 187 | :min-height 100 188 | :max-height 100} 189 | {:fx/type :row-constraints 190 | :vgrow :always}] 191 | :children [(assoc filtered-type-map 192 | :grid-pane/column 0 193 | :grid-pane/row 0 194 | :grid-pane/row-span 2 195 | :fx/type help-list-view 196 | :key :type) 197 | {:fx/type help-ui-syntax-view 198 | :grid-pane/column 1 199 | :grid-pane/row 0 200 | :key :type-hover 201 | :hover type-hover 202 | :syntax (if selected-type 203 | (let [type-map (get (:types registry) selected-type)] 204 | (str "Cljfx type: " selected-type 205 | (when (symbol? (:of type-map)) 206 | (str "\nInstance class: " (:of type-map))) 207 | (when (:req type-map) 208 | (if (set? (:req type-map)) 209 | (str "\nRequired props, either:\n" 210 | (str/join "\n" (for [req (:req type-map)] 211 | (str "- " (str/join ", " (sort req)))))))) 212 | (when (and (not selected-props) (:spec type-map)) 213 | (str "\nSpec: " (pr-str (s/describe (:spec type-map))))))) 214 | "")} 215 | {:fx/type :tab-pane 216 | :grid-pane/row 1 217 | :grid-pane/column 1 218 | :tabs 219 | [{:fx/type :tab 220 | :text "Props" 221 | :closable false 222 | :content {:fx/type :h-box 223 | :children [{:fx/type ext-recreate-on-key-changed 224 | :key selected-type 225 | :desc (assoc filtered-prop-map :fx/type help-list-view :key :prop)} 226 | {:fx/type help-ui-syntax-view 227 | :h-box/hgrow :always 228 | :key :prop-hover 229 | :hover prop-hover 230 | :syntax (if selected-prop 231 | (long-prop-help-syntax selected-prop) 232 | "")}]}} 233 | {:fx/type :tab 234 | :text "Javadoc" 235 | :disable (nil? javadoc) 236 | :closable false 237 | :content {:fx/type :web-view 238 | :url (str javadoc-prefix javadoc)}}]}]}}})) 239 | 240 | (defn- launch-help-ui! [] 241 | (let [state (atom {:registry @registry 242 | :type {:selection nil 243 | :filter-term ""} 244 | :prop {:selection nil 245 | :filter-term ""}}) 246 | render (fx/create-renderer 247 | :opts {:fx.opt/type->lifecycle type->lifecycle 248 | :fx.opt/map-event-handler #(swap! state (:fn %) %)} 249 | :middleware (fx/wrap-map-desc #(assoc % :fx/type help-ui-view)))] 250 | (fx/mount-renderer state render))) 251 | -------------------------------------------------------------------------------- /src/cljfx/dev/validation.clj: -------------------------------------------------------------------------------- 1 | (in-ns 'cljfx.dev) 2 | 3 | (require '[cljfx.component :as component] 4 | '[cljfx.coerce :as coerce] 5 | '[cljfx.fx.parent :as fx.parent] 6 | '[cljfx.prop :as prop] 7 | '[cljfx.mutator :as mutator] 8 | '[cljfx.css :as css] 9 | '[cljfx.ext.tree-view :as ext.tree-view]) 10 | (import '[javafx.scene Scene Node] 11 | '[javafx.event EventDispatcher] 12 | '[javafx.stage Popup] 13 | '[javafx.scene.text Text] 14 | '[javafx.scene.input KeyEvent Clipboard ClipboardContent] 15 | '[javafx.scene.control Label TreeItem TableView TablePosition]) 16 | 17 | (def ^:private help-ui-css 18 | (let [primary-color "#4E84E0"] 19 | (css/register ::css 20 | {"*" {:-fx-font-size 13 :-fx-font-family "system"} 21 | ".list-view" 22 | {:-fx-background-color :transparent 23 | :-fx-border-width [0 1 0 0] 24 | :-fx-border-color "#aaa" 25 | ":focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused" 26 | {:-fx-background-color primary-color}} 27 | ".tab-pane:focused > .tab-header-area > .headers-region > .tab:selected .focus-indicator" 28 | {:-fx-border-color primary-color 29 | :-fx-border-width 2 30 | :-fx-border-insets [-4 -4 -15 -5] 31 | :-fx-border-radius "5"} 32 | ".tab" {:-fx-background-color "#aaa, #c2c2c2" 33 | :-fx-background-radius "6 6 0 0, 5 5 0 0" 34 | ":selected" {:-fx-background-color "#aaa, #ccc"}} 35 | ".tab-header-background" {:-fx-background-color "#aaa, #ccc, #ccc"} 36 | ".popup-root" {:-fx-background-color "#ddd" 37 | :-fx-effect "dropshadow(gaussian, #0006, 8, 0, 0, 2)"} 38 | ".filter-term" {:-fx-background-color "#42B300" 39 | :-fx-background-radius 2 40 | :-fx-effect "dropshadow(gaussian, #0006, 4, 0, 0, 2)" 41 | :-fx-padding [2 4]} 42 | ".list-cell" {:-fx-background-color :transparent 43 | :-fx-text-fill "#000" 44 | :-fx-font-size 14 45 | :-fx-padding [2 4] 46 | ":selected" {:-fx-background-color "#4E84E033"}} 47 | ".text-area" {:-fx-background-color :transparent 48 | :-fx-focus-traversable false 49 | :-fx-text-fill "#000" 50 | " .content" {:-fx-background-color :transparent}} 51 | ".scroll-pane" {:-fx-background-color :transparent 52 | :-fx-padding 0 53 | "> .viewport" {:-fx-background-color :transparent}} 54 | ".scroll-bar" {:-fx-background-color :transparent 55 | "> .thumb" {:-fx-background-color "#999" 56 | :-fx-background-insets 0 57 | :-fx-background-radius 4 58 | ":hover" {:-fx-background-color "#9c9c9c"} 59 | ":pressed" {:-fx-background-color "#aaa"}} 60 | ":horizontal" {"> .increment-button > .increment-arrow" {:-fx-pref-height 7} 61 | "> .decrement-button > .decrement-arrow" {:-fx-pref-height 7}} 62 | ":vertical" {"> .increment-button > .increment-arrow" {:-fx-pref-width 7} 63 | "> .decrement-button > .decrement-arrow" {:-fx-pref-width 7}} 64 | "> .decrement-button" {:-fx-padding 0 65 | "> .decrement-arrow" {:-fx-shape nil 66 | :-fx-padding 0}} 67 | "> .increment-button" {:-fx-padding 0 68 | "> .increment-arrow" {:-fx-shape nil 69 | :-fx-padding 0}}} 70 | ".corner" {:-fx-background-color :transparent} 71 | ".tree-view" {:-fx-background-color :transparent 72 | :-fx-fixed-cell-size 24 73 | ":focused > .virtual-flow > .clipped-container > .sheet > .tree-cell:focused" 74 | {:-fx-background-color primary-color}} 75 | ".tree-cell" {:-fx-background-color :transparent 76 | :-fx-text-fill "#000"} 77 | ".inspector" {"-backdrop" {:-fx-background-color "#ccc" 78 | :-fx-effect "dropshadow(gaussian, #0006, 10, 0, 0, 5)"} 79 | "-root" {:-fx-pref-width 300 80 | :-fx-max-width 300 81 | :-fx-pref-height 500 82 | :-fx-max-height 500 83 | :-fx-background-color :transparent}} 84 | ".table" {"-view" {:-fx-background-color :transparent 85 | :-fx-fixed-cell-size 25 86 | :-fx-border-color ["#aaa" :transparent :transparent :transparent] 87 | "> .column-header-background" 88 | {:-fx-background-color :transparent 89 | ">.filler" {:-fx-background-color :transparent}} 90 | ":focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell > .table-cell:selected" 91 | {:-fx-background-color primary-color}} 92 | "-cell" {:-fx-border-color [:transparent "#aaa" :transparent :transparent] 93 | :-fx-text-fill "#000" 94 | ":focused" {:-fx-background-color "#4E84E033"}} 95 | "-column" {:-fx-background-color :transparent 96 | :-fx-border-color [:transparent "#aaa" :transparent :transparent] 97 | " .label" {:-fx-alignment :center-left :-fx-text-fill "#000"}} 98 | "-row-cell" {:-fx-background-color :transparent}}}))) 99 | 100 | (defn- type->string [type->id fx-type] 101 | (str (or (type->id fx-type) fx-type))) 102 | 103 | (defn- re-throw-with-stack [type->id ^Throwable ex stack] 104 | (if (::cause (ex-data ex)) 105 | (throw ex) 106 | (throw (doto (ex-info 107 | (str (ex-message ex) 108 | "\n\nCljfx component stack:\n " 109 | (->> stack 110 | (map :type) 111 | (map #(type->string type->id %)) 112 | (str/join "\n ")) 113 | "\n") 114 | (with-meta {::cause ex} {:type ::hidden})) 115 | (.setStackTrace (.getStackTrace ex)))))) 116 | 117 | (defmethod print-method ::hidden [_ _]) 118 | 119 | (defn- explain-str [explain-data] 120 | (->> explain-data 121 | ::s/problems 122 | (mapcat (fn [{:keys [pred val in] :as problem}] 123 | (cond 124 | (and (sequential? pred) 125 | (= `only-keys (first pred))) 126 | (let [ks (set/difference (set (keys val)) (second pred))] 127 | (for [k ks] 128 | (assoc problem :val k :reason "unexpected prop"))) 129 | 130 | (and (sequential? pred) 131 | (= `keys-satisfy (first pred))) 132 | (if (map? val) 133 | (let [k->spec (second pred)] 134 | (for [[k spec-form] k->spec 135 | :let [v (get val k ::not-found)] 136 | :when (not= v ::not-found) 137 | :let [spec (eval spec-form)] 138 | problem (::s/problems (s/explain-data spec v))] 139 | (update problem :in #(into (conj (or in []) k) %)))) 140 | [(assoc problem :reason 'map?)]) 141 | 142 | (= `valid-fx-type? pred) 143 | (if (contains? val :fx/type) 144 | [(-> problem 145 | (update :val :fx/type) 146 | (update :in conj :fx/type))] 147 | [(assoc problem :reason "(contains? % :fx/type)")]) 148 | 149 | 150 | :else 151 | [problem]))) 152 | (map (fn [{:keys [pred val in reason]}] 153 | (str val 154 | " - failed: " 155 | (or reason (let [abbrev (s/abbrev pred)] 156 | (cond-> abbrev (sequential? abbrev) pr-str))) 157 | (when (seq in) 158 | (str " in " in))))) 159 | (str/join "\n"))) 160 | 161 | (defn- ensure-valid-desc [desc fx-type type->lifecycle type->id] 162 | (binding [*type->lifecycle* type->lifecycle 163 | *type->id* type->id] 164 | (when-let [explain-data (s/explain-data :cljfx/desc (assoc desc :fx/type fx-type))] 165 | (throw (ex-info (str "Invalid cljfx description of " (type->string type->id fx-type) " type:\n" 166 | (explain-str explain-data)) 167 | explain-data))))) 168 | 169 | (def ^:private ext-value-lifecycle 170 | (reify lifecycle/Lifecycle 171 | (create [_ {:keys [value]} _] value) 172 | (advance [_ _ {:keys [value]} _] value) 173 | (delete [_ _ _] nil))) 174 | 175 | (def ^:private ext-with-parent-props 176 | (fx/make-ext-with-props fx.parent/props)) 177 | 178 | (defn- inspector-handle-root-event [state {:keys [fx/event shortcut]}] 179 | (if (instance? KeyEvent event) 180 | (let [^KeyEvent event event] 181 | (if (= KeyEvent/KEY_PRESSED (.getEventType event)) 182 | (if (.match ^KeyCombination shortcut event) 183 | (update state :showing not) 184 | state) 185 | state)) 186 | state)) 187 | 188 | (defn- inspector-hide-popup [state _] 189 | (assoc state :showing false)) 190 | 191 | (def ^:private ext-with-popup-on-props 192 | (fx/make-ext-with-props 193 | {:on (prop/make 194 | (mutator/adder-remover 195 | (fn [^Popup popup ^Node node] 196 | (let [p (.localToScreen node 0.0 0.0)] 197 | (.show popup node (.getX p) (- (.getY p) 5.0)))) 198 | (fn [^Popup popup _] 199 | (.hide popup))) 200 | lifecycle/dynamic)})) 201 | 202 | (defn- inspector-popup-view [{:keys [on desc] :as props}] 203 | {:fx/type fx/ext-let-refs 204 | :refs {::desc desc} 205 | :desc {:fx/type fx/ext-let-refs 206 | :refs {::popup {:fx/type ext-with-popup-on-props 207 | :props (when on {:on on}) 208 | :desc (-> props 209 | (assoc :fx/type :popup) 210 | (dissoc :on :desc))}} 211 | :desc {:fx/type fx/ext-get-ref 212 | :ref ::desc}}}) 213 | 214 | (defn- make-inspector-tree-item [path [{:keys [type] :as info} {:keys [children component]}] type->id] 215 | (let [path (conj path info)] 216 | {:fx/type :tree-item 217 | :expanded true 218 | :value {:type (or (type->id type) type) 219 | :component component 220 | :path path} 221 | :children (mapv #(make-inspector-tree-item path % type->id) children)})) 222 | 223 | (defn- inspector-tree-cell [{:keys [type component]}] 224 | (let [instance (component/instance component) 225 | text (not-empty 226 | (condp instance? instance 227 | Label (.getText ^Label instance) 228 | Text (.getText ^Text instance) 229 | nil))] 230 | {:text (str type (when text (str " - " (pr-str text))))})) 231 | 232 | (defn on-inspector-tree-view-selection-changed [state {:keys [^TreeItem fx/event]}] 233 | (if event 234 | (assoc state :path (subvec (:path (.getValue event)) 1)) 235 | (dissoc state :path))) 236 | 237 | (defn- initialize-inspector-table! [^TableView table] 238 | (.selectFirst (.getSelectionModel table)) 239 | (.addListener (.getItems table) 240 | (reify javafx.collections.ListChangeListener 241 | (onChanged [_ _] 242 | (.selectFirst (.getSelectionModel table))))) 243 | (.setCellSelectionEnabled (.getSelectionModel table) true)) 244 | 245 | (defn- inspector-cell-item->str [x] 246 | (let [x (if (-> x meta (contains? `component/instance)) 247 | '... 248 | x)] 249 | (binding [*print-level* 5 250 | *print-length* 10] 251 | (pr-str x)))) 252 | 253 | (defn- on-inspector-table-key-pressed [^KeyEvent e] 254 | (when (and (.isShortcutDown e) (= KeyCode/C (.getCode e))) 255 | (.consume e) 256 | (let [^TableView table (.getTarget e) 257 | ^TablePosition pos (first (.getSelectedCells (.getSelectionModel table)))] 258 | (.setContent 259 | (Clipboard/getSystemClipboard) 260 | (doto (ClipboardContent.) 261 | (.putString (inspector-cell-item->str (.getCellData (.getTableColumn pos) (.getRow pos))))))))) 262 | 263 | (defn- inspector-component-view [{:keys [component]}] 264 | (let [ext-with-props? (every-pred :child :props :props-lifecycle) 265 | component->props 266 | (fn [component] 267 | (cond 268 | ;; fn component? 269 | (and (:child-desc component) (:desc component) (:child component)) 270 | (dissoc (:desc component) :fx/type) 271 | 272 | ;; composite component? 273 | (and (:props component) (:instance component)) 274 | (:props component) 275 | 276 | ;; ext-let-refs? 277 | (and (:refs component) (:child component)) 278 | (:refs component) 279 | 280 | ;; ext-with-props? 281 | (ext-with-props? component) 282 | (->> component 283 | (iterate #(when (ext-with-props? %) (:child %))) 284 | (take-while some?) 285 | (map :props) 286 | (apply merge)) 287 | 288 | :else 289 | component))] 290 | {:fx/type fx/ext-on-instance-lifecycle 291 | :on-created initialize-inspector-table! 292 | :desc {:fx/type :table-view 293 | :on-key-pressed on-inspector-table-key-pressed 294 | :columns [{:fx/type :table-column 295 | :text "prop" 296 | :cell-value-factory key 297 | :cell-factory {:fx/cell-type :table-cell 298 | :describe (fn [x] 299 | {:text (str x)})}} 300 | {:fx/type :table-column 301 | :text "value" 302 | :cell-value-factory val 303 | :cell-factory {:fx/cell-type :table-cell 304 | :describe (fn [x] 305 | {:text (inspector-cell-item->str x)})}}] 306 | :items (vec (component->props component))}})) 307 | 308 | (defn- initialize-inspector-tree-view! [^Node tree-view] 309 | (let [dispatcher (.getEventDispatcher tree-view)] 310 | (.setEventDispatcher tree-view 311 | (reify EventDispatcher 312 | (dispatchEvent [_ e next] 313 | (if (and (instance? KeyEvent e) 314 | (= KeyEvent/KEY_PRESSED (.getEventType e)) 315 | (#{KeyCode/ESCAPE KeyCode/SPACE} (.getCode ^KeyEvent e))) 316 | e 317 | (.dispatchEvent dispatcher e next))))))) 318 | 319 | (defn- inspector-root-view [{:keys [components showing type->id path inspector-shortcut]}] 320 | (let [^Scene scene (->> components 321 | (tree-seq :children #(vals (:children %))) 322 | (keep :component) 323 | (map component/instance) 324 | (some #(when (instance? Scene %) %))) 325 | root (some-> scene .getRoot) 326 | shortcut (coerce/key-combination inspector-shortcut)] 327 | (if root 328 | {:fx/type ext-with-parent-props 329 | :props {:event-filter {:fn #'inspector-handle-root-event 330 | :shortcut shortcut}} 331 | :desc (cond-> {:fx/type inspector-popup-view 332 | :auto-hide true 333 | :hide-on-escape true 334 | :consume-auto-hiding-events true 335 | :on-auto-hide {:fn #'inspector-hide-popup} 336 | :anchor-location :window-top-right 337 | :desc {:fx/type ext-value-lifecycle 338 | :value root} 339 | :content 340 | [{:fx/type :stack-pane 341 | :style-class "inspector-root" 342 | :stylesheets [(::css/url help-ui-css)] 343 | :children 344 | [{:fx/type :region 345 | :style-class "inspector-backdrop"} 346 | {:fx/type :grid-pane 347 | :column-constraints [{:fx/type :column-constraints 348 | :hgrow :always}] 349 | :row-constraints [{:fx/type :row-constraints 350 | :percent-height 70} 351 | {:fx/type :row-constraints 352 | :percent-height 30}] 353 | :children 354 | [{:fx/type ext.tree-view/with-selection-props 355 | :grid-pane/row 0 356 | :props {:on-selected-item-changed {:fn #'on-inspector-tree-view-selection-changed}} 357 | :desc {:fx/type fx/ext-on-instance-lifecycle 358 | :on-created initialize-inspector-tree-view! 359 | :desc {:fx/type :tree-view 360 | :cell-factory {:fx/cell-type :tree-cell 361 | :describe #'inspector-tree-cell} 362 | :show-root false 363 | :root (make-inspector-tree-item 364 | [] 365 | [{:type :root} components] 366 | type->id)}}} 367 | {:fx/type inspector-component-view 368 | :grid-pane/row 1 369 | :component (or (:component (get-in components (interleave (repeat :children) path))) 370 | (-> components :children vals first :component))}]}]}]} 371 | 372 | 373 | showing 374 | (assoc :on {:fx/type ext-value-lifecycle 375 | :value root}))} 376 | {:fx/type :region}))) 377 | 378 | (defn- init-state! [state] 379 | (let [r (fx/create-renderer 380 | :opts {:fx.opt/map-event-handler #(swap! state (:fn %) %)} 381 | :middleware (fx/wrap-map-desc #(assoc % :fx/type inspector-root-view)))] 382 | (fx/mount-renderer state r))) 383 | 384 | (defn- update-in-if-exists [m k f & args] 385 | (let [x (get-in m k ::not-found)] 386 | (if (identical? x ::not-found) 387 | m 388 | (apply update-in m k f args)))) 389 | 390 | (defn- wrap-lifecycle [fx-type type->lifecycle type->id inspector-shortcut] 391 | (let [lifecycle (or (type->lifecycle fx-type) fx-type)] 392 | (reify lifecycle/Lifecycle 393 | (create [_ desc opts] 394 | (let [component-info {:id (gensym "component") :type fx-type} 395 | old-stack (::stack opts) 396 | stack (conj old-stack component-info) 397 | state (or (::state opts) (doto (atom {:components {} 398 | :inspector-shortcut inspector-shortcut 399 | :type->id type->id}) init-state!)) 400 | opts (assoc opts ::stack stack ::state state)] 401 | (try 402 | (ensure-valid-desc desc fx-type type->lifecycle type->id) 403 | (let [child (lifecycle/create lifecycle desc opts)] 404 | (swap! state 405 | update-in (interpose :children (cons :components (reverse stack))) 406 | assoc :component child) 407 | (with-meta {:child child :info component-info :state state} 408 | {`component/instance #(-> % :child component/instance)})) 409 | (catch Exception ex (re-throw-with-stack type->id ex stack))))) 410 | (advance [_ component desc opts] 411 | (let [state (:state component) 412 | stack (conj (::stack opts) (:info component)) 413 | opts (assoc opts ::stack stack ::state state)] 414 | (try 415 | (ensure-valid-desc desc fx-type type->lifecycle type->id) 416 | (let [child (lifecycle/advance lifecycle (:child component) desc opts)] 417 | (swap! state 418 | update-in (interpose :children (cons :components (reverse stack))) 419 | assoc :component child) 420 | (assoc component :child child)) 421 | (catch Exception ex (re-throw-with-stack type->id ex stack))))) 422 | (delete [_ component opts] 423 | (let [state (:state component) 424 | old-stack (::stack opts) 425 | stack (conj old-stack (:info component)) 426 | opts (assoc opts ::stack stack ::state state)] 427 | (swap! state 428 | update-in-if-exists (interpose :children (cons :components (reverse old-stack))) 429 | update :children dissoc (:info component)) 430 | (try 431 | (lifecycle/delete lifecycle (:child component) opts) 432 | (catch Exception ex (re-throw-with-stack type->id ex stack)))))))) 433 | -------------------------------------------------------------------------------- /test/cljfx/dev_test.clj: -------------------------------------------------------------------------------- 1 | (ns cljfx.dev-test 2 | (:require [clojure.test :refer :all] 3 | [cljfx.api :as fx] 4 | [cljfx.dev :as fx.dev] 5 | [clojure.string :as str]) 6 | (:import [java.io PrintStream ByteArrayOutputStream])) 7 | 8 | (defn- validated [desc] 9 | @(fx/on-fx-thread 10 | (-> desc 11 | (fx/create-component {:fx.opt/type->lifecycle fx.dev/type->lifecycle}) 12 | fx/instance))) 13 | 14 | (deftest create-test 15 | (is (some? (validated {:fx/type :label}))) 16 | (is (thrown-with-msg? Exception #"Cljfx component stack" (validated {:fx/type :not-a-valid-id}))) 17 | (is (thrown-with-msg? Exception #"Cljfx component stack" (validated {:fx/type :label 18 | :text :not-a-string}))) 19 | (is (validated {:fx/type :label 20 | :text "a string"})) 21 | (is (validated {:fx/type :v-box 22 | :children [{:fx/type :label 23 | :v-box/vgrow :always 24 | :text "a string"}]})) 25 | (is (thrown-with-msg? Exception #"Cljfx component stack" 26 | (validated {:fx/type :v-box 27 | :children [{:fx/type :label 28 | :v-box/vgrow true 29 | :text "a string"}]})))) 30 | 31 | (deftest advance-test 32 | (let [opts {:fx.opt/type->lifecycle fx.dev/type->lifecycle} 33 | c (fx/create-component 34 | {:fx/type :label 35 | :text "foo"} 36 | opts) 37 | _ (is (some? (fx/instance c))) 38 | _ (is (thrown-with-msg? Exception #"Cljfx component stack" (fx/advance-component 39 | c 40 | {:fx/type :label 41 | :text :not-a-string} 42 | opts)))])) 43 | 44 | (deftest extensions-test 45 | (is (some? (validated {:fx/type :stage 46 | :scene {:fx/type :scene 47 | :root {:fx/type :label}}}))) 48 | (is (some? (validated {:fx/type :stage 49 | :scene {:fx/type fx/ext-on-instance-lifecycle 50 | :desc {:fx/type :scene 51 | :root {:fx/type :label}}}}))) 52 | (is (thrown-with-msg? Exception #"desc-of" 53 | (validated {:fx/type :stage 54 | :scene {:fx/type fx/ext-on-instance-lifecycle 55 | :desc {:fx/type :label}}}))) 56 | (is (some? (validated {:fx/type fx/ext-let-refs 57 | :refs nil 58 | :desc {:fx/type :label}})))) 59 | 60 | (deftest cell-factories 61 | ;; can't catch since exceptions happen in JavaFX and end up printed to stderr, 62 | ;; but at least they are dev exceptions 63 | (let [baos (ByteArrayOutputStream.) 64 | err (PrintStream. baos) 65 | old-err System/err] 66 | (System/setErr err) 67 | @(fx/on-fx-thread 68 | (let [opts {:fx.opt/type->lifecycle fx.dev/type->lifecycle} 69 | c (fx/create-component 70 | {:fx/type :stage 71 | :showing true 72 | :scene {:fx/type :scene 73 | :root {:fx/type :list-view 74 | :cell-factory {:fx/cell-type :list-cell 75 | :describe (fn [i] 76 | {:graphic {:fx/type :label 77 | :text :not-a-string}})} 78 | :items (range 1)}}} 79 | opts)] 80 | (fx/delete-component c opts))) 81 | (is (str/includes? (.toString baos) "Cljfx component stack")) 82 | (System/setErr old-err))) 83 | 84 | --------------------------------------------------------------------------------