Skip to content

Commit d6b4847

Browse files
committed
Add help UI
1 parent 23db297 commit d6b4847

File tree

4 files changed

+303
-3
lines changed

4 files changed

+303
-3
lines changed

deps.edn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
{:deps {cljfx/cljfx {:mvn/version "1.7.22"}}
1+
{:deps {cljfx/cljfx {:mvn/version "1.7.22"}
2+
cljfx/css {:mvn/version "1.1.0"}}
23
:aliases {;; clj -T:build deploy
34
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}
45
clj-commons/pomegranate {:mvn/version "1.2.0"}}

src/cljfx/dev.clj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,15 @@
360360
(println (explain-str explain-data))
361361
(println "Success!"))))
362362

363+
(load "dev/ui")
364+
365+
(defn help-ui
366+
"Open a window with cljfx type and prop reference"
367+
[]
368+
(launch-help-ui!)
369+
nil)
370+
363371
;; stretch goals
364-
;; - ui reference for searching the props/types/etc
372+
;; - integrate javadocs
365373
;; - dev cljfx type->lifecycle wrapper that adds inspector capabilities.
366374
;; - dev ui builder

src/cljfx/dev/extensions.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
:spec (s/keys :req-un [:cljfx.ext-many/desc])
4040
:of 'java.util.Collection)
4141

42-
(s/def :cljfx.make-ext-with-props/props map?)
42+
(s/def :cljfx.make-ext-with-props/props (s/nilable map?))
4343

