-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathBlocksViewController.swift
1222 lines (1043 loc) · 58.8 KB
/
BlocksViewController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// BlocksViewController.swift
// BlocksForAll
//
// ViewController for the workspace where the block program is being created
//
// Created by Lauren Milne on 5/9/17.
// Copyright © 2017 Lauren Milne. All rights reserved.
//
import UIKit
import AVFoundation
// Global variables
var functionsDict = [String : [Block]]() // dictionary containing the different functions (composed as a list of Blocks) in the program
var currentWorkspace = String() // workspace you are currently editing on screen (i.e. the main workspace or a user-defined function)
//MARK: - Block Selection Delegate Protocol
/* Sends information about which blocks are selected to SelectedBlockViewController when moving blocks in workspace. */
protocol BlockSelectionDelegate{
func beginMovingBlocks(_ blocks:[Block])
func finishMovingBlocks()
func setParentViewController(_ myVC:UIViewController)
}
//MARK: - BlocksViewController
/* Used to display the Main Workspace */
class BlocksViewController: RobotControlViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, BlockSelectionDelegate {
var currentProject: Project? = nil
//MARK: Variables
// Views
@IBOutlet var mainView: UIView!
@IBOutlet weak var workspaceContainerView: UIView!
@IBOutlet weak var toolboxView: UIView!
private var allBlockViews = [BlockView]() // Top-level controller for toolbox view controllers
private var containerViewController: UINavigationController?
@IBOutlet weak var blocksProgram: UICollectionView! // View on bottom of screen that shows blocks in workspace
// Main workspace buttons
@IBOutlet weak var mainMenuButton: UIButton! // Home button. Brings you to main menu.
//@IBOutlet weak var clearAllButton: CustomButton! // the clear all button has been removed
@IBOutlet weak var mainWorkspaceButton: UIButton! // Arrow button that shows when you are working on a function. Brings you back to the main workspace
@IBOutlet weak var playTrashToggleButton: UIButton! // Play button
// Other View Controller elements
@IBOutlet weak var workspaceTitle: UILabel! // Label at top of screen
// Robot variables
internal var robotRunning = false // True if the robot is running. Used to disable code editing while robot is active.
// Variables for display
internal var stopIsOption = false // True if the stop button can be shown
private var movingBlocks = false // True if the user is currently moving a block. Disable modifier blocks if this is true.
private var arrowToPlaceFirstBlock: UIImageView? = nil // The arrow image that gets shown when the user is about to place the first block in the workspace
// Block variables
private var blocksBeingMoved = [Block]() // Blocks currently being moved (includes nested blocks)
private var indexOfMovingBlock: Int? = nil // Optional Variable that tracks where block originally was from in the workspace, used to place block back in workspace if move is stopped (navigate to a different screen) set to nil if moving block is from toolbox
var blockSize = 150
private let blockSpacing = 1
private let startIndex = 0
//private var endIndex: Int { return functionsDict[currentWorkspace]!.count - 1 } //TODO: add back in functions?
private var endIndex: Int = 0
// Modifier block variables
private var startingHeight = 0 // A value for calculating the y position of BlockViews
private var count = 0 // Number of blocks in the workspace
internal var allModifierBlocks = [UIButton]() // A list of all the modifier blocks in the workspace
private var modifierBlockIndex: Int? // An integer used to identify which modifier block was clicked when going to other screens.
var galleryType = String()
//MARK: - View Controller Methods
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//Order contents of workspace to be more intuitive with Switch Control and VoiceOver
mainView.accessibilityElements = [toolboxView!, workspaceContainerView!]
// TODO: allow scrolling in the workspace with Switch Control
// blocksProgram.isAccessibilityElement = true // allows workspace scrolling with Switch Control, but you can no longer access the blocks inside
workspaceContainerView.accessibilityElements = [blocksProgram!, playTrashToggleButton!, mainMenuButton!, mainWorkspaceButton!]
}
override func viewDidLoad() {
super.viewDidLoad()
if currentProject?.projectType == ProjectType.Robot {
currentWorkspace = "Main Workspace"
}
// TODO: next line fails in physical robot opening a new function
endIndex = currentProject!.currentActor!.functionDict[currentWorkspace]!.count - 1
updateStyling()
if isWorkspaceCustomFunction(name: currentWorkspace) {
setUpForCustomFunction()
} else {
setUpForMainWorkspace()
}
blocksProgram.delegate = self
blocksProgram.dataSource = self
allModifierBlocks.removeAll()
}
// MARK: Screen Setup
/// Set fonts and other style options
private func updateStyling() {
// Fonts
workspaceTitle.adjustsFontForContentSizeCategory = true
workspaceTitle.font = UIFont.accessibleBoldFont(withStyle: .largeTitle, size: 34.0)
// Set block size based on block size from settings or 150 by default
blockSize = defaults.value(forKey: "blockSize") as? Int ?? 150
// Other Visual Elements
self.navigationController?.isNavigationBarHidden = true
}
/// Set up workspace to be editing a custom function.
/// Creates the "start function" and "end function" blocks and "back to main workspace" button
private func setUpForCustomFunction() {
mainWorkspaceButton.isHidden = false // Show Back to Main Workspace arrow button
if #available(iOS 13.0, *) {
workspaceTitle.textColor = .label
} else {
workspaceTitle.textColor = .black
}
workspaceTitle.text = "Return to Main Workspace"
// Add start and end function blocks
if currentProject!.currentActor!.functionDict[currentWorkspace]!.isEmpty{
let startBlock = Block.init(
name: "\(currentWorkspace) Function Start",
colorName: "light_purple_block",
double: true,
isModifiable: false)
let endBlock = Block.init(
name: "\(currentWorkspace) Function End",
colorName: "light_purple_block",
double: true,
isModifiable: false)
startBlock!.counterpart = [endBlock!]
endBlock!.counterpart = [startBlock!]
currentProject!.currentActor!.functionDict[currentWorkspace]?.append(startBlock!)
currentProject!.currentActor!.functionDict[currentWorkspace]?.append(endBlock!)
}
}
/// Set up workspace to be editing the main program
private func setUpForMainWorkspace() {
addEventIndicatorBlocks()
mainWorkspaceButton.isHidden = true // Hide Back to Main Workspace arrow button. Already in Main Workspace.
workspaceTitle.textColor = UIColor(named: "navy_text")
workspaceTitle.text = "Main Workspace"
}
/// Adds "Start" blocks for freeplay mode events like "On Tap"
/// For freeplay mode only
func addEventIndicatorBlocks() {
if currentProject?.projectType == ProjectType.Robot { // Don't add the blocks if currently in a Robot Workspace
return
}
for actor in currentProject!.actors {
for function in actor.functionDict.keys {
if function == ON_RUN_STRING || function == ON_BUMP_STRING || function == ON_TAP_STRING {
if actor.functionDict[function]!.isEmpty{
let startBlock = Block.init(
name: "\(function) Start", //TODO: update block name
colorName: "light_purple_block",
double: false,
isModifiable: false)
actor.functionDict[function]?.append(startBlock!)
}
}
}
}
}
// MARK: Navigation
/// Main Menu Segue
@IBAction func goToMainMenu(_ sender: UIButton) {
finishMovingBlocks()
isInFreeplay = false
performSegue(withIdentifier: "toMainMenu", sender: self)
}
/// Main Workspace Segue
@IBAction func goToMainWorkspace(_ sender: Any) {
finishMovingBlocks()
currentWorkspace = "Main Workspace"
//Segues from the main workspace to itself to reload the view (switches from functions workspace to main)
performSegue(withIdentifier: "mainToMain", sender: self)
}
override func viewWillDisappear(_ animated: Bool) {
// Save project snapshot every time the view will disappear saves an image more often than just saving it when the user goes to the main menu, since it doesn't save one when closing the app
// TODO: save a snapshot when closing the app
saveProjectSnapshot()
}
private func makeAnnouncement(_ announcement: String){
UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: NSLocalizedString(announcement, comment: ""))
}
// MARK: Saving Data
/// Save snapshot of the blocksProgram
func saveProjectSnapshot() {
// scroll to the beginning to take the snapshot
blocksProgram.scrollToItem(at: IndexPath(item: 0, section: 0), at: .left, animated: false)
// rendering view as image is from: https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage
let renderer = UIGraphicsImageRenderer(size: blocksProgram.bounds.size)
let image = renderer.image { ctx in
blocksProgram.drawHierarchy(in: blocksProgram.bounds, afterScreenUpdates: true)
}
currentProject!.image = image
currentProject!.imageName = generateImageName()
}
/// Generate a unique image name for the project snapshot
func generateImageName() -> String {
//TODO: make sure image names are unique and get deleted when projects are deleted
return String(galleryType + currentProject!.name + ".png")
}
// MARK: Memory/Data Methods
/// Dispose of any resources that can be recreated.
override func didReceiveMemoryWarning() {
// TODO: is this code redundant?
super.didReceiveMemoryWarning()
}
/// This function gets called from the RobotControllerViewController so that the block that is currently running gets highlighted
override func refreshScreen() {
blocksProgram.reloadData()
}
//TODO: add to virtual robot
/// Draw a bouncing arrow in the blocksProgram to indicate to user where to place the first block
private func showArrowToPlaceFirstBlock() {
let img = UIImage(named: "Back")
let resizedImage = HelperFunctions.resizeImage(image: img!, scaledToSize: CGSize(width: blockSize, height: blockSize)) // resize the image to scale correctly
let imv = UIImageView(image: resizedImage)
// Turn arrow to point down
imv.transform = imv.transform.rotated(by: -(.pi / 2))
// Position arrow vertically on the screen
imv.transform = imv.transform.translatedBy(x: -blocksProgram.frame.height / 2 , y: 0)
arrowToPlaceFirstBlock = imv
// Accessibility - uncomment to make the arrow an accessibility element
// arrowToPlaceFirstBlock?.isAccessibilityElement = true
// arrowToPlaceFirstBlock?.accessibilityLabel = "Image of arrow pointing down to show where the first block will be placed in the workspace."
// imv.isAccessibilityElement = true
// blocksProgram.accessibilityElements = [imv]
blocksProgram.addSubview(imv)
// Bounce the arrow up and down
// got the code to repeat and autoreverse an animation from https://developer.apple.com/forums/thread/666312
UIView.animate(withDuration: 1, delay: 0, options: [.repeat, .autoreverse], animations: {
let amountToMove = 70.0
imv.transform = imv.transform.translatedBy(x: amountToMove, y: 0)
})
}
//MARK: - Accessibility Methods
/// Creates the custom rotor action for SwitchControl to delete blocks
@objc func deleteBlockCustomAction() -> Bool {
// TODO: currently does not work
let focusedCell = UIAccessibility.focusedElement(using: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver) as! UICollectionViewCell
if let indexPath = blocksProgram?.indexPath(for: focusedCell) {
// perform the custom action here using the indexPath information
selectBlock(block: currentProject!.currentActor!.functionDict[currentWorkspace]![indexPath.row], location: indexPath.row)
}
return true
}
/// Adds VoiceOver label to blockView, which changes to placement info if blocks are being moved
/// - Parameters:
/// - blockView: view to be given the label
/// - block: block being displayed
/// - blockModifier: describes the state of the block modifier (e.g. 2 times for repeat 2 times)
/// - blockLocation: location of block in workspace (e.g. 2 of 4)
///
func addAccessibilityLabel(blockView: UIView, block:Block, blockModifier:String, blockLocation: Int, blockIndex: Int){
blockView.isAccessibilityElement = true
// Add Custom Action for deleting block
// if !block.name.contains("Function Start") && !block.name.contains("Function End") {
// let deleteBlock = UIAccessibilityCustomAction(
// name: "Delete Block",
// target: self,
// selector: #selector(deleteBlockCustomAction))
// blockView.accessibilityCustomActions = [deleteBlock]
// }
var accessibilityLabel = ""
var blockPlacementInfo = ". Workspace block " + String(blockLocation) + " of " + String(currentProject!.currentActor!.functionDict[currentWorkspace]!.count)
var accessibilityHint = ""
var movementInfo = ". Double tap to move block."
if isInFreeplay {
// slightly change when in freeplay because of event indicator blocks
if !block.name.contains(ON_RUN_STRING) && !block.name.contains(ON_TAP_STRING) { // not the event indicator block
accessibilityLabel = ""
blockPlacementInfo = ". Workspace block " + String(blockLocation - 1) + " of " + String(currentProject!.currentActor!.functionDict[currentWorkspace]!.count - 1) // must shift to one less due to event indicator block
accessibilityHint = ""
movementInfo = ". Double tap to move block."
} else {
blockPlacementInfo = ""
movementInfo = ""
}
}
if(!blocksBeingMoved.isEmpty){
// Moving blocks, so switch labels to indicated where blocks can be placed
if ((isWorkspaceCustomFunction(name: currentWorkspace) || isPremadeFunction(name: currentWorkspace)) && blockIndex == 1){
accessibilityLabel = "Place " + blocksBeingMoved[0].name + " at beginning of " + currentWorkspace + " function."
} else if (!isWorkspaceCustomFunction(name: currentWorkspace) && blockIndex == 0){
// in main workspace and setting 1st block accessibility info
accessibilityLabel = "Place " + blocksBeingMoved[0].name + " at beginning, before "
accessibilityLabel += block.name + " " + blockModifier + " " + blockPlacementInfo
} else {
accessibilityLabel = "Place " + blocksBeingMoved[0].name + " before "
accessibilityLabel += block.name + " " + blockModifier + " " + blockPlacementInfo
}
if ((isWorkspaceCustomFunction(name: currentWorkspace) || isPremadeFunction(name: currentWorkspace)) && blockIndex == 0){
accessibilityLabel = "Start of " + currentWorkspace + " function."
movementInfo = ""
} else {
movementInfo = ". Double tap to add " + blocksBeingMoved[0].name + " block here"
}
} else {
accessibilityLabel = block.name + " " + blockModifier + " " + blockPlacementInfo
}
accessibilityHint += movementInfo
blockView.accessibilityLabel = accessibilityLabel
createVoiceControlLabels(for: block, in: blockView)
blockView.accessibilityHint = accessibilityHint
}
// TODO: rewrite this method
// TODO: update for freeplay
func createVoiceControlLabels(for block: Block, in blockView: UIView) {
if #available (iOS 13.0, *) {
let color = block.colorName
switch color {
case "orange_block": // Control
if movingBlocks {
if block.name == "Wait for Time" {
blockView.accessibilityUserInputLabels = ["Before Wait", "Before \(block.name)"]
}
} else {
blockView.accessibilityUserInputLabels = ["Wait", "\(block.name)"]
}
case "green_block": // Drive
var voiceControlLabel = block.name
if block.name.contains("Drive") {
let wordToRemove = "Drive "
if let range = voiceControlLabel.range(of: wordToRemove){
voiceControlLabel.removeSubrange(range)
}
} else if block.name.contains("Turn") {
let wordToRemove = "Turn "
if let range = voiceControlLabel.range(of: wordToRemove){
voiceControlLabel.removeSubrange(range)
}
}
if movingBlocks {
blockView.accessibilityUserInputLabels = ["Before \(block.name)", "Before \(voiceControlLabel)"]
} else {
blockView.accessibilityUserInputLabels = ["\(block.name)", "\(voiceControlLabel)"]
}
case "gold_block": // Lights
var voiceControlLabel = block.name
let wordToRemove = "Set "
if let range = voiceControlLabel.range(of: wordToRemove){
voiceControlLabel.removeSubrange(range)
}
var voiceControlLabel2 = voiceControlLabel
if block.name != "Set All Lights" {
let wordToRemove2 = " Light"
if let range = voiceControlLabel2.range(of: wordToRemove2) {
voiceControlLabel2.removeSubrange(range)
}
}
if movingBlocks {
blockView.accessibilityUserInputLabels = ["Before \(voiceControlLabel)", "Before \(voiceControlLabel2)", "Before \(block.name)"]
} else {
blockView.accessibilityUserInputLabels = ["\(voiceControlLabel)", "\(voiceControlLabel2)", "\(block.name)"]
}
case "red_block": // Look
var voiceControlLabel = block.name
let wordToRemove = "Look "
if let range = voiceControlLabel.range(of: wordToRemove){
voiceControlLabel.removeSubrange(range)
}
if movingBlocks {
blockView.accessibilityUserInputLabels = ["Before \(block.name)", "Before \(voiceControlLabel)"]
} else {
blockView.accessibilityUserInputLabels = ["\(block.name)", "\(voiceControlLabel)"]
}
default:
blockView.accessibilityUserInputLabels = ["\(block.name)"]
}
}
}
// MARK: - Block Selection Delegate functions
/// Called when blocks are placed in workspace, so clears blocksBeingMoved
func finishMovingBlocks() {
if indexOfMovingBlock != nil { // Replaces the block in the Workspace if it is from the workspace
addBlocks(blocksBeingMoved, at: indexOfMovingBlock!)
}
movingBlocks = false
blocksBeingMoved.removeAll()
changePlayTrashButton() // Toggling the play/trash button
indexOfMovingBlock = nil
// Remove the arrow in the workspace when blocks are done moving
if arrowToPlaceFirstBlock != nil {
arrowToPlaceFirstBlock?.removeFromSuperview()
blocksProgram.accessibilityElements = []
}
}
/// Called when blocks have been selected to be moved, saves them to blocksBeingMoved
/// - Parameter blocks: blocks selected to be moved
func beginMovingBlocks(_ blocks: [Block]) {
movingBlocks = true
blocksBeingMoved = blocks
blocksProgram.reloadData()
changePlayTrashButton()
// If there are no blocks in the workspace, show the arrow of where the first block will go
if currentProject!.currentActor!.functionDict[currentWorkspace]!.count == 0 {
showArrowToPlaceFirstBlock()
}
}
//TODO: LAUREN, figure out what this code is for
func setParentViewController(_ myVC: UIViewController) {
containerViewController = myVC as? UINavigationController
}
//MARK: - Play/Stop/Trash Methods
/// Changes the play button back and forth from trash to play
internal func changePlayTrashButton() {
if movingBlocks {
playTrashToggleButton.setBackgroundImage(#imageLiteral(resourceName: "Trashcan"), for: .normal)
playTrashToggleButton.accessibilityLabel = "Place in Trash"
if #available(iOS 13.0, *)
{ playTrashToggleButton.accessibilityUserInputLabels = ["Trash"] }
playTrashToggleButton.accessibilityHint = "Delete selected blocks"
} else if stopIsOption {
playTrashToggleButton.setBackgroundImage(#imageLiteral(resourceName: "stop"), for: .normal)
playTrashToggleButton.accessibilityLabel = "Stop"
if #available(iOS 13.0, *)
{ playTrashToggleButton.accessibilityUserInputLabels = ["Stop"] }
playTrashToggleButton.accessibilityHint = "Stop your robot!"
} else {
playTrashToggleButton.setBackgroundImage(#imageLiteral(resourceName: "GreenArrow"), for: .normal)
playTrashToggleButton.accessibilityLabel = "Play"
if #available(iOS 13.0, *)
{ playTrashToggleButton.accessibilityUserInputLabels = ["Play"] }
playTrashToggleButton.accessibilityHint = "Make your robot go!"
}
}
/// Determine what to do based on the state of the play button when it was clicked. Delete blocks if moving blocks, stop blocks if stopIsOption, or play program.
@IBAction func playButtonClicked(_ sender: Any) {
if (movingBlocks)
{ trashClicked() }
else if stopIsOption
{ stopClicked() }
else {
playClicked()
}
}
/// Run the actual program when the trash button is clicked
private func trashClicked() {
indexOfMovingBlock = nil
let announcement = blocksBeingMoved[0].name + " placed in trash."
playTrashToggleButton.accessibilityLabel = announcement
self.containerViewController?.popViewController(animated: false)
blocksProgram.reloadData()
finishMovingBlocks()
}
/// Run the actual program when the play button is clicked
func playClicked() {
if(!areRobotsConnected()) {
//no robots
let announcement = "Connect to the dash robot. "
UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: announcement)
performSegue(withIdentifier: "AddRobotSegue", sender: nil)
} else if(currentProject!.currentActor!.functionDict[currentWorkspace]!.isEmpty) {
changePlayTrashButton()
let announcement = "Your robot has nothing to do! Add some blocks to your workspace."
playTrashToggleButton.accessibilityLabel = announcement
} else {
stopIsOption = true
changePlayTrashButton()
//Calls RobotControllerViewController play function
for actor in currentProject!.actors { // reset all actors stopWasPressed value
actor.executingProgram?.stopWasPressed = false
}
play(functionsDictToPlay: currentProject!.currentActor!.functionDict)
robotRunning = true
// disable modifier blocks while the robot is running
for modifierBlock in allModifierBlocks {
modifierBlock.isEnabled = false
modifierBlock.isAccessibilityElement = false
}
}
refreshScreen()
}
/// Stop the program
private func stopClicked() {
self.executingProgram = nil
programHasCompleted()
}
/// Called when the program is either stopped or finishes on its own
override func programHasCompleted() {
movingBlocks = false
stopIsOption = false
changePlayTrashButton()
robotRunning = false
// reenable modifier blocks
for modifierBlock in allModifierBlocks {
modifierBlock.isEnabled = true
modifierBlock.isAccessibilityElement = true
}
for actor in currentProject!.actors {
actor.executingProgram?.stopWasPressed = true
actor.isRunning = false
if actor.functionDict[currentWorkspace] != nil {
for block in actor.functionDict[currentWorkspace]! {
block.isRunning = false
}
}
}
refreshScreen()
}
func updateCurrentWorkspace (name: String) {
currentWorkspace = name
refreshScreen()
}
/// Returns true if the given workspace name is a custom function and false if it is a premade workspace like "Main Workspace" or "On Run"
func isWorkspaceCustomFunction(name: String) -> Bool {
if (PREMADE_FUNCTION_NAMES.contains(name)) {
return false
}
return true
}
func isPremadeFunction(name: String) -> Bool {
return PREMADE_FUNCTION_NAMES.contains(name)
}
// MARK: - Blocks Methods
/// Called after selecting a place to add a block to the workspace, makes accessibility announcements and place blocks in the blockProgram stack, etc...
private func addBlocks(_ blocks: [Block], at index: Int) {
//change for beginning
var announcement = ""
if blocks.count == 0 { // for some reason the app is crashing from this method with an index out of range error when accessing blocks[0]. Not sure why but this should fix it for now
return
}
if (index != 0) {
let myBlock = currentProject!.currentActor!.functionDict[currentWorkspace]![index-1]
announcement = blocks[0].name + " placed after " + myBlock.name
} else {
announcement = blocks[0].name + " placed at beginning"
}
indexOfMovingBlock = nil
makeAnnouncement(announcement)
//add a completion block here
if blocks[0].double {
if isWorkspaceCustomFunction(name: currentWorkspace) && index > endIndex {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(contentsOf: blocks, at: endIndex)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
} else if isWorkspaceCustomFunction(name: currentWorkspace) && index <= startIndex {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(contentsOf: blocks, at: startIndex+1)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
} else {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(contentsOf: blocks, at: index)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
}
} else {
if isWorkspaceCustomFunction(name: currentWorkspace) && index > endIndex {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(blocks[0], at: endIndex)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
} else if isWorkspaceCustomFunction(name: currentWorkspace) && index <= startIndex {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(blocks[0], at: startIndex+1)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
} else {
currentProject!.currentActor!.functionDict[currentWorkspace]!.insert(blocks[0], at: index)
blocksBeingMoved.removeAll()
blocksProgram.reloadData()
}
}
}
private func createBlock(_ block: Block, withFrame frame: CGRect) -> UILabel {
let myLabel = UILabel.init(frame: frame)
myLabel.text = block.name
myLabel.textAlignment = .center
myLabel.textColor = UIColor(named: "\(block.colorName)")
myLabel.numberOfLines = 0
myLabel.backgroundColor = UIColor(named: "\(block.colorName)")
return myLabel
}
// MARK: - Collection View Methods
func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
collectionView.remembersLastFocusedIndexPath = false
return currentProject!.currentActor!.functionDict[currentWorkspace]!.count + 1
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var size = CGSize(width: CGFloat(blockSize), height: collectionView.frame.height)
collectionView.remembersLastFocusedIndexPath = false
if indexPath.row == currentProject!.currentActor!.functionDict[currentWorkspace]!.count {
// expands the size of the last cell in the collectionView, so it's easier to add a block at the end with VoiceOver on
if currentProject!.currentActor!.functionDict[currentWorkspace]!.count < 8 {
// TODO: eventually simplify this section without blocksStack.count < 8
// blocksStack.count < 8 means that the orignal editor only fit up to 8 blocks of a fixed size horizontally, but we may want to change that too
let myWidth = collectionView.frame.width
size = CGSize(width: myWidth, height: collectionView.frame.height)
} else {
size = CGSize(width: CGFloat(blockSize), height: collectionView.frame.height)
}
}
return size
}
/// CollectionView contains the actual collection of blocks (i.e. the program that is being created with the blocks) This method creates and returns the cell at a given index
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
collectionView.remembersLastFocusedIndexPath = false
let collectionReuseIdentifier = "BlockCell"
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionReuseIdentifier, for: indexPath)
// Configure the cell
for myView in cell.subviews{
myView.removeFromSuperview()
}
cell.isAccessibilityElement = false
if indexPath.row == currentProject!.currentActor!.functionDict[currentWorkspace]!.count { // The last cell in the collectionView is an empty cell so you can place blocks at the end
if !blocksBeingMoved.isEmpty{
cell.isAccessibilityElement = true
if currentProject!.currentActor!.functionDict[currentWorkspace]!.count == 0 {
cell.accessibilityLabel = "Place " + blocksBeingMoved[0].name + " at Beginning"
if #available (iOS 13.0, *) { cell.accessibilityUserInputLabels = ["Workspace"] }
} else {
if !isWorkspaceCustomFunction(name: currentWorkspace) {
cell.accessibilityLabel = "Place " + blocksBeingMoved[0].name + " at End"
if #available (iOS 13.0, *) { cell.accessibilityUserInputLabels = ["End of workspace"] }
} else {
cell.accessibilityLabel = "Place " + blocksBeingMoved[0].name + " at End of " + currentWorkspace + " function"
if #available (iOS 13.0, *) { cell.accessibilityUserInputLabels = ["End of function workspace"] }
}
}
}
} else {
startingHeight = Int(cell.frame.height)-blockSize
let block = currentProject!.currentActor!.functionDict[currentWorkspace]![indexPath.row]
var blocksToAdd = [Block]()
//check if block is nested (or nested multiple times) and adds in "inside" repeat/if blocks
for i in 0...indexPath.row {
if currentProject!.currentActor!.functionDict[currentWorkspace]![i].double {
if !currentProject!.currentActor!.functionDict[currentWorkspace]![i].name.contains("End") {
if i != indexPath.row {
blocksToAdd.append(currentProject!.currentActor!.functionDict[currentWorkspace]![i])
}
} else {
if !blocksToAdd.isEmpty {
blocksToAdd.removeLast()
}
}
}
}
count = 0
for b in blocksToAdd {
let myView = createBlock(b, withFrame: CGRect(
x: -blockSpacing,
y: startingHeight + blockSize / 2 - count * (blockSize / 2 + blockSpacing),
width: blockSize + 2 * blockSpacing,
height: blockSize / 2))
if b.name.contains("Function Start") {
myView.accessibilityLabel = "Inside \(currentWorkspace) function"
myView.text = "Inside \(currentWorkspace) function"
} else {
myView.accessibilityLabel = "Inside " + b.name
myView.text = "Inside " + b.name
myView.isAccessibilityElement = true
}
cell.addSubview(myView)
cell.accessibilityElements = [myView]
count += 1
}
let name = block.name
let modifierInformation = ""
if isModifierBlock(name: name) {
setUpModifierButton(block: block, blockName : name, indexPath: indexPath, cell: cell)
} else {
switch name {
// block exists but is a non-modifier block
case "End If", "End Repeat", "End Repeat Forever", "Repeat Forever", "Look Forward", "Look Toward Voice", "Look Right", "Look Left", "Look Straight", "Look Down", "Look Up", "Wiggle", "Nod", "Spiral Light", "Move to Center", "\(ON_RUN_STRING) Start", "\(ON_BUMP_STRING) Start", "\(ON_TAP_STRING) Start":
let blockView = BlockView(frame: CGRect(x: 0, y: startingHeight-count*(blockSize/2+blockSpacing), width: blockSize, height: blockSize), block: [block], myBlockSize: blockSize)
addAccessibilityLabel(blockView: blockView, block: block, blockModifier: modifierInformation, blockLocation: indexPath.row+1, blockIndex: indexPath.row)
cell.addSubview(blockView)
// if the block is nested in another block, add that item to accessibility elements
let nestedBlock = cell.accessibilityElement(at: 0)
if (nestedBlock != nil) {
cell.accessibilityElements = [blockView, nestedBlock!]
}
allBlockViews.append(blockView)
default:
// the block is a custom function
let functions: [String] = Array(currentProject!.currentActor!.functionDict.keys) // All the names of the functions a user creates placed in an array
if (functions.contains(block.name) || block.name.contains("Function Start") || block.name.contains("Function End")) {
let blockView = BlockView(frame: CGRect(x: 0, y: startingHeight-count*(blockSize/2+blockSpacing), width: blockSize, height: blockSize), block: [block], myBlockSize: blockSize)
addAccessibilityLabel(blockView: blockView, block: block, blockModifier: "function", blockLocation: indexPath.row+1, blockIndex: indexPath.row)
cell.addSubview(blockView)
allBlockViews.append(blockView)
} else {
print("Non matching case. \(name) could not be found. Check collectionView() method in BlocksViewController.")
}
}
}
}
// Deactivates all modifier blocks in the workspace while a block is being moved.
// Switch control and VO will also skip over the modifier block.
if (movingBlocks || robotRunning) {
for modifierBlock in allModifierBlocks {
modifierBlock.isEnabled = false
modifierBlock.isAccessibilityElement = false
}
} else {
for modifierBlock in allModifierBlocks {
modifierBlock.isEnabled = true
modifierBlock.isAccessibilityElement = true
}
}
return cell
}
//selects a block to be moved in the workspace
func selectBlock (block myBlock:Block, location blocksStackIndex:Int ){
if myBlock.double == true {
var indexOfCounterpart = -1
var blockcounterparts = [Block]()
for i in 0..<currentProject!.currentActor!.functionDict[currentWorkspace]!.count {
for block in myBlock.counterpart{
if block === currentProject!.currentActor!.functionDict[currentWorkspace]![i]{
indexOfCounterpart = i
blockcounterparts.append(block)
}
}
}
var indexPathArray = [IndexPath]()
var tempBlockStack = [Block]()
for i in min(indexOfCounterpart, blocksStackIndex)...max(indexOfCounterpart, blocksStackIndex){
indexPathArray += [IndexPath.init(row: i, section: 0)]
tempBlockStack += [currentProject!.currentActor!.functionDict[currentWorkspace]![i]]
}
blocksBeingMoved = tempBlockStack
currentProject!.currentActor!.functionDict[currentWorkspace]!.removeSubrange(min(indexOfCounterpart, blocksStackIndex)...max(indexOfCounterpart, blocksStackIndex))
} else { //only a single block to be removed
blocksBeingMoved = [currentProject!.currentActor!.functionDict[currentWorkspace]![blocksStackIndex]]
currentProject!.currentActor!.functionDict[currentWorkspace]!.remove(at: blocksStackIndex)
}
blocksProgram.reloadData()
}
/// Called when a block is selected in the collectionView, so either selects block to move or places blocks
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.remembersLastFocusedIndexPath = false
if !robotRunning { // disable editing while robot is running
if movingBlocks {
if indexPath.row < currentProject!.currentActor!.functionDict[currentWorkspace]!.count { // clicked somewhere before the empty space at the end
let blocksStackIndex = indexPath.row
let myBlock = currentProject!.currentActor!.functionDict[currentWorkspace]![blocksStackIndex]
guard !myBlock.name.contains("\(ON_RUN_STRING) Start") else { // Don't allow placing blocks before event indicators
return
}
guard !myBlock.name.contains("\(ON_BUMP_STRING) Start") else { // Don't allow placing blocks before event indicators
return
}
guard !myBlock.name.contains("\(ON_TAP_STRING) Start") else { // Don't allow placing blocks before event indicators
return
}
}
addBlocks(blocksBeingMoved, at: indexPath.row)
containerViewController?.popViewController(animated: false)
finishMovingBlocks()
} else {
if indexPath.row < currentProject!.currentActor!.functionDict[currentWorkspace]!.count { // otherwise empty block at end
movingBlocks = true
let blocksStackIndex = indexPath.row
let myBlock = currentProject!.currentActor!.functionDict[currentWorkspace]![blocksStackIndex]
guard !myBlock.name.contains("Function Start") else {
movingBlocks = false
return
}
guard !myBlock.name.contains("Function End") else {
movingBlocks = false
return
}
guard !myBlock.name.contains("\(ON_RUN_STRING) Start") else {
movingBlocks = false
return
}
guard !myBlock.name.contains("\(ON_BUMP_STRING) Start") else {
movingBlocks = false
return
}
guard !myBlock.name.contains("\(ON_TAP_STRING) Start") else {
movingBlocks = false
return
}
selectBlock(block: myBlock, location: blocksStackIndex)
indexOfMovingBlock = blocksStackIndex
let mySelectedBlockVC = self.storyboard?.instantiateViewController(withIdentifier: "SelectedBlockViewController") as! SelectedBlockViewController
mySelectedBlockVC.currentProject = currentProject
mySelectedBlockVC.delegate = self
containerViewController?.pushViewController(mySelectedBlockVC, animated: false)
mySelectedBlockVC.blocks = blocksBeingMoved
changePlayTrashButton()
} else {
// clicked empty block at end
movingBlocks = true
}
}
}
}
//MARK: - Modifier Button Methods
/// Use for modifier buttons. Calculates the width, height, position, and z-index of the modifier button and returns a CustomButton with those values
func createModifierCustomButton(block: Block, currentProject: Project?, modifierData: ModifierButtonData) -> ModifierButton {
let buttonFrame = CGRect(
x: blockSize / 11,
y: startingHeight - ((blockSize / 5) * 4) - count * (blockSize / 2 + blockSpacing),
width: (blockSize / 7) * 6,
height: (blockSize / 7) * 6)
let tempButton = ModifierButton(frame: buttonFrame, block: block, currentProject: currentProject, modifierData: modifierData)
tempButton.layer.zPosition = 1
allModifierBlocks.append(tempButton)
return tempButton
}
/// Sets up a modifier button based on the name inputted
private func setUpModifierButton(block : Block, blockName name : String, indexPath : IndexPath, cell : UICollectionViewCell) {
let dict = HelperFunctions.getPListDictionary(resourceName: "ModifierProperties") // holds properties of all modifier blocks
let (selector, defaultValue, attributeName, accessibilityHint, imagePath, displaysText, secondAttributeName, secondDefault, showTextImage) = getModifierData(name: name, dict: dict!) // constants taken from dict based on name
let modifierButtonData = ModifierButtonData(modifierButton: nil, blockName: name, selector: selector, defaultValue: defaultValue, attributeName: attributeName, accessibilityHint: accessibilityHint, imagePath: imagePath, displaysText: displaysText, secondAttributeName: secondAttributeName, secondDefault: secondDefault, showTextImage: showTextImage)
// Create the block
if block.addedBlocks.isEmpty{
let placeholderBlock = Block(name: name, colorName: "gray_color", double: false, type: "Boolean", isModifiable: true)
block.addedBlocks.append(placeholderBlock!)
placeholderBlock?.addAttributes(key: attributeName, value: "\(defaultValue)")
if secondAttributeName != nil && secondDefault != nil
{ placeholderBlock?.addAttributes(key: secondAttributeName!, value: "\(secondDefault!)") }
}
// renamed block.addedBlocks[0] for simplicity
let placeHolderBlock = block.addedBlocks[0]
let button = createModifierCustomButton(block: placeHolderBlock, currentProject: currentProject, modifierData: modifierButtonData) // set up button sizing and layering
button.addTarget(self, action: selector, for: .touchUpInside) // connect what happens when the button is pressed
var modifierInformation = button.getModifierInformation() // the current state of the block modifier - used for voiceOver
button.tag = indexPath.row
cell.addSubview(button) // add button to cell
//create blockView for the modifier
let blockView = BlockView(frame: CGRect(x: 0, y: startingHeight-count*(blockSize/2+blockSpacing), width: blockSize, height: blockSize), block: [block], myBlockSize: blockSize)
allBlockViews.append(blockView)
cell.addSubview(blockView)
// update addedBlocks
block.addedBlocks[0] = button.getBlock()
// Accessibility
// set voiceOver information
button.accessibilityHint = accessibilityHint
button.isAccessibilityElement = true
//TODO: this line doesn't really do anything, it is just the same as modifierInformation
let voiceControlLabel = modifierInformation
//TODO: test on different operating systems
if #available(iOS 13.0, *) {
button.accessibilityUserInputLabels = ["\(voiceControlLabel)", "\(modifierInformation)"]
}
addAccessibilityLabel(blockView: blockView, block: block, blockModifier: modifierInformation, blockLocation: indexPath.row+1, blockIndex: indexPath.row)
// the main part of the block is focused first, then the modifier button
// if the block is nested in another block, add that item to accessibility elements
let nestedBlock = cell.accessibilityElement(at: 0)
if (nestedBlock != nil) {
cell.accessibilityElements = [blockView, button, nestedBlock!]
} else {
cell.accessibilityElements = [blockView, button]
}
button.accessibilityLabel = modifierInformation
}
/// Gets values for modifier blocks from a dictionary and returns them as a tuple. Prints errors if properties cannot be found
private func getModifierData (name : String, dict : NSDictionary) -> (Selector, String, String, String, String?, Bool, String?, String?, String?) {
if dict[name] == nil {
print("\(name) could not be found in modifier block dictionary")
}
let selector = getModifierSelector(name: name) ?? nil // getModifierSelector() has an error statement built in already
let subDictionary = dict.value(forKey: name) as! NSDictionary // renamed for simplicity
let defaultValue = subDictionary.value(forKey: "default")
if defaultValue == nil {
print("default value for \(name) could not be found")
}