/
SKTGraphicView.rb
1673 lines (1447 loc) · 74.2 KB
/
SKTGraphicView.rb
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
# The names of the bindings supported by this class, in addition to the ones
# whose support is inherited from NSView.
SKTGraphicViewGraphicsBindingName = "graphics"
SKTGraphicViewSelectionIndexesBindingName = "selectionIndexes"
SKTGraphicViewGridBindingName = "grid"
# The type name that this class uses when putting flattened graphics on the
# pasteboard during cut, copy, and paste operations. The format that's
# identified by it is not the exact same thing as the native document format
# used by SKTDocument, because SKTDocuments store NSPrintInfos (and maybe
# other stuff too in the future). We could easily use the exact same format
# for pasteboard data and document files if we decide it's worth it, but so
# far we haven't.
SKTGraphicViewPasteboardType = "Apple Sketch 2 pasteboard type"
# The default value by which repetitively pasted sets of graphics are offset
# from each other, so the user can paste repeatedly and not end up with a pile
# of graphics that overlay each other so perfectly only the top set can be
# selected with the mouse.
SKTGraphicViewDefaultPasteCascadeDelta = 10.0
# # Some methods that are invoked by methods above them in this file.
# @interface SKTGraphicView(SKTForwardDeclarations)
# - (NSArray *)graphics;
# - (void)stopEditing;
# - (void)stopObservingGraphics:(NSArray *)graphics;
# @end
#
#
# # Some methods that really should be declared in AppKit's NSWindow.h, but are not. You can consider them public. (In general though Cocoa methods that are not declared in header files are not public, and you run a bad risk of your application breaking on future versions of Mac OS X if you invoke or override them.) See their uses down below in SKTGraphicView's own implementations of -undo: and -redo:.
# @interface NSWindow(SKTWellTheyrePublicNow)
# def undo:(id)sender;
# def redo:(id)sender;
# @end
GraphicInfo = Struct.new(:graphic, :index, :isSelected, :handle)
class SKTGraphicView < NSView
attr_reader :grid # for KVO to work
# An override of the superclass' designated initializer.
def initWithFrame (frame)
super
# Create the proxy objects used during observing
@gridObserver = SKTObserver.new(self, :gridAnyDidChange)
@graphicsBoundsObserver = SKTObserver.new(self, :graphicsBoundDidChange)
@graphicsContentObserver = SKTObserver.new(self, :graphicsContentDidChange)
@graphicsSelectionIndexesObserver = SKTObserver.new(self, :graphicsSelectionIndicesDidChange)
@graphicsContainerObserver = SKTObserver.new(self, :graphicsContainerDidChange)
@marqueeSelectionBounds = NSZeroRect
# Specify what kind of pasteboard types this view can handle being dropped
# on it.
registerForDraggedTypes([NSColorPboardType, NSFilenamesPboardType] + NSImage.imagePasteboardTypes)
# Initalize the cascading of pasted graphics.
@pasteboardChangeCount = -1
@pasteCascadeNumber = 0
@pasteCascadeDelta = NSMakePoint(SKTGraphicViewDefaultPasteCascadeDelta, SKTGraphicViewDefaultPasteCascadeDelta)
return self;
end
# - (void)dealloc {
#
# # If we've set a timer to show handles invalidate it so it doesn't send a message to this object's zombie.
# [_handleShowingTimer invalidate];
#
# # Make sure any outstanding editing view doesn't cause leaks.
# [self stopEditing];
#
# # Stop observing grid changes.
# [_grid removeObserver:self forKeyPath:SKTGridAnyKey];
#
# # Stop observing objects for the bindings whose support isn't implemented using NSObject's default implementations.
# [self unbind:SKTGraphicViewGraphicsBindingName];
# [self unbind:SKTGraphicViewSelectionIndexesBindingName];
#
# # Do the regular Cocoa thing.
# [_grid release];
# [super dealloc];
#
# }
# *** Bindings ***
def graphics ()
# A graphic view doesn't hold onto an array of the graphics it's
# presenting. That would be a cache that hasn't been justified by
# performance measurement (not yet anyway). Get the array of graphics
# from the bound-to object (an array controller, in Sketch's case).
# It's poor practice for a method that returns a collection to return
# nil, so never return nil.
@graphicsContainer.valueForKeyPath(@graphicsKeyPath) || []
end
def mutableGraphics ()
# Get a mutable array of graphics from the bound-to object (an array
# controller, in Sketch's case). The bound-to object is responsible for
# being KVO-compliant enough that all observers of the bound-to property
# get notified of whatever mutation we perform on the returned array.
# Trying to mutate the graphics of a graphic view whose graphics aren't
# bound to anything is a programming error.
@graphicsContainer.mutableArrayValueForKeyPath(@graphicsKeyPath)
end
def selectionIndexes
# A graphic view doesn't hold onto the selection indexes. That would be a
# cache that hasn't been justified by performance measurement (not yet
# anyway). Get the selection indexes from the bound-to object (an array
# controller, in Sketch's case). It's poor practice for a method that
# returns a collection (and an index set is a collection) to return nil,
# so never return nil.
@selectionIndexesContainer.valueForKeyPath(@selectionIndexesKeyPath) || NSIndexSet.indexSet
end
# Why isn't this method called -setSelectionIndexes:? Mostly to encourage a
# naming convention that's useful for a few reasons: NSObject's default
# implementation of key-value binding (KVB) uses key-value coding (KVC) to
# invoke methods like -set<BindingName>: on the bound object when the bound-to
# property changes, to make it simple to support binding in the simple case of
# a view property that affects the way a view is drawn but whose value isn't
# directly manipulated by the user. If NSObject's default implementation of
# KVB were good enough to use for this "selectionIndexes" property maybe we
# _would_ implement a -setSelectionIndexes: method instead of stuffing so much
# code in -observeValueForKeyPath:ofObject:change:context: down below (but
# it's not, because it doesn't provide a way to get at the old and new
# selection indexes when they change). So, this method isn't here to take
# advantage of NSObject's default implementation of KVB. It's here to
# centralize the bindings work that must be done when the user changes the
# selection (check out all of the places it's invoked down below). Hopefully
# the different verb in this method name is a good reminder of the
# distinction.
# A person who assumes that a -set... method always succeeds, and always sets
# the exact value that was passed in (or throws an exception for invalid
# values to signal the need for some debugging), isn't assuming anything
# unreasonable. Setters that invalidate that assumption make a class'
# interface unnecessarily unpredictable and hard to program against. Sometimes
# they require people to write code that sets a value and then gets it right
# back again to keep multiple copies of the value synchronized, in case the
# setting didn't "take." So, avoid that. When validation is appropriate don't
# put it in your setter. Instead, implement a separate validation method.
# Follow the naming pattern established by KVC's -validateValue:forKey:error:
# when applicable. Now, _this_ method can't guarantee that, when it's invoked,
# an immediately subsequent invocation of -selectionIndexes will return the
# passed-in value. It's supposed to set the value of a property in the
# bound-to object using KVC, but only after asking the bound-to object to
# validate the value. So, again, -setSelectionIndexes: wouldn't be a very good
# name for it.
def changeSelectionIndexes (indexes)
# After all of that talk, this method isn't invoking
# -validateValue:forKeyPath:error:. It will, once we come up with an
# example of invalid selection indexes for this case. It will also someday
# take any value transformer specified as a binding option into account,
# so you have an example of how to do that.
# Set the selection index set in the bound-to object (an array controller,
# in Sketch's case). The bound-to object is responsible for being
# KVO-compliant enough that all observers of the bound-to property get
# notified of the setting. Trying to set the selection indexes of a
# graphic view whose selection indexes aren't bound to anything is a
# programming error.
raise "An SKTGraphicView's 'selectionIndexes' property is not bound to anything." if !(@selectionIndexesContainer && @selectionIndexesKeyPath)
@selectionIndexesContainer.setValue(indexes, forKeyPath: @selectionIndexesKeyPath)
end
def setGrid (grid)
# Weed out redundant invocations.
if grid != @grid
# Stop observing changes in the old grid.
@grid.removeObserver(@gridObserver, forKeyPath: SKTGridAnyKey) if @grid
@grid = grid
# Start observing changes in the new grid so we know when to redraw it.
@grid.addObserver(@gridObserver, forKeyPath: SKTGridAnyKey, options: 0, context: nil)
end
end
def startObservingGraphics (graphics)
# Start observing "drawingBounds" in each of the graphics. Use KVO's
# options for getting the old and new values in change notifications so we
# can invalidate just the old and new drawing bounds of changed graphics
# when they move or change size, instead of the whole view. (The new
# drawing bounds is easy to otherwise get using regular KVC, but the old
# one would otherwise have been forgotten by the time we get the
# notification.) Instances of SKTGraphic must therefore be KVC- and
# KVO-compliant for drawingBounds. SKTGraphics's use of KVO's dependency
# mechanism means that being KVO-compliant for drawingBounds when
# subclassing is as easy as overriding -drawingBounds (to compute an
# accurate value) and +keyPathsForValuesAffectingDrawingBounds (to trigger
# KVO's dependency mechanism) though.
allGraphicIndexes = NSIndexSet.indexSetWithIndexesInRange(NSMakeRange(0, graphics.count))
graphics.addObserver(@graphicsBoundsObserver, toObjectsAtIndexes: allGraphicIndexes,
forKeyPath: SKTGraphicDrawingBoundsKey,
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,
context: nil)
# Start observing "drawingContents" in each of the graphics. Don't bother
# using KVO's options for getting the old and new values because there is
# no value for drawingContents. It's just something that depends on all of
# the properties that affect drawing of a graphic but don't affect the
# drawing bounds of the graphic. Similar to what we do for drawingBounds,
# SKTGraphics' use of KVO's dependency mechanism means that being
# KVO-compliant for drawingContents when subclassing is as easy as
# overriding +keyPathsForValuesAffectingDrawingContents (there is no
# -drawingContents method to override).
graphics.addObserver(@graphicsContentObserver, toObjectsAtIndexes: allGraphicIndexes,
forKeyPath: SKTGraphicDrawingContentsKey,
options: 0, context: nil)
end
def stopObservingGraphics (graphics)
# Undo what we do in -startObservingGraphics:.
allGraphicIndexes = NSIndexSet.indexSetWithIndexesInRange(NSMakeRange(0, graphics.count))
graphics.removeObserver(@graphicsContentObserver, fromObjectsAtIndexes: allGraphicIndexes,
forKeyPath: SKTGraphicDrawingContentsKey)
graphics.removeObserver(@graphicsBoundsObserver, fromObjectsAtIndexes: allGraphicIndexes,
forKeyPath: SKTGraphicDrawingBoundsKey)
end
# An override of the NSObject(NSKeyValueBindingCreation) method.
def bind (bindingName, toObject: observableObject, withKeyPath: observableKeyPath, options: options)
# SKTGraphicView supports several different bindings.
if bindingName == SKTGraphicViewGraphicsBindingName
# We don't have any options to support for our custom "graphics" binding.
raise "SKTGraphicView doesn't support any options for the 'graphics' binding." if options && options.count == 0
# Rebinding is just as valid as resetting.
unbind(SKTGraphicViewGraphicsBindingName) if @graphicsContainer || @graphicsKeyPath
# Record the information about the binding.
@graphicsContainer = observableObject
@graphicsKeyPath = observableKeyPath
# Start observing changes to the array of graphics to which we're bound,
# and also start observing properties of the graphics themselves that
# might require redrawing.
@graphicsContainer.addObserver(@graphicsContainerObserver, forKeyPath: @graphicsKeyPath,
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,
context: nil)
startObservingGraphics(@graphicsContainer.valueForKeyPath(@graphicsKeyPath))
# Redraw the whole view to make the binding take immediate visual
# effect. We could be much cleverer about this and just redraw the part
# of the view that needs it, but in typical usage the view isn't even
# visible yet, so that would probably be a waste of time (the
# programmer's and the computer's). If this view ever gets reused in
# some wildly dynamic situation where the bindings come and go we can
# reconsider optimization decisions like this then.
setNeedsDisplay(true)
elsif bindingName == SKTGraphicViewSelectionIndexesBindingName
# We don't have any options to support for our custom "selectionIndexes"
# binding either. Maybe in the future someone will imagine a use for a
# value transformer on this, and we'll add support for it then.
raise "SKTGraphicView doesn't support any options for the 'selectionIndexes' binding." if options && options.count == 0
# Rebinding is just as valid as resetting.
unbind(SKTGraphicViewSelectionIndexesBindingName) if @selectionIndexesContainer || @selectionIndexesKeyPath
# Record the information about the binding.
@selectionIndexesContainer = observableObject
@selectionIndexesKeyPath = observableKeyPath
# Start observing changes to the selection indexes to which we're bound.
@selectionIndexesContainer.addObserver(@graphicsSelectionIndexesObserver, forKeyPath: @selectionIndexesKeyPath,
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,
context: nil)
# Same comment as above.
setNeedsDisplay(true)
else
# For every binding except "graphics" and "selectionIndexes" just use
# NSObject's default implementation. It will start observing the
# bound-to property. When a KVO notification is sent for the bound-to
# property, this object will be sent a [self setValue:theNewValue
# forKey:theBindingName] message, so this class just has to be
# KVC-compliant for a key that is the same as the binding name, like
# "grid." That's why this class has a -setGrid: method. Also, NSView
# supports a few simple bindings of its own, and there's no reason to
# get in the way of those.
super
end
end
# An override of the NSObject(NSKeyValueBindingCreation) method.
def unbind (bindingName)
# SKTGraphicView supports several different bindings. For the ones that
# don't use NSObject's default implementation of key-value binding, undo
# what we do in -bind:toObject:withKeyPath:options:, and then redraw the
# whole view to make the unbinding take immediate visual effect.
if bindingName == SKTGraphicViewGraphicsBindingName
stopObservingGraphics(graphics)
@graphicsContainer.removeObserver(@graphicsContainerObserver, forKeyPath: @graphicsKeyPath) if @graphicsContainer
@graphicsContainer = nil
@graphicsKeyPath = nil
setNeedsDisplay(true)
elsif bindingName == SKTGraphicViewSelectionIndexesBindingName
@selectionIndexesContainer.removeObserver(@graphicsSelectionIndexesObserver, forKeyPath: @selectionIndexesKeyPath)
@selectionIndexesContainer = nil
@selectionIndexesKeyPath = nil
setNeedsDisplay(true)
else
# For every binding except "graphics" and "selectionIndexes" just use
# NSObject's default implementation. Also, NSView supports a few simple
# bindings of its own, and there's no reason to get in the way of those.
super
end
end
def graphicsContainerDidChange (keyPath, observedObject, change)
# The "old value" or "new value" in a change dictionary will be NSNull,
# instead of just not existing, if the corresponding option was
# specified at KVO registration time and the value for some key in the
# key path is nil. In Sketch's case there are times in an
# SKTGraphicView's life cycle when it's bound to the graphics of a
# window controller's document, and the window controller's document is
# nil. Don't redraw the graphic view when we get notifications about
# that.
# Have graphics been removed from the bound-to container?
oldGraphics = change[NSKeyValueChangeOldKey]
if oldGraphics
# Yes. Stop observing them because we don't want to leave dangling observations.
stopObservingGraphics(oldGraphics)
# Redraw just the parts of the view that they used to occupy.
oldGraphics.each {|g| setNeedsDisplayInRect(g.drawingBounds)}
# If a graphic is being edited right now, and the graphic is being
# removed, stop the editing. This way we don't strand an editing view
# whose graphic has been pulled out from under it. This situation can
# arise from undoing and scripting.
stopEditing if @editingGraphic && oldGraphics.index(@editingGraphic)
end
# Have graphics been added to the bound-to container?
newGraphics = change[NSKeyValueChangeNewKey]
if newGraphics
# Yes. Start observing them so we know when we need to redraw the
# parts of the view where they sit.
startObservingGraphics(newGraphics)
# Redraw just the parts of the view that they now occupy.
newGraphics.each {|g| setNeedsDisplayInRect(g.drawingBounds)}
# If undoing or redoing is being done we have to select the graphics
# that are being added. For NSKeyValueChangeSetting the change
# dictionary has no NSKeyValueChangeIndexesKey entry, so we have to
# figure out the indexes ourselves, which is easy. For
# NSKeyValueChangeRemoval the indexes are not the indexes of anything
# being added. You might notice that this is only place in this entire
# method that we check the value of the NSKeyValueChangeKindKey entry.
# In general, doing so should be pretty uncommon in overrides of
# -observeValueForKeyPath:ofObject:change:context:, because the values
# of the other entries are usually all you need, and handling all of
# the possible NSKeyValueChange values requires care. In Sketch we'll
# never see NSKeyValueChangeSetting or NSKeyValueChangeReplacement but
# we want to demonstrate a reusable class so we handle them anyway.
additionalUndoSelectionIndexes = nil;
changeKind = change[NSKeyValueChangeKindKey].to_i
if changeKind == NSKeyValueChangeSetting
additionalUndoSelectionIndexes = NSIndexSet.indexSetWithIndexesInRange(NSMakeRange(0, newGraphics.count))
elsif changeKind != NSKeyValueChangeRemoval
additionalUndoSelectionIndexes = change[NSKeyValueChangeIndexesKey]
end
if additionalUndoSelectionIndexes && @undoSelectionIndexes
# Use -[NSIndexSet addIndexes:] instead of just replacing the value
# of _undoSelectionIndexes because we don't know that a single undo
# action won't include more than one addition of graphics.
@undoSelectionIndexes.addIndexes(additionalUndoSelectionIndexes)
end
end
end
def gridAnyDidChange (keyPath, observedObject, change)
# Either a new grid is to be used (this only happens once in Sketch) or
# one of the properties of the grid has changed. Regardless, redraw
# everything.
setNeedsDisplay(true)
end
def graphicsBoundDidChange (keyPath, observedObject, change)
# Redraw the part of the view that the graphic used to occupy, and the
# part that it now occupies.
setNeedsDisplay(change[NSKeyValueChangeOldKey])
setNeedsDisplayInRect(change[NSKeyValueChangeNewKey])
# If undoing or redoing is being done add this graphic to the set that
# will be selected at the end of the undo action. -[NSArray
# indexOfObject:] is a dangerous method from a performance standpoint.
# Maybe an undo action that affects many graphics at once will be slow.
# Maybe something else in this very simple-looking bit of code will be a
# problem. We just don't yet know whether there will be a performance
# problem that the user can notice here. We'll check when we do real
# performance measurement on Sketch someday. At least we've limited the
# potential problem to undoing and redoing by checking
# _undoSelectionIndexes!=nil. One thing we do know right now is that we're
# not using memory to record selection changes on the undo/redo stacks,
# and that's a good thing.
if @undoSelectionIndexes
graphicIndex = graphics.index(observedObject)
if graphicIndex
@undoSelectionIndexes.addIndex(graphicIndex)
end
end
end
def graphicsContentDidChange (keyPath, observedObject, change)
# The graphic's drawing bounds hasn't changed, so just redraw the part
# of the view that it occupies right now.
setNeedsDisplayInRect(observedObject.drawingBounds)
# If undoing or redoing is being done add this graphic to the set that
# will be selected at the end of the undo action. -[NSArray
# indexOfObject:] is a dangerous method from a performance standpoint.
# Maybe an undo action that affects many graphics at once will be slow.
# Maybe something else in this very simple-looking bit of code will be a
# problem. We just don't yet know whether there will be a performance
# problem that the user can notice here. We'll check when we do real
# performance measurement on Sketch someday. At least we've limited the
# potential problem to undoing and redoing by checking
# _undoSelectionIndexes!=nil. One thing we do know right now is that we're
# not using memory to record selection changes on the undo/redo stacks,
# and that's a good thing.
if @undoSelectionIndexes
graphicIndex = graphics.index(observedObject)
if graphicIndex
@undoSelectionIndexes.addIndex(graphicIndex)
end
end
end
def graphicsSelectionIndicesDidChange (keyPath, observedObject, change)
# Some selection indexes might have been removed, some might have been
# added. Redraw the selection handles for any graphic whose selectedness
# has changed, unless the binding is changing completely (signalled by
# null old or new value), in which case just redraw the whole view.
oldSelectionIndexes = change[NSKeyValueChangeOldKey]
newSelectionIndexes = change[NSKeyValueChangeNewKey]
if oldSelectionIndexes && newSelectionIndexes
oldSelectionIndex = oldSelectionIndexes.firstIndex
while oldSelectionIndex != NSNotFound
if !newSelectionIndexes.containsIndex(oldSelectionIndex)
deselectedGraphic = graphics[oldSelectionIndex]
setNeedsDisplayInRect(deselectedGraphic.drawingBounds)
end
oldSelectionIndex = oldSelectionIndexes.indexGreaterThanIndex(oldSelectionIndex)
end
newSelectionIndex = newSelectionIndexes.firstIndex
while newSelectionIndex != NSNotFound
if !oldSelectionIndexes.containsIndex(newSelectionIndex)
selectedGraphic = graphics[newSelectionIndex]
setNeedsDisplayInRect(selectedGraphic.drawingBounds)
end
newSelectionIndex = newSelectionIndexes.indexGreaterThanIndex(newSelectionIndex)
end
else
setNeedsDisplay(true)
end
end
# This doesn't contribute to any KVC or KVO compliance. It's just a convenience method that's invoked down below.
def selectedGraphics ()
# Simple, because we made sure -graphics and -selectionIndexes never return nil.
graphics.objectsAtIndexes(selectionIndexes)
end
# *** Drawing ***
# An override of the NSView method.
def drawRect (rect)
# Draw the background background.
NSColor.whiteColor.set
NSRectFill(rect)
# Draw the grid.
@grid.drawRect(rect, inView: self)
# Draw every graphic that intersects the rectangle to be drawn. In Sketch
# the frontmost graphics have the lowest indexes.
currentContext = NSGraphicsContext.currentContext
(graphics.count - 1).downto(0) do |index|
graphic = graphics[index]
graphicDrawingBounds = graphic.drawingBounds
if NSIntersectsRect(rect, graphicDrawingBounds)
# Figure out whether or not to draw selection handles on the graphic.
# Selection handles are drawn for all selected objects except:
# - While the selected objects are being moved. - For the object
# actually being created or edited, if there is one.
drawSelectionHandles = false
if !@isHidingHandles && graphic != @creatingGraphic && graphic != @editingGraphic
drawSelectionHandles = selectionIndexes.containsIndex(index)
end
# Draw the graphic, possibly with selection handles.
currentContext.saveGraphicsState
NSBezierPath.clipRect(graphicDrawingBounds)
graphic.drawContentsInView(self, isBeingCreateOrEdited: graphic == @creatingGraphic || graphic == @editingGraphic)
graphic.drawHandlesInView(self) if drawSelectionHandles
currentContext.restoreGraphicsState
end
end
# If the user is in the middle of selecting draw the selection rectangle.
if !NSEqualRects(@marqueeSelectionBounds, NSZeroRect)
NSColor.knobColor.set
NSFrameRect(@marqueeSelectionBounds)
end
end
def beginEchoingMoveToRulers (echoRect)
horizontalRuler = enclosingScrollView.horizontalRulerView
verticalRuler = enclosingScrollView.verticalRulerView
newHorizontalRect = convertRect(echoRect, toView: horizontalRuler)
newVerticalRect = convertRect(echoRect, toView: verticalRuler)
horizontalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMinX(newHorizontalRect))
horizontalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMidX(newHorizontalRect))
horizontalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMaxX(newHorizontalRect))
verticalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMinY(newVerticalRect))
verticalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMidY(newVerticalRect))
verticalRuler.moveRulerlineFromLocation(-1.0, toLocation: NSMaxY(newVerticalRect))
@rulerEchoedBounds = echoRect.dup
end
def continueEchoingMoveToRulers (echoRect)
horizontalRuler = enclosingScrollView.horizontalRulerView
verticalRuler = enclosingScrollView.verticalRulerView
oldHorizontalRect = convertRect(@rulerEchoedBounds, toView: horizontalRuler)
oldVerticalRect = convertRect(@rulerEchoedBounds, toView: verticalRuler)
newHorizontalRect = convertRect(echoRect, toView: horizontalRuler)
newVerticalRect = convertRect(echoRect, toView: verticalRuler)
horizontalRuler.moveRulerlineFromLocation(NSMinX(oldHorizontalRect), toLocation: NSMinX(newHorizontalRect))
horizontalRuler.moveRulerlineFromLocation(NSMidX(oldHorizontalRect), toLocation: NSMidX(newHorizontalRect))
horizontalRuler.moveRulerlineFromLocation(NSMaxX(oldHorizontalRect), toLocation: NSMaxX(newHorizontalRect))
verticalRuler.moveRulerlineFromLocation(NSMinY(oldVerticalRect), toLocation: NSMinY(newVerticalRect))
verticalRuler.moveRulerlineFromLocation(NSMidY(oldVerticalRect), toLocation: NSMidY(newVerticalRect))
verticalRuler.moveRulerlineFromLocation(NSMaxY(oldVerticalRect), toLocation: NSMaxY(newVerticalRect))
# Need to store actual extent rather than reference of bounds object with may be being updated.
@rulerEchoedBounds = echoRect.dup
end
def stopEchoingMoveToRulers ()
horizontalRuler = enclosingScrollView.horizontalRulerView
verticalRuler = enclosingScrollView.verticalRulerView
oldHorizontalRect = convertRect(@rulerEchoedBounds, toView:horizontalRuler)
oldVerticalRect = convertRect(@rulerEchoedBounds, toView:verticalRuler)
horizontalRuler.moveRulerlineFromLocation(NSMinX(oldHorizontalRect), toLocation: -1.0);
horizontalRuler.moveRulerlineFromLocation(NSMidX(oldHorizontalRect), toLocation: -1.0);
horizontalRuler.moveRulerlineFromLocation(NSMaxX(oldHorizontalRect), toLocation: -1.0);
verticalRuler.moveRulerlineFromLocation(NSMinY(oldVerticalRect), toLocation: -1.0);
verticalRuler.moveRulerlineFromLocation(NSMidY(oldVerticalRect), toLocation: -1.0);
verticalRuler.moveRulerlineFromLocation(NSMaxY(oldVerticalRect), toLocation: -1.0);
@rulerEchoedBounds = NSZeroRect
end
# *** Editing Subviews ***
def setNeedsDisplayForEditingViewFrameChangeNotification (viewFrameDidChangeNotification)
# If the editing view got smaller we have to redraw where it was or cruft
# will be left on the screen. If the editing view got larger we might be
# doing some redundant invalidation (not a big deal), but we're not doing
# any redundant drawing (which might be a big deal). If the editing view
# actually moved then we might be doing substantial redundant drawing, but
# so far that wouldn't happen in Sketch.
# In Sketch this prevents cruft being left on the screen when the user 1)
# creates a great big text area and fills it up with text, 2) sizes the text
# area so not all of the text fits, 3) starts editing the text area but
# doesn't actually change it, so the text area hasn't been automatically
# resized and the text editing view is actually bigger than the text area,
# and 4) deletes so much text in one motion (Select All, then Cut) that the
# text editing view suddenly becomes smaller than the text area. In every
# other text editing situation the text editing view's invalidation or the
# fact that the SKTText's "drawingBounds" changes is enough to cause the
# proper redrawing.
newEditingViewFrame = viewFrameDidChangeNotification.object.frame
setNeedsDisplayInRect(NSUnionRect(@editingViewFrame, newEditingViewFrame))
@editingViewFrame = newEditingViewFrame
end
def startEditingGraphic (graphic)
# It's the responsibility of invokers to not invoke this method when
# editing has already been started.
raise "#{SKTGraphicView.startEditingGraphic} is being mis-invoked." if !(!@editingGraphic && !@editingView)
# Can the graphic even provide an editing view?
@editingView = graphic.newEditingViewWithSuperviewBounds(bounds)
if @editingView
# Keep a pointer to the graphic around so we can ask it to draw its
# "being edited" look, and eventually send it a -finalizeEditingView:
# message.
@editingGraphic = graphic
# If the editing view adds a ruler accessory view we're going to remove
# it when editing is done, so we have to remember the old reserved
# accessory view thickness so we can restore it. Otherwise there will be
# a big blank space in the ruler.
@oldReservedThicknessForRulerAccessoryView = enclosingScrollView.horizontalRulerView.reservedThicknessForAccessoryView
# Make the editing view a subview of this one. It was the graphic's job
# to make sure that it was created with the right frame and bounds.
addSubview(@editingView)
# Make the editing view the first responder so it takes key events and
# relevant menu item commands.
window.makeFirstResponder(@editingView)
# Get notified if the editing view's frame gets smaller, because we may
# have to force redrawing when that happens. Record the view's frame
# because it won't be available when we get the notification.
NSNotificationCenter.defaultCenter.addObserver(self,
selector: 'setNeedsDisplayForEditingViewFrameChangeNotification:',
name: NSViewFrameDidChangeNotification, object: @editingView)
@editingViewFrame = @editingView.frame
# Give the graphic being edited a chance to draw one more time. In Sketch, SKTText draws a focus ring.
setNeedsDisplayInRect(@editingGraphic.drawingBounds)
end
end
def stopEditing ()
# Make it harmless to invoke this method unnecessarily.
if @editingView
# Undo what we did in -startEditingGraphic:.
NSNotificationCenter.defaultCenter.removeObserver(self, name: NSViewFrameDidChangeNotification, object: @editingView)
# Pull the editing view out of this one. When editing is being stopped
# because the user has clicked in this view, outside of the editing
# view, NSWindow will have already made this view the window's first
# responder, and that's good. However, when editing is being stopped
# because the edited graphic is being removed (by undoing or scripting,
# for example), the invocation of -[NSView removeFromSuperview] we do
# here will leave the window as its own first responder, and that would
# be bad, so also fix the window's first responder if appropriate. It
# wouldn't be appropriate to steal first-respondership from sibling
# views here.
makeSelfFirstResponder = window.firstResponder == @editingView ? true : false
@editingView.removeFromSuperview
window.makeFirstResponder(self) if makeSelfFirstResponder
# If the editing view added a ruler accessory view then remove it
# because it's not applicable anymore, and get rid of the blank space in
# the ruler that would otherwise result. In Sketch the NSTextViews
# created by SKTTexts leave horizontal ruler accessory views.
horizontalRulerView = enclosingScrollView.horizontalRulerView
horizontalRulerView.accessoryView = nil
horizontalRulerView.reservedThicknessForAccessoryView = @oldReservedThicknessForRulerAccessoryView
# Give the graphic that created the editing view a chance to tear down their relationships and then forget about them both.
@editingGraphic.finalizeEditingView(@editingView)
@editingGraphic = nil
@editingView = nil
end
end
# *** Mouse Event Handling ***
def graphicUnderPoint (point)
# Search through all of the graphics, front to back, looking for one that
# claims that the point is on a selection handle (if it's selected) or in
# the contents of the graphic itself.
gi = GraphicInfo.new
graphics.each_with_index do |graphic, index|
# Do a quick check to weed out graphics that aren't even in the neighborhood.
if NSPointInRect(point, graphic.drawingBounds)
# Check the graphic's selection handles first, because they take
# precedence when they overlap the graphic's contents.
graphicIsSelected = selectionIndexes.containsIndex(index)
if graphicIsSelected
handle = graphic.handleUnderPoint(point)
if handle != SKTGraphicNoHandle
# The user clicked on a handle of a selected graphic.
gi.graphic = graphic
gi.handle = handle
end
end
if !gi.graphic
clickedOnGraphicContents = graphic.isContentsUnderPoint(point)
if clickedOnGraphicContents
# The user clicked on the contents of a graphic.
gi.graphic = graphic
gi.handle = SKTGraphicNoHandle
end
end
if gi.graphic
# Return values and stop looking.
gi.index = index
gi.isSelected = graphicIsSelected
break;
end
end
end
return gi
end
def moveSelectedGraphicsWithEvent (event)
selGraphics = selectedGraphics
didMove = false
isMoving = false
echoToRulers = enclosingScrollView.rulersVisible
selBounds = SKTGraphic.boundsOfGraphics(selGraphics)
c = selGraphics.count
lastPoint = convertPoint(event.locationInWindow, fromView: nil)
selOriginOffset = NSMakePoint((lastPoint.x - selBounds.origin.x), (lastPoint.y - selBounds.origin.y))
beginEchoingMoveToRulers(selBounds) if echoToRulers
while event.type != NSLeftMouseUp
event = window.nextEventMatchingMask(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
autoscroll(event)
curPoint = convertPoint(event.locationInWindow, fromView: nil)
if !isMoving && ((curPoint.x - lastPoint.x).abs >= 2.0) || ((curPoint.y - lastPoint.y).abs >= 2.0)
isMoving = true;
@isHidingHandles = true
end
if isMoving
if @grid
boundsOrigin = NSPoint.new(curPoint.x - selOriginOffset.x, curPoint.y - selOriginOffset.y)
boundsOrigin = @grid.constrainedPoint(boundsOrigin)
curPoint.x = boundsOrigin.x + selOriginOffset.x
curPoint.y = boundsOrigin.y + selOriginOffset.y
end
if !NSEqualPoints(lastPoint, curPoint)
SKTGraphic.translateGraphics(selGraphics, byX: (curPoint.x - lastPoint.x), y: (curPoint.y - lastPoint.y))
didMove = true
if echoToRulers
continueEchoingMoveToRulers(NSMakeRect(curPoint.x - selOriginOffset.x, curPoint.y - selOriginOffset.y,
NSWidth(selBounds), NSHeight(selBounds)))
end
# Adjust the delta that is used for cascading pastes. Pasting and
# then moving the pasted graphic is the way you determine the
# cascade delta for subsequent pastes.
@pasteCascadeDelta.x += (curPoint.x - lastPoint.x)
@pasteCascadeDelta.y += (curPoint.y - lastPoint.y)
end
lastPoint = curPoint
end
end
stopEchoingMoveToRulers if echoToRulers
if isMoving
@isHidingHandles = false
setNeedsDisplayInRect(SKTGraphic.drawingBoundsOfGraphics(selGraphics))
if didMove
# Only if we really moved.
undoManager.setActionName(NSLocalizedStringFromTable("Move", "UndoStrings", "Action name for moves."))
end
end
end
def resizeGraphic (graphic, usingHandle: handle, withEvent: event)
echoToRulers = enclosingScrollView.rulersVisible
beginEchoingMoveToRulers(graphic.bounds) if echoToRulers
while event.type != NSLeftMouseUp
event = window.nextEventMatchingMask(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
autoscroll(event)
handleLocation = convertPoint(event.locationInWindow, fromView: nil)
handleLocation = @grid.constrainedPoint(handleLocation) if @grid
handle = graphic.resizeByMovingHandle(handle, toPoint: handleLocation)
continueEchoingMoveToRulers(graphic.bounds) if echoToRulers
end
stopEchoingMoveToRulers if echoToRulers
undoManager.setActionName(NSLocalizedStringFromTable("Resize", "UndoStrings", "Action name for resizes."))
end
def indexesOfGraphicsIntersectingRect (rect)
indexSetToReturn = NSMutableIndexSet.indexSet
graphics.each_with_index do |graphic, index|
indexSetToReturn.addIndex(index) if NSIntersectsRect(rect, graphic.drawingBounds)
end
return indexSetToReturn
end
def createGraphicOfClass (graphicClass, withEvent: event)
# Before we invoke -[NSUndoManager beginUndoGrouping] turn off automatic
# per-event-loop group creation. If we don't turn it off now,
# -beginUndoGrouping will actually create _two_ undo groups: the top-level
# automatically-created one and then the nested one that we're explicitly
# creating. When we invoke -undoNestedGroup down below, the
# automatically-created undo group will be left on the undo stack. It will
# be ended automatically at the end of the event loop, which is good, and
# it will be empty, which is expected, but it will be left on the undo
# stack so the user will see a useless undo action in the Edit menu, which
# is bad. Is this a bug in NSUndoManager? Well it's certainly surprising
# that NSUndoManager isn't bright enough to ignore empty undo groups,
# especially ones that it itself created automatically, so NSUndoManager
# could definitely use a little improvement here.
undoManagerWasGroupingByEvent = undoManager.groupsByEvent
undoManager.groupsByEvent = false
# We will want to undo the creation of the graphic if the user sizes it to
# nothing, so create a new group for everything undoable that's going to
# happen during graphic creation.
undoManager.beginUndoGrouping
# Clear the selection.
changeSelectionIndexes(NSIndexSet.indexSet)
# Where is the mouse pointer as graphic creation is starting? Should the
# location be constrained to the grid?
graphicOrigin = convertPoint(event.locationInWindow, fromView: nil)
graphicOrigin = @grid.constrainedPoint(graphicOrigin) if @grid
# Create the new graphic and set what little we know of its location.
@creatingGraphic = graphicClass.alloc.init
@creatingGraphic.setBounds(NSMakeRect(graphicOrigin.x, graphicOrigin.y, 0.0, 0.0))
# Add it to the set of graphics right away so that it will show up in
# other views of the same array of graphics as the user sizes it.
mutableGraphics.insertObject(@creatingGraphic, atIndex: 0)
# Let the user size the new graphic until they let go of the mouse.
# Because different kinds of graphics have different kinds of handles,
# first ask the graphic class what handle the user is dragging during this
# initial sizing.
resizeGraphic(@creatingGraphic, usingHandle: graphicClass.creationSizingHandle, withEvent: event)
# Why don't we do [undoManager endUndoGrouping] here, once, instead of
# twice in the following paragraphs? Because of the [undoManager
# setGroupsByEvent:NO] game we're playing. If we invoke -[NSUndoManager
# setActionName:] down below after invoking [undoManager endUndoGrouping]
# there won't be any open undo group, and NSUndoManager will raise an
# exception. If we weren't playing the [undoManager setGroupsByEvent:NO]
# game then it would be OK to invoke -[NSUndoManager setActionName:] after
# invoking [undoManager endUndoGrouping] because the action name would
# apply to the top-level automatically-created undo group, which is fine.
# Did we really create a graphic? Don't check with
# !NSIsEmptyRect(createdGraphicBounds) because the bounds of a perfectly
# horizontal or vertical line is "empty" but of course we want to let
# people create those.
createdGraphicBounds = @creatingGraphic.bounds
if NSWidth(createdGraphicBounds) != 0.0 || NSHeight(createdGraphicBounds) != 0.0
# Select it.
changeSelectionIndexes(NSIndexSet.indexSetWithIndex(0))
# The graphic wasn't sized to nothing during mouse tracking. Present its
# editing interface it if it's that kind of graphic (like Sketch's
# SKTTexts). Invokers of the method we're in right now should have
# already cleared out _editingView.
startEditingGraphic(@creatingGraphic)
# Overwrite whatever undo action name was registered during all of that with a more specific one.
cn = NSBundle.mainBundle.localizedStringForKey(graphicClass.to_s, value: "", table: "GraphicClassNames")
undoManager.setActionName(NSLocalizedStringFromTable("Create #{cn}", "UndoStrings", "Action name for newly created graphics. Class name is inserted at the substitution."))
# Balance the invocation of -[NSUndoManager beginUndoGrouping] that we did up above.
undoManager.endUndoGrouping
else
# Balance the invocation of -[NSUndoManager beginUndoGrouping] that we did up above.
undoManager.endUndoGrouping
# The graphic was sized to nothing during mouse tracking. Undo
# everything that was just done. Disable undo registration while undoing
# so that we don't create a spurious redo action.
undoManager.disableUndoRegistration
undoManager.undoNestedGroup
undoManager.enableUndoRegistration
end
# Balance the invocation of -[NSUndoManager setGroupsByEvent:] that we did
# up above. We're careful to restore the old value instead of merely
# invoking -setGroupsByEvent:YES because we don't know that the method
# we're in right now won't in the future be invoked by some other method
# that plays its own NSUndoManager games.
undoManager.groupsByEvent = undoManagerWasGroupingByEvent
# Done.
@creatingGraphic = nil
end
def marqueeSelectWithEvent (event)
# Dequeue and handle mouse events until the user lets go of the mouse button.
oldSelectionIndexes = selectionIndexes
originalMouseLocation = convertPoint(event.locationInWindow, fromView: nil)
while event.type != NSLeftMouseUp
event = window.nextEventMatchingMask(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
autoscroll(event)
currentMouseLocation = convertPoint(event.locationInWindow, fromView: nil)
# Figure out a new a selection rectangle based on the mouse location.
newMarqueeSelectionBounds = NSMakeRect([originalMouseLocation.x, currentMouseLocation.x].min,
[originalMouseLocation.y, currentMouseLocation.y].min,
(currentMouseLocation.x - originalMouseLocation.x).abs,
(currentMouseLocation.y - originalMouseLocation.y).abs)
if !NSEqualRects(newMarqueeSelectionBounds, @marqueeSelectionBounds)
# Erase the old selection rectangle and draw the new one.
setNeedsDisplayInRect(@marqueeSelectionBounds)
@marqueeSelectionBounds = newMarqueeSelectionBounds
setNeedsDisplayInRect(@marqueeSelectionBounds)
# Either select or deselect all of the graphics that intersect the
# selection rectangle.
indexesOfGraphicsInRubberBand = indexesOfGraphicsIntersectingRect(@marqueeSelectionBounds)
newSelectionIndexes = oldSelectionIndexes.mutableCopy
# TODO extend NSIndexSet with an each method
index = indexesOfGraphicsInRubberBand.firstIndex
while index != NSNotFound
if newSelectionIndexes.containsIndex(index)
newSelectionIndexes.removeIndex(index)
else
newSelectionIndexes.addIndex(index)
end
index = indexesOfGraphicsInRubberBand.indexGreaterThanIndex(index)
end
changeSelectionIndexes(newSelectionIndexes)
end
end
# Schedule the drawing of the place wherew the rubber band isn't anymore.
setNeedsDisplayInRect(@marqueeSelectionBounds)
# Make it not there.
@marqueeSelectionBounds = NSZeroRect
end
def selectAndTrackMouseWithEvent (event)
# Are we changing the existing selection instead of setting a new one?
modifyingExistingSelection = (event.modifierFlags & NSShiftKeyMask) == NSShiftKeyMask
# Has the user clicked on a graphic?
mouseLocation = convertPoint(event.locationInWindow, fromView: nil)
clickedGraphic = graphicUnderPoint(mouseLocation)
if clickedGraphic.graphic
# Clicking on a graphic knob takes precedence.
if clickedGraphic.handle != SKTGraphicNoHandle
# The user clicked on a graphic's handle. Let the user drag it around.
resizeGraphic(clickedGraphic.graphic, usingHandle: clickedGraphic.handle, withEvent: event)
else
# The user clicked on a graphic's contents. Update the selection.
if modifyingExistingSelection
if clickedGraphic.isSelected
# Remove the graphic from the selection.
newSelectionIndexes = selectionIndexes.mutableCopy
newSelectionIndexes.removeIndex(clickedGraphic.index)
changeSelectionIndexes(newSelectionIndexes)
clickedGraphic.isSelected = false
else
# Add the graphic to the selection.
newSelectionIndexes = selectionIndexes.mutableCopy
newSelectionIndexes.addIndex(clickedGraphic.index)
changeSelectionIndexes(newSelectionIndexes)
clickedGraphic.isSelected = true
end
else
# If the graphic wasn't selected before then it is now, and none of the rest are.
if !clickedGraphic.isSelected
changeSelectionIndexes(NSIndexSet.indexSetWithIndex(clickedGraphic.index))
clickedGraphic.isSelected = true
end
end
# Is the graphic that the user has clicked on now selected?
if clickedGraphic.isSelected
# Yes. Let the user move all of the selected objects.
moveSelectedGraphicsWithEvent(event)
else
# No. Just swallow mouse events until the user lets go of the mouse
# button. We don't even bother autoscrolling here.
while event.type != NSLeftMouseUp
event = window.nextEventMatchingMask(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
end
end
end
else
# The user clicked somewhere other than on a graphic. Clear the selection,
# unless the user is holding down the shift key.
changeSelectionIndexes(NSIndexSet.indexSet) if !modifyingExistingSelection