4444
(register-type! `fx/make-ext-with-props
4545
:spec (s/keys :req-un [:cljfx/desc :cljfx.make-ext-with-props/props])

src/cljfx/dev/ui.clj

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
(in-ns 'cljfx.dev)
2+
(import '[javafx.scene.input KeyEvent]
3+
'[javafx.stage Popup]
4+
'[javafx.scene Node])
5+
(require '[cljfx.ext.list-view :as fx.ext.list-view]
6+
'[cljfx.prop :as prop]
7+
'[cljfx.mutator :as mutator]
8+
'[cljfx.component :as component]
9+
'[cljfx.css :as css])
10+
11+
(def ^:private help-ui-css
12+
(css/register ::css
13+
{".list-view"
14+
{:-fx-background-color :transparent
15+
:-fx-border-width [0 1 0 0]
16+
:-fx-border-color "#aaa"
17+
":focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused"
18+
{:-fx-background-color "#4E84E0"}}
19+
".popup-root" {:-fx-background-color "#ddd"
20+
:-fx-effect "dropshadow(gaussian, #0006, 8, 0, 0, 2)"}
21+
".filter-term" {:-fx-background-color "#42B300"
22+
:-fx-background-radius 2
23+
:-fx-effect "dropshadow(gaussian, #0006, 4, 0, 0, 2)"
24+
:-fx-padding [2 4]}
25+
".list-cell" {:-fx-background-color :transparent
26+
:-fx-text-fill "#000"
27+
:-fx-font-size 14
28+
:-fx-padding [2 4]
29+
":selected" {:-fx-background-color "#4E84E033"}}
30+
".text-area" {:-fx-background-color :transparent
31+
:-fx-focus-traversable false
32+
:-fx-text-fill "#000"
33+
" .content" {:-fx-background-color :transparent}}
34+
".scroll-pane" {:-fx-background-color :transparent
35+
:-fx-padding 0
36+
"> .viewport" {:-fx-background-color :transparent}}
37+
".scroll-bar" {:-fx-background-color :transparent
38+
"> .thumb" {:-fx-background-color "#999"
39+
:-fx-background-insets 0
40+
:-fx-background-radius 4
41+
":hover" {:-fx-background-color "#9c9c9c"}
42+
":pressed" {:-fx-background-color "#aaa"}}
43+
":horizontal" {"> .increment-button > .increment-arrow" {:-fx-pref-height 7}
44+
"> .decrement-button > .decrement-arrow" {:-fx-pref-height 7}}
45+
":vertical" {"> .increment-button > .increment-arrow" {:-fx-pref-width 7}
46+
"> .decrement-button > .decrement-arrow" {:-fx-pref-width 7}}
47+
"> .decrement-button" {:-fx-padding 0
48+
"> .decrement-arrow" {:-fx-shape nil
49+
:-fx-padding 0}}
50+
"> .increment-button" {:-fx-padding 0
51+
"> .increment-arrow" {:-fx-shape nil
52+
:-fx-padding 0}}}
53+
".corner" {:-fx-background-color :transparent}}))
54+
55+
(defn- set-help-ui-selection [state {:keys [key fx/event]}]
56+
(update state key assoc :selection event))
57+
58+
(defn- set-help-ui-filter-term [state {:keys [key ^KeyEvent fx/event]}]
59+
(let [ch (.getCharacter event)]
60+
(cond
61+
(and (= 1 (count ch)) (#{127 27} (int (first ch))))
62+
(update state key assoc :filter-term "")
63+
64+
(= ch "\b")
65+
(update-in state [key :filter-term] #(cond-> % (pos? (count %)) (subs 0 (dec (count %)))))
66+
67+
(= ch "\t")
68+
state
69+
70+
(re-matches #"^[a-zA-Z0-9:/\-.+!@#$%^&*()-={}\[\]<>?,/\\'\"]$" ch)
71+
(update-in state [key :filter-term] str (.getCharacter event))
72+
73+
:else
74+
state)))
75+
76+
(defn- help-list-view [{:keys [filter-term selection items key]}]
77+
{:fx/type :stack-pane
78+
:children [{:fx/type fx.ext.list-view/with-selection-props
79+
:props {:selected-item selection
80+
:on-selected-item-changed {:fn #'set-help-ui-selection :key key}}
81+
:desc {:fx/type :list-view
82+
:on-key-typed {:fn #'set-help-ui-filter-term :key key}
83+
:items items}}
84+
{:fx/type :label
85+
:visible (boolean (seq filter-term))
86+
:style-class ["label" "filter-term"]
87+
:stack-pane/margin 7
88+
:stack-pane/alignment :bottom-right
89+
:text filter-term}]})
90+
91+
(defn- process-filter-selection [{:keys [filter-term selection] :as m} items]
92+
(let [items (cond->> items
93+
(not (str/blank? filter-term))
94+
(filter #(str/includes? % filter-term)))
95+
items (sort-by str items)
96+
selection (or (and selection (some #{selection} items))
97+
(first items))]
98+
(assoc m :items items :selection selection)))
99+
100+
(def ^:private ext-recreate-on-key-changed
101+
(reify lifecycle/Lifecycle
102+
(create [_ {:keys [key desc]} opts]
103+
(with-meta {:key key
104+
:child (lifecycle/create lifecycle/dynamic desc opts)}
105+
{`component/instance #(-> % :child component/instance)}))
106+
(advance [this component {:keys [key desc] :as this-desc} opts]
107+
(if (= (:key component) key)
108+
(update component :child #(lifecycle/advance lifecycle/dynamic % desc opts))
109+
(do (lifecycle/delete this component opts)
110+
(lifecycle/create this this-desc opts))))
111+
(delete [_ component opts]
112+
(lifecycle/delete lifecycle/dynamic (:child component) opts))))
113+
114+
(def ^:private ext-with-shown-on
115+
(fx/make-ext-with-props
116+
{:shown-on (prop/make
117+
(mutator/adder-remover
118+
(fn [^Popup popup ^Node node]
119+
(let [bounds (.getBoundsInLocal node)
120+
node-pos (.localToScreen node
121+
-8
122+
(- (.getHeight bounds) 6))]
123+
(.show popup node
124+
(.getX node-pos)
125+
(.getY node-pos))))
126+
(fn [^Popup popup _]
127+
(.hide popup)))
128+
lifecycle/dynamic)}))
129+
130+
(defn- hover-help-syntax-element [state {:keys [path key]}]
131+
(assoc state key path))
132+
133+
(defn- hide-hover-help-popup [state {:keys [key]}]
134+
(update state key pop))
135+
136+
(defn- help-ui-syntax-view [{:keys [syntax key hover] :as props}]
137+
(letfn [(apply-syntax [syntax path]
138+
(cond
139+
(string? syntax)
140+
[{:fx/type :text
141+
:font {:family "monospace" :size 14}
142+
:text syntax}]
143+
144+
(vector? syntax)
145+
(map-indexed
146+
(fn [i x]
147+
(cond
148+
(string? x)
149+
{:fx/type :text
150+
:font {:family "monospace" :size 14}
151+
:text x}
152+
153+
(vector? x)
154+
(let [[text syntax] x
155+
current-path (conj path i)]
156+
{:fx/type fx/ext-let-refs
157+
:refs
158+
{::view
159+
{:fx/type :label
160+
:underline true
161+
:font {:family "monospace" :size 14}
162+
:text-fill "#000e26"
163+
:on-mouse-entered
164+
{:fn #'hover-help-syntax-element
165+
:path current-path
166+
:key key}
167+
:text text}}
168+
:desc
169+
{:fx/type fx/ext-let-refs
170+
:refs
171+
{::popup
172+
{:fx/type ext-with-shown-on
173+
:props (if (= current-path (take (count current-path) hover))
174+
{:shown-on
175+
{:fx/type fx/ext-get-ref
176+
:ref ::view}}
177+
{})
178+
:desc {:fx/type :popup
179+
:anchor-location :window-top-left
180+
:auto-hide true
181+
:hide-on-escape true
182+
:on-auto-hide {:fn #'hide-hover-help-popup
183+
:key key}
184+
:content [{:fx/type :stack-pane
185+
:max-width 960
186+
:pref-height :use-computed-size
187+
:max-height 600
188+
:style-class "popup-root"
189+
:children
190+
[{:fx/type :scroll-pane
191+
:content {:fx/type :text-flow
192+
:padding 5
193+
:children (apply-syntax syntax current-path)}}]}]}}}
194+
:desc {:fx/type fx/ext-get-ref :ref ::view}}})
195+
196+
:else
197+
(throw (ex-info "Invalid syntax" {:syntax x}))))
198+
199+
syntax)
200+
201+
:else
202+
(throw (ex-info "Invalid syntax" {:syntax syntax}))))]
203+
(-> props
204+
(dissoc :syntax :key :hover)
205+
(assoc
206+
:fx/type :scroll-pane
207+
:content
208+
{:fx/type :text-flow
209+
:padding 5
210+
:children (apply-syntax syntax [])}))))
211+
212+
(defn- help-ui-view [{:keys [registry type prop type-hover prop-hover]}]
213+
(let [filtered-type-map (process-filter-selection type (keys (:types registry)))
214+
selected-type (:selection filtered-type-map)
215+
selected-props (-> registry :props (get selected-type))
216+
filtered-prop-map (process-filter-selection prop (keys selected-props))
217+
selected-prop-id (:selection filtered-prop-map)
218+
selected-prop (get selected-props selected-prop-id)]
219+
{:fx/type :stage
220+
:showing true
221+
:width 900
222+
:scene
223+
{:fx/type :scene
224+
:stylesheets [(::css/url help-ui-css)]
225+
:root {:fx/type :grid-pane
226+
:style {:-fx-background-color "#ccc"}
227+
:column-constraints [{:fx/type :column-constraints
228+
:min-width 150
229+
:max-width 150}
230+
{:fx/type :column-constraints
231+
:min-width 150
232+
:max-width 150}
233+
{:fx/type :column-constraints
234+
:hgrow :always}]
235+
:row-constraints [{:fx/type :row-constraints
236+
:min-height 100
237+
:max-height 100}
238+
{:fx/type :row-constraints
239+
:vgrow :always}]
240+
:children [(assoc filtered-type-map
241+
:grid-pane/column 0
242+
:grid-pane/row 0
243+
:grid-pane/row-span 2
244+
:fx/type help-list-view
245+
:key :type)
246+
{:fx/type help-ui-syntax-view
247+
:style {:-fx-border-width [0 0 1 0]
248+
:-fx-border-color "#aaa"}
249+
:grid-pane/column 1
250+
:grid-pane/column-span 2
251+
:grid-pane/row 0
252+
:key :type-hover
253+
:hover type-hover
254+
:syntax (if selected-type
255+
(let [type-map (get (:types registry) selected-type)]
256+
(str "Cljfx type: " selected-type
257+
(when (symbol? (:of type-map))
258+
(str "\nInstance class: " (:of type-map)))
259+
(when (:req type-map)
260+
(if (set? (:req type-map))
261+
(str "\nRequired props, either:\n"
262+
(str/join "\n" (for [req (:req type-map)]
263+
(str "- " (str/join ", " (sort req))))))))
264+
(when (and (not selected-props) (:spec type-map))
265+
(str "\nSpec: " (pr-str (s/describe (:spec type-map)))))))
266+
"")}
267+
{:fx/type ext-recreate-on-key-changed
268+
:grid-pane/row 1
269+
:grid-pane/column 1
270+
:key selected-type
271+
:desc (assoc filtered-prop-map :fx/type help-list-view :key :prop)}
272+
{:fx/type help-ui-syntax-view
273+
:grid-pane/row 1
274+
:grid-pane/column 2
275+
:key :prop-hover
276+
:hover prop-hover
277+
:syntax (if selected-prop
278+
(long-prop-help-syntax selected-prop)
279+
"")}]}}}))
280+
281+
(defn- launch-help-ui! []
282+
(let [state (atom {:registry @registry
283+
:type {:selection nil
284+
:filter-term ""}
285+
:prop {:selection nil
286+
:filter-term ""}})
287+
render (fx/create-renderer
288+
:opts {:fx.opt/type->lifecycle type->lifecycle
289+
:fx.opt/map-event-handler #(swap! state (:fn %) %)}
290+
:middleware (fx/wrap-map-desc #(assoc % :fx/type help-ui-view)))]
291+
(fx/mount-renderer state render)))

0 commit comments

Comments
 (0)