Line data Source code
1 : /* Copyright (C) 2023 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : // This file is included directly into actual implementation files.
19 :
20 : #include "JSInterface_GUIProxy.h"
21 :
22 : #include "gui/CGUI.h"
23 : #include "gui/CGUISetting.h"
24 : #include "gui/ObjectBases/IGUIObject.h"
25 : #include "ps/CLogger.h"
26 : #include "scriptinterface/FunctionWrapper.h"
27 : #include "scriptinterface/ScriptExtraHeaders.h"
28 : #include "scriptinterface/ScriptRequest.h"
29 :
30 : #include <string>
31 : #include <string_view>
32 :
33 : template <typename T>
34 38 : JSI_GUIProxy<T>& JSI_GUIProxy<T>::Singleton()
35 : {
36 38 : static JSI_GUIProxy<T> s;
37 38 : return s;
38 : }
39 :
40 : // Call this for every specialised type. You will need to override IGUIObject::CreateJSObject() in your class interface.
41 : #define DECLARE_GUIPROXY(Type) \
42 : void Type::CreateJSObject() \
43 : { \
44 : ScriptRequest rq(m_pGUI.GetScriptInterface()); \
45 : using ProxyHandler = JSI_GUIProxy<std::remove_pointer_t<decltype(this)>>; \
46 : m_JSObject = ProxyHandler::CreateJSObject(rq, this, GetGUI().GetProxyData(&ProxyHandler::Singleton())); \
47 : } \
48 : template class JSI_GUIProxy<Type>;
49 :
50 : // Use a common namespace to avoid duplicating the symbols un-necessarily.
51 : namespace JSInterface_GUIProxy
52 : {
53 : // All proxy objects share a class definition.
54 13 : JSClass& ClassDefinition()
55 : {
56 : static JSClass c = PROXY_CLASS_DEF("GUIObjectProxy", JSCLASS_HAS_CACHED_PROTO(JSProto_Proxy) | JSCLASS_HAS_RESERVED_SLOTS(1));
57 13 : return c;
58 : }
59 :
60 : // Default implementation of the cache via unordered_map
61 30 : class MapCache : public GUIProxyProps
62 : {
63 : public:
64 60 : virtual ~MapCache() {};
65 :
66 0 : virtual bool has(const std::string& name) const override
67 : {
68 0 : return m_Functions.find(name) != m_Functions.end();
69 : }
70 :
71 0 : virtual JSObject* get(const std::string& name) const override
72 : {
73 0 : return m_Functions.at(name).get();
74 : }
75 :
76 174 : virtual bool setFunction(const ScriptRequest& rq, const std::string& name, JSFunction* function) override
77 : {
78 174 : m_Functions[name].init(rq.cx, JS_GetFunctionObject(function));
79 174 : return true;
80 : }
81 :
82 : protected:
83 : std::unordered_map<std::string, JS::PersistentRootedObject> m_Functions;
84 : };
85 : }
86 :
87 : template<>
88 9 : IGUIObject* IGUIProxyObject::FromPrivateSlot<IGUIObject>(JSObject* obj)
89 : {
90 9 : if (!obj)
91 0 : return nullptr;
92 9 : if (JS::GetClass(obj) != &JSInterface_GUIProxy::ClassDefinition())
93 0 : return nullptr;
94 9 : return UnsafeFromPrivateSlot<IGUIObject>(obj);
95 : }
96 :
97 : // The default propcache is a MapCache.
98 : template<typename T>
99 : struct JSI_GUIProxy<T>::PropCache
100 : {
101 : using type = JSInterface_GUIProxy::MapCache;
102 : };
103 :
104 : template <typename T>
105 0 : T* JSI_GUIProxy<T>::FromPrivateSlot(const ScriptRequest&, JS::CallArgs& args)
106 : {
107 : // Call the unsafe version - this is only ever called from actual proxy objects.
108 0 : return IGUIProxyObject::UnsafeFromPrivateSlot<T>(args.thisv().toObjectOrNull());
109 : }
110 :
111 : template <typename T>
112 0 : bool JSI_GUIProxy<T>::PropGetter(JS::HandleObject proxy, const std::string& propName, JS::MutableHandleValue vp) const
113 : {
114 : using PropertyCache = typename PropCache::type;
115 : // Since we know at compile time what the type actually is, avoid the virtual call.
116 0 : const PropertyCache* data = static_cast<const PropertyCache*>(static_cast<const GUIProxyProps*>(js::GetProxyReservedSlot(proxy, 0).toPrivate()));
117 0 : if (data->has(propName))
118 : {
119 0 : vp.setObjectOrNull(data->get(propName));
120 0 : return true;
121 : }
122 0 : return false;
123 : }
124 :
125 : template <typename T>
126 30 : std::pair<const js::BaseProxyHandler*, GUIProxyProps*> JSI_GUIProxy<T>::CreateData(ScriptInterface& scriptInterface)
127 : {
128 : using PropertyCache = typename PropCache::type;
129 30 : PropertyCache* data = new PropertyCache();
130 60 : ScriptRequest rq(scriptInterface);
131 :
132 : // Functions common to all children of IGUIObject.
133 30 : JSI_GUIProxy<IGUIObject>::CreateFunctions(rq, data);
134 :
135 : // Let derived classes register their own interface.
136 : if constexpr (!std::is_same_v<T, IGUIObject>)
137 24 : CreateFunctions(rq, data);
138 60 : return { &Singleton(), data };
139 : }
140 :
141 : template<typename T>
142 : template<auto callable>
143 174 : void JSI_GUIProxy<T>::CreateFunction(const ScriptRequest& rq, GUIProxyProps* cache, const std::string& name)
144 : {
145 174 : cache->setFunction(rq, name, ScriptFunction::Create<callable, FromPrivateSlot>(rq, name.c_str()));
146 174 : }
147 :
148 : template<typename T>
149 4 : std::unique_ptr<IGUIProxyObject> JSI_GUIProxy<T>::CreateJSObject(const ScriptRequest& rq, T* ptr, GUIProxyProps* dataPtr)
150 : {
151 4 : js::ProxyOptions options;
152 4 : options.setClass(&JSInterface_GUIProxy::ClassDefinition());
153 :
154 4 : auto ret = std::make_unique<IGUIProxyObject>();
155 4 : ret->m_Ptr = static_cast<IGUIObject*>(ptr);
156 :
157 8 : JS::RootedValue cppObj(rq.cx), data(rq.cx);
158 4 : cppObj.get().setPrivate(ret->m_Ptr);
159 4 : data.get().setPrivate(static_cast<void*>(dataPtr));
160 4 : ret->m_Object.init(rq.cx, js::NewProxyObject(rq.cx, &Singleton(), cppObj, nullptr, options));
161 4 : js::SetProxyReservedSlot(ret->m_Object, 0, data);
162 8 : return ret;
163 : }
164 :
165 : template <typename T>
166 0 : bool JSI_GUIProxy<T>::get(JSContext* cx, JS::HandleObject proxy, JS::HandleValue UNUSED(receiver), JS::HandleId id, JS::MutableHandleValue vp) const
167 : {
168 0 : ScriptRequest rq(cx);
169 :
170 0 : T* e = IGUIProxyObject::FromPrivateSlot<T>(proxy.get());
171 0 : if (!e)
172 0 : return false;
173 :
174 0 : JS::RootedValue idval(rq.cx);
175 0 : if (!JS_IdToValue(rq.cx, id, &idval))
176 0 : return false;
177 :
178 0 : std::string propName;
179 0 : if (!Script::FromJSVal(rq, idval, propName))
180 0 : return false;
181 :
182 : // Return function properties. Specializable.
183 0 : if (PropGetter(proxy, propName, vp))
184 0 : return true;
185 :
186 : // Use onWhatever to access event handlers
187 0 : if (propName.substr(0, 2) == "on")
188 : {
189 0 : CStr eventName(propName.substr(2));
190 0 : std::map<CStr, JS::Heap<JSObject*>>::iterator it = e->m_ScriptHandlers.find(eventName);
191 0 : if (it == e->m_ScriptHandlers.end())
192 0 : vp.setNull();
193 : else
194 0 : vp.setObject(*it->second.get());
195 0 : return true;
196 : }
197 :
198 0 : if (propName == "parent")
199 : {
200 0 : IGUIObject* parent = e->GetParent();
201 :
202 0 : if (parent)
203 0 : vp.set(JS::ObjectValue(*parent->GetJSObject()));
204 : else
205 0 : vp.set(JS::NullValue());
206 :
207 0 : return true;
208 : }
209 0 : else if (propName == "children")
210 : {
211 0 : Script::CreateArray(rq, vp);
212 :
213 0 : for (size_t i = 0; i < e->m_Children.size(); ++i)
214 0 : Script::SetPropertyInt(rq, vp, i, e->m_Children[i]);
215 :
216 0 : return true;
217 : }
218 0 : else if (propName == "name")
219 : {
220 0 : Script::ToJSVal(rq, vp, e->GetName());
221 0 : return true;
222 : }
223 0 : else if (e->SettingExists(propName))
224 : {
225 0 : e->m_Settings[propName]->ToJSVal(rq, vp);
226 0 : return true;
227 : }
228 :
229 0 : LOGERROR("Property '%s' does not exist!", propName.c_str());
230 0 : return false;
231 : }
232 :
233 :
234 : template <typename T>
235 5 : bool JSI_GUIProxy<T>::set(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::HandleValue vp,
236 : JS::HandleValue UNUSED(receiver), JS::ObjectOpResult& result) const
237 : {
238 5 : T* e = IGUIProxyObject::FromPrivateSlot<T>(proxy.get());
239 5 : if (!e)
240 : {
241 0 : LOGERROR("C++ GUI Object could not be found");
242 0 : return result.fail(JSMSG_OBJECT_REQUIRED);
243 : }
244 :
245 10 : ScriptRequest rq(cx);
246 :
247 10 : JS::RootedValue idval(rq.cx);
248 5 : if (!JS_IdToValue(rq.cx, id, &idval))
249 0 : return result.fail(JSMSG_BAD_PROP_ID);
250 :
251 10 : std::string propName;
252 5 : if (!Script::FromJSVal(rq, idval, propName))
253 0 : return result.fail(JSMSG_BAD_PROP_ID);
254 :
255 5 : if (propName == "name")
256 : {
257 0 : std::string value;
258 0 : if (!Script::FromJSVal(rq, vp, value))
259 0 : return result.fail(JSMSG_BAD_PROP_ID);
260 0 : e->SetName(value);
261 0 : return result.succeed();
262 : }
263 :
264 10 : JS::RootedObject vpObj(cx);
265 5 : if (vp.isObject())
266 5 : vpObj = &vp.toObject();
267 :
268 : // Use onWhatever to set event handlers
269 5 : if (propName.substr(0, 2) == "on")
270 : {
271 5 : if (vp.isPrimitive() || vp.isNull() || !JS_ObjectIsFunction(&vp.toObject()))
272 : {
273 0 : LOGERROR("on- event-handlers must be functions");
274 0 : return result.fail(JSMSG_NOT_FUNCTION);
275 : }
276 :
277 10 : CStr eventName(propName.substr(2));
278 5 : e->SetScriptHandler(eventName, vpObj);
279 :
280 5 : return result.succeed();
281 : }
282 :
283 0 : if (e->SettingExists(propName))
284 0 : return e->m_Settings[propName]->FromJSVal(rq, vp, true) ? result.succeed() : result.fail(JSMSG_USER_DEFINED_ERROR);
285 :
286 0 : LOGERROR("Property '%s' does not exist!", propName.c_str());
287 0 : return result.fail(JSMSG_BAD_PROP_ID);
288 : }
289 :
290 : template<typename T>
291 4 : bool JSI_GUIProxy<T>::delete_(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, JS::ObjectOpResult& result) const
292 : {
293 4 : T* e = IGUIProxyObject::FromPrivateSlot<T>(proxy.get());
294 4 : if (!e)
295 : {
296 0 : LOGERROR("C++ GUI Object could not be found");
297 0 : return result.fail(JSMSG_OBJECT_REQUIRED);
298 : }
299 :
300 8 : ScriptRequest rq(cx);
301 :
302 8 : JS::RootedValue idval(rq.cx);
303 4 : if (!JS_IdToValue(rq.cx, id, &idval))
304 0 : return result.fail(JSMSG_BAD_PROP_ID);
305 :
306 8 : std::string propName;
307 4 : if (!Script::FromJSVal(rq, idval, propName))
308 0 : return result.fail(JSMSG_BAD_PROP_ID);
309 :
310 : // event handlers
311 4 : if (std::string_view{propName}.substr(0, 2) == "on")
312 : {
313 8 : CStr eventName(propName.substr(2));
314 4 : e->UnsetScriptHandler(eventName);
315 4 : return result.succeed();
316 : }
317 :
318 0 : LOGERROR("Only event handlers can be deleted from GUI objects!");
319 0 : return result.fail(JSMSG_BAD_PROP_ID);
320 : }
|