@m2c2kit/addons 0.3.14 → 0.3.16

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Composite, WebColors, Shape, Label, CanvasKitHelpers, M2EventType, MutablePath, Easings, Story, Transition, TransitionDirection, LabelHorizontalAlignmentMode, Scene, Dimensions, Sprite, Action } from '@m2c2kit/core';
1
+ import { M2c2KitHelpers, Composite, WebColors, Shape, EventStoreMode, Equal, Label, CanvasKitHelpers, M2EventType, Timer, MutablePath, Easings, Story, Transition, TransitionDirection, LabelHorizontalAlignmentMode, Scene, Dimensions, Sprite, Action } from '@m2c2kit/core';
2
2
 
3
3
  class Grid extends Composite {
4
4
  /**
@@ -13,16 +13,16 @@ class Grid extends Composite {
13
13
  */
14
14
  constructor(options) {
15
15
  super(options);
16
- this.compositeType = "grid";
16
+ this.compositeType = "Grid";
17
17
  // Grid options
18
- // TODO: make getter, setter for these so they can be changed after initial construction
19
- this.rows = 0;
20
- this.columns = 0;
21
- // default Grid is: transparent gray, red lines, line width 1
22
- this.gridBackgroundColor = [0, 0, 233, 0.25];
23
- this.gridLineColor = WebColors.Red;
24
- this.gridLineWidth = 1;
25
- this.gridChildren = new Array();
18
+ this._rows = 0;
19
+ this._columns = 0;
20
+ // default Grid is: transparent blue, red lines, line width 1
21
+ this._gridBackgroundColor = [0, 0, 255, 0.25];
22
+ this._gridLineColor = WebColors.Red;
23
+ this._gridLineWidth = 1;
24
+ this._gridChildren = new Array();
25
+ this.cellContainers = new Array();
26
26
  if (options.size) {
27
27
  this.size = options.size;
28
28
  } else {
@@ -32,19 +32,19 @@ class Grid extends Composite {
32
32
  if (options.rows >= 1) {
33
33
  this.rows = options.rows;
34
34
  } else {
35
- throw new Error("rows must be at least 1");
35
+ throw new Error("grid rows must be at least 1");
36
36
  }
37
37
  } else {
38
- throw new Error("rows must be specified");
38
+ throw new Error("grid rows must be specified");
39
39
  }
40
40
  if (options.columns) {
41
41
  if (options.columns >= 1) {
42
42
  this.columns = options.columns;
43
43
  } else {
44
- throw new Error("columns must be at least 1");
44
+ throw new Error("grid columns must be at least 1");
45
45
  }
46
46
  } else {
47
- throw new Error("columns must be specified");
47
+ throw new Error("grid columns must be specified");
48
48
  }
49
49
  if (options.backgroundColor) {
50
50
  this.gridBackgroundColor = options.backgroundColor;
@@ -57,56 +57,114 @@ class Grid extends Composite {
57
57
  }
58
58
  this.cellWidth = this.size.width / this.columns;
59
59
  this.cellHeight = this.size.height / this.rows;
60
+ this.saveNodeNewEvent();
61
+ }
62
+ get completeNodeOptions() {
63
+ return {
64
+ ...this.options,
65
+ ...this.getNodeOptions(),
66
+ ...this.getDrawableOptions(),
67
+ rows: this.rows,
68
+ columns: this.columns,
69
+ size: this.size,
70
+ backgroundColor: this.gridBackgroundColor,
71
+ gridLineWidth: this.gridLineWidth,
72
+ gridLineColor: this.gridLineColor
73
+ };
60
74
  }
61
75
  initialize() {
62
- super.removeAllChildren();
76
+ this.descendants.forEach((d) => {
77
+ if (d.parent === this) {
78
+ super.removeChild(d);
79
+ } else {
80
+ d.parent?.removeChild(d);
81
+ }
82
+ });
63
83
  this.gridBackground = new Shape({
64
- name: "_" + this.name + "-gridBackground",
84
+ name: "__" + this.name + "-gridRectangle",
65
85
  rect: { size: this.size },
66
- //size: this.size,
67
86
  fillColor: this.gridBackgroundColor,
68
87
  strokeColor: this.gridLineColor,
69
88
  lineWidth: this.gridLineWidth,
70
- isUserInteractionEnabled: this.isUserInteractionEnabled
89
+ isUserInteractionEnabled: this.isUserInteractionEnabled,
90
+ suppressEvents: true
71
91
  });
72
- this.addChild(this.gridBackground);
92
+ super.addChild(this.gridBackground);
73
93
  this.gridBackground.isUserInteractionEnabled = this.isUserInteractionEnabled;
74
94
  for (let col = 1; col < this.columns; col++) {
75
95
  const verticalLine = new Shape({
76
- name: "_" + this.name + "-gridVerticalLine-" + col,
96
+ name: "__" + this.name + "-gridVerticalLine-" + (col - 1),
77
97
  rect: {
78
98
  size: { width: this.gridLineWidth, height: this.size.height },
79
99
  origin: { x: -this.size.width / 2 + this.cellWidth * col, y: 0 }
80
100
  },
81
- fillColor: this.gridLineColor
101
+ fillColor: this.gridLineColor,
102
+ suppressEvents: true
82
103
  });
83
104
  this.gridBackground.addChild(verticalLine);
84
105
  }
85
106
  for (let row = 1; row < this.rows; row++) {
86
107
  const horizontalLine = new Shape({
87
- name: "_" + this.name + "-gridHorizontalLine-" + row,
108
+ name: "__" + this.name + "-gridHorizontalLine-" + (row - 1),
88
109
  rect: {
89
110
  size: { width: this.size.width, height: this.gridLineWidth },
90
111
  origin: { x: 0, y: -this.size.height / 2 + this.cellHeight * row }
91
112
  },
92
- fillColor: this.gridLineColor
113
+ fillColor: this.gridLineColor,
114
+ suppressEvents: true
93
115
  });
94
116
  this.gridBackground.addChild(horizontalLine);
95
117
  }
96
- if (this.gridChildren) {
118
+ this.cellContainers = new Array(this.rows).fill([]).map(() => new Array(this.columns));
119
+ for (let row = 0; row < this.rows; row++) {
120
+ for (let col = 0; col < this.columns; col++) {
121
+ const cellContainer = new Shape({
122
+ name: "__" + this.name + "-gridCellContainer-" + row + "-" + col,
123
+ rect: {
124
+ size: { width: this.cellWidth, height: this.cellHeight },
125
+ origin: {
126
+ x: -this.size.width / 2 + this.cellWidth * col + this.cellWidth / 2,
127
+ y: -this.size.height / 2 + this.cellHeight * row + this.cellHeight / 2
128
+ }
129
+ },
130
+ fillColor: WebColors.Transparent,
131
+ lineWidth: 0,
132
+ suppressEvents: true
133
+ });
134
+ this.gridBackground.addChild(cellContainer);
135
+ this.cellContainers[row][col] = cellContainer;
136
+ }
137
+ }
138
+ if (this.gridChildren.length > 0) {
97
139
  this.gridChildren.forEach((gridChild) => {
98
140
  if (!this.cellWidth || !this.cellHeight || !this.gridBackground) {
99
141
  throw new Error(
100
142
  "cellWidth, cellHeight, or gridBackground undefined or null"
101
143
  );
102
144
  }
103
- const x = -this.size.width / 2 + this.cellWidth / 2 + gridChild.column * this.cellWidth;
104
- const y = -this.size.height / 2 + this.cellHeight / 2 + gridChild.row * this.cellHeight;
105
- gridChild.node.position = {
106
- x: x + gridChild.node.position.x,
107
- y: y + gridChild.node.position.y
108
- };
109
- this.gridBackground.addChild(gridChild.node);
145
+ if (this.game.eventStore.mode === EventStoreMode.Replay) {
146
+ const childNode = [
147
+ ...this.game.nodes,
148
+ ...this.game.materializedNodes
149
+ ].find(
150
+ (n) => (
151
+ // gridChild.node is the uuid of the child node here!
152
+ n.uuid === gridChild.node
153
+ )
154
+ );
155
+ if (!childNode) {
156
+ throw new Error("grid: child node not found");
157
+ }
158
+ childNode?.parent?.removeChild(childNode);
159
+ this.cellContainers[gridChild.row][gridChild.column].addChild(
160
+ childNode
161
+ );
162
+ } else {
163
+ gridChild.node.parent?.removeChild(gridChild.node);
164
+ this.cellContainers[gridChild.row][gridChild.column].addChild(
165
+ gridChild.node
166
+ );
167
+ }
110
168
  });
111
169
  }
112
170
  this.needsInitialization = false;
@@ -120,6 +178,60 @@ class Grid extends Composite {
120
178
  set gridBackground(gridBackground) {
121
179
  this._gridBackground = gridBackground;
122
180
  }
181
+ /**
182
+ * note: below we do not have getter and setter for size because the getter
183
+ * and setter in M2Node will handle it.
184
+ */
185
+ get rows() {
186
+ return this._rows;
187
+ }
188
+ set rows(rows) {
189
+ if (Equal.value(this._rows, rows)) {
190
+ return;
191
+ }
192
+ this._rows = rows;
193
+ this.needsInitialization = true;
194
+ }
195
+ get columns() {
196
+ return this._columns;
197
+ }
198
+ set columns(columns) {
199
+ if (Equal.value(this._columns, columns)) {
200
+ return;
201
+ }
202
+ this._columns = columns;
203
+ this.needsInitialization = true;
204
+ }
205
+ get gridBackgroundColor() {
206
+ return this._gridBackgroundColor;
207
+ }
208
+ set gridBackgroundColor(backgroundColor) {
209
+ if (Equal.value(this._gridBackgroundColor, backgroundColor)) {
210
+ return;
211
+ }
212
+ this._gridBackgroundColor = backgroundColor;
213
+ this.needsInitialization = true;
214
+ }
215
+ get gridLineWidth() {
216
+ return this._gridLineWidth;
217
+ }
218
+ set gridLineWidth(gridLineWidth) {
219
+ if (Equal.value(this._gridLineWidth, gridLineWidth)) {
220
+ return;
221
+ }
222
+ this._gridLineWidth = gridLineWidth;
223
+ this.needsInitialization = true;
224
+ }
225
+ get gridLineColor() {
226
+ return this._gridLineColor;
227
+ }
228
+ set gridLineColor(gridLineColor) {
229
+ if (Equal.value(this._gridLineColor, gridLineColor)) {
230
+ return;
231
+ }
232
+ this._gridLineColor = gridLineColor;
233
+ this.needsInitialization = true;
234
+ }
123
235
  // all nodes that make up grid are added as children, so they
124
236
  // have their own dispose methods
125
237
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -168,25 +280,49 @@ class Grid extends Composite {
168
280
  child.warmup(canvas);
169
281
  });
170
282
  }
171
- // override M2Node.RemoveAllChildren() so that when RemoveAllChildren() is called on a Grid,
172
- // it removes only nodes added to the grid cells (what we call grid children), not the grid lines!
173
283
  /**
174
- * Removes all children from the grid, but retains grid lines.
284
+ * The child nodes that have been added to the grid.
285
+ *
286
+ * @remarks Do not set this property directly. Use the methods for adding
287
+ * and removing grid children, such as `addAtCell()`, `removeAllAtCell()`,
288
+ * `removeGridChild()`, and `removeAllGridChildren()`.
175
289
  */
176
- removeAllChildren() {
290
+ get gridChildren() {
291
+ return this._gridChildren;
292
+ }
293
+ set gridChildren(gridChildren) {
294
+ this._gridChildren = gridChildren;
295
+ this.needsInitialization = true;
296
+ if (this.game.eventStore.mode === EventStoreMode.Record) {
297
+ this.savePropertyChangeEvent(
298
+ "gridChildren",
299
+ this.gridChildren.map(
300
+ (gridChild) => ({
301
+ node: gridChild.node.uuid,
302
+ row: gridChild.row,
303
+ column: gridChild.column
304
+ })
305
+ )
306
+ );
307
+ }
308
+ }
309
+ /**
310
+ * Removes all grid children from the grid.
311
+ *
312
+ * @remarks This retains grid lines and grid appearance.
313
+ */
314
+ removeAllGridChildren() {
177
315
  if (this.gridChildren.length === 0) {
178
316
  return;
179
317
  }
180
318
  while (this.gridChildren.length) {
181
- const gridChild = this.gridChildren.pop();
182
- if (gridChild) {
183
- this.gridBackground.removeChild(gridChild.node);
184
- }
319
+ this.gridChildren = this.gridChildren.slice(0, -1);
185
320
  }
186
321
  this.needsInitialization = true;
187
322
  }
188
323
  /**
189
- * Adds a node to the grid at the specified row and column position.
324
+ * Adds a node as a grid child to the grid at the specified row and column
325
+ * position.
190
326
  *
191
327
  * @param node - node to add to the grid
192
328
  * @param row - row position within grid to add node; zero-based indexing
@@ -198,49 +334,68 @@ class Grid extends Composite {
198
334
  `warning: addAtCell() requested to add node at row ${row}, column ${column}. This is outside the bounds of grid ${this.name}, which is size ${this.rows}x${this.columns}. Note that addAtCell() uses zero-based indexing. AddAtCell() will proceed, but may draw nodes outside the grid`
199
335
  );
200
336
  }
201
- this.gridChildren.push({ node, row, column });
337
+ this.gridChildren = [
338
+ ...this.gridChildren,
339
+ { node, row, column }
340
+ ];
202
341
  this.needsInitialization = true;
203
342
  }
204
343
  /**
205
- * Removes all child nodes at the specified row and column position.
344
+ * Removes all grid child nodes at the specified row and column position.
206
345
  *
207
- * @param row - row position within grid at which to remove children; zero-based indexing
208
- * @param column - column position within grid at which to remove children; zero-based indexing
346
+ * @param row - row position within grid at which to remove grid children; zero-based indexing
347
+ * @param column - column position within grid at which to remove grid children; zero-based indexing
209
348
  */
210
349
  removeAllAtCell(row, column) {
211
- const gridChildrenToRemove = this.gridChildren.filter(
212
- (gridChild) => gridChild.row === row && gridChild.column === column
213
- );
214
- if (gridChildrenToRemove.length === 0) {
215
- return;
216
- }
217
- this.gridBackground.removeChildren(
218
- gridChildrenToRemove.map((gridChild) => gridChild.node)
219
- );
220
350
  this.gridChildren = this.gridChildren.filter(
221
351
  (gridChild) => gridChild.row !== row && gridChild.column !== column
222
352
  );
223
353
  this.needsInitialization = true;
224
354
  }
225
- // override M2Node.RemoveChild() so that when RemoveChild() is called on a Grid, it removes the
226
- // node from the gridBackground rectangle AND our grid's own list of children (in gridChildren)
227
355
  /**
228
- * Removes the child node from the grid.
356
+ * Removes the grid child node from the grid.
229
357
  *
230
358
  * @param node - node to remove
231
359
  */
232
- removeChild(node) {
233
- this.gridBackground.removeChild(node);
360
+ removeGridChild(node) {
234
361
  this.gridChildren = this.gridChildren.filter(
235
362
  (gridChild) => gridChild.node != node
236
363
  );
237
364
  this.needsInitialization = true;
238
365
  }
366
+ // The Grid manages its own children (background, lines, and cell
367
+ // containers). It is probably a mistake when the user tries to add or remove
368
+ // these children. The user probably meant to add or remove grid children
369
+ // instead. Warn the user about this.
370
+ addChild(child) {
371
+ console.warn(
372
+ "Grid.addChild() was called -- did you mean to call addAtCell() instead?"
373
+ );
374
+ super.addChild(child);
375
+ }
376
+ removeAllChildren() {
377
+ console.warn(
378
+ "Grid.removeAllChildren() was called -- did you mean to call removeAllGridChildren() instead?"
379
+ );
380
+ super.removeAllChildren();
381
+ }
382
+ removeChild(child) {
383
+ console.warn(
384
+ "Grid.removeChild() was called -- did you mean to call removeGridChild() instead?"
385
+ );
386
+ super.removeChild(child);
387
+ }
388
+ removeChildren(children) {
389
+ console.warn(
390
+ "Grid.removeChildren() was called -- did you mean to call removeGridChild() instead?"
391
+ );
392
+ super.removeChildren(children);
393
+ }
239
394
  }
395
+ M2c2KitHelpers.registerM2NodeClass(Grid);
240
396
 
241
397
  class Button extends Composite {
242
- // todo: add getters/setters so button can respond to changes in its options
243
- // todo: add default "behaviors" (?) like button click animation?
398
+ // TODO: add default "behaviors" (?) like button click animation?
244
399
  /**
245
400
  * A simple button of rectangle with text centered inside.
246
401
  *
@@ -252,23 +407,33 @@ class Button extends Composite {
252
407
  */
253
408
  constructor(options) {
254
409
  super(options);
255
- this.compositeType = "button";
410
+ this.compositeType = "Button";
411
+ this.isText = true;
256
412
  // Button options
257
413
  this._backgroundColor = WebColors.Black;
258
- this.size = { width: 200, height: 50 };
259
- this.cornerRadius = 9;
260
- this.fontSize = 20;
414
+ this._cornerRadius = 9;
415
+ this._fontSize = 20;
261
416
  this._text = "";
262
417
  this._fontColor = WebColors.White;
418
+ this._interpolation = {};
419
+ this._localize = true;
263
420
  if (options.text) {
264
421
  this.text = options.text;
265
422
  }
266
423
  if (options.size) {
267
424
  this.size = options.size;
425
+ } else {
426
+ this.size = { width: 200, height: 50 };
268
427
  }
269
428
  if (options.cornerRadius !== void 0) {
270
429
  this.cornerRadius = options.cornerRadius;
271
430
  }
431
+ if (options.fontName) {
432
+ this.fontName = options.fontName;
433
+ }
434
+ if (options.fontNames) {
435
+ this.fontNames = options.fontNames;
436
+ }
272
437
  if (options.fontSize !== void 0) {
273
438
  this.fontSize = options.fontSize;
274
439
  }
@@ -278,6 +443,25 @@ class Button extends Composite {
278
443
  if (options.backgroundColor) {
279
444
  this.backgroundColor = options.backgroundColor;
280
445
  }
446
+ if (options.interpolation) {
447
+ this.interpolation = options.interpolation;
448
+ }
449
+ if (options.localize !== void 0) {
450
+ this.localize = options.localize;
451
+ }
452
+ this.saveNodeNewEvent();
453
+ }
454
+ get completeNodeOptions() {
455
+ return {
456
+ ...this.options,
457
+ ...this.getNodeOptions(),
458
+ ...this.getDrawableOptions(),
459
+ ...this.getTextOptions(),
460
+ size: this.size,
461
+ cornerRadius: this.cornerRadius,
462
+ backgroundColor: this.backgroundColor,
463
+ fontNames: this.fontNames
464
+ };
281
465
  }
282
466
  initialize() {
283
467
  this.removeAllChildren();
@@ -292,15 +476,23 @@ class Button extends Composite {
292
476
  );
293
477
  this.backgroundPaint.setStyle(this.canvasKit.PaintStyle.Fill);
294
478
  const buttonRectangle = new Shape({
479
+ name: "__" + this.name + "-buttonRectangle",
295
480
  rect: { size: this.size },
296
481
  cornerRadius: this.cornerRadius,
297
- fillColor: this._backgroundColor
482
+ fillColor: this._backgroundColor,
483
+ suppressEvents: true
298
484
  });
299
485
  this.addChild(buttonRectangle);
300
486
  const buttonLabel = new Label({
487
+ name: "__" + this.name + "-buttonLabel",
301
488
  text: this.text,
489
+ localize: this.localize,
490
+ interpolation: this.interpolation,
491
+ fontName: this.fontName,
492
+ fontNames: this.fontNames,
302
493
  fontSize: this.fontSize,
303
- fontColor: this.fontColor
494
+ fontColor: this.fontColor,
495
+ suppressEvents: true
304
496
  });
305
497
  buttonRectangle.addChild(buttonLabel);
306
498
  this.needsInitialization = false;
@@ -312,22 +504,101 @@ class Button extends Composite {
312
504
  return this._text;
313
505
  }
314
506
  set text(text) {
507
+ if (Equal.value(this._text, text)) {
508
+ return;
509
+ }
315
510
  this._text = text;
316
511
  this.needsInitialization = true;
512
+ this.savePropertyChangeEvent("text", text);
317
513
  }
318
514
  get backgroundColor() {
319
515
  return this._backgroundColor;
320
516
  }
321
517
  set backgroundColor(backgroundColor) {
518
+ if (Equal.value(this._backgroundColor, backgroundColor)) {
519
+ return;
520
+ }
322
521
  this._backgroundColor = backgroundColor;
323
522
  this.needsInitialization = true;
523
+ this.savePropertyChangeEvent("backgroundColor", backgroundColor);
324
524
  }
325
525
  get fontColor() {
326
526
  return this._fontColor;
327
527
  }
328
528
  set fontColor(fontColor) {
529
+ if (Equal.value(this._fontColor, fontColor)) {
530
+ return;
531
+ }
329
532
  this._fontColor = fontColor;
330
533
  this.needsInitialization = true;
534
+ this.savePropertyChangeEvent("fontColor", fontColor);
535
+ }
536
+ get fontName() {
537
+ return this._fontName;
538
+ }
539
+ set fontName(fontName) {
540
+ if (this._fontName === fontName) {
541
+ return;
542
+ }
543
+ this._fontName = fontName;
544
+ this.needsInitialization = true;
545
+ this.savePropertyChangeEvent("fontName", fontName);
546
+ }
547
+ get fontNames() {
548
+ return this._fontNames;
549
+ }
550
+ set fontNames(fontNames) {
551
+ if (Equal.value(this._fontNames, fontNames)) {
552
+ return;
553
+ }
554
+ this._fontNames = fontNames;
555
+ this.needsInitialization = true;
556
+ this.savePropertyChangeEvent("fontNames", fontNames);
557
+ }
558
+ get cornerRadius() {
559
+ return this._cornerRadius;
560
+ }
561
+ set cornerRadius(cornerRadius) {
562
+ if (Equal.value(this._cornerRadius, cornerRadius)) {
563
+ return;
564
+ }
565
+ this._cornerRadius = cornerRadius;
566
+ this.needsInitialization = true;
567
+ this.savePropertyChangeEvent("cornerRadius", cornerRadius);
568
+ }
569
+ get fontSize() {
570
+ return this._fontSize;
571
+ }
572
+ set fontSize(fontSize) {
573
+ if (Equal.value(this._fontSize, fontSize)) {
574
+ return;
575
+ }
576
+ this._fontSize = fontSize;
577
+ this.needsInitialization = true;
578
+ this.savePropertyChangeEvent("fontSize", fontSize);
579
+ }
580
+ get interpolation() {
581
+ return this._interpolation;
582
+ }
583
+ set interpolation(interpolation) {
584
+ if (Equal.value(this._interpolation, interpolation)) {
585
+ return;
586
+ }
587
+ this._interpolation = interpolation;
588
+ Object.freeze(this._interpolation);
589
+ this.needsInitialization = true;
590
+ this.savePropertyChangeEvent("interpolation", interpolation);
591
+ }
592
+ get localize() {
593
+ return this._localize;
594
+ }
595
+ set localize(localize) {
596
+ if (Equal.value(this._localize, localize)) {
597
+ return;
598
+ }
599
+ this._localize = localize;
600
+ this.needsInitialization = true;
601
+ this.savePropertyChangeEvent("localize", localize);
331
602
  }
332
603
  /**
333
604
  * Duplicates a node using deep copy.
@@ -348,7 +619,11 @@ class Button extends Composite {
348
619
  cornerRadius: this.cornerRadius,
349
620
  backgroundColor: this.backgroundColor,
350
621
  fontColor: this.fontColor,
351
- name: newName
622
+ name: newName,
623
+ localize: this.localize,
624
+ interpolation: JSON.parse(JSON.stringify(this.interpolation)),
625
+ fontName: this.fontName,
626
+ fontNames: JSON.parse(JSON.stringify(this.fontNames))
352
627
  });
353
628
  if (this.children.length > 0) {
354
629
  dest.children = this.children.map((child) => {
@@ -372,6 +647,7 @@ class Button extends Composite {
372
647
  });
373
648
  }
374
649
  }
650
+ M2c2KitHelpers.registerM2NodeClass(Button);
375
651
 
376
652
  var DialogResult = /* @__PURE__ */ ((DialogResult2) => {
377
653
  DialogResult2["Dismiss"] = "Dismiss";
@@ -391,9 +667,9 @@ class Dialog extends Composite {
391
667
  this.contentText = "";
392
668
  this.positiveButtonText = "";
393
669
  this.negativeButtonText = "";
670
+ this._fontColor = WebColors.White;
394
671
  this.zPosition = Number.MAX_VALUE;
395
672
  this.hidden = true;
396
- this._fontColor = WebColors.White;
397
673
  if (!options) {
398
674
  return;
399
675
  }
@@ -427,7 +703,7 @@ class Dialog extends Composite {
427
703
  }
428
704
  onDialogResult(callback, options) {
429
705
  const eventListener = {
430
- type: M2EventType.CompositeCustom,
706
+ type: M2EventType.Composite,
431
707
  compositeType: "DialogResult",
432
708
  nodeUuid: this.uuid,
433
709
  callback
@@ -443,10 +719,10 @@ class Dialog extends Composite {
443
719
  this.removeAllChildren();
444
720
  const overlay = new Shape({
445
721
  rect: {
446
- width: Globals.canvasCssWidth,
447
- height: Globals.canvasCssHeight,
448
- x: Globals.canvasCssWidth / 2,
449
- y: Globals.canvasCssHeight / 2
722
+ width: m2c2Globals.canvasCssWidth,
723
+ height: m2c2Globals.canvasCssHeight,
724
+ x: m2c2Globals.canvasCssWidth / 2,
725
+ y: m2c2Globals.canvasCssHeight / 2
450
726
  },
451
727
  fillColor: [0, 0, 0, this.overlayAlpha],
452
728
  zPosition: -1,
@@ -456,12 +732,14 @@ class Dialog extends Composite {
456
732
  e.handled = true;
457
733
  this.hidden = true;
458
734
  if (this.eventListeners.length > 0) {
459
- this.eventListeners.filter((listener) => listener.type === M2EventType.CompositeCustom).forEach((listener) => {
735
+ this.eventListeners.filter((listener) => listener.type === M2EventType.Composite).forEach((listener) => {
460
736
  const dialogEvent = {
461
- type: M2EventType.CompositeCustom,
737
+ type: M2EventType.Composite,
462
738
  target: this,
463
739
  handled: false,
464
- dialogResult: "Dismiss" /* Dismiss */
740
+ dialogResult: "Dismiss" /* Dismiss */,
741
+ timestamp: Timer.now(),
742
+ iso8601Timestamp: (/* @__PURE__ */ new Date()).toISOString()
465
743
  };
466
744
  listener.callback(dialogEvent);
467
745
  });
@@ -472,8 +750,8 @@ class Dialog extends Composite {
472
750
  rect: {
473
751
  width: 300,
474
752
  height: 150,
475
- x: Globals.canvasCssWidth / 2,
476
- y: Globals.canvasCssHeight / 2
753
+ x: m2c2Globals.canvasCssWidth / 2,
754
+ y: m2c2Globals.canvasCssHeight / 2
477
755
  },
478
756
  cornerRadius: this.cornerRadius,
479
757
  fillColor: this.backgroundColor,
@@ -504,12 +782,14 @@ class Dialog extends Composite {
504
782
  e.handled = true;
505
783
  this.hidden = true;
506
784
  if (this.eventListeners.length > 0) {
507
- this.eventListeners.filter((listener) => listener.type === M2EventType.CompositeCustom).forEach((listener) => {
785
+ this.eventListeners.filter((listener) => listener.type === M2EventType.Composite).forEach((listener) => {
508
786
  const dialogEvent = {
509
- type: M2EventType.CompositeCustom,
787
+ type: M2EventType.Composite,
510
788
  target: this,
511
789
  handled: false,
512
- dialogResult: "Negative" /* Negative */
790
+ dialogResult: "Negative" /* Negative */,
791
+ timestamp: Timer.now(),
792
+ iso8601Timestamp: (/* @__PURE__ */ new Date()).toISOString()
513
793
  };
514
794
  listener.callback(dialogEvent);
515
795
  });
@@ -526,12 +806,14 @@ class Dialog extends Composite {
526
806
  e.handled = true;
527
807
  this.hidden = true;
528
808
  if (this.eventListeners.length > 0) {
529
- this.eventListeners.filter((listener) => listener.type === M2EventType.CompositeCustom).forEach((listener) => {
809
+ this.eventListeners.filter((listener) => listener.type === M2EventType.Composite).forEach((listener) => {
530
810
  const dialogEvent = {
531
- type: M2EventType.CompositeCustom,
811
+ type: M2EventType.Composite,
532
812
  target: this,
533
813
  handled: false,
534
- dialogResult: "Positive" /* Positive */
814
+ dialogResult: "Positive" /* Positive */,
815
+ timestamp: Timer.now(),
816
+ iso8601Timestamp: (/* @__PURE__ */ new Date()).toISOString()
535
817
  };
536
818
  listener.callback(dialogEvent);
537
819
  });
@@ -555,6 +837,13 @@ class Dialog extends Composite {
555
837
  this._fontColor = fontColor;
556
838
  this.needsInitialization = true;
557
839
  }
840
+ get hidden() {
841
+ return this._hidden;
842
+ }
843
+ set hidden(hidden) {
844
+ this._hidden = hidden;
845
+ this.needsInitialization;
846
+ }
558
847
  /**
559
848
  * Duplicates a node using deep copy.
560
849
  *
@@ -566,7 +855,7 @@ class Dialog extends Composite {
566
855
  * provided, name will be the new uuid
567
856
  */
568
857
  duplicate(newName) {
569
- throw new Error("duplicate not implemented");
858
+ throw new Error(`duplicate not implemented. ${newName}`);
570
859
  }
571
860
  update() {
572
861
  super.update();
@@ -617,6 +906,7 @@ class DrawPad extends Composite {
617
906
  * of all interactions with each DrawPadStroke.
618
907
  */
619
908
  this.strokes = new Array();
909
+ this.originalOptions = JSON.parse(JSON.stringify(options));
620
910
  if (options.isUserInteractionEnabled === void 0) {
621
911
  this.isUserInteractionEnabled = true;
622
912
  }
@@ -648,6 +938,15 @@ class DrawPad extends Composite {
648
938
  if (options.continuousDrawingOnlyExceptionDistance !== void 0) {
649
939
  this.continuousDrawingOnlyExceptionDistance = options.continuousDrawingOnlyExceptionDistance;
650
940
  }
941
+ this.saveNodeNewEvent();
942
+ }
943
+ get completeNodeOptions() {
944
+ return {
945
+ ...this.options,
946
+ ...this.getNodeOptions(),
947
+ ...this.getDrawableOptions(),
948
+ ...this.originalOptions
949
+ };
651
950
  }
652
951
  initialize() {
653
952
  this.initializeDrawShape();
@@ -670,7 +969,8 @@ class DrawPad extends Composite {
670
969
  if (!this.drawArea) {
671
970
  this.drawArea = new Shape({
672
971
  rect: { size: this.size },
673
- isUserInteractionEnabled: true
972
+ isUserInteractionEnabled: true,
973
+ suppressEvents: true
674
974
  });
675
975
  this.addChild(this.drawArea);
676
976
  this.drawArea.onTapDown((e) => {
@@ -714,7 +1014,8 @@ class DrawPad extends Composite {
714
1014
  type: DrawPadEventType.StrokeStart,
715
1015
  target: this,
716
1016
  handled: false,
717
- position: e.point
1017
+ position: e.point,
1018
+ ...M2c2KitHelpers.createTimestamps()
718
1019
  };
719
1020
  this.strokes.push([
720
1021
  {
@@ -745,7 +1046,8 @@ class DrawPad extends Composite {
745
1046
  type: DrawPadEventType.StrokeMove,
746
1047
  target: this,
747
1048
  handled: false,
748
- position: interpolatedPoint
1049
+ position: interpolatedPoint,
1050
+ ...M2c2KitHelpers.createTimestamps()
749
1051
  };
750
1052
  this.strokes[strokeCount - 1].push({
751
1053
  type: DrawPadEventType.StrokeMove,
@@ -775,7 +1077,8 @@ class DrawPad extends Composite {
775
1077
  type: DrawPadEventType.StrokeEnd,
776
1078
  position: this.strokes[strokeCount - 1][strokeInteractionCount - 1].position,
777
1079
  target: this,
778
- handled: false
1080
+ handled: false,
1081
+ ...M2c2KitHelpers.createTimestamps()
779
1082
  };
780
1083
  this.strokes[strokeCount - 1].push({
781
1084
  type: DrawPadEventType.StrokeEnd,
@@ -803,7 +1106,8 @@ class DrawPad extends Composite {
803
1106
  type: DrawPadEventType.StrokeEnd,
804
1107
  position: this.strokes[strokeCount - 1][strokeInteractionCount - 1].position,
805
1108
  target: this,
806
- handled: false
1109
+ handled: false,
1110
+ ...M2c2KitHelpers.createTimestamps()
807
1111
  };
808
1112
  this.strokes[strokeCount - 1].push({
809
1113
  type: DrawPadEventType.StrokeEnd,
@@ -831,7 +1135,8 @@ class DrawPad extends Composite {
831
1135
  type: DrawPadEventType.StrokeMove,
832
1136
  target: this,
833
1137
  handled: false,
834
- position: e.point
1138
+ position: e.point,
1139
+ ...M2c2KitHelpers.createTimestamps()
835
1140
  };
836
1141
  const strokeCount = this.strokes.length;
837
1142
  this.strokes[strokeCount - 1].push({
@@ -986,7 +1291,8 @@ class DrawPad extends Composite {
986
1291
  node.isStrokeWithinBounds = true;
987
1292
  const drawPadItemEvent = {
988
1293
  type: DrawPadItemEventType.StrokeEnter,
989
- target: node
1294
+ target: node,
1295
+ ...M2c2KitHelpers.createTimestamps()
990
1296
  };
991
1297
  this.raiseDrawPadItemEvent(node, drawPadItemEvent);
992
1298
  }
@@ -998,7 +1304,8 @@ class DrawPad extends Composite {
998
1304
  node.isStrokeWithinBounds = true;
999
1305
  const drawPadItemEvent = {
1000
1306
  type: DrawPadItemEventType.StrokeEnter,
1001
- target: node
1307
+ target: node,
1308
+ ...M2c2KitHelpers.createTimestamps()
1002
1309
  };
1003
1310
  this.raiseDrawPadItemEvent(node, drawPadItemEvent);
1004
1311
  }
@@ -1010,7 +1317,8 @@ class DrawPad extends Composite {
1010
1317
  node.isStrokeWithinBounds = false;
1011
1318
  const drawPadItemEvent = {
1012
1319
  type: DrawPadItemEventType.StrokeLeave,
1013
- target: node
1320
+ target: node,
1321
+ ...M2c2KitHelpers.createTimestamps()
1014
1322
  };
1015
1323
  this.raiseDrawPadItemEvent(node, drawPadItemEvent);
1016
1324
  }
@@ -1021,7 +1329,8 @@ class DrawPad extends Composite {
1021
1329
  node.isStrokeWithinBounds = false;
1022
1330
  const drawPadItemEvent = {
1023
1331
  type: DrawPadItemEventType.StrokeLeave,
1024
- target: node
1332
+ target: node,
1333
+ ...M2c2KitHelpers.createTimestamps()
1025
1334
  };
1026
1335
  this.raiseDrawPadItemEvent(node, drawPadItemEvent);
1027
1336
  }
@@ -1044,10 +1353,10 @@ class DrawPad extends Composite {
1044
1353
  if (!drawArea) {
1045
1354
  throw new Error("DrawPad.takeScreenshot(): no drawArea");
1046
1355
  }
1047
- const sx = (drawArea.absolutePosition.x - drawArea.size.width / 2) * Globals.canvasScale;
1048
- const sy = (drawArea.absolutePosition.y - drawArea.size.height / 2) * Globals.canvasScale;
1049
- const sw = drawArea.size.width * Globals.canvasScale;
1050
- const sh = drawArea.size.height * Globals.canvasScale;
1356
+ const sx = (drawArea.absolutePosition.x - drawArea.size.width / 2) * m2c2Globals.canvasScale;
1357
+ const sy = (drawArea.absolutePosition.y - drawArea.size.height / 2) * m2c2Globals.canvasScale;
1358
+ const sw = drawArea.size.width * m2c2Globals.canvasScale;
1359
+ const sh = drawArea.size.height * m2c2Globals.canvasScale;
1051
1360
  const imageInfo = {
1052
1361
  alphaType: this.game.canvasKit.AlphaType.Unpremul,
1053
1362
  colorType: this.game.canvasKit.ColorType.RGBA_8888,
@@ -1186,9 +1495,10 @@ class DrawPad extends Composite {
1186
1495
  this.needsInitialization = true;
1187
1496
  }
1188
1497
  duplicate(newName) {
1189
- throw new Error("DrawPad.duplicate(): Method not implemented.");
1498
+ throw new Error(`DrawPad.duplicate(): Method not implemented. ${newName}`);
1190
1499
  }
1191
1500
  }
1501
+ M2c2KitHelpers.registerM2NodeClass(DrawPad);
1192
1502
 
1193
1503
  class VirtualKeyboard extends Composite {
1194
1504
  /**
@@ -1198,8 +1508,15 @@ class VirtualKeyboard extends Composite {
1198
1508
  */
1199
1509
  constructor(options) {
1200
1510
  super(options);
1201
- this.rowsConfiguration = new Array();
1511
+ this.compositeType = "VirtualKeyboard";
1512
+ this.keyboardRows = new Array();
1202
1513
  this.shiftActivated = false;
1514
+ this.keyShapes = new Array();
1515
+ this.keyLabels = new Array();
1516
+ this.originalOptions = JSON.parse(JSON.stringify(options));
1517
+ if (options.isUserInteractionEnabled === void 0) {
1518
+ this._isUserInteractionEnabled = true;
1519
+ }
1203
1520
  this.size = options.size;
1204
1521
  this.position = options.position ?? { x: 0, y: 0 };
1205
1522
  this.keyboardHorizontalPaddingPercent = options.keyboardHorizontalPaddingPercent ?? 0.02;
@@ -1207,11 +1524,12 @@ class VirtualKeyboard extends Composite {
1207
1524
  this.keyHorizontalPaddingPercent = options.keyHorizontalPaddingPercent ?? 0.1;
1208
1525
  this.keyVerticalPaddingPercent = options.keyVerticalPaddingPercent ?? 0.1;
1209
1526
  if (options.rows !== void 0) {
1210
- this.rowsConfiguration = options.rows.map((row) => {
1527
+ this.keyboardRows = options.rows.map((row) => {
1211
1528
  const internalRow = row.map((key) => {
1212
1529
  if (key instanceof Object && !Array.isArray(key)) {
1213
1530
  const internalKeyConfig = key;
1214
1531
  if (key.keyIconShapeOptions) {
1532
+ key.keyIconShapeOptions.suppressEvents = true;
1215
1533
  internalKeyConfig.keyIcon = new Shape(key.keyIconShapeOptions);
1216
1534
  internalKeyConfig.keyIconShapeOptions = void 0;
1217
1535
  }
@@ -1233,156 +1551,48 @@ class VirtualKeyboard extends Composite {
1233
1551
  this.specialKeyDownColor = options.specialKeyDownColor ?? WebColors.LightSteelBlue;
1234
1552
  this.backgroundColor = options.backgroundColor ?? [242, 240, 244, 1];
1235
1553
  this.showKeyDownPreview = options.showKeyDownPreview ?? true;
1554
+ this.saveNodeNewEvent();
1555
+ }
1556
+ get completeNodeOptions() {
1557
+ return {
1558
+ ...this.options,
1559
+ ...this.getNodeOptions(),
1560
+ ...this.getDrawableOptions(),
1561
+ ...this.originalOptions
1562
+ };
1236
1563
  }
1237
1564
  initialize() {
1238
- if (this.rowsConfiguration.length === 0) {
1239
- const numKeys = [
1240
- ["1", "!"],
1241
- ["2", "@"],
1242
- ["3", "#"],
1243
- ["4", "$"],
1244
- ["5", "%"],
1245
- ["6", "^"],
1246
- ["7", "&"],
1247
- ["8", "*"],
1248
- ["9", "("],
1249
- ["0", ")"]
1250
- ];
1251
- const row1 = [
1252
- "q",
1253
- "w",
1254
- "e",
1255
- "r",
1256
- "t",
1257
- "y",
1258
- "u",
1259
- "i",
1260
- "o",
1261
- "p"
1262
- ];
1263
- const row2 = [
1264
- "a",
1265
- "s",
1266
- "d",
1267
- "f",
1268
- "g",
1269
- "h",
1270
- "j",
1271
- "k",
1272
- "l"
1273
- ];
1274
- const shiftArrowShapeOptions = {
1275
- path: {
1276
- // Public Domain from https://www.freesvg.org
1277
- pathString: "m288-6.6849e-14 -288 288h144v288h288v-288h144l-288-288z",
1278
- width: 24
1279
- },
1280
- lineWidth: 2,
1281
- strokeColor: WebColors.Black,
1282
- fillColor: WebColors.Transparent
1283
- };
1284
- const backspaceShapeOptions = {
1285
- path: {
1286
- // CC0 from https://www.svgrepo.com
1287
- pathString: "M10.625 5.09 0 22.09l10.625 17H44.18v-34H10.625zm31.555 32H11.734l-9.375-15 9.375-15H42.18v30zm-23.293-6.293 7.293-7.293 7.293 7.293 1.414-1.414-7.293-7.293 7.293-7.293-1.414-1.414-7.293 7.293-7.293-7.293-1.414 1.414 7.293 7.293-7.293 7.293",
1288
- width: 24
1289
- },
1290
- lineWidth: 1,
1291
- strokeColor: WebColors.Black,
1292
- fillColor: WebColors.Red
1293
- };
1294
- const row3 = [
1295
- {
1296
- code: "Shift",
1297
- isShift: true,
1298
- widthRatio: 1.5,
1299
- keyIcon: new Shape(shiftArrowShapeOptions)
1300
- },
1301
- "z",
1302
- "x",
1303
- "c",
1304
- "v",
1305
- "b",
1306
- "n",
1307
- "m",
1308
- {
1309
- code: "Backspace",
1310
- widthRatio: 1.5,
1311
- keyIcon: new Shape(backspaceShapeOptions)
1312
- }
1313
- ];
1314
- const row4 = [
1315
- { code: " ", labelText: "SPACE", widthRatio: 5 }
1316
- ];
1317
- const keyboardRows = [
1318
- numKeys,
1319
- row1,
1320
- row2,
1321
- row3,
1322
- row4
1323
- ];
1324
- this.rowsConfiguration = keyboardRows;
1325
- this.keysPerRow = this.rowsConfiguration.reduce(
1565
+ if (this.game.eventStore.mode === EventStoreMode.Replay) {
1566
+ this._isUserInteractionEnabled = false;
1567
+ }
1568
+ if (this.keyboardRows.length === 0) {
1569
+ this.keyboardRows = this.createDefaultKeyboardRows();
1570
+ this.keysPerRow = this.keyboardRows.reduce(
1326
1571
  (max, row) => Math.max(max, row.length),
1327
1572
  0
1328
1573
  );
1329
- this.fontSize = this.size.height / this.rowsConfiguration.length / 2.5;
1574
+ this.fontSize = this.size.height / this.keyboardRows.length / 2.5;
1330
1575
  }
1331
1576
  const keyboardRectangle = new Shape({
1332
1577
  rect: { size: this.size },
1333
- fillColor: this.backgroundColor
1578
+ fillColor: this.backgroundColor,
1579
+ suppressEvents: true
1334
1580
  });
1335
1581
  this.addChild(keyboardRectangle);
1336
- const rows = this.rowsConfiguration.map((row) => {
1337
- return row.map((key) => {
1338
- let widthRatio = 1;
1339
- const heightRatio = 1;
1340
- let code;
1341
- let label;
1342
- let labelShifted;
1343
- let keyIcon;
1344
- let isShift = false;
1345
- if (typeof key === "string") {
1346
- code = key;
1347
- if (this.capitalLettersOnly) {
1348
- label = code.toUpperCase();
1349
- } else {
1350
- label = code;
1351
- }
1352
- labelShifted = code.toUpperCase();
1353
- } else if (Array.isArray(key)) {
1354
- code = key[0];
1355
- label = code;
1356
- labelShifted = key[1];
1357
- } else {
1358
- code = key.code;
1359
- label = key.labelText ?? "";
1360
- labelShifted = key.labelTextShifted ?? label;
1361
- widthRatio = key.widthRatio ?? 1;
1362
- keyIcon = key.keyIcon;
1363
- isShift = key.isShift ?? false;
1364
- }
1365
- return {
1366
- widthRatio,
1367
- heightRatio,
1368
- code,
1369
- labelText: label,
1370
- labelTextShifted: labelShifted,
1371
- keyIcon,
1372
- isShift
1373
- };
1374
- });
1375
- });
1582
+ const keyboard = this.internalKeyboardRowsToInternalKeyboardConfiguration(
1583
+ this.keyboardRows
1584
+ );
1376
1585
  const keyboardOrigin = {
1377
1586
  x: -keyboardRectangle.size.width / 2,
1378
1587
  y: -keyboardRectangle.size.height / 2
1379
1588
  };
1380
1589
  const keyboardVerticalPadding = (this.keyboardVerticalPaddingPercent ?? 0.025) * this.size.height;
1381
1590
  const keyboardHorizontalPadding = (this.keyboardHorizontalPaddingPercent ?? 0.02) * this.size.width;
1382
- const keyBoxHeight = (this.size.height - 2 * keyboardVerticalPadding) / rows.length;
1591
+ const keyBoxHeight = (this.size.height - 2 * keyboardVerticalPadding) / keyboard.length;
1383
1592
  const keyBoxWidth = (this.size.width - 2 * keyboardHorizontalPadding) / this.keysPerRow;
1384
- for (let r = 0; r < rows.length; r++) {
1385
- const row = rows[r];
1593
+ this.keyShapes = [];
1594
+ for (let r = 0; r < keyboard.length; r++) {
1595
+ const row = keyboard[r];
1386
1596
  const rowSumKeyWidths = row.reduce(
1387
1597
  (sum, key) => sum + (key.widthRatio ?? 1),
1388
1598
  0
@@ -1393,7 +1603,7 @@ class VirtualKeyboard extends Composite {
1393
1603
  }
1394
1604
  for (let k = 0; k < row.length; k++) {
1395
1605
  const key = row[k];
1396
- if (this.hiddenKeys?.split(",").map((s) => s.trim()).includes(key.code)) {
1606
+ if (this.hiddenKeys?.split(",").map((s) => s === " " ? " " : s.trim()).includes(key.code)) {
1397
1607
  continue;
1398
1608
  }
1399
1609
  const keyBoxWidthsSoFar = row.slice(0, k).reduce((sum, key2) => sum + (key2.widthRatio ?? 1), 0) * keyBoxWidth;
@@ -1410,7 +1620,8 @@ class VirtualKeyboard extends Composite {
1410
1620
  position: {
1411
1621
  x: extraPadding + keyboardOrigin.x + keyboardHorizontalPadding + keyBoxWidthsSoFar + (key.widthRatio ?? 1) * keyBoxWidth / 2,
1412
1622
  y: keyboardOrigin.y + keyboardVerticalPadding + r * keyBoxHeight + keyBoxHeight / 2
1413
- }
1623
+ },
1624
+ suppressEvents: true
1414
1625
  });
1415
1626
  const keyWidth = keyBoxWidth * (key.widthRatio ?? 1) - 2 * this.keyHorizontalPaddingPercent * keyBoxWidth;
1416
1627
  const keyHeight = keyBoxHeight - (key.heightRatio ?? 1) - 2 * this.keyVerticalPaddingPercent * keyBoxHeight;
@@ -1419,163 +1630,50 @@ class VirtualKeyboard extends Composite {
1419
1630
  cornerRadius: 4,
1420
1631
  fillColor: this.keyColor,
1421
1632
  lineWidth: 0,
1422
- isUserInteractionEnabled: true
1633
+ isUserInteractionEnabled: this.isUserInteractionEnabled,
1634
+ suppressEvents: true
1423
1635
  });
1636
+ keyShape.userData = { code: key.code };
1424
1637
  keyBox.addChild(keyShape);
1425
- keyShape.onTapUp((tapEvent) => {
1426
- let keyAsString = "";
1427
- if (!key.isShift) {
1428
- if (this.shiftActivated) {
1429
- this.shiftActivated = false;
1430
- if (this.shiftKeyShape) {
1431
- this.shiftKeyShape.fillColor = this.keyColor;
1432
- if (key.keyIcon) {
1433
- key.keyIcon.fillColor = WebColors.Transparent;
1434
- }
1435
- }
1436
- rows.flatMap((k2) => k2).forEach((k2) => {
1437
- if (k2.keyLabel?.text !== void 0) {
1438
- k2.keyLabel.text = k2.labelText ?? "";
1439
- }
1440
- });
1441
- keyAsString = key.labelTextShifted ?? key.code;
1442
- } else {
1443
- keyAsString = key.labelText ?? key.code;
1444
- }
1445
- if (key.code === " " || key.code === "Backspace") {
1446
- keyAsString = key.code;
1447
- }
1448
- keyShape.fillColor = this.keyColor;
1449
- } else {
1450
- if (!this.shiftActivated) {
1451
- keyShape.fillColor = this.keyColor;
1452
- } else {
1453
- keyShape.fillColor = this.specialKeyDownColor;
1454
- }
1455
- keyAsString = key.code;
1456
- }
1457
- letterCircle.hidden = true;
1458
- if (this.eventListeners.length > 0) {
1459
- this.eventListeners.filter(
1460
- (listener) => listener.type === M2EventType.CompositeCustom && listener.compositeType === "VirtualKeyboardKeyUp"
1461
- ).forEach((listener) => {
1462
- const virtualKeyboardEvent = {
1463
- type: M2EventType.CompositeCustom,
1464
- target: this,
1465
- handled: false,
1466
- key: keyAsString,
1467
- code: key.code,
1468
- shiftKey: this.shiftActivated,
1469
- keyTapMetadata: {
1470
- size: keyShape.size,
1471
- point: tapEvent.point,
1472
- buttons: tapEvent.buttons
1473
- }
1474
- };
1475
- listener.callback(virtualKeyboardEvent);
1476
- });
1477
- }
1478
- });
1479
- keyShape.onTapDown((tapEvent) => {
1480
- if (key.isShift) {
1481
- this.shiftActivated = !this.shiftActivated;
1482
- if (this.shiftActivated) {
1483
- keyShape.fillColor = this.specialKeyDownColor;
1484
- if (key.keyIcon) {
1485
- key.keyIcon.fillColor = WebColors.Black;
1486
- }
1487
- rows.flatMap((k2) => k2).forEach((k2) => {
1488
- if (k2.keyLabel?.text !== void 0) {
1489
- k2.keyLabel.text = k2.labelTextShifted ?? k2.labelText ?? "";
1490
- }
1491
- });
1492
- } else {
1493
- keyShape.fillColor = this.keyColor;
1494
- if (key.keyIcon) {
1495
- key.keyIcon.fillColor = WebColors.Transparent;
1496
- }
1497
- rows.flatMap((k2) => k2).forEach((k2) => {
1498
- if (k2.keyLabel?.text !== void 0) {
1499
- k2.keyLabel.text = k2.labelText ?? "";
1500
- }
1501
- });
1502
- }
1503
- }
1504
- let keyAsString = "";
1505
- if (key.isShift || key.code === " " || key.code === "Backspace") {
1506
- keyShape.fillColor = this.specialKeyDownColor;
1507
- keyAsString = key.code;
1508
- } else {
1509
- keyShape.fillColor = this.keyDownColor;
1510
- if (this.showKeyDownPreview) {
1511
- letterCircle.hidden = false;
1512
- letterCircle.position.x = keyBox.position.x;
1513
- letterCircle.position.y = keyBox.position.y - keyHeight * 1.2;
1514
- if (this.shiftActivated) {
1515
- letterCircleLabel.text = key.labelTextShifted ?? key.code;
1516
- } else {
1517
- letterCircleLabel.text = key.labelText ?? key.code;
1518
- }
1519
- }
1520
- if (this.shiftActivated) {
1521
- keyAsString = key.labelTextShifted ?? key.code;
1522
- } else {
1523
- keyAsString = key.labelText ?? key.code;
1524
- }
1525
- }
1526
- if (this.eventListeners.length > 0) {
1527
- this.eventListeners.filter(
1528
- (listener) => listener.type === M2EventType.CompositeCustom && listener.compositeType === "VirtualKeyboardKeyDown"
1529
- ).forEach((listener) => {
1530
- const virtualKeyboardEvent = {
1531
- type: M2EventType.CompositeCustom,
1532
- target: this,
1533
- handled: false,
1534
- key: keyAsString,
1535
- code: key.code,
1536
- shiftKey: this.shiftActivated,
1537
- keyTapMetadata: {
1538
- size: keyShape.size,
1539
- point: tapEvent.point,
1540
- buttons: tapEvent.buttons
1541
- }
1542
- };
1543
- listener.callback(virtualKeyboardEvent);
1544
- });
1545
- }
1546
- });
1547
- keyShape.onTapLeave(() => {
1548
- keyShape.fillColor = this.keyColor;
1549
- letterCircle.hidden = true;
1550
- });
1638
+ this.keyShapes.push(keyShape);
1551
1639
  const keyLabel = new Label({
1552
1640
  text: key.labelText,
1553
1641
  fontSize: this.fontSize,
1554
- fontNames: this.fontNames
1642
+ fontNames: this.fontNames,
1643
+ suppressEvents: true
1555
1644
  });
1645
+ keyLabel.userData = { code: key.code };
1556
1646
  keyBox.addChild(keyLabel);
1557
- key.keyLabel = keyLabel;
1558
- if (key.isShift) {
1559
- this.shiftKeyShape = keyShape;
1560
- }
1647
+ this.keyLabels.push(keyLabel);
1561
1648
  if (key.keyIcon) {
1562
1649
  keyBox.addChild(key.keyIcon);
1563
1650
  }
1564
1651
  keyboardRectangle.addChild(keyBox);
1652
+ keyShape.onTapUp((tapEvent) => {
1653
+ this.handleKeyShapeTapUp(key, keyShape, tapEvent);
1654
+ });
1655
+ keyShape.onTapDown((tapEvent) => {
1656
+ this.handleKeyShapeTapDown(key, keyShape, tapEvent);
1657
+ });
1658
+ keyShape.onTapLeave((tapEvent) => {
1659
+ this.handleKeyShapeTapLeave(key, keyShape, tapEvent);
1660
+ });
1565
1661
  }
1566
1662
  }
1567
- const letterCircle = new Shape({
1663
+ this.letterCircle = new Shape({
1568
1664
  circleOfRadius: 28,
1569
1665
  fillColor: WebColors.Silver,
1570
- hidden: true
1666
+ hidden: true,
1667
+ suppressEvents: true
1571
1668
  });
1572
- keyboardRectangle.addChild(letterCircle);
1573
- const letterCircleLabel = new Label({
1669
+ keyboardRectangle.addChild(this.letterCircle);
1670
+ this.letterCircleLabel = new Label({
1574
1671
  text: "",
1575
1672
  fontSize: this.fontSize,
1576
- fontNames: this.fontNames
1673
+ fontNames: this.fontNames,
1674
+ suppressEvents: true
1577
1675
  });
1578
- letterCircle.addChild(letterCircleLabel);
1676
+ this.letterCircle.addChild(this.letterCircleLabel);
1579
1677
  this.needsInitialization = false;
1580
1678
  }
1581
1679
  /**
@@ -1586,8 +1684,9 @@ class VirtualKeyboard extends Composite {
1586
1684
  */
1587
1685
  onKeyDown(callback, options) {
1588
1686
  const eventListener = {
1589
- type: M2EventType.CompositeCustom,
1590
- compositeType: "VirtualKeyboardKeyDown",
1687
+ type: M2EventType.Composite,
1688
+ compositeEventType: "VirtualKeyboardKeyDown",
1689
+ compositeType: this.compositeType,
1591
1690
  nodeUuid: this.uuid,
1592
1691
  callback
1593
1692
  };
@@ -1601,20 +1700,35 @@ class VirtualKeyboard extends Composite {
1601
1700
  */
1602
1701
  onKeyUp(callback, options) {
1603
1702
  const eventListener = {
1604
- type: M2EventType.CompositeCustom,
1605
- compositeType: "VirtualKeyboardKeyUp",
1703
+ type: M2EventType.Composite,
1704
+ compositeEventType: "VirtualKeyboardKeyUp",
1705
+ compositeType: this.compositeType,
1606
1706
  nodeUuid: this.uuid,
1607
1707
  callback
1608
1708
  };
1609
1709
  this.addVirtualKeyboardEventListener(eventListener, options);
1610
1710
  }
1611
- addVirtualKeyboardEventListener(eventListener, options) {
1612
- if (options?.replaceExisting) {
1613
- this.eventListeners = this.eventListeners.filter(
1614
- (listener) => !(listener.nodeUuid === eventListener.nodeUuid && listener.type === eventListener.type && listener.compositeType === eventListener.compositeType)
1615
- );
1616
- }
1617
- this.eventListeners.push(eventListener);
1711
+ /**
1712
+ * Executes a callback when the user has pressed a key with the pointer, but
1713
+ * moves the pointer outside the key bounds before releasing the pointer.
1714
+ *
1715
+ * @remarks Typically, this event will not be used, since it is a user's
1716
+ * inaccurate interaction with the keyboard. However, it can be used to
1717
+ * provide feedback to the user that they have moved the pointer outside the
1718
+ * key bounds, and thus the key stroke will not be registered.
1719
+ *
1720
+ * @param callback - function to execute
1721
+ * @param options
1722
+ */
1723
+ onKeyLeave(callback, options) {
1724
+ const eventListener = {
1725
+ type: M2EventType.Composite,
1726
+ compositeEventType: "VirtualKeyboardKeyLeave",
1727
+ compositeType: this.compositeType,
1728
+ nodeUuid: this.uuid,
1729
+ callback
1730
+ };
1731
+ this.addVirtualKeyboardEventListener(eventListener, options);
1618
1732
  }
1619
1733
  update() {
1620
1734
  super.update();
@@ -1629,9 +1743,417 @@ class VirtualKeyboard extends Composite {
1629
1743
  });
1630
1744
  }
1631
1745
  duplicate(newName) {
1632
- throw new Error("Method not implemented.");
1746
+ throw new Error(`Method not implemented. ${newName}`);
1747
+ }
1748
+ handleKeyShapeTapDown(key, keyShape, tapEvent) {
1749
+ if (key.isShift) {
1750
+ this.shiftActivated = !this.shiftActivated;
1751
+ }
1752
+ const keyAsString = this.getKeyAsString(key);
1753
+ const virtualKeyboardEvent = {
1754
+ type: M2EventType.Composite,
1755
+ compositeType: "VirtualKeyboard",
1756
+ compositeEventType: "VirtualKeyboardKeyDown",
1757
+ target: this,
1758
+ handled: false,
1759
+ key: keyAsString,
1760
+ code: key.code,
1761
+ shiftKey: this.shiftActivated,
1762
+ keyTapMetadata: {
1763
+ size: keyShape.size,
1764
+ point: tapEvent.point,
1765
+ buttons: tapEvent.buttons
1766
+ },
1767
+ ...M2c2KitHelpers.createTimestamps()
1768
+ };
1769
+ this.handleCompositeEvent(virtualKeyboardEvent);
1770
+ this.saveEvent(virtualKeyboardEvent);
1771
+ if (this.eventListeners.length > 0) {
1772
+ this.eventListeners.filter(
1773
+ (listener) => listener.type === M2EventType.Composite && listener.compositeType === "VirtualKeyboard" && listener.compositeEventType === "VirtualKeyboardKeyDown"
1774
+ ).forEach((listener) => {
1775
+ listener.callback(virtualKeyboardEvent);
1776
+ });
1777
+ }
1778
+ }
1779
+ handleKeyShapeTapUp(key, keyShape, tapEvent) {
1780
+ const keyAsString = this.getKeyAsString(key);
1781
+ const virtualKeyboardEvent = {
1782
+ type: M2EventType.Composite,
1783
+ compositeType: "VirtualKeyboard",
1784
+ compositeEventType: "VirtualKeyboardKeyUp",
1785
+ target: this,
1786
+ handled: false,
1787
+ key: keyAsString,
1788
+ code: key.code,
1789
+ shiftKey: this.shiftActivated,
1790
+ keyTapMetadata: {
1791
+ size: keyShape.size,
1792
+ point: tapEvent.point,
1793
+ buttons: tapEvent.buttons
1794
+ },
1795
+ ...M2c2KitHelpers.createTimestamps()
1796
+ };
1797
+ this.handleCompositeEvent(virtualKeyboardEvent);
1798
+ this.saveEvent(virtualKeyboardEvent);
1799
+ if (this.eventListeners.length > 0) {
1800
+ this.eventListeners.filter(
1801
+ (listener) => listener.type === M2EventType.Composite && listener.compositeType === "VirtualKeyboard" && listener.compositeEventType === "VirtualKeyboardKeyUp"
1802
+ ).forEach((listener) => {
1803
+ listener.callback(virtualKeyboardEvent);
1804
+ });
1805
+ }
1806
+ }
1807
+ handleKeyShapeTapLeave(key, keyShape, tapEvent) {
1808
+ const keyAsString = this.getKeyAsString(key);
1809
+ const virtualKeyboardEvent = {
1810
+ type: M2EventType.Composite,
1811
+ compositeType: "VirtualKeyboard",
1812
+ compositeEventType: "VirtualKeyboardKeyLeave",
1813
+ target: this,
1814
+ handled: false,
1815
+ key: keyAsString,
1816
+ code: key.code,
1817
+ shiftKey: this.shiftActivated,
1818
+ keyTapMetadata: {
1819
+ size: keyShape.size,
1820
+ point: tapEvent.point,
1821
+ buttons: tapEvent.buttons
1822
+ },
1823
+ ...M2c2KitHelpers.createTimestamps()
1824
+ };
1825
+ this.handleCompositeEvent(virtualKeyboardEvent);
1826
+ this.saveEvent(virtualKeyboardEvent);
1827
+ if (this.eventListeners.length > 0) {
1828
+ this.eventListeners.filter(
1829
+ (listener) => listener.type === M2EventType.Composite && listener.compositeType === "VirtualKeyboard" && listener.compositeEventType === "VirtualKeyboardKeyLeave"
1830
+ ).forEach((listener) => {
1831
+ listener.callback(virtualKeyboardEvent);
1832
+ });
1833
+ }
1834
+ }
1835
+ getKeyAsString(key) {
1836
+ if (key.isShift || key.code === " " || key.code === "Backspace") {
1837
+ return key.code;
1838
+ } else {
1839
+ if (this.shiftActivated) {
1840
+ return key.labelTextShifted ?? key.code;
1841
+ } else {
1842
+ return key.labelText ?? key.code;
1843
+ }
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Converts the keyboard rows to the internal keyboard configuration.
1848
+ *
1849
+ * @remarks This normalizes the keyboard rows so that each key is a
1850
+ * full `KeyConfigurationWithShape` object, instead of a string or array of
1851
+ * strings.
1852
+ *
1853
+ * @param keyboardRows
1854
+ * @returns the keyboard for internal use
1855
+ */
1856
+ internalKeyboardRowsToInternalKeyboardConfiguration(keyboardRows) {
1857
+ return keyboardRows.map((row) => {
1858
+ return row.map((key) => {
1859
+ let widthRatio = 1;
1860
+ const heightRatio = 1;
1861
+ let code;
1862
+ let label;
1863
+ let labelShifted;
1864
+ let keyIcon;
1865
+ let isShift = false;
1866
+ if (typeof key === "string") {
1867
+ code = key;
1868
+ if (this.capitalLettersOnly) {
1869
+ label = code.toUpperCase();
1870
+ } else {
1871
+ label = code;
1872
+ }
1873
+ labelShifted = code.toUpperCase();
1874
+ } else if (Array.isArray(key)) {
1875
+ code = key[0];
1876
+ label = code;
1877
+ labelShifted = key[1];
1878
+ } else {
1879
+ code = key.code;
1880
+ label = key.labelText ?? "";
1881
+ labelShifted = key.labelTextShifted ?? label;
1882
+ widthRatio = key.widthRatio ?? 1;
1883
+ keyIcon = key.keyIcon;
1884
+ isShift = key.isShift ?? false;
1885
+ }
1886
+ return {
1887
+ widthRatio,
1888
+ heightRatio,
1889
+ code,
1890
+ labelText: label,
1891
+ labelTextShifted: labelShifted,
1892
+ keyIcon,
1893
+ isShift
1894
+ };
1895
+ });
1896
+ });
1897
+ }
1898
+ handleCompositeEvent(event) {
1899
+ const keyboard = this.internalKeyboardRowsToInternalKeyboardConfiguration(
1900
+ this.keyboardRows
1901
+ );
1902
+ const keyShape = this.keyShapes.find((k) => k.userData.code === event.code);
1903
+ if (!keyShape) {
1904
+ throw new Error("keyShape is not defined");
1905
+ }
1906
+ this.shiftActivated = event.shiftKey;
1907
+ switch (event.compositeEventType) {
1908
+ case "VirtualKeyboardKeyDown": {
1909
+ this.handleKeyDownEvent(event, keyboard, keyShape);
1910
+ break;
1911
+ }
1912
+ case "VirtualKeyboardKeyUp": {
1913
+ this.handleKeyUpEvent(event, keyboard, keyShape);
1914
+ break;
1915
+ }
1916
+ case "VirtualKeyboardKeyLeave": {
1917
+ this.handleKeyLeaveEvent(event, keyboard, keyShape);
1918
+ break;
1919
+ }
1920
+ default: {
1921
+ throw new Error(
1922
+ `Unknown VirtualKeyboardEvent: ${event.compositeEventType}`
1923
+ );
1924
+ }
1925
+ }
1926
+ }
1927
+ handleKeyDownEvent(event, keyboard, keyShape) {
1928
+ if (event.code.toLowerCase().includes("shift")) {
1929
+ if (event.shiftKey) {
1930
+ this.showKeyboardShifted(keyboard);
1931
+ } else {
1932
+ this.showKeyboardNotShifted(keyboard);
1933
+ }
1934
+ } else if (event.code === " " || event.code === "Backspace") {
1935
+ keyShape.fillColor = this.specialKeyDownColor;
1936
+ } else {
1937
+ keyShape.fillColor = this.keyDownColor;
1938
+ if (this.showKeyDownPreview) {
1939
+ if (!this.letterCircle || !this.letterCircleLabel) {
1940
+ throw new Error("letterCircle is not defined");
1941
+ }
1942
+ this.letterCircle.hidden = false;
1943
+ const keyBox = keyShape.parent;
1944
+ this.letterCircle.position.x = keyBox.position.x;
1945
+ if (keyShape.rect?.size?.height === void 0) {
1946
+ throw new Error("keyShape.rect.height is undefined");
1947
+ }
1948
+ this.letterCircle.position.y = keyBox.position.y - keyShape.rect.size.height * 1.2;
1949
+ const keyboard2 = this.internalKeyboardRowsToInternalKeyboardConfiguration(
1950
+ this.keyboardRows
1951
+ );
1952
+ const key = keyboard2.flat().find((k) => k.code === event.code);
1953
+ if (!key) {
1954
+ throw new Error("key is not defined");
1955
+ }
1956
+ if (this.shiftActivated) {
1957
+ this.letterCircleLabel.text = key.labelTextShifted ?? key.code;
1958
+ } else {
1959
+ this.letterCircleLabel.text = key.labelText ?? key.code;
1960
+ }
1961
+ }
1962
+ }
1963
+ }
1964
+ handleKeyUpEvent(event, keyboard, keyShape) {
1965
+ if (event.code.toLowerCase().includes("shift") && event.shiftKey) {
1966
+ return;
1967
+ }
1968
+ if (event.code.toLowerCase().includes("shift") && !event.shiftKey) {
1969
+ this.shiftActivated = false;
1970
+ this.showKeyboardNotShifted(keyboard);
1971
+ return;
1972
+ }
1973
+ keyShape.fillColor = this.keyColor;
1974
+ if (!this.letterCircle) {
1975
+ throw new Error("letterCircle is not defined");
1976
+ }
1977
+ this.letterCircle.hidden = true;
1978
+ if (!event.code.toLowerCase().includes("shift") && event.shiftKey) {
1979
+ this.shiftActivated = false;
1980
+ this.showKeyboardNotShifted(keyboard);
1981
+ }
1982
+ }
1983
+ handleKeyLeaveEvent(event, keyboard, keyShape) {
1984
+ if (event.code.toLowerCase().includes("shift")) {
1985
+ if (event.shiftKey) {
1986
+ this.showKeyboardNotShifted(keyboard);
1987
+ this.shiftActivated = false;
1988
+ } else {
1989
+ this.showKeyboardShifted(keyboard);
1990
+ this.shiftActivated = true;
1991
+ }
1992
+ return;
1993
+ }
1994
+ keyShape.fillColor = this.keyColor;
1995
+ if (!this.letterCircle) {
1996
+ throw new Error("letterCircle is not defined");
1997
+ }
1998
+ this.letterCircle.hidden = true;
1999
+ }
2000
+ showKeyboardShifted(keyboard) {
2001
+ const shiftKeyShapes = this.keyShapes.filter(
2002
+ (shape) => shape.userData.code.toLowerCase().includes("shift")
2003
+ );
2004
+ shiftKeyShapes.forEach((shape) => {
2005
+ shape.fillColor = this.specialKeyDownColor;
2006
+ });
2007
+ const shiftKeys = keyboard.flat().filter((k) => k.isShift);
2008
+ shiftKeys.forEach((k) => {
2009
+ if (k.keyIcon) {
2010
+ k.keyIcon.fillColor = WebColors.Black;
2011
+ }
2012
+ });
2013
+ keyboard.flatMap((k) => k).forEach((k) => {
2014
+ const keyLabel = this.keyLabels.find((l) => l.userData.code === k.code);
2015
+ if (!keyLabel) {
2016
+ throw new Error("keyLabel is not defined");
2017
+ }
2018
+ if (keyLabel.text !== void 0) {
2019
+ keyLabel.text = k.labelTextShifted ?? "";
2020
+ }
2021
+ });
2022
+ }
2023
+ showKeyboardNotShifted(keyboard) {
2024
+ const shiftKeyShapes = this.keyShapes.filter(
2025
+ (shape) => shape.userData.code.toLowerCase().includes("shift")
2026
+ );
2027
+ shiftKeyShapes.forEach((shape) => {
2028
+ shape.fillColor = this.keyColor;
2029
+ });
2030
+ const shiftKeys = keyboard.flat().filter((k) => k.isShift);
2031
+ shiftKeys.forEach((k) => {
2032
+ if (k.keyIcon) {
2033
+ k.keyIcon.fillColor = WebColors.Transparent;
2034
+ }
2035
+ });
2036
+ keyboard.flatMap((k) => k).forEach((k) => {
2037
+ const keyLabel = this.keyLabels.find((l) => l.userData.code === k.code);
2038
+ if (!keyLabel) {
2039
+ throw new Error("keyLabel is not defined");
2040
+ }
2041
+ if (keyLabel.text !== void 0) {
2042
+ keyLabel.text = k.labelText ?? "";
2043
+ }
2044
+ });
2045
+ }
2046
+ createDefaultKeyboardRows() {
2047
+ const numKeys = [
2048
+ ["1", "!"],
2049
+ ["2", "@"],
2050
+ ["3", "#"],
2051
+ ["4", "$"],
2052
+ ["5", "%"],
2053
+ ["6", "^"],
2054
+ ["7", "&"],
2055
+ ["8", "*"],
2056
+ ["9", "("],
2057
+ ["0", ")"]
2058
+ ];
2059
+ const row1 = [
2060
+ "q",
2061
+ "w",
2062
+ "e",
2063
+ "r",
2064
+ "t",
2065
+ "y",
2066
+ "u",
2067
+ "i",
2068
+ "o",
2069
+ "p"
2070
+ ];
2071
+ const row2 = [
2072
+ "a",
2073
+ "s",
2074
+ "d",
2075
+ "f",
2076
+ "g",
2077
+ "h",
2078
+ "j",
2079
+ "k",
2080
+ "l"
2081
+ ];
2082
+ const shiftArrowShapeOptions = {
2083
+ path: {
2084
+ // Public Domain from https://www.freesvg.org
2085
+ pathString: "m288-6.6849e-14 -288 288h144v288h288v-288h144l-288-288z",
2086
+ width: 24
2087
+ },
2088
+ lineWidth: 2,
2089
+ strokeColor: WebColors.Black,
2090
+ fillColor: WebColors.Transparent,
2091
+ suppressEvents: true
2092
+ };
2093
+ const backspaceShapeOptions = {
2094
+ path: {
2095
+ // CC0 from https://www.svgrepo.com
2096
+ pathString: "M10.625 5.09 0 22.09l10.625 17H44.18v-34H10.625zm31.555 32H11.734l-9.375-15 9.375-15H42.18v30zm-23.293-6.293 7.293-7.293 7.293 7.293 1.414-1.414-7.293-7.293 7.293-7.293-1.414-1.414-7.293 7.293-7.293-7.293-1.414 1.414 7.293 7.293-7.293 7.293",
2097
+ width: 24
2098
+ },
2099
+ lineWidth: 1,
2100
+ strokeColor: WebColors.Black,
2101
+ fillColor: WebColors.Red,
2102
+ suppressEvents: true
2103
+ };
2104
+ const row3 = [
2105
+ {
2106
+ code: "Shift",
2107
+ isShift: true,
2108
+ widthRatio: 1.5,
2109
+ keyIcon: new Shape(shiftArrowShapeOptions)
2110
+ },
2111
+ "z",
2112
+ "x",
2113
+ "c",
2114
+ "v",
2115
+ "b",
2116
+ "n",
2117
+ "m",
2118
+ {
2119
+ code: "Backspace",
2120
+ widthRatio: 1.5,
2121
+ keyIcon: new Shape(backspaceShapeOptions)
2122
+ }
2123
+ ];
2124
+ const row4 = [
2125
+ { code: " ", labelText: "SPACE", widthRatio: 5 }
2126
+ ];
2127
+ const keyboardRows = [numKeys, row1, row2, row3, row4];
2128
+ return keyboardRows;
2129
+ }
2130
+ addVirtualKeyboardEventListener(eventListener, options) {
2131
+ if (options?.replaceExisting) {
2132
+ this.eventListeners = this.eventListeners.filter(
2133
+ (listener) => !(listener.nodeUuid === eventListener.nodeUuid && listener.type === eventListener.type && listener.compositeType === eventListener.compositeType)
2134
+ );
2135
+ }
2136
+ this.eventListeners.push(eventListener);
2137
+ }
2138
+ /**
2139
+ * Does the `VirtualKeyboard` respond to user events? Default is true.
2140
+ */
2141
+ get isUserInteractionEnabled() {
2142
+ return this._isUserInteractionEnabled;
2143
+ }
2144
+ /**
2145
+ * Does the `VirtualKeyboard` respond to user events? Default is true.
2146
+ */
2147
+ set isUserInteractionEnabled(isUserInteractionEnabled) {
2148
+ this._isUserInteractionEnabled = isUserInteractionEnabled;
2149
+ this.keyShapes?.forEach((keyShape) => {
2150
+ keyShape.isUserInteractionEnabled = isUserInteractionEnabled;
2151
+ });
1633
2152
  }
1634
2153
  }
2154
+ M2c2KitHelpers.registerM2NodeClass(
2155
+ VirtualKeyboard
2156
+ );
1635
2157
 
1636
2158
  const SCENE_TRANSITION_EASING$1 = Easings.sinusoidalInOut;
1637
2159
  const SCENE_TRANSITION_DURATION = 500;
@@ -1981,7 +2503,469 @@ class CountdownScene extends Scene {
1981
2503
  }
1982
2504
  }
1983
2505
 
1984
- console.log("\u26AA @m2c2kit/addons version 0.3.14 (ebbdc605)");
2506
+ class LocalePicker extends Composite {
2507
+ /**
2508
+ * An icon and dialog box for selecting a locale from a list of options.
2509
+ *
2510
+ * @remarks This composite node is composed of a dialog box that appears
2511
+ * when the user taps a globe icon. Typically, the `LocalePicker` will be
2512
+ * added as a free node to the game so that it exists independently of
2513
+ * the game's scenes. The dialog box contains a list of locales that the
2514
+ * user can choose from. By default, this list is populated with the locales
2515
+ * in the game's `Translation` object. When the user selects a locale, the
2516
+ * dialog box disappears and the locale is set as the game's current locale.
2517
+ * The dialog box is automatically sized to fit the number of locale
2518
+ * options.
2519
+ *
2520
+ * @example
2521
+ * let localePicker: LocalePicker;
2522
+ * if (game.getParameter<boolean>("show_locale_picker")) {
2523
+ * localePicker = new LocalePicker();
2524
+ * game.addFreeNode(localePicker);
2525
+ * }
2526
+ *
2527
+ * @param options - {@link LocalePickerOptions}
2528
+ */
2529
+ constructor(options) {
2530
+ super(options);
2531
+ this.compositeType = "LocalePicker";
2532
+ this.DEFAULT_FONT_SIZE = 24;
2533
+ this.automaticallyChangeLocale = true;
2534
+ this._localeOptions = new Array();
2535
+ this._backgroundColor = WebColors.White;
2536
+ this._fontSize = this.DEFAULT_FONT_SIZE;
2537
+ this._fontColor = WebColors.Black;
2538
+ this._cornerRadius = 8;
2539
+ this._overlayAlpha = 0.5;
2540
+ this._icon = {
2541
+ // public domain SVG from https://commons.wikimedia.org/wiki/File:Globe_icon.svg
2542
+ svgString: `<svg xmlns="http://www.w3.org/2000/svg" width="420" height="420" stroke="#000" fill="none"><path stroke-width="26" d="M209 15a195 195 0 1 0 2 0z"/><path stroke-width="18" d="M210 15v390m195-195H15M59 90a260 260 0 0 0 302 0m0 240a260 260 0 0 0-302 0M195 20a250 250 0 0 0 0 382m30 0a250 250 0 0 0 0-382"/></svg>`,
2543
+ height: 32,
2544
+ width: 32
2545
+ };
2546
+ this._iconPosition = { x: 32, y: 32 };
2547
+ /**
2548
+ * Wrap displayed locale in double angle quotes if it is the current locale.
2549
+ * Note: Although the code editor will allow us to enter almost any
2550
+ * unicode character, it will not render correctly if the font does
2551
+ * not support the character. Thus, be careful to use characters that
2552
+ * are supported by the font. For example, check a page like
2553
+ * https://www.fontspace.com/roboto-font-f13281 to see which characters
2554
+ * are supported by Roboto Regular, which is often the default font in
2555
+ * m2c2kit. Emoji or checkmarks like ✓ are not in Roboto Regular!
2556
+ */
2557
+ this.LEFT_SELECTION_INDICATOR = "\xAB";
2558
+ this.RIGHT_SELECTION_INDICATOR = "\xBB";
2559
+ this.zPosition = Number.MAX_VALUE;
2560
+ if (!options) {
2561
+ return;
2562
+ }
2563
+ if (options.localeOptions) {
2564
+ this.localeOptions = options.localeOptions;
2565
+ }
2566
+ if (options.backgroundColor) {
2567
+ this.backgroundColor = options.backgroundColor;
2568
+ }
2569
+ if (options.overlayAlpha !== void 0) {
2570
+ this.overlayAlpha = options.overlayAlpha;
2571
+ }
2572
+ if (options.fontSize !== void 0) {
2573
+ this.fontSize = options.fontSize;
2574
+ }
2575
+ if (options.fontColor) {
2576
+ this.fontColor = options.fontColor;
2577
+ }
2578
+ if (options.cornerRadius) {
2579
+ this.cornerRadius = options.cornerRadius;
2580
+ }
2581
+ if (options.currentLocale !== void 0) {
2582
+ this.currentLocale = options.currentLocale;
2583
+ }
2584
+ if (options.icon) {
2585
+ this.icon = options.icon;
2586
+ }
2587
+ if (options.automaticallyChangeLocale !== void 0) {
2588
+ this.automaticallyChangeLocale = options.automaticallyChangeLocale;
2589
+ }
2590
+ }
2591
+ /**
2592
+ * Executes a callback when the user selects a locale.
2593
+ *
2594
+ * @param callback - function to execute
2595
+ * @param options - {@link CallbackOptions}
2596
+ */
2597
+ onResult(callback, options) {
2598
+ const eventListener = {
2599
+ type: M2EventType.Composite,
2600
+ compositeType: "LocalePickerResult",
2601
+ nodeUuid: this.uuid,
2602
+ callback
2603
+ };
2604
+ if (options?.replaceExisting) {
2605
+ this.eventListeners = this.eventListeners.filter(
2606
+ (listener) => !(listener.nodeUuid === eventListener.nodeUuid && listener.type === "LocalePickerResult")
2607
+ );
2608
+ }
2609
+ this.eventListeners.push(eventListener);
2610
+ }
2611
+ initialize() {
2612
+ if (this.currentLocale === void 0) {
2613
+ this.currentLocale = this.game.i18n?.locale;
2614
+ }
2615
+ if (this.localeOptions.length === 0) {
2616
+ const locales = Object.keys(this.game.i18n?.translation || {});
2617
+ locales.filter((locale) => locale !== "configuration").forEach((locale) => {
2618
+ this.localeOptions.push({
2619
+ text: this.game.i18n?.translation[locale].localeName || locale,
2620
+ locale,
2621
+ svg: this.game.i18n?.translation[locale].localeSvg
2622
+ });
2623
+ });
2624
+ }
2625
+ if (this.localeOptions.length === 0) {
2626
+ throw new Error("No locales available for LocalePicker");
2627
+ }
2628
+ this.children.filter((child) => child.name !== "localePickerIcon").forEach((child) => this.removeChild(child));
2629
+ this.game.imageManager.loadImages([
2630
+ {
2631
+ imageName: "__localePickerIcon",
2632
+ svgString: this.icon.svgString,
2633
+ height: this.icon.height,
2634
+ width: this.icon.width
2635
+ }
2636
+ ]);
2637
+ if (!this.iconSprite) {
2638
+ this.iconSprite = new Sprite({
2639
+ // name is how we refer to this sprite, as a node.
2640
+ name: "localePickerIcon",
2641
+ // imageName is the loaded image that we can assign to the sprite
2642
+ imageName: "__localePickerIcon",
2643
+ position: this.iconPosition,
2644
+ isUserInteractionEnabled: true
2645
+ });
2646
+ this.addChild(this.iconSprite);
2647
+ this.iconSprite.onTapDown(() => {
2648
+ this.setDialogVisibility(true);
2649
+ });
2650
+ }
2651
+ const overlay = new Shape({
2652
+ rect: {
2653
+ width: m2c2Globals.canvasCssWidth,
2654
+ height: m2c2Globals.canvasCssHeight,
2655
+ x: m2c2Globals.canvasCssWidth / 2,
2656
+ y: m2c2Globals.canvasCssHeight / 2
2657
+ },
2658
+ fillColor: [0, 0, 0, this.overlayAlpha],
2659
+ zPosition: -1,
2660
+ isUserInteractionEnabled: true,
2661
+ hidden: true
2662
+ });
2663
+ overlay.onTapDown((e) => {
2664
+ e.handled = true;
2665
+ if (this.eventListeners.length > 0) {
2666
+ this.eventListeners.filter((listener) => listener.type === "LocalePickerResult").forEach((listener) => {
2667
+ const languagePickerEvent = {
2668
+ type: M2EventType.Composite,
2669
+ compositeType: this.compositeType,
2670
+ compositeEventType: "LocalePickerResult",
2671
+ target: this,
2672
+ handled: false,
2673
+ result: {
2674
+ locale: void 0
2675
+ },
2676
+ timestamp: Timer.now(),
2677
+ iso8601Timestamp: (/* @__PURE__ */ new Date()).toISOString()
2678
+ };
2679
+ listener.callback(languagePickerEvent);
2680
+ });
2681
+ }
2682
+ this.setDialogVisibility(false);
2683
+ });
2684
+ this.addChild(overlay);
2685
+ const lineHeight = this.fontSize / this.DEFAULT_FONT_SIZE * 50;
2686
+ const dialogHeight = this.localeOptions.length * lineHeight;
2687
+ const dialogWidth = m2c2Globals.canvasCssWidth / 2;
2688
+ const sceneCenter = {
2689
+ x: m2c2Globals.canvasCssWidth / 2,
2690
+ y: m2c2Globals.canvasCssHeight / 2
2691
+ };
2692
+ const localeDialog = new Shape({
2693
+ rect: {
2694
+ width: dialogWidth,
2695
+ height: dialogHeight,
2696
+ x: sceneCenter.x,
2697
+ y: sceneCenter.y
2698
+ },
2699
+ cornerRadius: this.cornerRadius,
2700
+ fillColor: this.backgroundColor,
2701
+ isUserInteractionEnabled: true,
2702
+ hidden: true
2703
+ });
2704
+ localeDialog.onTapDown((e) => {
2705
+ e.handled = true;
2706
+ });
2707
+ this.addChild(localeDialog);
2708
+ for (let i = 0; i < this.localeOptions.length; i++) {
2709
+ const localeOption = this.localeOptions[i];
2710
+ if (!localeOption.svg) {
2711
+ let labelText = localeOption.text;
2712
+ if (this.currentLocale === localeOption.locale) {
2713
+ labelText = `${this.LEFT_SELECTION_INDICATOR} ${labelText} ${this.RIGHT_SELECTION_INDICATOR}`;
2714
+ }
2715
+ const text = new Label({
2716
+ text: labelText,
2717
+ fontSize: this.fontSize,
2718
+ fontColor: this.fontColor,
2719
+ position: {
2720
+ x: sceneCenter.x,
2721
+ y: sceneCenter.y + i * lineHeight - dialogHeight / 2 + lineHeight / 2
2722
+ },
2723
+ isUserInteractionEnabled: true,
2724
+ zPosition: 1,
2725
+ hidden: true,
2726
+ // do not localize the text of each language option
2727
+ localize: false
2728
+ });
2729
+ text.onTapDown((e) => {
2730
+ this.handleLocaleSelection(e, localeOption);
2731
+ });
2732
+ this.addChild(text);
2733
+ } else {
2734
+ this.game.imageManager.loadImages([
2735
+ {
2736
+ imageName: localeOption.text,
2737
+ svgString: localeOption.svg.svgString,
2738
+ height: localeOption.svg.height,
2739
+ width: localeOption.svg.width
2740
+ }
2741
+ ]);
2742
+ const localeSprite = new Sprite({
2743
+ imageName: localeOption.text,
2744
+ position: {
2745
+ x: sceneCenter.x,
2746
+ y: sceneCenter.y + i * lineHeight - dialogHeight / 2 + lineHeight / 2
2747
+ },
2748
+ isUserInteractionEnabled: true,
2749
+ zPosition: 1,
2750
+ hidden: true
2751
+ });
2752
+ this.addChild(localeSprite);
2753
+ if (this.currentLocale === localeOption.locale) {
2754
+ const leftSelectionIndicator = new Label({
2755
+ text: this.LEFT_SELECTION_INDICATOR,
2756
+ fontSize: this.fontSize,
2757
+ fontColor: this.fontColor,
2758
+ position: {
2759
+ x: sceneCenter.x - localeOption.svg.width / 2,
2760
+ y: sceneCenter.y + i * lineHeight - dialogHeight / 2 + lineHeight / 2
2761
+ },
2762
+ hidden: true,
2763
+ // do not localize the left selection indicator
2764
+ localize: false
2765
+ });
2766
+ this.addChild(leftSelectionIndicator);
2767
+ const rightSelectionIndicator = new Label({
2768
+ text: this.RIGHT_SELECTION_INDICATOR,
2769
+ fontSize: this._fontSize,
2770
+ fontColor: this.fontColor,
2771
+ position: {
2772
+ x: sceneCenter.x + localeOption.svg.width / 2,
2773
+ y: sceneCenter.y + i * lineHeight - dialogHeight / 2 + lineHeight / 2
2774
+ },
2775
+ hidden: true,
2776
+ // do not localize the left selection indicator
2777
+ localize: false
2778
+ });
2779
+ this.addChild(rightSelectionIndicator);
2780
+ }
2781
+ localeSprite.onTapDown((e) => {
2782
+ this.handleLocaleSelection(e, localeOption);
2783
+ });
2784
+ }
2785
+ }
2786
+ this.needsInitialization = false;
2787
+ }
2788
+ handleLocaleSelection(e, localeOption) {
2789
+ e.handled = true;
2790
+ if (this.eventListeners.length > 0) {
2791
+ this.eventListeners.filter(
2792
+ (listener) => listener.type === M2EventType.Composite && listener.compositeType === "LocalePickerResult" && listener.nodeUuid == this.uuid
2793
+ ).forEach((listener) => {
2794
+ const languagePickerEvent = {
2795
+ type: M2EventType.Composite,
2796
+ compositeType: this.compositeType,
2797
+ compositeEventType: "LocalePickerResult",
2798
+ target: this,
2799
+ handled: false,
2800
+ result: {
2801
+ locale: localeOption.locale
2802
+ },
2803
+ timestamp: Timer.now(),
2804
+ iso8601Timestamp: (/* @__PURE__ */ new Date()).toISOString()
2805
+ };
2806
+ listener.callback(languagePickerEvent);
2807
+ });
2808
+ }
2809
+ this.setDialogVisibility(false);
2810
+ if (this.automaticallyChangeLocale) {
2811
+ this.game.i18n?.switchToLocale(localeOption.locale);
2812
+ this.currentLocale = localeOption.locale;
2813
+ }
2814
+ }
2815
+ setDialogVisibility(visible) {
2816
+ this.children.filter((child) => child.name !== "localePickerIcon").forEach((child) => {
2817
+ child.hidden = !visible;
2818
+ });
2819
+ }
2820
+ get backgroundColor() {
2821
+ return this._backgroundColor;
2822
+ }
2823
+ set backgroundColor(backgroundColor) {
2824
+ this._backgroundColor = backgroundColor;
2825
+ this.needsInitialization = true;
2826
+ }
2827
+ get fontSize() {
2828
+ return this._fontSize;
2829
+ }
2830
+ set fontSize(fontSize) {
2831
+ this._fontSize = fontSize;
2832
+ this.needsInitialization = true;
2833
+ }
2834
+ get fontColor() {
2835
+ return this._fontColor;
2836
+ }
2837
+ set fontColor(fontColor) {
2838
+ this._fontColor = fontColor;
2839
+ this.needsInitialization = true;
2840
+ }
2841
+ get cornerRadius() {
2842
+ return this._cornerRadius;
2843
+ }
2844
+ set cornerRadius(cornerRadius) {
2845
+ this._cornerRadius = cornerRadius;
2846
+ this.needsInitialization = true;
2847
+ }
2848
+ get overlayAlpha() {
2849
+ return this._overlayAlpha;
2850
+ }
2851
+ set overlayAlpha(alpha) {
2852
+ this._overlayAlpha = alpha;
2853
+ this.needsInitialization = true;
2854
+ }
2855
+ get icon() {
2856
+ const localePicker = this;
2857
+ return {
2858
+ get svgString() {
2859
+ return localePicker._icon.svgString;
2860
+ },
2861
+ set svgString(svgString) {
2862
+ localePicker._icon.svgString = svgString;
2863
+ localePicker.needsInitialization = true;
2864
+ },
2865
+ get imageName() {
2866
+ return localePicker._icon.imageName;
2867
+ },
2868
+ set imageName(imageName) {
2869
+ localePicker._icon.imageName = imageName;
2870
+ localePicker.needsInitialization = true;
2871
+ },
2872
+ get height() {
2873
+ return localePicker._icon.height;
2874
+ },
2875
+ set height(height) {
2876
+ localePicker._icon.height = height;
2877
+ localePicker.needsInitialization = true;
2878
+ },
2879
+ get width() {
2880
+ return localePicker._icon.width;
2881
+ },
2882
+ set width(width) {
2883
+ localePicker._icon.width = width;
2884
+ localePicker.needsInitialization = true;
2885
+ }
2886
+ };
2887
+ }
2888
+ set icon(icon) {
2889
+ this._icon = icon;
2890
+ this.needsInitialization = true;
2891
+ }
2892
+ get iconPosition() {
2893
+ const localePicker = this;
2894
+ return {
2895
+ get x() {
2896
+ return localePicker._iconPosition.x;
2897
+ },
2898
+ set x(x) {
2899
+ localePicker._iconPosition.x = x;
2900
+ if (localePicker.iconSprite) {
2901
+ localePicker.iconSprite.position = localePicker._iconPosition;
2902
+ }
2903
+ localePicker.needsInitialization = true;
2904
+ },
2905
+ get y() {
2906
+ return localePicker._iconPosition.y;
2907
+ },
2908
+ set y(y) {
2909
+ localePicker._iconPosition.y = y;
2910
+ if (localePicker.iconSprite) {
2911
+ localePicker.iconSprite.position = localePicker._iconPosition;
2912
+ }
2913
+ localePicker.needsInitialization = true;
2914
+ }
2915
+ };
2916
+ }
2917
+ set iconPosition(position) {
2918
+ this._iconPosition = position;
2919
+ if (this.iconSprite) {
2920
+ this.iconSprite.position = position;
2921
+ }
2922
+ this.needsInitialization = true;
2923
+ }
2924
+ get localeOptions() {
2925
+ return this._localeOptions;
2926
+ }
2927
+ set localeOptions(options) {
2928
+ this._localeOptions = options;
2929
+ this.needsInitialization = true;
2930
+ }
2931
+ get currentLocale() {
2932
+ return this._currentLocale;
2933
+ }
2934
+ set currentLocale(locale) {
2935
+ if (locale === this.currentLocale) {
2936
+ return;
2937
+ }
2938
+ this._currentLocale = locale;
2939
+ this.needsInitialization = true;
2940
+ }
2941
+ update() {
2942
+ super.update();
2943
+ }
2944
+ draw(canvas) {
2945
+ super.drawChildren(canvas);
2946
+ }
2947
+ warmup(canvas) {
2948
+ this.initialize();
2949
+ this.children.filter((child) => child.isDrawable).forEach((child) => {
2950
+ child.warmup(canvas);
2951
+ });
2952
+ }
2953
+ /**
2954
+ * Duplicates a node using deep copy.
2955
+ *
2956
+ * @remarks This is a deep recursive clone (node and children).
2957
+ * The uuid property of all duplicated nodes will be newly created,
2958
+ * because uuid must be unique.
2959
+ *
2960
+ * @param newName - optional name of the new, duplicated node. If not
2961
+ * provided, name will be the new uuid
2962
+ */
2963
+ duplicate(newName) {
2964
+ throw new Error(`duplicate not implemented. ${newName}`);
2965
+ }
2966
+ }
2967
+
2968
+ console.log("\u26AA @m2c2kit/addons version 0.3.16 (0679492d)");
1985
2969
 
1986
- export { Button, CountdownScene, Dialog, DialogResult, DrawPad, DrawPadEventType, DrawPadItemEventType, Grid, Instructions, VirtualKeyboard };
2970
+ export { Button, CountdownScene, Dialog, DialogResult, DrawPad, DrawPadEventType, DrawPadItemEventType, Grid, Instructions, LocalePicker, VirtualKeyboard };
1987
2971
  //# sourceMappingURL=index.js.map