1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Console mode dialog for object properties and script interface */
18
19#include <C4PropertyDlg.h>
20
21#include <C4Console.h>
22#include <C4Application.h>
23#include <C4Object.h>
24#include <C4Wrappers.h>
25#include <C4Player.h>
26
27#include <format>
28
29#ifdef _WIN32
30#include "StdRegistry.h"
31#include "StdStringEncodingConverter.h"
32#include "res/engine_resource.h"
33#endif
34
35#ifdef WITH_DEVELOPER_MODE
36#include <C4DevmodeDlg.h>
37
38#include <gtk/gtk.h>
39#endif
40
41#ifdef _WIN32
42INT_PTR CALLBACK PropertyDlgProc(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
43{
44 switch (Msg)
45 {
46 case WM_CLOSE:
47 Console.PropertyDlg.Clear();
48 break;
49
50 case WM_DESTROY:
51 StoreWindowPosition(hDlg, "Property", Config.GetSubkeyPath("Console"), false);
52 break;
53
54 case WM_INITDIALOG:
55 SendMessage(hDlg, DM_SETDEFID, IDOK, 0);
56 return TRUE;
57
58 case WM_COMMAND:
59 // Evaluate command
60 switch (LOWORD(wParam))
61 {
62 case IDOK:
63 {
64 // IDC_COMBOINPUT to Console.EditCursor.In()
65 const std::wstring text{C4Console::GetDialogItemText(hDlg, IDC_COMBOINPUT)};
66 if (!text.empty())
67 {
68 Console.EditCursor.In(StdStringEncodingConverter::Utf16ToWinAcp(text).c_str());
69 }
70
71 return TRUE;
72 }
73
74 case IDC_BUTTONRELOADDEF:
75 Game.ReloadDef(Console.PropertyDlg.idSelectedDef);
76 return TRUE;
77 }
78 return FALSE;
79 }
80 return FALSE;
81}
82#endif
83
84C4PropertyDlg::C4PropertyDlg()
85{
86 Default();
87}
88
89C4PropertyDlg::~C4PropertyDlg()
90{
91 Clear();
92
93#ifdef WITH_DEVELOPER_MODE
94 if (vbox != nullptr)
95 {
96 g_signal_handler_disconnect(G_OBJECT(C4DevmodeDlg::GetWindow()), handler_id: handlerHide);
97 C4DevmodeDlg::RemovePage(widget: vbox);
98 vbox = nullptr;
99 }
100#endif // WITH_DEVELOPER_MODE
101}
102
103bool C4PropertyDlg::Open()
104{
105#ifdef _WIN32
106 if (hDialog) return true;
107 hDialog = CreateDialog(Application.hInstance,
108 MAKEINTRESOURCE(IDD_PROPERTIES),
109 Console.hWindow,
110 PropertyDlgProc);
111 if (!hDialog) return false;
112 // Set text
113 SetWindowText(hDialog, StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_DLG_PROPERTIES)).c_str());
114 // Enable controls
115 EnableWindow(GetDlgItem(hDialog, IDOK), Console.Editing);
116 EnableWindow(GetDlgItem(hDialog, IDC_COMBOINPUT), Console.Editing);
117 EnableWindow(GetDlgItem(hDialog, IDC_BUTTONRELOADDEF), Console.Editing);
118 // Show window
119 RestoreWindowPosition(hDialog, "Property", Config.GetSubkeyPath("Console"));
120 SetWindowPos(hDialog, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
121 ShowWindow(hDialog, SW_SHOWNORMAL | SW_SHOWNA);
122#else // _WIN32
123#ifdef WITH_DEVELOPER_MODE
124 if (vbox == nullptr)
125 {
126 vbox = gtk_box_new(orientation: GTK_ORIENTATION_VERTICAL, spacing: 6);
127
128 GtkWidget *scrolled_wnd = gtk_scrolled_window_new(hadjustment: nullptr, vadjustment: nullptr);
129 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_wnd), hscrollbar_policy: GTK_POLICY_AUTOMATIC, vscrollbar_policy: GTK_POLICY_AUTOMATIC);
130 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled_wnd), type: GTK_SHADOW_IN);
131
132 textview = gtk_text_view_new();
133 entry = gtk_entry_new();
134
135 gtk_container_add(GTK_CONTAINER(scrolled_wnd), widget: textview);
136 gtk_box_pack_start(GTK_BOX(vbox), child: scrolled_wnd, TRUE, TRUE, padding: 0);
137 gtk_box_pack_start(GTK_BOX(vbox), child: entry, FALSE, FALSE, padding: 0);
138
139 gtk_text_view_set_editable(GTK_TEXT_VIEW(textview), FALSE);
140 gtk_widget_set_sensitive(widget: entry, sensitive: Console.Editing);
141
142 gtk_widget_show_all(widget: vbox);
143
144 C4DevmodeDlg::AddPage(widget: vbox, GTK_WINDOW(Console.window), title: LoadResStrGtk(id: C4ResStrTableKey::IDS_DLG_PROPERTIES).c_str());
145
146 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(OnScriptActivate), this);
147
148 handlerHide = g_signal_connect(G_OBJECT(C4DevmodeDlg::GetWindow()), "hide", G_CALLBACK(OnWindowHide), this);
149 }
150
151 C4DevmodeDlg::SwitchPage(widget: vbox);
152#endif // WITH_DEVELOPER_MODE
153#endif // _WIN32
154 Active = true;
155 return true;
156}
157
158bool C4PropertyDlg::Update(C4ObjectList &rSelection)
159{
160 if (!Active) return false;
161 // Set new selection
162 Selection.Copy(rList: rSelection);
163 // Update input control
164 UpdateInputCtrl(pObj: Selection.GetObject());
165 // Update contents
166 return Update();
167}
168
169bool C4PropertyDlg::Update()
170{
171 if (!Active) return false;
172
173 std::string output;
174
175 idSelectedDef = C4ID_None;
176
177 // Compose info text by selected object(s)
178 switch (Selection.ObjectCount())
179 {
180 // No selection
181 case 0:
182 output = LoadResStr(id: C4ResStrTableKey::IDS_CNS_NOOBJECT);
183 break;
184 // One selected object
185 case 1:
186 {
187 C4Object *cobj = Selection.GetObject();
188 // Type
189 output = LoadResStr(id: C4ResStrTableKey::IDS_CNS_TYPE, args: cobj->GetName(), args: C4IdText(id: cobj->Def->id));
190 // Owner
191 if (ValidPlr(plr: cobj->Owner))
192 {
193 output += LineFeed;
194 output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_OWNER, args: Game.Players.Get(iPlayer: cobj->Owner)->GetName());
195 }
196 // Contents
197 if (cobj->Contents.ObjectCount())
198 {
199 output += LineFeed;
200 output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_CONTENTS);
201 output += cobj->Contents.GetNameList(rDefs&: Game.Defs);
202 }
203 // Action
204 if (cobj->Action.Act != ActIdle)
205 {
206 output += LineFeed;
207 output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_ACTION);
208 output += cobj->Def->ActMap[cobj->Action.Act].Name;
209 }
210 // Locals
211 int cnt; bool fFirstLocal = true;
212 for (cnt = 0; cnt < cobj->Local.GetSize(); cnt++)
213 if (cobj->Local[cnt])
214 {
215 // Header
216 if (fFirstLocal) { output += LineFeed; output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_LOCALS); fFirstLocal = false; }
217 output += LineFeed;
218 // Append id
219 output += std::format(fmt: " Local({}) = ", args&: cnt);
220 // write value
221 output += cobj->Local[cnt].GetDataString();
222 }
223 // Locals (named)
224 for (cnt = 0; cnt < cobj->LocalNamed.GetAnzItems(); cnt++)
225 {
226 // Header
227 if (fFirstLocal) { output += LineFeed; output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_LOCALS); fFirstLocal = false; }
228 output += LineFeed " ";
229 // Append name
230 output += cobj->LocalNamed.pNames->pNames[cnt];
231 output += " = ";
232 // write value
233 output += cobj->LocalNamed.pData[cnt].GetDataString();
234 }
235 // Effects
236 for (C4Effect *pEffect = cobj->pEffects; pEffect; pEffect = pEffect->pNext)
237 {
238 // Header
239 if (pEffect == cobj->pEffects)
240 {
241 output += LineFeed;
242 output += LoadResStr(id: C4ResStrTableKey::IDS_CNS_EFFECTS);
243 }
244 output += LineFeed;
245 // Effect name
246 output += std::format(fmt: " {}: Interval {}", args: +pEffect->Name, args&: pEffect->iIntervall);
247 }
248 // Store selected def
249 idSelectedDef = cobj->id;
250 break;
251 }
252 // Multiple selected objects
253 default:
254 output = LoadResStr(id: C4ResStrTableKey::IDS_CNS_MULTIPLEOBJECTS, args: Selection.ObjectCount());
255 break;
256 }
257 // Update info edit control
258#ifdef _WIN32
259 const auto iLine = SendDlgItemMessage(hDialog, IDC_EDITOUTPUT, EM_GETFIRSTVISIBLELINE, 0, 0);
260 SetDlgItemText(hDialog, IDC_EDITOUTPUT, StdStringEncodingConverter::WinAcpToUtf16(output).c_str());
261 SendDlgItemMessage(hDialog, IDC_EDITOUTPUT, EM_LINESCROLL, 0, iLine);
262 UpdateWindow(GetDlgItem(hDialog, IDC_EDITOUTPUT));
263#elif defined(WITH_DEVELOPER_MODE)
264 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
265 gtk_text_buffer_set_text(buffer, text: C4Console::ClonkToGtk(text: output).c_str(), len: -1);
266#endif
267 return true;
268}
269
270void C4PropertyDlg::Default()
271{
272#ifdef _WIN32
273 hDialog = nullptr;
274#elif defined(WITH_DEVELOPER_MODE)
275 vbox = nullptr;
276#endif
277 Active = false;
278 idSelectedDef = C4ID_None;
279 Selection.Default();
280}
281
282void C4PropertyDlg::Clear()
283{
284 Selection.Clear();
285#ifdef _WIN32
286 if (hDialog) DestroyWindow(hDialog); hDialog = nullptr;
287#endif
288 Active = false;
289}
290
291void C4PropertyDlg::UpdateInputCtrl(C4Object *pObj)
292{
293 int cnt;
294#ifdef _WIN32
295 HWND hCombo = GetDlgItem(hDialog, IDC_COMBOINPUT);
296 // Remember old window text
297 std::wstring lastText;
298 const LRESULT textSize{SendMessage(hCombo, WM_GETTEXTLENGTH, 0, 0)};
299
300 lastText.resize_and_overwrite(textSize, [hCombo, textSize](wchar_t *const ptr, const std::size_t size)
301 {
302 return GetWindowText(hCombo, ptr, textSize + 1);
303 });
304
305 // Clear
306 SendMessage(hCombo, CB_RESETCONTENT, 0, 0);
307#else // _WIN32
308#ifdef WITH_DEVELOPER_MODE
309
310 GtkEntryCompletion *completion = gtk_entry_get_completion(GTK_ENTRY(entry));
311 GtkListStore *store;
312
313 // Uncouple list store from completion so that the completion is not
314 // notified for every row we are going to insert. This enhances
315 // performance significantly.
316 if (!completion)
317 {
318 completion = gtk_entry_completion_new();
319 store = gtk_list_store_new(n_columns: 1, G_TYPE_STRING);
320
321 gtk_entry_completion_set_text_column(completion, column: 0);
322 gtk_entry_set_completion(GTK_ENTRY(entry), completion);
323 g_object_unref(G_OBJECT(completion));
324 }
325 else
326 {
327 store = GTK_LIST_STORE(gtk_entry_completion_get_model(completion));
328 g_object_ref(G_OBJECT(store));
329 gtk_entry_completion_set_model(completion, model: nullptr);
330 }
331
332 GtkTreeIter iter;
333 gtk_list_store_clear(list_store: store);
334#endif // WITH_DEVELOPER_MODE
335#endif // _WIN32
336
337 // add global and standard functions
338 for (C4AulFunc *pFn = Game.ScriptEngine.GetFirstFunc(); pFn; pFn = Game.ScriptEngine.GetNextFunc(pFunc: pFn))
339 if (pFn->GetPublic())
340 {
341#ifdef _WIN32
342 SendMessage(hCombo, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(std::format(L"{}()", StdStringEncodingConverter::WinAcpToUtf16(pFn->Name)).c_str()));
343#elif defined(WITH_DEVELOPER_MODE)
344 gtk_list_store_append(list_store: store, iter: &iter);
345 gtk_list_store_set(list_store: store, iter: &iter, 0, pFn->Name, -1);
346#endif
347 }
348 // Add object script functions
349#ifdef _WIN32
350 bool fDivider = false;
351#endif
352 C4AulScriptFunc *pRef;
353 // Object script available
354 if (pObj && pObj->Def)
355 // Scan all functions
356 for (cnt = 0; pRef = pObj->Def->Script.GetSFunc(iIndex: cnt); cnt++)
357 // Public functions only
358 if (pRef->Access == AA_PUBLIC)
359 {
360#ifdef _WIN32
361 // Insert divider if necessary
362 if (!fDivider) { SendMessage(hCombo, CB_INSERTSTRING, 0, reinterpret_cast<LPARAM>(L"----------")); fDivider = true; }
363#endif
364 // Add function
365#ifdef _WIN32
366 SendMessage(hCombo, CB_INSERTSTRING, 0, reinterpret_cast<LPARAM>(std::format(L"{}()", StdStringEncodingConverter::WinAcpToUtf16(pRef->Name)).c_str()));
367#elif defined(WITH_DEVELOPER_MODE)
368 gtk_list_store_append(list_store: store, iter: &iter);
369 gtk_list_store_set(list_store: store, iter: &iter, 0, pRef->Name, -1);
370#endif
371 }
372
373#ifdef _WIN32
374 // Restore old text
375 SetWindowText(hCombo, lastText.c_str());
376#elif WITH_DEVELOPER_MODE
377 // Reassociate list store with completion
378 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(store));
379#endif
380}
381
382void C4PropertyDlg::Execute()
383{
384 if (!Tick35) Update();
385}
386
387void C4PropertyDlg::ClearPointers(C4Object *pObj)
388{
389 Selection.ClearPointers(pObj);
390}
391
392#ifdef WITH_DEVELOPER_MODE
393// GTK+ callbacks
394void C4PropertyDlg::OnScriptActivate(GtkWidget *widget, gpointer data)
395{
396 const gchar *text = gtk_entry_get_text(GTK_ENTRY(widget));
397 if (text && text[0])
398 Console.EditCursor.In(szText: text);
399}
400
401void C4PropertyDlg::OnWindowHide(GtkWidget *widget, gpointer user_data)
402{
403 static_cast<C4PropertyDlg *>(user_data)->Active = false;
404}
405
406#endif
407