@zogien/native-sdk 0.11.0-fork.4 → 0.11.0-fork.5
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/build/app.zig +50 -34
- package/package.json +9 -9
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/skill-data/core/references/bridge-security-native-capabilities.md +4 -0
- package/src/platform/linux/cef_host.cpp +22 -0
- package/src/platform/linux/gtk_host.c +317 -34
- package/src/platform/linux/gtk_host.h +37 -0
- package/src/platform/linux/root.zig +114 -20
- package/src/platform/macos/appkit_host.h +44 -0
- package/src/platform/macos/appkit_host.m +301 -67
- package/src/platform/macos/cef_host.mm +21 -0
- package/src/platform/macos/root.zig +117 -23
- package/src/platform/null_platform.zig +136 -0
- package/src/platform/root.zig +6 -0
- package/src/platform/types.zig +81 -0
- package/src/platform/windows/dialog_test.cpp +303 -0
- package/src/platform/windows/host_loop_test.cpp +169 -0
- package/src/platform/windows/root.zig +225 -39
- package/src/platform/windows/session_end_test.cpp +303 -0
- package/src/platform/windows/webview2_host.cpp +1064 -280
- package/src/root.zig +7 -0
- package/src/runtime/builtin_bridge.zig +108 -13
- package/src/runtime/core.zig +24 -3
- package/src/runtime/dialog_tests.zig +367 -0
- package/src/runtime/dialogs.zig +385 -0
- package/src/runtime/effects.zig +71 -2
- package/src/runtime/effects_host_tests.zig +69 -1
- package/src/runtime/flow.zig +14 -6
- package/src/runtime/flow_reference.zig +14 -6
- package/src/runtime/platform_bridge_tests.zig +1 -0
- package/src/runtime/root.zig +5 -0
- package/src/runtime/system_services.zig +64 -82
- package/src/runtime/ui_app.zig +67 -0
- package/tools/native-sdk/main.zig +1 -1
package/src/root.zig
CHANGED
|
@@ -270,6 +270,13 @@ pub const SaveDialogOptions = platform.SaveDialogOptions;
|
|
|
270
270
|
pub const MessageDialogStyle = platform.MessageDialogStyle;
|
|
271
271
|
pub const MessageDialogResult = platform.MessageDialogResult;
|
|
272
272
|
pub const MessageDialogOptions = platform.MessageDialogOptions;
|
|
273
|
+
pub const DialogRequestId = platform.DialogRequestId;
|
|
274
|
+
pub const DialogKind = platform.DialogKind;
|
|
275
|
+
pub const DialogOptions = platform.DialogOptions;
|
|
276
|
+
pub const DialogOutcome = platform.DialogOutcome;
|
|
277
|
+
pub const DialogResult = platform.DialogResult;
|
|
278
|
+
pub const DialogCompletion = runtime.DialogCompletion;
|
|
279
|
+
pub const DialogRequestOptions = runtime.DialogRequestOptions;
|
|
273
280
|
pub const NotificationOptions = platform.NotificationOptions;
|
|
274
281
|
pub const StatusItemId = platform.StatusItemId;
|
|
275
282
|
pub const primary_status_item_id = platform.primary_status_item_id;
|
|
@@ -52,6 +52,7 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
52
52
|
const App = runtime_api.App(Runtime);
|
|
53
53
|
const SystemServiceMethods = runtime_system_services.RuntimeSystemServices(Runtime);
|
|
54
54
|
const WindowViewMethods = runtime_window_views.RuntimeWindowViews(Runtime);
|
|
55
|
+
const AsyncBridgeResponseSlot = @import("async_bridge.zig").AsyncBridgeResponseSlot(Runtime);
|
|
55
56
|
|
|
56
57
|
return struct {
|
|
57
58
|
const Self = @This();
|
|
@@ -125,15 +126,106 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
125
126
|
return bridge.writeSuccessResponse(response_buffer, request.id, result);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
129
|
+
fn dispatchCompiledDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
129
130
|
const plan = builtin_coordination.route(self.builtin_coordination_policy.?, .dialog, request.command);
|
|
130
|
-
const
|
|
131
|
-
.dialog_openFile =>
|
|
132
|
-
.dialog_saveFile =>
|
|
133
|
-
.dialog_showMessage =>
|
|
131
|
+
const kind: platform.DialogKind = switch (plan.verb()) {
|
|
132
|
+
.dialog_openFile => .open,
|
|
133
|
+
.dialog_saveFile => .save,
|
|
134
|
+
.dialog_showMessage => .message,
|
|
134
135
|
else => return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, plan.unknownMessage()),
|
|
135
136
|
};
|
|
136
|
-
return
|
|
137
|
+
return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Queue the dialog and answer the bridge request when it
|
|
141
|
+
/// finishes. Null means the response is deferred; a returned
|
|
142
|
+
/// response is an immediate refusal, or, on a platform without
|
|
143
|
+
/// asynchronous dialogs, the blocking dialog's answer.
|
|
144
|
+
fn startDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
145
|
+
var storage = json.StringStorage.init(result_buffer);
|
|
146
|
+
const options = SystemServiceMethods.dialogOptionsFromJson(kind, request.payload, &storage) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
147
|
+
if (!self.options.platform.services.supportsAsyncDialogs()) return Self.answerDialogBridgeCommand(self, kind, request.id, options, result_buffer, response_buffer);
|
|
148
|
+
const slot =Self.reserveDialogBridgeResponse(self, message) catch |err| return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
149
|
+
var tag: [1 + bridge.max_id_bytes]u8 = undefined;
|
|
150
|
+
tag[0] = @intFromEnum(kind);
|
|
151
|
+
@memcpy(tag[1..][0..request.id.len], request.id);
|
|
152
|
+
_ = self.requestDialog(.{
|
|
153
|
+
.options = options,
|
|
154
|
+
.parent_window_id = Self.openWindowId(self, message.window_id),
|
|
155
|
+
}, .{
|
|
156
|
+
.context = slot,
|
|
157
|
+
.tag = tag[0 .. 1 + request.id.len],
|
|
158
|
+
.on_result = Self.finishDialogBridgeCommand,
|
|
159
|
+
}) catch |err| {
|
|
160
|
+
slot.release();
|
|
161
|
+
return Self.writeDialogBridgeError(self, request.id, err, response_buffer);
|
|
162
|
+
};
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn finishDialogBridgeCommand(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
|
|
167
|
+
_ = runtime;
|
|
168
|
+
const slot: *AsyncBridgeResponseSlot = @ptrCast(@alignCast(context));
|
|
169
|
+
const kind: platform.DialogKind = @enumFromInt(tag[0]);
|
|
170
|
+
const id = tag[1..];
|
|
171
|
+
var result_buffer: [bridge.max_result_bytes]u8 = undefined;
|
|
172
|
+
var response_buffer: [bridge.max_response_bytes]u8 = undefined;
|
|
173
|
+
const response = if (result.outcome == .failed)
|
|
174
|
+
bridge.writeErrorResponse(&response_buffer, id, .internal_error, "Dialog could not be shown")
|
|
175
|
+
else if (SystemServiceMethods.writeDialogResultJson(kind, result, &result_buffer)) |value|
|
|
176
|
+
bridge.writeSuccessResponse(&response_buffer, id, value)
|
|
177
|
+
else |err|
|
|
178
|
+
bridge.writeErrorResponse(&response_buffer, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
|
|
179
|
+
try slot.respond(response);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Platforms without asynchronous dialogs (Chromium builds, the
|
|
183
|
+
/// null platform) answer from the blocking call, at once, as they
|
|
184
|
+
/// always have. `options` borrows `result_buffer`, which is free
|
|
185
|
+
/// again once the dialog has returned.
|
|
186
|
+
fn answerDialogBridgeCommand(self: *Runtime, kind: platform.DialogKind, id: []const u8, options: platform.DialogOptions, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
187
|
+
var paths: [platform.max_dialog_paths_bytes]u8 = undefined;
|
|
188
|
+
const result: platform.DialogResult = switch (options) {
|
|
189
|
+
.open => |open| opened: {
|
|
190
|
+
const opened = self.showOpenDialog(open, &paths) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
191
|
+
break :opened .{ .id = 0, .outcome = if (opened.count == 0) .cancelled else .selected, .paths = opened.paths, .count = opened.count };
|
|
192
|
+
},
|
|
193
|
+
.save => |save| saved: {
|
|
194
|
+
const saved = self.showSaveDialog(save, paths[0..platform.max_dialog_path_bytes]) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
195
|
+
const path = saved orelse break :saved .{ .id = 0, .outcome = .cancelled };
|
|
196
|
+
break :saved .{ .id = 0, .outcome = .selected, .paths = path, .count = 1 };
|
|
197
|
+
},
|
|
198
|
+
.message => |dialog| .{
|
|
199
|
+
.id = 0,
|
|
200
|
+
.outcome = .selected,
|
|
201
|
+
.button = self.showMessageDialog(dialog) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const value = SystemServiceMethods.writeDialogResultJson(kind, result, result_buffer) catch |err| return Self.writeDialogBridgeError(self, id, err, response_buffer);
|
|
205
|
+
return bridge.writeSuccessResponse(response_buffer, id, value);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn writeDialogBridgeError(self: *Runtime, id: []const u8, err: anyerror, output: []u8) []const u8 {
|
|
209
|
+
if (self.builtin_coordination_policy != null) return Self.writeCompiledBuiltinError(self, id, err, output);
|
|
210
|
+
return bridge.writeErrorResponse(output, id, builtinBridgeErrorCode(err), builtinBridgeErrorMessage(err));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn reserveDialogBridgeResponse(self: *Runtime, message: platform.BridgeMessage) !*AsyncBridgeResponseSlot {
|
|
214
|
+
for (&self.async_bridge_responses) |*slot| {
|
|
215
|
+
if (slot.in_use) continue;
|
|
216
|
+
try slot.init(self, .{ .origin = message.origin, .window_id = message.window_id, .webview_label = message.webview_label });
|
|
217
|
+
return slot;
|
|
218
|
+
}
|
|
219
|
+
return error.AsyncBridgeResponseLimitReached;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// The requesting window when the runtime knows it as open;
|
|
223
|
+
/// otherwise null lets the broker pick a parent.
|
|
224
|
+
fn openWindowId(self: *Runtime, window_id: platform.WindowId) ?platform.WindowId {
|
|
225
|
+
for (self.windows[0..self.window_count]) |window| {
|
|
226
|
+
if (window.info.id == window_id and window.info.open) return window_id;
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
137
229
|
}
|
|
138
230
|
|
|
139
231
|
fn dispatchCompiledOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
|
@@ -306,17 +398,20 @@ pub fn RuntimeBuiltinBridge(comptime Runtime: type) type {
|
|
|
306
398
|
return bridge.writeSuccessResponse(response_buffer, request.id, result);
|
|
307
399
|
}
|
|
308
400
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
401
|
+
/// Dialog commands answer asynchronously: null means the response
|
|
402
|
+
/// will follow when the dialog finishes (see
|
|
403
|
+
/// `startDialogBridgeCommand`).
|
|
404
|
+
pub fn dispatchDialogBridgeCommand(self: *Runtime, request: bridge.Request, message: platform.BridgeMessage, result_buffer: []u8, response_buffer: []u8) ?[]const u8 {
|
|
405
|
+
if (self.builtin_coordination_policy != null) return Self.dispatchCompiledDialogBridgeCommand(self, request, message, result_buffer, response_buffer);
|
|
406
|
+
const kind: platform.DialogKind = if (std.mem.eql(u8, request.command, "native-sdk.dialog.openFile"))
|
|
407
|
+
.open
|
|
313
408
|
else if (std.mem.eql(u8, request.command, "native-sdk.dialog.saveFile"))
|
|
314
|
-
|
|
409
|
+
.save
|
|
315
410
|
else if (std.mem.eql(u8, request.command, "native-sdk.dialog.showMessage"))
|
|
316
|
-
|
|
411
|
+
.message
|
|
317
412
|
else
|
|
318
413
|
return bridge.writeErrorResponse(response_buffer, request.id, .unknown_command, "Unknown dialog command");
|
|
319
|
-
return
|
|
414
|
+
return Self.startDialogBridgeCommand(self, kind, request, message, result_buffer, response_buffer);
|
|
320
415
|
}
|
|
321
416
|
|
|
322
417
|
pub fn dispatchOsBridgeCommand(self: *Runtime, request: bridge.Request, origin: []const u8, result_buffer: []u8, response_buffer: []u8) []const u8 {
|
package/src/runtime/core.zig
CHANGED
|
@@ -30,6 +30,7 @@ const runtime_session_state = @import("session_state.zig");
|
|
|
30
30
|
const session_journal = @import("session_journal.zig");
|
|
31
31
|
const runtime_state = @import("state.zig");
|
|
32
32
|
const runtime_system_services = @import("system_services.zig");
|
|
33
|
+
const runtime_dialogs = @import("dialogs.zig");
|
|
33
34
|
const runtime_builtin_bridge = @import("builtin_bridge.zig");
|
|
34
35
|
const runtime_view = @import("view.zig");
|
|
35
36
|
const runtime_window_views = @import("window_views.zig");
|
|
@@ -110,6 +111,10 @@ const FocusTraversalDirection = runtime_state.FocusTraversalDirection;
|
|
|
110
111
|
const copySourceInto = runtime_state.copySourceInto;
|
|
111
112
|
const sourceWebViewUrl = runtime_state.sourceWebViewUrl;
|
|
112
113
|
const AsyncBridgeResponseSlot = runtime_async_bridge.AsyncBridgeResponseSlot(Runtime);
|
|
114
|
+
pub const DialogCompletion = runtime_dialogs.Completion(Runtime);
|
|
115
|
+
pub const DialogRequestOptions = runtime_dialogs.DialogRequestOptions;
|
|
116
|
+
pub const max_dialog_requests = runtime_dialogs.max_dialog_requests;
|
|
117
|
+
pub const max_dialog_tag_bytes = runtime_dialogs.max_dialog_tag_bytes;
|
|
113
118
|
const platformCursorFromCanvas = widget_bridge.platformCursorFromCanvas;
|
|
114
119
|
const widgetRoleName = widget_bridge.widgetRoleName;
|
|
115
120
|
const platformWidgetAccessibilityRole = widget_bridge.platformWidgetAccessibilityRole;
|
|
@@ -351,6 +356,8 @@ pub const Runtime = struct {
|
|
|
351
356
|
/// copy bound, so `initAt` assigns it explicitly.
|
|
352
357
|
frame_profile: runtime_frame_profile.FrameProfile = .{},
|
|
353
358
|
async_bridge_responses: [max_async_bridge_responses]AsyncBridgeResponseSlot = [_]AsyncBridgeResponseSlot{.{}} ** max_async_bridge_responses,
|
|
359
|
+
/// Asynchronous dialogs in flight (see `dialogs.zig`).
|
|
360
|
+
dialog_broker: runtime_dialogs.Broker(Runtime) = .{},
|
|
354
361
|
automation_windows: [automation.snapshot.max_windows]automation.snapshot.Window = undefined,
|
|
355
362
|
/// Snapshot-side storage for the frame profile's per-stage stats
|
|
356
363
|
/// (the snapshot Input only references runtime-owned memory).
|
|
@@ -562,6 +569,9 @@ pub const Runtime = struct {
|
|
|
562
569
|
item.title = "";
|
|
563
570
|
item.item_count = 0;
|
|
564
571
|
}
|
|
572
|
+
// The dialog broker's result buffer exceeds the copy bound too;
|
|
573
|
+
// reset its bookkeeping and leave the buffer alone.
|
|
574
|
+
self.dialog_broker.reset();
|
|
565
575
|
self.started_timestamp_ns = timestampToU64(nowNanoseconds());
|
|
566
576
|
self.text_measure_provider = if (options.platform.services.measure_text_fn) |measure_fn|
|
|
567
577
|
.{
|
|
@@ -600,6 +610,7 @@ pub const Runtime = struct {
|
|
|
600
610
|
/// host) call it to return the storage.
|
|
601
611
|
pub fn deinit(self: *Runtime) void {
|
|
602
612
|
self.disarmMediaSurfaceWakes();
|
|
613
|
+
self.releaseDialogs();
|
|
603
614
|
for (self.views[0..self.view_count]) |*view| {
|
|
604
615
|
if (view.widget_text_bytes_heap_owned) self.owned_allocator.free(view.widget_text_bytes);
|
|
605
616
|
if (view.canvas_widget_text_history_bytes_heap_owned) self.owned_allocator.free(view.canvas_widget_text_history_bytes);
|
|
@@ -832,6 +843,19 @@ pub const Runtime = struct {
|
|
|
832
843
|
pub const showOpenDialog = SystemServiceMethods.showOpenDialog;
|
|
833
844
|
pub const showSaveDialog = SystemServiceMethods.showSaveDialog;
|
|
834
845
|
pub const showMessageDialog = SystemServiceMethods.showMessageDialog;
|
|
846
|
+
|
|
847
|
+
const DialogMethods = runtime_dialogs.RuntimeDialogs(Runtime);
|
|
848
|
+
pub const requestDialog = DialogMethods.requestDialog;
|
|
849
|
+
pub const requestOpenDialog = DialogMethods.requestOpenDialog;
|
|
850
|
+
pub const requestSaveDialog = DialogMethods.requestSaveDialog;
|
|
851
|
+
pub const requestMessageDialog = DialogMethods.requestMessageDialog;
|
|
852
|
+
pub const cancelDialog = DialogMethods.cancelDialog;
|
|
853
|
+
pub const adoptDialogResults = DialogMethods.adoptDialogResults;
|
|
854
|
+
pub const cancelDialogsForWindow = DialogMethods.cancelDialogsForWindow;
|
|
855
|
+
pub const abandonDialogs = DialogMethods.abandonDialogs;
|
|
856
|
+
pub const pendingDialogCount = DialogMethods.pendingDialogCount;
|
|
857
|
+
const releaseDialogs = DialogMethods.releaseDialogs;
|
|
858
|
+
|
|
835
859
|
pub const showNotification = SystemServiceMethods.showNotification;
|
|
836
860
|
pub const setCredential = SystemServiceMethods.setCredential;
|
|
837
861
|
pub const getCredential = SystemServiceMethods.getCredential;
|
|
@@ -863,9 +887,6 @@ pub const Runtime = struct {
|
|
|
863
887
|
const revealPathFromJson = SystemServiceMethods.revealPathFromJson;
|
|
864
888
|
const addRecentDocumentFromJson = SystemServiceMethods.addRecentDocumentFromJson;
|
|
865
889
|
const clearRecentDocumentsFromJson = SystemServiceMethods.clearRecentDocumentsFromJson;
|
|
866
|
-
const openFileDialogFromJson = SystemServiceMethods.openFileDialogFromJson;
|
|
867
|
-
const saveFileDialogFromJson = SystemServiceMethods.saveFileDialogFromJson;
|
|
868
|
-
const showMessageDialogFromJson = SystemServiceMethods.showMessageDialogFromJson;
|
|
869
890
|
|
|
870
891
|
/// Start (or replace) a platform timer. The platform delivers `.timer`
|
|
871
892
|
/// events carrying `id` until `cancelTimer` (or after the first fire when
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
const support = @import("test_support.zig");
|
|
2
|
+
const std = support.std;
|
|
3
|
+
const geometry = support.geometry;
|
|
4
|
+
const platform = support.platform;
|
|
5
|
+
const App = support.App;
|
|
6
|
+
const Runtime = support.Runtime;
|
|
7
|
+
const TestHarness = support.TestHarness;
|
|
8
|
+
const DialogCompletion = support.runtime_module.DialogCompletion;
|
|
9
|
+
|
|
10
|
+
const Probe = struct {
|
|
11
|
+
calls: usize = 0,
|
|
12
|
+
ids: [8]platform.DialogRequestId = undefined,
|
|
13
|
+
outcomes: [8]platform.DialogOutcome = undefined,
|
|
14
|
+
last_paths: [256]u8 = undefined,
|
|
15
|
+
last_paths_len: usize = 0,
|
|
16
|
+
last_count: usize = 0,
|
|
17
|
+
last_button: platform.MessageDialogResult = .primary,
|
|
18
|
+
last_tag: [64]u8 = undefined,
|
|
19
|
+
last_tag_len: usize = 0,
|
|
20
|
+
|
|
21
|
+
fn completion(self: *Probe, tag: []const u8) DialogCompletion {
|
|
22
|
+
return .{ .context = self, .tag = tag, .on_result = onResult };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn onResult(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
|
|
26
|
+
_ = runtime;
|
|
27
|
+
const self: *Probe = @ptrCast(@alignCast(context));
|
|
28
|
+
self.ids[self.calls] = result.id;
|
|
29
|
+
self.outcomes[self.calls] = result.outcome;
|
|
30
|
+
self.calls += 1;
|
|
31
|
+
self.last_paths_len = result.paths.len;
|
|
32
|
+
@memcpy(self.last_paths[0..result.paths.len], result.paths);
|
|
33
|
+
self.last_count = result.count;
|
|
34
|
+
self.last_button = result.button;
|
|
35
|
+
self.last_tag_len = tag.len;
|
|
36
|
+
@memcpy(self.last_tag[0..tag.len], tag);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn paths(self: *const Probe) []const u8 {
|
|
40
|
+
return self.last_paths[0..self.last_paths_len];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn tagText(self: *const Probe) []const u8 {
|
|
44
|
+
return self.last_tag[0..self.last_tag_len];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fn dialogApp(harness: *TestHarness()) App {
|
|
49
|
+
return .{ .context = harness, .name = "dialogs", .source = platform.WebViewSource.html("<p>Dialogs</p>") };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// A started runtime whose null platform presents dialogs asynchronously.
|
|
53
|
+
fn started() !*TestHarness() {
|
|
54
|
+
const harness = try startedBlocking();
|
|
55
|
+
harness.null_platform.useAsyncDialogs(&harness.runtime.options.platform.services);
|
|
56
|
+
return harness;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The null platform as app tests get it: blocking dialogs that answer at
|
|
60
|
+
/// once.
|
|
61
|
+
fn startedBlocking() !*TestHarness() {
|
|
62
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
63
|
+
errdefer harness.destroy(std.testing.allocator);
|
|
64
|
+
try harness.runtime.dispatchPlatformEvent(dialogApp(harness), .app_start);
|
|
65
|
+
return harness;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Deliver the wakes the platform posted, as a host loop would.
|
|
69
|
+
fn deliverWakes(harness: *TestHarness()) !void {
|
|
70
|
+
while (harness.null_platform.takeWake()) |event| try harness.runtime.dispatchPlatformEvent(dialogApp(harness), event);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn closeMainWindow(harness: *TestHarness()) !void {
|
|
74
|
+
try harness.runtime.dispatchPlatformEvent(dialogApp(harness), .{ .window_frame_changed = .{
|
|
75
|
+
.id = 1,
|
|
76
|
+
.label = "main",
|
|
77
|
+
.title = "",
|
|
78
|
+
.frame = geometry.RectF.init(0, 0, 720, 480),
|
|
79
|
+
.scale_factor = 1,
|
|
80
|
+
.open = false,
|
|
81
|
+
.focused = false,
|
|
82
|
+
} });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test "dialog requests return at once and complete on a later wake" {
|
|
86
|
+
const harness = try started();
|
|
87
|
+
defer harness.destroy(std.testing.allocator);
|
|
88
|
+
var probe: Probe = .{};
|
|
89
|
+
|
|
90
|
+
const id = try harness.runtime.requestOpenDialog(.{ .title = "Open", .allow_multiple = true }, null, probe.completion("request-1"));
|
|
91
|
+
try std.testing.expectEqual(@as(usize, 0), probe.calls);
|
|
92
|
+
const shown = harness.null_platform.findPresentedDialog(id).?;
|
|
93
|
+
try std.testing.expectEqual(platform.DialogKind.open, shown.kind);
|
|
94
|
+
try std.testing.expectEqual(@as(?platform.WindowId, 1), shown.parent_window_id);
|
|
95
|
+
try std.testing.expectEqualStrings("Open", shown.titleText());
|
|
96
|
+
|
|
97
|
+
try harness.null_platform.finishDialog(id, .selected, "/a.txt\n/b.txt", .primary);
|
|
98
|
+
try std.testing.expectEqual(@as(usize, 0), probe.calls);
|
|
99
|
+
try deliverWakes(harness);
|
|
100
|
+
|
|
101
|
+
try std.testing.expectEqual(@as(usize, 1), probe.calls);
|
|
102
|
+
try std.testing.expectEqual(id, probe.ids[0]);
|
|
103
|
+
try std.testing.expectEqual(platform.DialogOutcome.selected, probe.outcomes[0]);
|
|
104
|
+
try std.testing.expectEqualStrings("/a.txt\n/b.txt", probe.paths());
|
|
105
|
+
try std.testing.expectEqual(@as(usize, 2), probe.last_count);
|
|
106
|
+
try std.testing.expectEqualStrings("request-1", probe.tagText());
|
|
107
|
+
try std.testing.expectEqual(@as(usize, 0), harness.runtime.pendingDialogCount());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
test "dialogs for one window show one at a time in request order" {
|
|
111
|
+
const harness = try started();
|
|
112
|
+
defer harness.destroy(std.testing.allocator);
|
|
113
|
+
var probe: Probe = .{};
|
|
114
|
+
|
|
115
|
+
const first = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
116
|
+
const second = try harness.runtime.requestMessageDialog(.{ .message = "Next" }, 1, probe.completion(""));
|
|
117
|
+
const detached = try harness.runtime.requestDialog(.{ .options = .{ .save = .{ .default_name = "a.txt" } }, .detached = true }, probe.completion(""));
|
|
118
|
+
try std.testing.expectEqual(@as(usize, 2), harness.null_platform.presentedDialogCount());
|
|
119
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(second) == null);
|
|
120
|
+
try std.testing.expectEqual(@as(?platform.WindowId, null), harness.null_platform.findPresentedDialog(detached).?.parent_window_id);
|
|
121
|
+
|
|
122
|
+
try harness.null_platform.finishDialog(first, .cancelled, "", .primary);
|
|
123
|
+
try deliverWakes(harness);
|
|
124
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(second) != null);
|
|
125
|
+
|
|
126
|
+
try harness.null_platform.finishDialog(detached, .selected, "/a.txt", .primary);
|
|
127
|
+
try deliverWakes(harness);
|
|
128
|
+
try harness.null_platform.finishDialog(second, .selected, "", .secondary);
|
|
129
|
+
try deliverWakes(harness);
|
|
130
|
+
|
|
131
|
+
try std.testing.expectEqual(@as(usize, 3), probe.calls);
|
|
132
|
+
try std.testing.expectEqualSlices(platform.DialogRequestId, &.{ first, detached, second }, probe.ids[0..3]);
|
|
133
|
+
try std.testing.expectEqual(platform.MessageDialogResult.secondary, probe.last_button);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
test "cancelling a queued dialog never shows it and a presented one closes" {
|
|
137
|
+
const harness = try started();
|
|
138
|
+
defer harness.destroy(std.testing.allocator);
|
|
139
|
+
var probe: Probe = .{};
|
|
140
|
+
|
|
141
|
+
const first = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
142
|
+
const second = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
143
|
+
harness.runtime.cancelDialog(second);
|
|
144
|
+
try std.testing.expectEqual(@as(usize, 0), probe.calls);
|
|
145
|
+
try deliverWakes(harness);
|
|
146
|
+
try std.testing.expectEqual(@as(usize, 1), probe.calls);
|
|
147
|
+
try std.testing.expectEqual(second, probe.ids[0]);
|
|
148
|
+
try std.testing.expectEqual(platform.DialogOutcome.cancelled, probe.outcomes[0]);
|
|
149
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.dialog_request_count);
|
|
150
|
+
|
|
151
|
+
harness.runtime.cancelDialog(first);
|
|
152
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.dialog_cancel_count);
|
|
153
|
+
try deliverWakes(harness);
|
|
154
|
+
try std.testing.expectEqual(@as(usize, 2), probe.calls);
|
|
155
|
+
try std.testing.expectEqual(platform.DialogOutcome.cancelled, probe.outcomes[1]);
|
|
156
|
+
harness.runtime.cancelDialog(first);
|
|
157
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.dialog_cancel_count);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
test "closing the parent window cancels its dialogs and leaves others" {
|
|
161
|
+
const harness = try started();
|
|
162
|
+
defer harness.destroy(std.testing.allocator);
|
|
163
|
+
var probe: Probe = .{};
|
|
164
|
+
|
|
165
|
+
const shown = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
166
|
+
const queued = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
167
|
+
const detached = try harness.runtime.requestDialog(.{ .options = .{ .open = .{} }, .detached = true }, probe.completion(""));
|
|
168
|
+
try closeMainWindow(harness);
|
|
169
|
+
try deliverWakes(harness);
|
|
170
|
+
|
|
171
|
+
// Platform-reported results drain before the runtime's own.
|
|
172
|
+
try std.testing.expectEqual(@as(usize, 2), probe.calls);
|
|
173
|
+
try std.testing.expectEqualSlices(platform.DialogRequestId, &.{ shown, queued }, probe.ids[0..2]);
|
|
174
|
+
try std.testing.expectEqualSlices(platform.DialogOutcome, &.{ .cancelled, .cancelled }, probe.outcomes[0..2]);
|
|
175
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(detached) != null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test "an app stop drops pending dialogs without running completions" {
|
|
179
|
+
const harness = try started();
|
|
180
|
+
defer harness.destroy(std.testing.allocator);
|
|
181
|
+
var probe: Probe = .{};
|
|
182
|
+
|
|
183
|
+
_ = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
184
|
+
_ = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
185
|
+
try harness.runtime.dispatchPlatformEvent(dialogApp(harness), .app_shutdown);
|
|
186
|
+
try std.testing.expectEqual(@as(usize, 0), harness.runtime.pendingDialogCount());
|
|
187
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.dialog_cancel_count);
|
|
188
|
+
try deliverWakes(harness);
|
|
189
|
+
try std.testing.expectEqual(@as(usize, 0), probe.calls);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
test "a dialog the platform cannot present reports failed" {
|
|
193
|
+
const harness = try started();
|
|
194
|
+
defer harness.destroy(std.testing.allocator);
|
|
195
|
+
var probe: Probe = .{};
|
|
196
|
+
|
|
197
|
+
harness.null_platform.fail_next_dialog_request = true;
|
|
198
|
+
const failed = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
199
|
+
const next = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
200
|
+
try deliverWakes(harness);
|
|
201
|
+
try std.testing.expectEqual(@as(usize, 1), probe.calls);
|
|
202
|
+
try std.testing.expectEqual(failed, probe.ids[0]);
|
|
203
|
+
try std.testing.expectEqual(platform.DialogOutcome.failed, probe.outcomes[0]);
|
|
204
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(next) != null);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
test "platforms without asynchronous dialogs fall back to the blocking call" {
|
|
208
|
+
const harness = try startedBlocking();
|
|
209
|
+
defer harness.destroy(std.testing.allocator);
|
|
210
|
+
var probe: Probe = .{};
|
|
211
|
+
|
|
212
|
+
_ = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
213
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.open_dialog_count);
|
|
214
|
+
try std.testing.expectEqual(@as(usize, 0), probe.calls);
|
|
215
|
+
try deliverWakes(harness);
|
|
216
|
+
try std.testing.expectEqual(@as(usize, 1), probe.calls);
|
|
217
|
+
try std.testing.expectEqualStrings("/tmp/native-sdk-open.txt", probe.paths());
|
|
218
|
+
|
|
219
|
+
harness.null_platform.message_dialog_result = .tertiary;
|
|
220
|
+
_ = try harness.runtime.requestMessageDialog(.{ .message = "Sure?" }, null, probe.completion(""));
|
|
221
|
+
try deliverWakes(harness);
|
|
222
|
+
try std.testing.expectEqual(platform.DialogOutcome.selected, probe.outcomes[1]);
|
|
223
|
+
try std.testing.expectEqual(platform.MessageDialogResult.tertiary, probe.last_button);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
test "a completion may request the next dialog without jumping the queue" {
|
|
227
|
+
const harness = try started();
|
|
228
|
+
defer harness.destroy(std.testing.allocator);
|
|
229
|
+
const Chain = struct {
|
|
230
|
+
harness: *TestHarness(),
|
|
231
|
+
probe: Probe = .{},
|
|
232
|
+
third: platform.DialogRequestId = 0,
|
|
233
|
+
|
|
234
|
+
fn onResult(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
|
|
235
|
+
const self: *@This() = @ptrCast(@alignCast(context));
|
|
236
|
+
try Probe.onResult(&self.probe, runtime, tag, result);
|
|
237
|
+
if (self.third == 0) self.third = try runtime.requestOpenDialog(.{}, 1, .{ .context = self, .on_result = onResult });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var chain: Chain = .{ .harness = harness };
|
|
241
|
+
|
|
242
|
+
const first = try harness.runtime.requestOpenDialog(.{}, null, .{ .context = &chain, .on_result = Chain.onResult });
|
|
243
|
+
const second = try harness.runtime.requestOpenDialog(.{}, null, .{ .context = &chain, .on_result = Chain.onResult });
|
|
244
|
+
try harness.null_platform.finishDialog(first, .cancelled, "", .primary);
|
|
245
|
+
try deliverWakes(harness);
|
|
246
|
+
try std.testing.expect(chain.third != 0);
|
|
247
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(second) != null);
|
|
248
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(chain.third) == null);
|
|
249
|
+
|
|
250
|
+
try harness.null_platform.finishDialog(second, .cancelled, "", .primary);
|
|
251
|
+
try deliverWakes(harness);
|
|
252
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(chain.third) != null);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
test "dialog requests are validated before they queue" {
|
|
256
|
+
const harness = try started();
|
|
257
|
+
defer harness.destroy(std.testing.allocator);
|
|
258
|
+
var probe: Probe = .{};
|
|
259
|
+
|
|
260
|
+
const long_tag = [_]u8{'t'} ** (support.runtime_module.max_dialog_tag_bytes + 1);
|
|
261
|
+
try std.testing.expectError(error.DialogTagTooLarge, harness.runtime.requestOpenDialog(.{}, null, probe.completion(&long_tag)));
|
|
262
|
+
try std.testing.expectError(error.InvalidDialogOptions, harness.runtime.requestMessageDialog(.{ .primary_button = "" }, null, probe.completion("")));
|
|
263
|
+
const long_title = [_]u8{'x'} ** (platform.max_dialog_title_bytes + 1);
|
|
264
|
+
try std.testing.expectError(error.DialogFieldTooLarge, harness.runtime.requestOpenDialog(.{ .title = &long_title }, null, probe.completion("")));
|
|
265
|
+
try std.testing.expectError(error.WindowNotFound, harness.runtime.requestOpenDialog(.{}, 99, probe.completion("")));
|
|
266
|
+
try std.testing.expectEqual(@as(usize, 0), harness.runtime.pendingDialogCount());
|
|
267
|
+
|
|
268
|
+
for (0..support.runtime_module.max_dialog_requests) |_| _ = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
269
|
+
try std.testing.expectError(error.DialogQueueFull, harness.runtime.requestOpenDialog(.{}, null, probe.completion("")));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
test "a failing completion is recorded and the queue moves on" {
|
|
273
|
+
const harness = try started();
|
|
274
|
+
defer harness.destroy(std.testing.allocator);
|
|
275
|
+
const Failing = struct {
|
|
276
|
+
fn onResult(context: *anyopaque, runtime: *Runtime, tag: []const u8, result: platform.DialogResult) anyerror!void {
|
|
277
|
+
_ = .{ context, runtime, tag, result };
|
|
278
|
+
return error.CompletionFailed;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var probe: Probe = .{};
|
|
282
|
+
const first = try harness.runtime.requestOpenDialog(.{}, null, .{ .context = &probe, .on_result = Failing.onResult });
|
|
283
|
+
const second = try harness.runtime.requestOpenDialog(.{}, null, probe.completion(""));
|
|
284
|
+
const errors_before = harness.runtime.dispatch_error_total;
|
|
285
|
+
try harness.null_platform.finishDialog(first, .cancelled, "", .primary);
|
|
286
|
+
try deliverWakes(harness);
|
|
287
|
+
try std.testing.expectEqual(errors_before + 1, harness.runtime.dispatch_error_total);
|
|
288
|
+
try std.testing.expect(harness.null_platform.findPresentedDialog(second) != null);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn allowDialogBridge(harness: *TestHarness()) void {
|
|
292
|
+
const permissions = struct {
|
|
293
|
+
const dialog = [_][]const u8{support.security.permission_dialog};
|
|
294
|
+
const commands = [_]support.bridge.CommandPolicy{
|
|
295
|
+
.{ .name = "native-sdk.dialog.openFile", .permissions = &dialog, .origins = &.{"zero://inline"} },
|
|
296
|
+
.{ .name = "native-sdk.dialog.saveFile", .permissions = &dialog, .origins = &.{"zero://inline"} },
|
|
297
|
+
.{ .name = "native-sdk.dialog.showMessage", .permissions = &dialog, .origins = &.{"zero://inline"} },
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
harness.runtime.options.security.permissions = &permissions.dialog;
|
|
301
|
+
harness.runtime.options.builtin_bridge = .{ .enabled = true, .commands = &permissions.commands };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn sendBridge(harness: *TestHarness(), bytes: []const u8) !void {
|
|
305
|
+
try harness.runtime.dispatchPlatformEvent(dialogApp(harness), .{ .bridge_message = .{ .bytes = bytes, .origin = "zero://inline", .window_id = 1 } });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
test "built-in bridge dialogs answer when the dialog finishes" {
|
|
309
|
+
const harness = try started();
|
|
310
|
+
defer harness.destroy(std.testing.allocator);
|
|
311
|
+
allowDialogBridge(harness);
|
|
312
|
+
|
|
313
|
+
try sendBridge(harness, "{\"id\":\"pick\",\"command\":\"native-sdk.dialog.openFile\",\"payload\":{\"title\":\"Pick\",\"allowMultiple\":true}}");
|
|
314
|
+
try std.testing.expectEqualStrings("", harness.null_platform.lastBridgeResponse());
|
|
315
|
+
const shown = harness.null_platform.findPresentedDialog(1).?;
|
|
316
|
+
try std.testing.expectEqualStrings("Pick", shown.titleText());
|
|
317
|
+
try std.testing.expectEqual(@as(?platform.WindowId, 1), shown.parent_window_id);
|
|
318
|
+
|
|
319
|
+
try harness.null_platform.finishDialog(1, .selected, "/a.txt\n/b.txt", .primary);
|
|
320
|
+
try deliverWakes(harness);
|
|
321
|
+
const response = harness.null_platform.lastBridgeResponse();
|
|
322
|
+
try std.testing.expect(std.mem.indexOf(u8, response, "\"id\":\"pick\"") != null);
|
|
323
|
+
try std.testing.expect(std.mem.indexOf(u8, response, "[\"/a.txt\",\"/b.txt\"]") != null);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
test "built-in bridge dialogs report dismissal, cancellation and failure" {
|
|
327
|
+
const harness = try started();
|
|
328
|
+
defer harness.destroy(std.testing.allocator);
|
|
329
|
+
allowDialogBridge(harness);
|
|
330
|
+
|
|
331
|
+
try sendBridge(harness, "{\"id\":\"ask\",\"command\":\"native-sdk.dialog.showMessage\",\"payload\":{\"message\":\"Delete?\",\"primaryButton\":\"Delete\",\"secondaryButton\":\"Keep\"}}");
|
|
332
|
+
try harness.null_platform.finishDialog(1, .cancelled, "", .primary);
|
|
333
|
+
try deliverWakes(harness);
|
|
334
|
+
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"dismissed\"") != null);
|
|
335
|
+
|
|
336
|
+
try sendBridge(harness, "{\"id\":\"save\",\"command\":\"native-sdk.dialog.saveFile\",\"payload\":{\"defaultName\":\"a.txt\"}}");
|
|
337
|
+
try harness.null_platform.finishDialog(2, .cancelled, "", .primary);
|
|
338
|
+
try deliverWakes(harness);
|
|
339
|
+
const saved = harness.null_platform.lastBridgeResponse();
|
|
340
|
+
try std.testing.expect(std.mem.indexOf(u8, saved, "\"id\":\"save\"") != null);
|
|
341
|
+
try std.testing.expect(std.mem.indexOf(u8, saved, "null") != null);
|
|
342
|
+
|
|
343
|
+
harness.null_platform.fail_next_dialog_request = true;
|
|
344
|
+
try sendBridge(harness, "{\"id\":\"broken\",\"command\":\"native-sdk.dialog.openFile\",\"payload\":{}}");
|
|
345
|
+
try deliverWakes(harness);
|
|
346
|
+
const broken = harness.null_platform.lastBridgeResponse();
|
|
347
|
+
try std.testing.expect(std.mem.indexOf(u8, broken, "\"id\":\"broken\"") != null);
|
|
348
|
+
try std.testing.expect(std.mem.indexOf(u8, broken, "\"internal_error\"") != null);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
test "built-in bridge dialogs on platforms without asynchronous dialogs answer at once" {
|
|
352
|
+
const harness = try startedBlocking();
|
|
353
|
+
defer harness.destroy(std.testing.allocator);
|
|
354
|
+
allowDialogBridge(harness);
|
|
355
|
+
|
|
356
|
+
try sendBridge(harness, "{\"id\":\"pick\",\"command\":\"native-sdk.dialog.openFile\",\"payload\":{\"title\":\"Pick\"}}");
|
|
357
|
+
const picked = harness.null_platform.lastBridgeResponse();
|
|
358
|
+
try std.testing.expect(std.mem.indexOf(u8, picked, "\"id\":\"pick\"") != null);
|
|
359
|
+
try std.testing.expect(std.mem.indexOf(u8, picked, "[\"/tmp/native-sdk-open.txt\"]") != null);
|
|
360
|
+
|
|
361
|
+
harness.null_platform.message_dialog_result = .secondary;
|
|
362
|
+
try sendBridge(harness, "{\"id\":\"ask\",\"command\":\"native-sdk.dialog.showMessage\",\"payload\":{\"message\":\"Delete?\",\"secondaryButton\":\"Keep\"}}");
|
|
363
|
+
try std.testing.expect(std.mem.indexOf(u8, harness.null_platform.lastBridgeResponse(), "\"secondary\"") != null);
|
|
364
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.open_dialog_count);
|
|
365
|
+
try std.testing.expectEqual(@as(usize, 1), harness.null_platform.message_dialog_count);
|
|
366
|
+
try std.testing.expectEqual(@as(usize, 0), harness.runtime.pendingDialogCount());
|
|
367
|
+
}
|