penpot/frontend/src/app/plugins/events.cljs
Dr. Dominik Jain af00860c13
🐛 Report the effective UI theme to plugins (#10677)
Penpot itself and plugins used different default theme selection
strategies when no theme is explicitly selected in Penpot by the user:

- Penpot itself defaults to dark.
- Plugins defaulted to whatever is configured in the user's
  system/browser, which could be light.

So with no explicit theme chosen in the profile — the common state —
every plugin could be told the theme was light while Penpot itself
showed dark, both on open (penpot.theme) and on themechange. For every
explicit choice (light, dark, system, and the legacy default) the two
resolutions already agreed; only the unset case diverged.

Extract the app's resolution into app.util.theme/resolve-theme (system
follows the system theme; default and unset mean dark) as the single
source of truth, and use it in the app's own use-initialize as well as
in the plugin runtime's getTheme and themechange handling. The
themechange handler's now-dead default-to-dark remapping is removed.

Fixes #10676

AI-assisted-by: claude-fable-5
2026-07-28 11:09:17 +02:00

124 lines
4.0 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.plugins.events
(:require
[app.common.data.macros :as dm]
[app.main.data.helpers :as dsh]
[app.main.store :as st]
[app.plugins.file :as file]
[app.plugins.page :as page]
[app.plugins.parser :as parser]
[app.plugins.shape :as shape]
[app.util.object :as obj]
[app.util.theme :as theme]
[goog.functions :as gf]))
(defmulti handle-state-change (fn [type _] type))
(defmethod handle-state-change "finish"
[_ _ old-val new-val _]
(let [old-file-id (:current-file-id old-val)
new-file-id (:current-file-id new-val)]
(if (and (some? old-file-id) (nil? new-file-id))
(str old-file-id)
::not-changed)))
(defmethod handle-state-change "filechange"
[_ plugin-id old-val new-val _]
(let [old-file-id (:current-file-id old-val)
new-file-id (:current-file-id new-val)]
(if (identical? old-file-id new-file-id)
::not-changed
(file/file-proxy plugin-id new-file-id))))
(defmethod handle-state-change "pagechange"
[_ plugin-id old-val new-val _]
(let [old-page-id (:current-page-id old-val)
new-page-id (:current-page-id new-val)]
(if (identical? old-page-id new-page-id)
::not-changed
(page/page-proxy plugin-id (:current-file-id new-val) new-page-id))))
(defmethod handle-state-change "selectionchange"
[_ _ old-val new-val _]
(let [old-selection (get-in old-val [:workspace-local :selected])
new-selection (get-in new-val [:workspace-local :selected])]
(if (identical? old-selection new-selection)
::not-changed
(apply array (map str new-selection)))))
(defn- get-theme
[state]
(theme/resolve-theme (get-in state [:profile :theme])
(theme/get-system-theme)))
(defmethod handle-state-change "themechange"
[_ _ old-val new-val _]
(let [old-theme (get-theme old-val)
new-theme (get-theme new-val)]
(if (identical? old-theme new-theme)
::not-changed
new-theme)))
(defmethod handle-state-change "shapechange"
[_ plugin-id old-val new-val props]
(if-let [shape-id (-> (obj/get props "shapeId") parser/parse-id)]
(let [old-shape (dsh/lookup-shape old-val shape-id)
new-shape (dsh/lookup-shape new-val shape-id)
file-id (:current-file-id new-val)
page-id (:current-page-id new-val)]
(if (and (identical? old-shape new-shape) (some? plugin-id) (some? file-id) (some? page-id) (some? shape-id))
::not-changed
(shape/shape-proxy plugin-id file-id page-id shape-id)))
::not-changed))
(defmethod handle-state-change "contentsave"
[_ _ old-val new-val _]
(let [old-status (dm/get-in old-val [:persistence :status])
new-status (dm/get-in new-val [:persistence :status])]
(if (and (= :saved new-status) (not= new-status old-status))
::void ;; Changed but void
::not-changed)))
(defmethod handle-state-change :default
[_ _ _ _]
::not-changed)
(defn add-listener
[type plugin-id callback props]
(let [plugin-id (parser/parse-id plugin-id)
key (js/Symbol)
;; We wrap the callback in an exception handler so the plugins
;; don't crash the application
safe-callback
(fn [value]
(try
(if (= ::void value)
(callback)
(callback value))
(catch :default cause
(.error js/console cause))))
;; We also debounce the callbacks so we don't get too many at the same time
debounced-callback (gf/debounce safe-callback 10)]
(add-watch
st/state key
(fn [_ _ old-val new-val]
(let [result (handle-state-change type plugin-id old-val new-val props)]
(when (not= ::not-changed result)
(debounced-callback result)))))
;; return the generated key
key))
(defn remove-listener
[key]
(remove-watch st/state key))