basalt-gtk 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
// Linux's half of the platform-services seam: clipboard, opening a URI, alerts.
|
|
2
|
+
|
|
3
|
+
#include "PlatformServices.h"
|
|
4
|
+
#include "ShareFallback.h"
|
|
5
|
+
|
|
6
|
+
#include <gtk/gtk.h>
|
|
7
|
+
|
|
8
|
+
#include <cstdlib>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <utility>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
namespace basalt {
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
GdkClipboard *defaultClipboard() {
|
|
19
|
+
GdkDisplay *display = gdk_display_get_default();
|
|
20
|
+
return display != nullptr ? gdk_display_get_clipboard(display) : nullptr;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
} // namespace
|
|
24
|
+
|
|
25
|
+
// --- Clipboard ---------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
// GdkClipboard reads asynchronously -- it may have to ask another process for
|
|
28
|
+
// the text -- and this seam is synchronous because `Clipboard.getString()`
|
|
29
|
+
// returns a promise that core resolves at once.
|
|
30
|
+
//
|
|
31
|
+
// The compromise is the content provider's *local* value, which is what this
|
|
32
|
+
// application last put there, plus nothing. Reading another application's
|
|
33
|
+
// clipboard needs the async read and a promise core does not yet hand down.
|
|
34
|
+
// Saying so here beats an empty string that looks like an empty clipboard.
|
|
35
|
+
std::string clipboardText() {
|
|
36
|
+
GdkClipboard *clipboard = defaultClipboard();
|
|
37
|
+
if (clipboard == nullptr) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
GdkContentProvider *provider = gdk_clipboard_get_content(clipboard);
|
|
42
|
+
if (provider == nullptr) {
|
|
43
|
+
// Owned by another application. See the note above.
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
GValue value = G_VALUE_INIT;
|
|
48
|
+
g_value_init(&value, G_TYPE_STRING);
|
|
49
|
+
std::string text;
|
|
50
|
+
if (gdk_content_provider_get_value(provider, &value, nullptr)) {
|
|
51
|
+
const char *string = g_value_get_string(&value);
|
|
52
|
+
if (string != nullptr) {
|
|
53
|
+
text = string;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
g_value_unset(&value);
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void setClipboardText(const std::string &text) {
|
|
61
|
+
GdkClipboard *clipboard = defaultClipboard();
|
|
62
|
+
if (clipboard == nullptr) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
gdk_clipboard_set_text(clipboard, text.c_str());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Opening things ----------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
bool canOpenUrl(const std::string &url) {
|
|
71
|
+
if (url.empty()) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
char *scheme = g_uri_parse_scheme(url.c_str());
|
|
75
|
+
if (scheme == nullptr) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// Whether anything is registered to handle the scheme. The desktop's own
|
|
79
|
+
// answer, from the same database that would do the opening.
|
|
80
|
+
GAppInfo *handler = g_app_info_get_default_for_uri_scheme(scheme);
|
|
81
|
+
g_free(scheme);
|
|
82
|
+
if (handler == nullptr) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
g_object_unref(handler);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
bool openUrl(const std::string &url) {
|
|
90
|
+
GError *error = nullptr;
|
|
91
|
+
const gboolean launched =
|
|
92
|
+
g_app_info_launch_default_for_uri(url.c_str(), nullptr, &error);
|
|
93
|
+
if (launched == FALSE) {
|
|
94
|
+
if (error != nullptr) {
|
|
95
|
+
g_warning("could not open %s: %s", url.c_str(), error->message);
|
|
96
|
+
g_clear_error(&error);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Sharing -----------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
// No share service on this desktop, and no portal for one either: the
|
|
106
|
+
// freedesktop specifications have `xdg-email` and nothing that offers a choice
|
|
107
|
+
// of destinations. So this is the picker core/ShareFallback.h builds out of a
|
|
108
|
+
// clipboard and a mail client, which is what every desktop does have.
|
|
109
|
+
//
|
|
110
|
+
// Nothing here beyond the call, because there is nothing platform-specific
|
|
111
|
+
// left in it -- it goes through showAlert, setClipboardText and openUrl, all of
|
|
112
|
+
// which are implemented above.
|
|
113
|
+
void shareContent(const ShareRequest &request, ShareCallback onDone) {
|
|
114
|
+
shareThroughFallbackPicker(request, std::move(onDone));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Alerts ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
namespace {
|
|
120
|
+
|
|
121
|
+
struct PendingAlert {
|
|
122
|
+
AlertRequest request;
|
|
123
|
+
AlertCallback onButton;
|
|
124
|
+
GtkWidget *field{nullptr};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
void onAlertResponse(GObject *source, GAsyncResult *result, gpointer userData) {
|
|
128
|
+
std::unique_ptr<PendingAlert> pending{static_cast<PendingAlert *>(userData)};
|
|
129
|
+
|
|
130
|
+
GError *error = nullptr;
|
|
131
|
+
const int index =
|
|
132
|
+
gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), result, &error);
|
|
133
|
+
if (error != nullptr) {
|
|
134
|
+
// Dismissed without choosing -- Escape, or the window closed. React Native
|
|
135
|
+
// has no "cancelled" here, and reporting the last button would run whatever
|
|
136
|
+
// it does; index 0 is the primary, which is what a plain alert's only
|
|
137
|
+
// button is.
|
|
138
|
+
g_clear_error(&error);
|
|
139
|
+
pending->onButton(0, {});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
pending->onButton(index, {});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
gboolean showAlertOnMainThread(gpointer userData) {
|
|
146
|
+
auto *pending = static_cast<PendingAlert *>(userData);
|
|
147
|
+
|
|
148
|
+
GtkAlertDialog *dialog = gtk_alert_dialog_new("%s", pending->request.title.c_str());
|
|
149
|
+
if (!pending->request.message.empty()) {
|
|
150
|
+
gtk_alert_dialog_set_detail(dialog, pending->request.message.c_str());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
std::vector<const char *> labels;
|
|
154
|
+
labels.reserve(pending->request.buttons.size() + 1);
|
|
155
|
+
for (const auto &label : pending->request.buttons) {
|
|
156
|
+
labels.push_back(label.c_str());
|
|
157
|
+
}
|
|
158
|
+
labels.push_back(nullptr);
|
|
159
|
+
gtk_alert_dialog_set_buttons(dialog, labels.data());
|
|
160
|
+
// The first is React Native's primary, and the same one onAlertResponse
|
|
161
|
+
// reports when the dialog is dismissed without a choice.
|
|
162
|
+
gtk_alert_dialog_set_default_button(dialog, 0);
|
|
163
|
+
gtk_alert_dialog_set_cancel_button(dialog, 0);
|
|
164
|
+
|
|
165
|
+
gtk_alert_dialog_choose(dialog, nullptr, nullptr, onAlertResponse, pending);
|
|
166
|
+
g_object_unref(dialog);
|
|
167
|
+
return G_SOURCE_REMOVE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} // namespace
|
|
171
|
+
|
|
172
|
+
void showAlert(const AlertRequest &request, AlertCallback onButton) {
|
|
173
|
+
if (request.hasTextInput) {
|
|
174
|
+
// GtkAlertDialog has no text field, and building one means a GtkDialog with
|
|
175
|
+
// its own layout. Not done: a prompt that silently became a plain alert
|
|
176
|
+
// would lose whatever the user was meant to type.
|
|
177
|
+
g_warning("Alert.prompt is not implemented on this platform");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
auto *pending = new PendingAlert{request, std::move(onButton), nullptr};
|
|
181
|
+
// Onto the GTK main thread. This is called from the JavaScript thread, and
|
|
182
|
+
// a dialog may only be created there.
|
|
183
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, showAlertOnMainThread, pending, nullptr);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- File dialogs -------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
namespace {
|
|
189
|
+
|
|
190
|
+
// The window a popup belongs to. GTK has no "the app's window": what it has is
|
|
191
|
+
// a list of toplevels, so this takes the active one, or the first visible one
|
|
192
|
+
// when nothing is focused -- which is what an automated run without a
|
|
193
|
+
// compositor looks like.
|
|
194
|
+
GtkWindow *activeWindow() {
|
|
195
|
+
GListModel *toplevels = gtk_window_get_toplevels();
|
|
196
|
+
if (toplevels == nullptr) {
|
|
197
|
+
return nullptr;
|
|
198
|
+
}
|
|
199
|
+
GtkWindow *fallback = nullptr;
|
|
200
|
+
const guint count = g_list_model_get_n_items(toplevels);
|
|
201
|
+
for (guint i = 0; i < count; i++) {
|
|
202
|
+
auto *window = static_cast<GtkWindow *>(g_list_model_get_item(toplevels, i));
|
|
203
|
+
if (window == nullptr) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const bool visible = gtk_widget_get_visible(GTK_WIDGET(window)) != FALSE;
|
|
207
|
+
if (visible && gtk_window_is_active(window)) {
|
|
208
|
+
g_object_unref(window);
|
|
209
|
+
return window;
|
|
210
|
+
}
|
|
211
|
+
if (visible && fallback == nullptr) {
|
|
212
|
+
fallback = window;
|
|
213
|
+
}
|
|
214
|
+
g_object_unref(window);
|
|
215
|
+
}
|
|
216
|
+
return fallback;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
struct PendingFileDialog {
|
|
220
|
+
FileDialogRequest request;
|
|
221
|
+
FileDialogCallback onDone;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// The filters, as a GListModel of GtkFileFilter. GTK takes them that way and
|
|
225
|
+
// not as a list of patterns, which is why this is not two lines.
|
|
226
|
+
GListModel *buildFilters(const std::vector<FileFilter> &filters) {
|
|
227
|
+
if (filters.empty()) {
|
|
228
|
+
return nullptr;
|
|
229
|
+
}
|
|
230
|
+
GListStore *store = g_list_store_new(GTK_TYPE_FILE_FILTER);
|
|
231
|
+
for (const FileFilter &filter : filters) {
|
|
232
|
+
GtkFileFilter *entry = gtk_file_filter_new();
|
|
233
|
+
gtk_file_filter_set_name(entry, filter.name.empty() ? nullptr : filter.name.c_str());
|
|
234
|
+
for (const std::string &extension : filter.extensions) {
|
|
235
|
+
// A glob rather than a MIME type: an extension is what the caller gave,
|
|
236
|
+
// and guessing a type from it would be a second guess on top of theirs.
|
|
237
|
+
const std::string pattern = "*." + extension;
|
|
238
|
+
gtk_file_filter_add_pattern(entry, pattern.c_str());
|
|
239
|
+
}
|
|
240
|
+
g_list_store_append(store, entry);
|
|
241
|
+
g_object_unref(entry);
|
|
242
|
+
}
|
|
243
|
+
return G_LIST_MODEL(store);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// One answer, whichever call produced it. GTK's three finishers return a GFile
|
|
247
|
+
// or a GListModel of them, and both arrive here as paths.
|
|
248
|
+
void answerWith(PendingFileDialog *pending, GFile *file, GListModel *files, GError *error) {
|
|
249
|
+
std::vector<std::string> paths;
|
|
250
|
+
if (file != nullptr) {
|
|
251
|
+
char *path = g_file_get_path(file);
|
|
252
|
+
if (path != nullptr) {
|
|
253
|
+
paths.emplace_back(path);
|
|
254
|
+
g_free(path);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (files != nullptr) {
|
|
258
|
+
const guint count = g_list_model_get_n_items(files);
|
|
259
|
+
for (guint i = 0; i < count; i++) {
|
|
260
|
+
auto *item = static_cast<GFile *>(g_list_model_get_item(files, i));
|
|
261
|
+
if (item == nullptr) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
char *path = g_file_get_path(item);
|
|
265
|
+
if (path != nullptr) {
|
|
266
|
+
paths.emplace_back(path);
|
|
267
|
+
g_free(path);
|
|
268
|
+
}
|
|
269
|
+
g_object_unref(item);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// An error here is a dismissal: GTK reports Cancel as
|
|
274
|
+
// GTK_DIALOG_ERROR_DISMISSED rather than as an empty answer. A real failure
|
|
275
|
+
// is reported the same way on purpose -- an app cannot do anything different
|
|
276
|
+
// with "the portal is not running" than with "the person said no", and
|
|
277
|
+
// rejecting the promise would make every caller write a catch for it.
|
|
278
|
+
const bool canceled = error != nullptr || paths.empty();
|
|
279
|
+
pending->onDone(canceled, paths);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void onOpenFinished(GObject *source, GAsyncResult *result, gpointer userData) {
|
|
283
|
+
std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
|
|
284
|
+
GError *error = nullptr;
|
|
285
|
+
GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), result, &error);
|
|
286
|
+
answerWith(pending.get(), file, nullptr, error);
|
|
287
|
+
g_clear_object(&file);
|
|
288
|
+
g_clear_error(&error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
void onOpenMultipleFinished(GObject *source, GAsyncResult *result, gpointer userData) {
|
|
292
|
+
std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
|
|
293
|
+
GError *error = nullptr;
|
|
294
|
+
GListModel *files = gtk_file_dialog_open_multiple_finish(GTK_FILE_DIALOG(source), result, &error);
|
|
295
|
+
answerWith(pending.get(), nullptr, files, error);
|
|
296
|
+
g_clear_object(&files);
|
|
297
|
+
g_clear_error(&error);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
void onSaveFinished(GObject *source, GAsyncResult *result, gpointer userData) {
|
|
301
|
+
std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
|
|
302
|
+
GError *error = nullptr;
|
|
303
|
+
GFile *file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), result, &error);
|
|
304
|
+
answerWith(pending.get(), file, nullptr, error);
|
|
305
|
+
g_clear_object(&file);
|
|
306
|
+
g_clear_error(&error);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
void onFolderFinished(GObject *source, GAsyncResult *result, gpointer userData) {
|
|
310
|
+
std::unique_ptr<PendingFileDialog> pending{static_cast<PendingFileDialog *>(userData)};
|
|
311
|
+
GError *error = nullptr;
|
|
312
|
+
GFile *folder = gtk_file_dialog_select_folder_finish(GTK_FILE_DIALOG(source), result, &error);
|
|
313
|
+
answerWith(pending.get(), folder, nullptr, error);
|
|
314
|
+
g_clear_object(&folder);
|
|
315
|
+
g_clear_error(&error);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
gboolean showFileDialogOnMainThread(gpointer userData) {
|
|
319
|
+
auto *pending = static_cast<PendingFileDialog *>(userData);
|
|
320
|
+
const FileDialogRequest &request = pending->request;
|
|
321
|
+
|
|
322
|
+
GtkFileDialog *dialog = gtk_file_dialog_new();
|
|
323
|
+
if (!request.title.empty()) {
|
|
324
|
+
gtk_file_dialog_set_title(dialog, request.title.c_str());
|
|
325
|
+
}
|
|
326
|
+
if (!request.confirmLabel.empty()) {
|
|
327
|
+
gtk_file_dialog_set_accept_label(dialog, request.confirmLabel.c_str());
|
|
328
|
+
}
|
|
329
|
+
if (GListModel *filters = buildFilters(request.filters)) {
|
|
330
|
+
gtk_file_dialog_set_filters(dialog, filters);
|
|
331
|
+
g_object_unref(filters);
|
|
332
|
+
}
|
|
333
|
+
if (!request.defaultPath.empty()) {
|
|
334
|
+
if (request.kind == FileDialogRequest::Kind::SaveFile) {
|
|
335
|
+
// A save takes a name and a folder separately, and the caller may have
|
|
336
|
+
// given either or both in one string.
|
|
337
|
+
GFile *file = g_file_new_for_path(request.defaultPath.c_str());
|
|
338
|
+
char *name = g_file_get_basename(file);
|
|
339
|
+
GFile *folder = g_file_get_parent(file);
|
|
340
|
+
if (name != nullptr) {
|
|
341
|
+
gtk_file_dialog_set_initial_name(dialog, name);
|
|
342
|
+
}
|
|
343
|
+
if (folder != nullptr) {
|
|
344
|
+
gtk_file_dialog_set_initial_folder(dialog, folder);
|
|
345
|
+
}
|
|
346
|
+
g_free(name);
|
|
347
|
+
g_clear_object(&folder);
|
|
348
|
+
g_object_unref(file);
|
|
349
|
+
} else {
|
|
350
|
+
GFile *folder = g_file_new_for_path(request.defaultPath.c_str());
|
|
351
|
+
gtk_file_dialog_set_initial_folder(dialog, folder);
|
|
352
|
+
g_object_unref(folder);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
GtkWindow *parent = activeWindow();
|
|
357
|
+
switch (request.kind) {
|
|
358
|
+
case FileDialogRequest::Kind::OpenFile:
|
|
359
|
+
if (request.multiple) {
|
|
360
|
+
gtk_file_dialog_open_multiple(dialog, parent, nullptr, onOpenMultipleFinished, pending);
|
|
361
|
+
} else {
|
|
362
|
+
gtk_file_dialog_open(dialog, parent, nullptr, onOpenFinished, pending);
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
case FileDialogRequest::Kind::SaveFile:
|
|
366
|
+
gtk_file_dialog_save(dialog, parent, nullptr, onSaveFinished, pending);
|
|
367
|
+
break;
|
|
368
|
+
case FileDialogRequest::Kind::OpenFolder:
|
|
369
|
+
gtk_file_dialog_select_folder(dialog, parent, nullptr, onFolderFinished, pending);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
g_object_unref(dialog);
|
|
373
|
+
return G_SOURCE_REMOVE;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
} // namespace
|
|
377
|
+
|
|
378
|
+
void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
|
|
379
|
+
auto *pending = new PendingFileDialog{request, std::move(onDone)};
|
|
380
|
+
// Onto the GTK main thread, for the same reason showAlert is: this is called
|
|
381
|
+
// from the JavaScript thread, and a dialog may only be made here.
|
|
382
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, showFileDialogOnMainThread, pending, nullptr);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- Menus -------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
namespace {
|
|
388
|
+
|
|
389
|
+
struct PendingMenu {
|
|
390
|
+
MenuRequest request;
|
|
391
|
+
MenuCallback onChosen;
|
|
392
|
+
GtkWidget *popover{nullptr};
|
|
393
|
+
bool answered{false};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// "Ctrl+R" into the "<Control>r" GTK wants for a displayed accelerator.
|
|
397
|
+
// Display only: nothing here binds the key. An unparseable string becomes
|
|
398
|
+
// empty, which shows no accelerator rather than a wrong one.
|
|
399
|
+
std::string toGtkAccelerator(const std::string &shortcut) {
|
|
400
|
+
if (shortcut.empty()) {
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
std::string out;
|
|
404
|
+
size_t start = 0;
|
|
405
|
+
while (true) {
|
|
406
|
+
const size_t plus = shortcut.find('+', start);
|
|
407
|
+
const std::string part = shortcut.substr(
|
|
408
|
+
start, plus == std::string::npos ? std::string::npos : plus - start);
|
|
409
|
+
if (plus == std::string::npos) {
|
|
410
|
+
// The key itself, lowercased: GTK matches keyval names, and "R" is not one.
|
|
411
|
+
for (char c : part) {
|
|
412
|
+
out += static_cast<char>(g_ascii_tolower(c));
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
if (part == "Ctrl" || part == "Control") {
|
|
417
|
+
out += "<Control>";
|
|
418
|
+
} else if (part == "Shift") {
|
|
419
|
+
out += "<Shift>";
|
|
420
|
+
} else if (part == "Alt" || part == "Option") {
|
|
421
|
+
out += "<Alt>";
|
|
422
|
+
} else if (part == "Cmd" || part == "Meta" || part == "Super") {
|
|
423
|
+
out += "<Meta>";
|
|
424
|
+
} else {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
start = plus + 1;
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Answers once, whichever comes first: an item activating, or the popover
|
|
433
|
+
// closing with nothing chosen. Both happen for a menu that was used, and the
|
|
434
|
+
// order is not guaranteed.
|
|
435
|
+
void finishMenu(PendingMenu *pending, int index) {
|
|
436
|
+
if (pending->answered) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
pending->answered = true;
|
|
440
|
+
pending->onChosen(index);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
void onMenuItemActivated(GSimpleAction *action, GVariant * /*parameter*/, gpointer userData) {
|
|
444
|
+
auto *pending = static_cast<PendingMenu *>(userData);
|
|
445
|
+
// The action is named "item<N>", which is where the index comes from: a
|
|
446
|
+
// GMenu carries no index of its own, and the position in the model is not
|
|
447
|
+
// the position in the vector once separators are sections.
|
|
448
|
+
const char *name = g_action_get_name(G_ACTION(action));
|
|
449
|
+
finishMenu(pending, name != nullptr ? std::atoi(name + 4) : -1);
|
|
450
|
+
if (pending->popover != nullptr) {
|
|
451
|
+
gtk_popover_popdown(GTK_POPOVER(pending->popover));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
void onMenuClosed(GtkPopover * /*popover*/, gpointer userData) {
|
|
456
|
+
auto *pending = static_cast<PendingMenu *>(userData);
|
|
457
|
+
finishMenu(pending, -1);
|
|
458
|
+
// Unparented from an idle rather than here: GTK is still inside the
|
|
459
|
+
// popover's own signal emission, and destroying the widget it is emitting
|
|
460
|
+
// from is how a popup menu turns into a use-after-free.
|
|
461
|
+
g_idle_add_full(
|
|
462
|
+
G_PRIORITY_DEFAULT_IDLE,
|
|
463
|
+
[](gpointer data) -> gboolean {
|
|
464
|
+
std::unique_ptr<PendingMenu> owned{static_cast<PendingMenu *>(data)};
|
|
465
|
+
if (owned->popover != nullptr) {
|
|
466
|
+
gtk_widget_unparent(owned->popover);
|
|
467
|
+
}
|
|
468
|
+
return G_SOURCE_REMOVE;
|
|
469
|
+
},
|
|
470
|
+
pending,
|
|
471
|
+
nullptr);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
gboolean showMenuOnMainThread(gpointer userData) {
|
|
475
|
+
auto *pending = static_cast<PendingMenu *>(userData);
|
|
476
|
+
|
|
477
|
+
GtkWindow *window = activeWindow();
|
|
478
|
+
GtkWidget *anchor = window != nullptr ? gtk_window_get_child(window) : nullptr;
|
|
479
|
+
if (anchor == nullptr) {
|
|
480
|
+
finishMenu(pending, -1);
|
|
481
|
+
delete pending;
|
|
482
|
+
return G_SOURCE_REMOVE;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Separators are sections rather than entries: a GMenu has no separator item,
|
|
486
|
+
// and two sections are drawn with a line between them. The indexes the
|
|
487
|
+
// caller gets back still count them, which is what the action names carry.
|
|
488
|
+
GMenu *model = g_menu_new();
|
|
489
|
+
GMenu *section = g_menu_new();
|
|
490
|
+
auto *actions = g_simple_action_group_new();
|
|
491
|
+
|
|
492
|
+
for (size_t i = 0; i < pending->request.entries.size(); i++) {
|
|
493
|
+
const MenuEntry &entry = pending->request.entries[i];
|
|
494
|
+
if (entry.isSeparator()) {
|
|
495
|
+
g_menu_append_section(model, nullptr, G_MENU_MODEL(section));
|
|
496
|
+
g_object_unref(section);
|
|
497
|
+
section = g_menu_new();
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const std::string name = "item" + std::to_string(i);
|
|
502
|
+
auto *action = g_simple_action_new(name.c_str(), nullptr);
|
|
503
|
+
g_simple_action_set_enabled(action, entry.enabled ? TRUE : FALSE);
|
|
504
|
+
g_signal_connect(action, "activate", G_CALLBACK(onMenuItemActivated), pending);
|
|
505
|
+
g_action_map_add_action(G_ACTION_MAP(actions), G_ACTION(action));
|
|
506
|
+
g_object_unref(action);
|
|
507
|
+
|
|
508
|
+
GMenuItem *item = g_menu_item_new(entry.label.c_str(), ("menu." + name).c_str());
|
|
509
|
+
const std::string accelerator = toGtkAccelerator(entry.shortcut);
|
|
510
|
+
if (!accelerator.empty()) {
|
|
511
|
+
g_menu_item_set_attribute(item, "accel", "s", accelerator.c_str());
|
|
512
|
+
}
|
|
513
|
+
g_menu_append_item(section, item);
|
|
514
|
+
g_object_unref(item);
|
|
515
|
+
}
|
|
516
|
+
g_menu_append_section(model, nullptr, G_MENU_MODEL(section));
|
|
517
|
+
g_object_unref(section);
|
|
518
|
+
|
|
519
|
+
GtkWidget *popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(model));
|
|
520
|
+
g_object_unref(model);
|
|
521
|
+
pending->popover = popover;
|
|
522
|
+
|
|
523
|
+
gtk_widget_insert_action_group(popover, "menu", G_ACTION_GROUP(actions));
|
|
524
|
+
g_object_unref(actions);
|
|
525
|
+
|
|
526
|
+
gtk_widget_set_parent(popover, anchor);
|
|
527
|
+
gtk_popover_set_has_arrow(GTK_POPOVER(popover), FALSE);
|
|
528
|
+
if (pending->request.x >= 0.0 && pending->request.y >= 0.0) {
|
|
529
|
+
const GdkRectangle at = {static_cast<int>(pending->request.x),
|
|
530
|
+
static_cast<int>(pending->request.y),
|
|
531
|
+
1,
|
|
532
|
+
1};
|
|
533
|
+
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &at);
|
|
534
|
+
}
|
|
535
|
+
g_signal_connect(popover, "closed", G_CALLBACK(onMenuClosed), pending);
|
|
536
|
+
gtk_popover_popup(GTK_POPOVER(popover));
|
|
537
|
+
return G_SOURCE_REMOVE;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
} // namespace
|
|
541
|
+
|
|
542
|
+
void showMenu(const MenuRequest &request, MenuCallback onChosen) {
|
|
543
|
+
auto *pending = new PendingMenu{request, std::move(onChosen), nullptr, false};
|
|
544
|
+
// Onto the GTK main thread, for the same reason showAlert is: this can be
|
|
545
|
+
// called from the JavaScript thread, and a widget may only be made here.
|
|
546
|
+
g_idle_add_full(G_PRIORITY_DEFAULT, showMenuOnMainThread, pending, nullptr);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
void postDelayed(double milliseconds, std::function<void()> work) {
|
|
550
|
+
// Heap-allocated because g_timeout_add takes a void*, and freed by the
|
|
551
|
+
// destroy notify whether the callback ran or the source was removed.
|
|
552
|
+
auto *held = new std::function<void()>(std::move(work));
|
|
553
|
+
g_timeout_add_full(
|
|
554
|
+
G_PRIORITY_DEFAULT,
|
|
555
|
+
static_cast<guint>(milliseconds),
|
|
556
|
+
[](gpointer data) -> gboolean {
|
|
557
|
+
(*static_cast<std::function<void()> *>(data))();
|
|
558
|
+
return G_SOURCE_REMOVE;
|
|
559
|
+
},
|
|
560
|
+
held,
|
|
561
|
+
[](gpointer data) { delete static_cast<std::function<void()> *>(data); });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
void postToUiThread(std::function<void()> work) {
|
|
565
|
+
auto *held = new std::function<void()>(std::move(work));
|
|
566
|
+
g_idle_add_full(
|
|
567
|
+
G_PRIORITY_DEFAULT,
|
|
568
|
+
[](gpointer data) -> gboolean {
|
|
569
|
+
(*static_cast<std::function<void()> *>(data))();
|
|
570
|
+
return G_SOURCE_REMOVE;
|
|
571
|
+
},
|
|
572
|
+
held,
|
|
573
|
+
[](gpointer data) { delete static_cast<std::function<void()> *>(data); });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
bool isUiThread() {
|
|
577
|
+
// The thread running the default main context is the one GTK draws from, and
|
|
578
|
+
// asking that rather than remembering a thread id keeps this true in a test
|
|
579
|
+
// binary that never starts a loop -- where the honest answer is "no".
|
|
580
|
+
return g_main_context_is_owner(g_main_context_default()) != 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
} // namespace basalt
|