mirror of
https://github.com/penpot/penpot.git
synced 2026-08-28 23:59:00 +00:00
489 lines
18 KiB
Clojure
489 lines
18 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns app.main.ui.workspace.top-toolbar
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.geom.point :as gpt]
|
|
[app.config :as cf]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.workspace :as dw]
|
|
[app.main.data.workspace.common :as dwc]
|
|
[app.main.data.workspace.drawing.common :as dwdc]
|
|
[app.main.data.workspace.mcp :as mcp]
|
|
[app.main.data.workspace.media :as dwm]
|
|
[app.main.data.workspace.shortcuts :as sc]
|
|
[app.main.features :as features]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]]
|
|
[app.main.ui.components.file-uploader :as file-uploader]
|
|
[app.main.ui.context :as ctx]
|
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.timers :as ts]
|
|
[beicon.v2.core :as rx]
|
|
[okulary.core :as l]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(def ^:private toolbar-hidden-ref
|
|
(l/derived (fn [state]
|
|
(let [visibility (get state :hide-toolbar)
|
|
path-edit-state (get state :edit-path)
|
|
selected (get state :selected)
|
|
edition (get state :edition)
|
|
|
|
is-single (= (count selected) 1)
|
|
is-path-editing (and is-single (some? (get path-edit-state edition)))]
|
|
|
|
(if is-path-editing true visibility)))
|
|
refs/workspace-local))
|
|
|
|
(def grouped-tools
|
|
{:shapes {:default-tool :rect
|
|
:tools {:rect {:icon i/rectangle}
|
|
:circle {:icon i/ellipse}
|
|
:line {:icon i/easing-linear}
|
|
:arrow {:icon i/stroke-arrow}}}
|
|
:free-draw {:default-tool :path
|
|
:tools {:path {:icon i/path}
|
|
:curve {:icon i/curve}}}})
|
|
|
|
(defn- tool-label
|
|
[tool]
|
|
(case tool
|
|
:move (tr "workspace.toolbar.move" (sc/get-tooltip :move))
|
|
:frame (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
|
|
:rect (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
|
|
:circle (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
|
|
:line (tr "workspace.toolbar.line" (sc/get-tooltip :draw-line))
|
|
:arrow (tr "workspace.toolbar.arrow" (sc/get-tooltip :draw-arrow))
|
|
:text (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
|
|
:path (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
|
|
:image (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))
|
|
:curve (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
|
|
:plugins (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins))
|
|
:debug "Debugging tool"
|
|
(name tool)))
|
|
|
|
(defn- active-group-tool
|
|
[group drawtool]
|
|
(if (contains? (:tools group) drawtool)
|
|
drawtool
|
|
(:default-tool group)))
|
|
|
|
(defn- is-selected-group
|
|
[group drawtool]
|
|
(contains? (:tools group) drawtool))
|
|
|
|
(defn- group-menu-label
|
|
[group drawtool]
|
|
(let [tool-id (active-group-tool group drawtool)]
|
|
(str (tr "labels.options") ": " (tool-label tool-id))))
|
|
|
|
(defn- cancel-timer!
|
|
[timer-ref*]
|
|
(when-let [timer (mf/ref-val timer-ref*)]
|
|
(ts/dispose! timer)
|
|
(mf/set-ref-val! timer-ref* nil)))
|
|
|
|
(mf/defc group-tool*
|
|
{::mf/private true
|
|
::mf/wrap [mf/memo]}
|
|
[{:keys [group drawtool on-select-tool]}]
|
|
(let [li-ref (mf/use-ref nil)
|
|
flyout-ref (mf/use-ref nil)
|
|
|
|
default-tool* (mf/use-state (active-group-tool group drawtool))
|
|
default-tool (deref default-tool*)
|
|
|
|
open* (mf/use-state false)
|
|
open (deref open*)
|
|
|
|
open-timer* (mf/use-ref nil)
|
|
close-timer* (mf/use-ref nil)
|
|
|
|
default-icon (:icon (get-in group [:tools default-tool]))
|
|
subtools (:tools group)
|
|
menu-label (group-menu-label group drawtool)
|
|
selected (boolean (is-selected-group group drawtool))
|
|
|
|
on-select-tool
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(reset! open* false)
|
|
(on-select-tool event)))
|
|
|
|
on-display-menu
|
|
(mf/use-fn
|
|
(fn []
|
|
(cancel-timer! close-timer*)
|
|
(cancel-timer! open-timer*)
|
|
(mf/set-ref-val!
|
|
open-timer*
|
|
(ts/schedule 350
|
|
#(do
|
|
(reset! open* true)
|
|
(mf/set-ref-val! open-timer* nil))))))
|
|
|
|
on-hide-menu
|
|
(mf/use-fn
|
|
(fn []
|
|
(cancel-timer! open-timer*)
|
|
(cancel-timer! close-timer*)
|
|
(mf/set-ref-val!
|
|
close-timer*
|
|
(ts/schedule 350
|
|
#(do
|
|
(reset! open* false)
|
|
(mf/set-ref-val! close-timer* nil))))))
|
|
|
|
on-main-key-down
|
|
(mf/use-fn
|
|
(mf/deps open)
|
|
(fn [event]
|
|
(cond
|
|
(and open (kbd/esc? event))
|
|
(reset! open* false)
|
|
|
|
(or (kbd/enter? event) (kbd/space? event))
|
|
(do
|
|
(dom/prevent-default event)
|
|
(if open
|
|
(reset! open* false)
|
|
(do
|
|
(cancel-timer! close-timer*)
|
|
(reset! open* true))))
|
|
|
|
(kbd/down-arrow? event)
|
|
(do
|
|
(dom/prevent-default event)
|
|
(cancel-timer! close-timer*)
|
|
(reset! open* true)))))
|
|
|
|
on-flyout-key-down
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [flyout (mf/ref-val flyout-ref)]
|
|
(when flyout
|
|
(let [items (vec (dom/query-all flyout "[role=menuitemradio]"))
|
|
active (dom/get-active)
|
|
idx (d/index-of-pred items #(identical? % active))]
|
|
(cond
|
|
(kbd/space? event)
|
|
(when active
|
|
(dom/prevent-default event)
|
|
(dom/click active))
|
|
|
|
(kbd/esc? event)
|
|
(do
|
|
(reset! open* false)
|
|
(when-let [li (mf/ref-val li-ref)]
|
|
(when-let [btn (.. li (querySelector "div[role=group] > button"))]
|
|
(dom/focus! btn))))
|
|
|
|
(kbd/tab? event)
|
|
(do
|
|
(dom/prevent-default event)
|
|
(let [shift? (.-shiftKey ^js event)
|
|
next-idx (if shift?
|
|
(if (or (nil? idx) (zero? idx)) (dec (count items)) (dec idx))
|
|
(if (or (nil? idx) (= idx (dec (count items)))) 0 (inc idx)))]
|
|
(dom/focus! (nth items next-idx))))))))))]
|
|
|
|
(mf/with-effect []
|
|
(fn []
|
|
(cancel-timer! open-timer*)
|
|
(cancel-timer! close-timer*)))
|
|
|
|
(mf/with-effect [drawtool group]
|
|
(reset! default-tool* (active-group-tool group drawtool)))
|
|
|
|
(mf/with-effect []
|
|
(let [subs (->> st/stream
|
|
(rx/filter #(= % :interrupt))
|
|
(rx/subs! #(reset! open* false)))]
|
|
#(rx/dispose! subs)))
|
|
|
|
[:li {:ref li-ref
|
|
:class (stl/css :toolbar-group)
|
|
:on-pointer-enter on-display-menu
|
|
:on-pointer-leave on-hide-menu}
|
|
[:div {:role "group"
|
|
:aria-label menu-label}
|
|
[:> icon-button* {:variant "ghost"
|
|
:flyout-indicator true
|
|
:aria-label (tool-label default-tool)
|
|
:aria-haspopup true
|
|
:aria-pressed selected
|
|
:aria-expanded open
|
|
:has-tooltip false
|
|
:icon default-icon
|
|
:on-click (fn [event]
|
|
(cancel-timer! open-timer*)
|
|
(cancel-timer! close-timer*)
|
|
(on-select-tool event))
|
|
:on-key-down on-main-key-down
|
|
:data-tool (name default-tool)}]
|
|
|
|
[:ul {:ref flyout-ref
|
|
:role "menu"
|
|
:class (stl/css-case :toolbar-group-flyout true
|
|
:open open)
|
|
:aria-label menu-label
|
|
:on-key-down on-flyout-key-down}
|
|
|
|
(for [[id {:keys [icon]}] subtools]
|
|
[:li {:key (name id)
|
|
:role "none"}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:role "menuitemradio"
|
|
:aria-label (tool-label id)
|
|
:aria-pressed (= drawtool id)
|
|
:aria-checked (= drawtool id)
|
|
:icon icon
|
|
:on-click on-select-tool
|
|
:data-tool (name id)}]])]]]))
|
|
|
|
(mf/defc image-upload-tool*
|
|
{::mf/private true
|
|
::mf/wrap [mf/memo]}
|
|
[]
|
|
(let [ref (mf/use-ref nil)
|
|
file-id (mf/use-ctx ctx/current-file-id)
|
|
|
|
on-display-uploader
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! :interrupt (dw/clear-edition-mode))
|
|
(dom/click (mf/ref-val ref))))
|
|
|
|
on-selected
|
|
(mf/use-fn
|
|
(mf/deps file-id)
|
|
(fn [blobs]
|
|
;; We don't want to add a ref because that redraws the component
|
|
;; for everychange. Better direct access on the callback.
|
|
(let [vbox (deref refs/vbox)
|
|
x (+ (:x vbox) (/ (:width vbox) 2))
|
|
y (+ (:y vbox) (/ (:height vbox) 2))
|
|
params {:file-id file-id
|
|
:blobs (seq blobs)
|
|
:position (gpt/point x y)}]
|
|
(st/emit! (dwm/upload-media-workspace params)))))]
|
|
|
|
[:*
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-label (tool-label :image)
|
|
:icon i/img
|
|
:on-click on-display-uploader}]
|
|
[:& file-uploader/file-uploader {:input-id "image-upload"
|
|
:accept dwm/accept-image-types
|
|
:multi true
|
|
:ref ref
|
|
:on-selected on-selected}]]))
|
|
|
|
(mf/defc mcp-tool*
|
|
{::mf/private true
|
|
::mf/wrap [mf/memo]}
|
|
[{:keys [is-mcp-connected]}]
|
|
(let [menu-open* (mf/use-state false)
|
|
menu-open? (deref menu-open*)
|
|
|
|
on-toggle-menu
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(dom/stop-propagation event)
|
|
(swap! menu-open* not)))
|
|
|
|
on-close-menu
|
|
(mf/use-fn
|
|
#(reset! menu-open* false))
|
|
|
|
on-connect
|
|
(mf/use-fn
|
|
#(st/emit! (mcp/connect-mcp)
|
|
(ev/event {::ev/name "connect-mcp-plugin"
|
|
::ev/origin "workspace:toolbar"})))]
|
|
|
|
[:*
|
|
[:> button* {:variant "ghost"
|
|
:on-click on-toggle-menu
|
|
:aria-pressed menu-open?
|
|
:data-tool "mcp"
|
|
:data-testid "mcp-btn"}
|
|
[:div {:class (stl/css-case :toolbar-mcp-button true
|
|
:selected menu-open?)}
|
|
[:span {:class (stl/css-case :toolbar-mcp-button-dot true
|
|
:connected is-mcp-connected)}]
|
|
[:span {:class (stl/css-case :toolbar-mcp-button-label true
|
|
:connected is-mcp-connected)}
|
|
(tr "workspace.toolbar.mcp")]]]
|
|
|
|
[:div {:class (stl/css :toolbar-mcp-menu)}
|
|
[:> dropdown-menu* {:show menu-open?
|
|
:on-close on-close-menu
|
|
:class (stl/css :toolbar-mcp-dropdown)}
|
|
(if is-mcp-connected
|
|
[:li {:class (stl/css :toolbar-mcp-dropdown-info)
|
|
:role "presentation"}
|
|
(tr "workspace.toolbar.mcp-connected")]
|
|
[:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item)
|
|
:on-click on-connect}
|
|
(tr "workspace.toolbar.mcp-connect-here")])]]]))
|
|
|
|
(mf/defc top-toolbar*
|
|
{::mf/wrap [mf/memo]}
|
|
[{:keys [layout]}]
|
|
(let [selected-drawing-tool (mf/deref refs/selected-drawing-tool)
|
|
selected-edition (mf/deref refs/selected-edition)
|
|
rulers-enabled (mf/deref refs/rulers?)
|
|
toolbar-hidden (mf/deref toolbar-hidden-ref)
|
|
mcp (mf/deref refs/mcp)
|
|
|
|
plugins-enabled? (features/active-feature? @st/state "plugins/runtime")
|
|
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
|
|
|
mcp-conn-status (get mcp :connection-status)
|
|
mcp-valid-token? (get mcp :token-valid)
|
|
mcp-enabled? (get mcp :enabled)
|
|
|
|
mcp-connected? (= "connected" mcp-conn-status)
|
|
mcp-show? (and (contains? cf/flags :mcp)
|
|
mcp-enabled?
|
|
mcp-valid-token?)
|
|
|
|
separator? (or plugins-enabled? *assert* mcp-show?)
|
|
|
|
on-display-plugins-manager
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! (ev/event {::ev/name "open-plugins-manager"
|
|
::ev/origin "workspace:toolbar"})
|
|
(modal/show :plugin-management {}))))
|
|
|
|
on-toggle-debug-panel
|
|
(mf/use-fn
|
|
(mf/deps layout)
|
|
(fn []
|
|
(let [is-sidebar-closed (contains? layout :collapse-left-sidebar)]
|
|
(when is-sidebar-closed
|
|
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
|
|
(st/emit! (dw/remove-layout-flag :shortcuts)
|
|
(-> (dw/toggle-layout-flag :debug-panel)
|
|
(vary-meta assoc ::ev/origin "workspace-left-toolbar"))))))
|
|
|
|
on-interrupt
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! :interrupt
|
|
(dw/clear-edition-mode)
|
|
(dwdc/clear-drawing))))
|
|
|
|
on-select-tool
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [tool (-> (dom/get-current-target event)
|
|
(dom/get-data "tool")
|
|
(keyword))]
|
|
(st/emit! :interrupt
|
|
(dw/clear-edition-mode)
|
|
(dw/select-for-drawing tool)))))
|
|
|
|
on-toggle-toolbar
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(dom/blur! (dom/get-target event))
|
|
(st/emit! (dwc/toggle-toolbar-visibility))))]
|
|
|
|
(when-not ^boolean read-only?
|
|
[:div {:role "toolbar"
|
|
:aria-label (tr "workspace.toolbar.label")
|
|
:tab-index "0"
|
|
:class (stl/css-case :toolbar true
|
|
:no-rulers (not rulers-enabled)
|
|
:hidden toolbar-hidden)}
|
|
[:ul {:class (stl/css :toolbar-options)
|
|
:data-testid "toolbar-options"}
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-pressed (and (nil? selected-drawing-tool)
|
|
(not selected-edition))
|
|
:aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move))
|
|
:icon i/move
|
|
:on-click on-interrupt}]]
|
|
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-pressed (= selected-drawing-tool :frame)
|
|
:aria-label (tool-label :frame)
|
|
:icon i/board
|
|
:on-click on-select-tool
|
|
:data-tool "frame"}]]
|
|
|
|
[:> group-tool* {:key :shapes
|
|
:group (get grouped-tools :shapes)
|
|
:drawtool selected-drawing-tool
|
|
:on-select-tool on-select-tool}]
|
|
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-pressed (= selected-drawing-tool :text)
|
|
:aria-label (tool-label :text)
|
|
:icon i/text
|
|
:on-click on-select-tool
|
|
:data-tool "text"}]]
|
|
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> image-upload-tool*]]
|
|
|
|
[:> group-tool* {:key :free-draw
|
|
:group (get grouped-tools :free-draw)
|
|
:drawtool selected-drawing-tool
|
|
:on-select-tool on-select-tool}]
|
|
|
|
(when separator?
|
|
[:div {:class (stl/css :toolbar-separator)}])
|
|
|
|
(when plugins-enabled?
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-label (tool-label :plugins)
|
|
:icon i/puzzle
|
|
:on-click on-display-plugins-manager
|
|
:data-tool "plugins"}]])
|
|
|
|
(when *assert*
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> icon-button* {:variant "ghost"
|
|
:tooltip-placement "bottom"
|
|
:aria-pressed (contains? layout :debug-panel)
|
|
:aria-label (tool-label :debug)
|
|
:icon i/bug
|
|
:on-click on-toggle-debug-panel}]])
|
|
|
|
(when mcp-show?
|
|
[:li {:class (stl/css :toolbar-option)}
|
|
[:> mcp-tool* {:is-mcp-connected mcp-connected?}]])]
|
|
|
|
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
|
|
:aria-label (tr "workspace.toolbar.toggle-toolbar")
|
|
:class (stl/css :toolbar-handler)
|
|
:on-click on-toggle-toolbar}
|
|
[:div {:class (stl/css :toolbar-handler-indicator)}]]])))
|