-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathController.m
1473 lines (1236 loc) · 51.2 KB
/
Controller.m
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
//
// Controller.m
// RegExhibit
//
// Copyright 2007 Roger Jolly. All rights reserved.
//
#import "Controller.h"
@implementation Controller
#pragma mark-
#pragma mark Starting up and shutting down
- (id) init
{
self = [super init];
if (self != nil) {
if (![self checkMinimumVersion]) {
// The user is not running the minimally required version of OS X.
NSString *pathToInfoPList = [[NSBundle mainBundle] pathForResource: @"Info" ofType: @"plist"];
NSDictionary *factoryDefaults = [NSDictionary dictionaryWithContentsOfFile: pathToInfoPList];
NSString *minimumVersion = [factoryDefaults objectForKey: @"minimumVersionRequired"];
NSLog (@"Minimum required version of OS X not found.\n");
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText: [NSString stringWithFormat: NSLocalizedString(@"Version %@ too low", nil), minimumVersion]];
[alert addButtonWithTitle: NSLocalizedString(@"Quit", nil)];
[alert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"Warning version %@ too low", nil), minimumVersion]];
[alert setAlertStyle: NSCriticalAlertStyle];
[alert runModal];
[alert release];
[NSApp terminate:self]; // Exit.
}
// Is Perl available?
// Thanks to Brian Bergstrand for simplifying the detection of perl.
NSString *string = [[NSString alloc] initWithString:@"/usr/bin/perl"];
if (![[NSFileManager defaultManager] fileExistsAtPath:string]) {
// Can't find Perl at "/usr/bin/perl".
NSLog (@"Error locating \"Perl\": %@.\n", string);
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText: NSLocalizedString(@"No Perl", nil)];
[alert addButtonWithTitle: NSLocalizedString(@"Quit", nil)];
[alert setInformativeText: NSLocalizedString(@"Warning no Perl", nil)];
[alert setAlertStyle: NSCriticalAlertStyle];
[alert runModal];
[alert release];
[NSApp terminate:self]; // Exit.
}
// If we get here, all is well.
[string release];
// Set default preferences.
[self setFactoryDefaults];
// Register with notificationcenter to hear when colours are changed.
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver: self
selector: @selector(handleColourChange:)
name: @"NSColorPanelColorDidChangeNotification"
object: nil];
// Register with notificationcenter to hear when a new match should be started.
[nc addObserver: self
selector: @selector(startMatch:)
name: @"RDJStartMatch"
object: nil];
// Register with notificationcenter to hear when a new replace (without an accompanying match) should be started.
[nc addObserver: self
selector: @selector(startReplace:)
name: @"RDJStartReplace"
object: nil];
// Register with notificationcenter to hear when match is finished.
[nc addObserver: self
selector: @selector(matchFinished:)
name: @"RDJRegExFinished"
object: nil];
// Set some globals.
regExResults = [[RegExRoot alloc] init];
matchInProgress = FALSE;
matchAborted = FALSE;
interruptMatchInProgress = FALSE;
forceMatchDrawing = FALSE;
}
return self;
}
- (BOOL) checkMinimumVersion
// Use the file /System/Library/CoreServices/SystemVersion.plist to check the user uses the minimum required version.
{
NSString *pathToInfoPList = [[NSBundle mainBundle] pathForResource: @"Info" ofType: @"plist"];
NSDictionary *factoryDefaults = [NSDictionary dictionaryWithContentsOfFile: pathToInfoPList];
NSString *minimumVersion = [factoryDefaults objectForKey: @"minimumVersionRequired"];
// NSString *minimumVersion = [NSString stringWithString: minimumVersionRequired];
BOOL versionOK = FALSE;
int major = 0;
int minor = 0;
int bugfix = 0;
NSString *systemVersion = [[NSDictionary dictionaryWithContentsOfFile:@"/System/Library/CoreServices/SystemVersion.plist"] objectForKey:@"ProductVersion"];
NSArray *systemVersionComponents = [systemVersion componentsSeparatedByString:@"."];
switch ([systemVersionComponents count]) {
case 3:
bugfix = [[systemVersionComponents objectAtIndex:2] intValue];
case 2:
minor = [[systemVersionComponents objectAtIndex:1] intValue];
case 1:
major = [[systemVersionComponents objectAtIndex:0] intValue];
break;
default:
return FALSE;
}
NSArray *minimumVersionComponents = [minimumVersion componentsSeparatedByString:@"."];
int numberOfComponents = [minimumVersionComponents count];
if (major > [[minimumVersionComponents objectAtIndex:0] intValue]) {
versionOK = TRUE;
} else if (major < [[minimumVersionComponents objectAtIndex:0] intValue]) {
versionOK = FALSE;
} else if (numberOfComponents < 2) { // major is required minimum and no minor specified
versionOK = TRUE;
} else if (minor > [[minimumVersionComponents objectAtIndex:1] intValue]) { // major is required minimum, so check minor
versionOK = TRUE;
} else if (minor < [[minimumVersionComponents objectAtIndex:1] intValue]) {
versionOK = FALSE;
} else if (numberOfComponents < 3) { // minor is required minimum and no bugfix specified
versionOK = TRUE;
} else if (bugfix > [[minimumVersionComponents objectAtIndex:2] intValue]) { // minor is required minimum, so check bugfix
versionOK = TRUE;
} else if (bugfix < [[minimumVersionComponents objectAtIndex:2] intValue]) {
versionOK = FALSE;
} else { // bugfix is required minimum
versionOK = TRUE;
}
return versionOK;
}
- (void) setFactoryDefaults
// This method registers the default values of the preferences. It the user changes them in the preference window, Cocoa bindings will take care of saving those.
{
NSString *pathToInfoPList = [[NSBundle mainBundle] pathForResource: @"Info" ofType: @"plist"];
NSDictionary *factoryDefaults = [NSDictionary dictionaryWithContentsOfFile: pathToInfoPList];
NSMutableDictionary *regExhibitDefaults = [NSMutableDictionary dictionary];
NSColor *aColour = [[NSColorList colorListNamed: @"Crayons"] colorWithKey: [factoryDefaults objectForKey: @"matchColour"]];
if (!aColour) {
aColour = [NSColor blueColor];
}
[regExhibitDefaults setObject: [NSArchiver archivedDataWithRootObject:aColour] forKey: @"matchColour"];
aColour = [[NSColorList colorListNamed: @"Crayons"] colorWithKey: [factoryDefaults objectForKey: @"captureColour"]];
if (!aColour) {
aColour = [NSColor redColor];
}
[regExhibitDefaults setObject: [NSArchiver archivedDataWithRootObject:aColour] forKey: @"captureColour"];
aColour = [[NSColorList colorListNamed: @"Crayons"] colorWithKey: [factoryDefaults objectForKey: @"replaceColour"]];
if (!aColour) {
aColour = [NSColor purpleColor];
}
[regExhibitDefaults setObject: [NSArchiver archivedDataWithRootObject:aColour] forKey: @"replaceColour"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"underlineMatch"] forKey: @"underlineMatch"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"underlineCapture"] forKey: @"underlineCapture"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"shadeOverlappingCaptures"] forKey: @"shadeOverlappingCaptures"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"underlineReplace"] forKey: @"underlineReplace"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"allowCode"] forKey: @"AllowCode"];
[regExhibitDefaults setObject: [factoryDefaults objectForKey: @"liveMatching"] forKey: @"liveMatching"];
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues: regExhibitDefaults];
[[NSUserDefaults standardUserDefaults] registerDefaults: regExhibitDefaults];
}
- (void) awakeFromNib
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Set up these textview to use a special layoutmanager that can display underlines under leading and trailing spaces.
[[findMatchText textContainer] replaceLayoutManager: [[RegExhibitLayoutManager alloc] init]];
[[replaceMatchText textContainer] replaceLayoutManager: [[RegExhibitLayoutManager alloc] init]];
[[replaceResultText textContainer] replaceLayoutManager: [[RegExhibitLayoutManager alloc] init]];
[[splitMatchText textContainer] replaceLayoutManager: [[RegExhibitLayoutManager alloc] init]];
// Add subviews tot statusview
[regExStatusView addSubview: regExProgressIndicatorView];
[regExStatusView addSubview: regExInvalidView];
[regExInvalidView setHidden:TRUE];
[regExStatusView addSubview: regExAbortedView];
[regExAbortedView setHidden:TRUE];
// Get the correct colours to use from the nib.
[captureColour release];
captureColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"captureColour"]];
[captureColour retain];
[matchColour release];
matchColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"matchColour"]];
[matchColour retain];
[replaceColour release];
replaceColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"replaceColour"]];
[replaceColour retain];
// Set default font to use in textviews.
[NSFont setUserFont: MAIN_FONT];
// Set the status of the matching-cancel button
if ([defaults boolForKey: @"liveMatching"]) {
[toggleMatchingButton setTitle: NSLocalizedString(@"Cancel", nil)];
[toggleMatchingButton setEnabled:FALSE];
} else {
[toggleMatchingButton setTitle: NSLocalizedString(@"Match", nil)];
[toggleMatchingButton setEnabled:TRUE];
}
// Calculate a save width under which a text will be just one line, unless it contains a newline
[self setSafeWidth: [findMatchText bounds].size.width / [[NSFont systemFontOfSize: 13.0] maximumAdvancement].width];
// Trick the current tabviewitem in believing it will be selected, so it will perform some initialisations
[self tabView:tabView willSelectTabViewItem:[tabView selectedTabViewItem]];
// Register for notifications that the bounds of a view are changed.
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(boundsDidChangeNotification:)
name: NSViewBoundsDidChangeNotification
object: nil];
// Tell the relevant views, they need to send notifications if their bounds are changed.
[findMatchText setPostsBoundsChangedNotifications: YES];
[replaceMatchText setPostsBoundsChangedNotifications: YES];
[splitMatchText setPostsBoundsChangedNotifications: YES];
}
- (void) dealloc
{
[[[findMatchText textContainer] layoutManager] release];
[[[replaceMatchText textContainer] layoutManager] release];
[[[replaceResultText textContainer] layoutManager] release];
[[[splitMatchText textContainer] layoutManager] release];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
[replaceColour release];
[captureColour release];
[matchColour release];
[regExResults release];
[super dealloc];
}
#pragma mark-
#pragma mark Accessors
- (void) setInterruptMatchInProgress: (BOOL) aBool
{
interruptMatchInProgress = aBool;
}
- (BOOL) interruptMatchInProgress
{
return interruptMatchInProgress;
}
- (void) setMatchAborted: (BOOL) aBool
{
matchAborted = aBool;
if ([self matchAborted]) {
[self setMatchInProgress: FALSE];
}
}
- (BOOL) matchAborted
{
return matchAborted;
}
- (void) setMatchInProgress: (BOOL) aBool
{
matchInProgress = aBool;
}
- (BOOL) matchInProgress
{
return matchInProgress;
}
- (void) setSafeWidth: (int) aWidth
// safeWidth is the safe length of a string that will fit on one line, unless it contains a newline.
{
safeWidth = aWidth;
}
- (int) safeWidth
{
return safeWidth;
}
// Pseudo accessors
- (IBAction) setAllowCode: (id) sender
{
if ([sender intValue]) { // User checked box Allow code.
[allowCodeCheck setEnabled:FALSE];
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText: NSLocalizedString(@"Confirm allow code", nil)];
[alert addButtonWithTitle: NSLocalizedString(@"Cancel", nil)];
[alert addButtonWithTitle: NSLocalizedString(@"Allow code", nil)];
[alert setInformativeText: NSLocalizedString(@"Warning allow code", nil)];
[alert setAlertStyle: NSWarningAlertStyle];
[alert beginSheetModalForWindow:[sender window] modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
[alert release];
} else // User unchecked box Allow code.
if ([self liveMatching]) {
[self newMatch];
}
}
- (BOOL) allowCode
// If the user has checked Allow code in the preferences, but hasn't confirmed the alert sheet, allowCodeCheck is disabled.
{
return [allowCodeCheck isEnabled] && [[NSUserDefaults standardUserDefaults] boolForKey: @"allowCode"]; // Only allowed if checkbox is enabled and checked.
}
- (IBAction) setLiveMatching: (id) sender
{
if ([sender intValue]) { // Checkbox Live Matching is checked.
[toggleMatchingButton setTitle: NSLocalizedString(@"Cancel", nil)];
[toggleMatchingButton setEnabled:FALSE];
[self newMatch];
} else {
[toggleMatchingButton setTitle: NSLocalizedString(@"Match", nil)];
[toggleMatchingButton setEnabled:TRUE];
}
}
- (BOOL) liveMatching
{
return [[NSUserDefaults standardUserDefaults] boolForKey: @"liveMatching"];
}
#pragma mark-
#pragma mark User input
// TextView delegate
- (void) textDidChange: (NSNotification *) aNotification
// This is the delegate function for all textviews the user can edit. If text in any of these changes, it means we probably need to perform a match.
// The only time this isn't necessary, is when just the replacement string has been changed. In that case only an update op the replacementstrings is
// needed.
{
if (![self liveMatching]) { // Only proceed when live matching.
return;
}
if ([self matchAborted]) {
[self newMatch];
} else if ([[aNotification object] isEqualTo:replaceRegex] && [regExResults matchSucceeded]) {
[self newReplace]; // If only the replacementstring is changed, only a replace is needed.
} else {
[self newMatch];
}
}
- (void) boundsDidChangeNotification: (NSNotification *) aNotification
// This method is called when the bounds of a view are changed. In this case, the bounds of the textToMatch view.
// When this is the case, drawn the displayed part of the view.
{
[self displayMatchResults];
}
- (void) handleColourChange: (NSNotification *) note
// This method is called when the user changes colours in the preferences window.
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSColor *aColour;
BOOL colourChanged = FALSE;
aColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"captureColour"]];
if (![captureColour isEqual: aColour]) {
[captureColour release];
captureColour = aColour;
[captureColour retain];
colourChanged = TRUE;
}
aColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"matchColour"]];
if (![matchColour isEqual: aColour]) {
[matchColour release];
matchColour = aColour;
[matchColour retain];
colourChanged = TRUE;
}
aColour = (NSColor *)[NSUnarchiver unarchiveObjectWithData: [defaults dataForKey: @"replaceColour"]];
if (![replaceColour isEqual: aColour]) {
[replaceColour release];
replaceColour = aColour;
[replaceColour retain];
colourChanged = TRUE;
}
if (colourChanged) { // Redisplay the text, but don't rematch.
[self setMatchInProgress: TRUE];
[self runProgressIndicator: TRUE];
[self clearTextToMatch];
forceMatchDrawing = TRUE; // Will automatically be turned off after drawing.
[self prepareDrawing];
[self runProgressIndicator: FALSE];
[self setMatchInProgress: FALSE];
}
}
- (IBAction) preferredColoursChanged: (id) sender
// This method is called if the user changes a preference that influences the way matches etc. are displayed, e.g. underlining of captures.
{
[self setMatchInProgress: TRUE];
[self runProgressIndicator: TRUE];
[self clearTextToMatch];
forceMatchDrawing = TRUE; // Will automatically be turned off after drawing.
[self prepareDrawing];
[self runProgressIndicator: FALSE];
[self setMatchInProgress: FALSE];
}
- (IBAction) modifiersChanged: (id) sender
// This method is called if the user changes a regex modifier. The values are read out when matching, so no need to bother with them here.
{
if ([self liveMatching]) {
[self newMatch];
}
}
- (IBAction) toggleMatching: (id) sender
// This method is called when the user clicks the cancel/match button in the InfoView.
{
if ([self matchInProgress]) { // The user clicked the cancel-button.
[regExResults abortMatching];
[self regExAborted: TRUE];
} else { // The user clicked the match-button.
[self regExAborted: FALSE];
[self regExInvalid: FALSE];
[self newMatch];
}
}
- (void) alertDidEnd: (NSAlert *) alert
returnCode: (int) returnCode
contextInfo: (void *) contextInfo
// Called when the user selects to allow code execution in the preferences window.
{
[allowCodeCheck setEnabled:TRUE];
if (returnCode == NSAlertSecondButtonReturn) {
// User confirmed code should be allowed
[[NSUserDefaults standardUserDefaults] setBool: TRUE forKey: @"allowCode"];
if ([self liveMatching]) { // Conditions have changed, so re-match of allowed.
[self newMatch];
}
} else {
// User recanted, so clear.
[[NSUserDefaults standardUserDefaults] setBool: FALSE forKey: @"allowCode"];
}
}
#pragma mark-
#pragma mark Perform match and show results
- (void) interruptMatch
// This method is called when the user requests a new match, usually by typing a new character, while the current match hasn't finished.
// It sets a flag and aborts the match. (This is only effective, if there is a match running in the background.)
// At several points the flag can be noticed by RegExhibit, and it will then continue with the new match.
{
[self setInterruptMatchInProgress: TRUE];
[regExResults abortMatching];
}
- (void) handleInterrupt
// This will always do a full match for safety, even if only a partial one is necessary.
{
[self setInterruptMatchInProgress: FALSE];
[[NSNotificationQueue defaultQueue] enqueueNotification: [NSNotification notificationWithName: @"RDJStartMatch" object: nil]
postingStyle: NSPostASAP // NSPostWhenIdle //
coalesceMask: NSNotificationCoalescingOnName
forModes: nil];
}
- (void) newMatch {
if ([self matchInProgress]) {
[self interruptMatch];
} else {
// Send a notification to start match. The notifications are coalesced.
[[NSNotificationQueue defaultQueue] enqueueNotification: [NSNotification notificationWithName: @"RDJStartMatch" object: nil]
postingStyle: NSPostWhenIdle // NSPostASAP //
coalesceMask: NSNotificationCoalescingOnName
forModes: nil];
}
}
- (void) newReplace {
if ([self matchInProgress]) {
[self interruptMatch];
} else {
// Send a notification to start match. The notifications are coalesced.
[[NSNotificationQueue defaultQueue] enqueueNotification: [NSNotification notificationWithName: @"RDJStartReplace" object: nil]
postingStyle: NSPostWhenIdle // NSPostASAP //
coalesceMask: NSNotificationCoalescingOnName
forModes: nil];
}
}
- (void) startMatch: (NSNotification *) aNotification
{
if ([self interruptMatchInProgress]) {
[self handleInterrupt];
return;
}
[self performMatch];
}
- (void) startReplace: (NSNotification *) aNotification
{
if ([self interruptMatchInProgress]) {
[self handleInterrupt];
return;
}
[self performReplace];
}
- (void) performMatch
{
// First set up for matching.
[detailsDrawer close];
[self regExAborted: FALSE];
[self regExInvalid: FALSE];
[infoView displayRect:[regExStatusView frame]];
[self runProgressIndicator: TRUE];
// Depending on the fact if live matching is allowed, the match/cancel button should either be enabled or be renamed to cancel.
if ([self liveMatching]) {
[toggleMatchingButton setEnabled:TRUE];
} else {
[toggleMatchingButton setTitle: NSLocalizedString(@"Cancel", nil)];
}
[self setMatchInProgress: TRUE];
// Perform the match.
[regExResults matchText: [textToMatch string]
toRegEx: [matchRegex string]
modifiers: [self regExModifiersSet]
replacement: [replaceRegex string]
allowCode: [self allowCode]];
}
- (void) performReplace
{
// First set up for replacing.
[detailsDrawer close];
[self regExAborted: FALSE];
[self regExInvalid: FALSE];
[infoView displayRect:[regExStatusView frame]];
[self runProgressIndicator: TRUE];
// Depending on the fact if live matching is allowed, the match/cancel button should either be enabled or be renamed to cancel.
if ([self liveMatching]) {
[toggleMatchingButton setEnabled:TRUE];
} else {
[toggleMatchingButton setTitle: NSLocalizedString(@"Cancel", nil)];
}
[self setMatchInProgress: TRUE];
// Perform the replacing.
[regExResults replaceInText: [textToMatch string]
regEx: [matchRegex string]
modifiers: [self regExModifiersSet]
replacement: [replaceRegex string]
allowCode: [self allowCode]];
}
- (NSSet *) regExModifiersSet
// Walk through the matrix and get the values of the modifiers.
{
NSEnumerator *enumerator;
NSCell *cell;
NSMutableSet *tempSet = [[NSMutableSet alloc]init];
NSSet *regExModifiersSet;
enumerator = [[modifierMatrix cells] objectEnumerator];
while (cell = [enumerator nextObject]) {
switch ([cell tag]) {
case regExFindAll:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExFindAll]];
}
break;
case regExCaseInsensitive:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExCaseInsensitive]];
}
break;
case regExWhiteSpace:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExWhiteSpace]];
}
break;
case regExDotMatchNEwline:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExDotMatchNEwline]];
}
break;
case regExMultiline:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExMultiline]];
}
break;
case regExUnicode:
if ([cell intValue]) {
[tempSet addObject:[NSString stringWithFormat:@"%d", regExUnicode]];
}
break;
default:
;
}
}
regExModifiersSet = [NSSet setWithSet:tempSet];
[tempSet release];
return regExModifiersSet;
}
- (void) matchFinished: (NSNotification *) note
// This method is called when the matching is done. It might still be necessary to perform a replacement.
{
if ([self interruptMatchInProgress]) {
[self clearTextToMatch];
[self handleInterrupt];
return;
}
// Do a split if necessary and possible.
if ((resultReplacing == splitResultTableView) && ([regExResults matchSucceeded]) && (![[note object] isEqualToString:@"splitting"])) {
[regExResults splitText: [textToMatch string]
onRegEx: [matchRegex string]
modifiers: [self regExModifiersSet]
allowCode: [self allowCode]];
[self clearTextToMatch]; // Clear here for splits, because this means there was a new match and matchDrawn was reset.
} else {
// The match is really finished, so display the results.
[toggleMatchingButton setEnabled:FALSE];
[infoView displayRect:[toggleMatchingButton frame]];
stillToDraw = [regExResults numberOfMatches];
if (![[note object] isEqualToString:@"splitting"]) { // Don't clear after a split, because matchDrawn is not updated when there was no new match,
[self clearTextToMatch]; // so clearing will result in undrawn matches.
}
[self prepareDrawing];
}
}
- (void) prepareDrawing
// This method is called in preparation for the real drawing. It tests whether there is something to be drawn, and if not exits.
{
[self regExInvalid: FALSE];
if (replaceRegex != nil) {
// If there is a replacementstring, clear the replaceresultstextview.
[replaceResultText replaceCharactersInRange: NSMakeRange(0,[[replaceResultText textStorage] length]) withString: @""];
}
if ([regExResults matchError]) {
[self regExInvalid: TRUE];
[detailsButton setEnabled:FALSE];
if (replaceRegex != nil) {
[replaceResultText replaceCharactersInRange: NSMakeRange(0,[[replaceResultText textStorage]length]) withString: [textToMatch string]];
[self clearReplaceResultText];
}
if (resultReplacing == splitResultTableView) {
[resultReplacing reloadData];
}
[self finishDrawing];
return; // No valid resuls, so no need to stay.
}
if ([regExResults numberOfMatches] == 0) {
[detailsButton setEnabled:FALSE];
if (replaceRegex != nil) {
[replaceResultText replaceCharactersInRange: NSMakeRange(0,[[replaceResultText textStorage]length]) withString: [textToMatch string]];
[self clearReplaceResultText];
}
if (resultReplacing == splitResultTableView) {
[resultReplacing reloadData];
}
[self finishDrawing];
return; // Valid, but nothing matched, so no need to stay.
}
// There are matches, show them.
[detailsButton setEnabled:TRUE];
[self displayMatchResults];
}
- (void) clearTextToMatch
// Clear the textToMatchView prior to a redisplaying with a new match.
{
int lengthTextToMatch = [[textToMatch textStorage] length];
[[textToMatch textStorage] beginEditing];
[[textToMatch textStorage] addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:0] range:NSMakeRange(0,lengthTextToMatch)];
[[textToMatch textStorage] addAttribute:NSForegroundColorAttributeName value:[NSColor blackColor] range:NSMakeRange(0,lengthTextToMatch)];
[[textToMatch textStorage] addAttribute: NSBackgroundColorAttributeName value: [NSColor whiteColor] range: NSMakeRange(0,lengthTextToMatch)];
[[textToMatch textStorage] endEditing];
}
- (void) clearReplaceResultText
// Clear the replaceResultTextView when a error has occurred. Otherwise when the match is successful,
// with certain "wrong" replacementstrings, the whole area may be coloured.
{
if (replaceRegex == nil) { // Only valid when replacing, not when splitting
return;
}
int lengthResultReplacing = [[resultReplacing textStorage] length];
[[resultReplacing textStorage] beginEditing];
[[resultReplacing textStorage] addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:0] range:NSMakeRange(0,lengthResultReplacing)];
[[resultReplacing textStorage] addAttribute:NSForegroundColorAttributeName value:[NSColor blackColor] range:NSMakeRange(0,lengthResultReplacing)];
[[resultReplacing textStorage] endEditing];
}
- (void) displayMatchResults
// This method does the real drawing. It first checks to see which characters are visible, and will only draw does matches, except when overridden.
{
// Exit if interrupted
if ([self interruptMatchInProgress]) {
[self clearTextToMatch];
[self finishDrawing];
return;
}
// Which characters are visible?
NSRect visibleRect = [textToMatch visibleRect];
NSPoint textContainerOrigin = [textToMatch textContainerOrigin];
visibleRect.origin.x -= textContainerOrigin.x;
visibleRect.origin.y -= textContainerOrigin.y;
NSLayoutManager *layoutManager = [textToMatch layoutManager];
NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:[textToMatch textContainer]];
NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
int firstVisibleCharacter = charRange.location;
int lastVisibleCharacter = firstVisibleCharacter + charRange.length;
// Prepare the textToMatchView.
NSTextStorage *textToMatchStorage = [textToMatch textStorage];
[textToMatchStorage beginEditing];
int captureNumber;
int lowestInSet;
int maxMatchNumber = [regExResults numberOfMatches];
int maxCaptureNumber;
BOOL underlineMatch = [[NSUserDefaults standardUserDefaults] boolForKey: @"underlineMatch"];
BOOL underlineCapture = [[NSUserDefaults standardUserDefaults] boolForKey: @"underlineCapture"];
BOOL shadeOverlappingCaptures = [[NSUserDefaults standardUserDefaults] boolForKey: @"shadeOverlappingCaptures"];
// No need to draw matches before the current visible one.
int matchNumber = [self firstMatchToDraw: firstVisibleCharacter lowerLimit: 1 upperLimit: maxMatchNumber];
// Only draw the matches when visible.
for (matchNumber; matchNumber <= maxMatchNumber && [[regExResults matchNumber: matchNumber] range].location < lastVisibleCharacter; matchNumber++) {
// Walk through the matches.
id currentMatch = [regExResults matchNumber: matchNumber];
if (!forceMatchDrawing) {
if (stillToDraw && [currentMatch matchDrawn]) {
continue;
} else {
stillToDraw--;
[currentMatch setMatchDrawn: YES];
}
}
NSRange matchRange = [currentMatch range];
NSCountedSet *overlappingCapturesSet = [[NSCountedSet alloc] init]; // Used to determine overlapping captures.
lowestInSet = [currentMatch beginPosition]; // Used to determine overlapping captures.
[textToMatchStorage addAttribute: NSForegroundColorAttributeName
value: matchColour
range: matchRange];
if (underlineMatch) {
[textToMatchStorage addAttribute: NSUnderlineColorAttributeName
value: matchColour
range: matchRange];
[textToMatchStorage addAttribute: NSUnderlineStyleAttributeName
value: [NSNumber numberWithInt:1]
range: matchRange];
}
maxCaptureNumber = [currentMatch numberOfCaptures];
for (captureNumber = 1; captureNumber <= maxCaptureNumber; captureNumber++) {
// For each match, walk through its captures.
id currentCapture = [currentMatch captureNumber: captureNumber];
NSRange captureRange = [currentCapture range];
[textToMatchStorage addAttribute: NSForegroundColorAttributeName
value: captureColour
range: captureRange];
if (underlineCapture) {
[textToMatchStorage addAttribute: NSUnderlineColorAttributeName
value: captureColour
range: captureRange];
[textToMatchStorage addAttribute: NSUnderlineStyleAttributeName
value: [NSNumber numberWithInt:1]
range: captureRange];
}
if (shadeOverlappingCaptures) {
if ([overlappingCapturesSet occurencesCount]) {
while (([overlappingCapturesSet occurencesCount]) &&
([currentCapture beginPosition] > lowestInSet)) {
// While there are overlapping captures and the beginposition of the current capture of the currend match is larger
// than the known lowest in the overlappingCapturesSet.
[overlappingCapturesSet removeObject:[NSString stringWithFormat:@"%d",lowestInSet]];
lowestInSet = [overlappingCapturesSet lowestInt];
}
if ([overlappingCapturesSet occurencesCount]) {
float shade = 0.7;
int i;
for (i = 0; i < [overlappingCapturesSet occurencesCount] -1; i++) { // Darken shade for each overlapping match.
shade *= 0.7;
}
[textToMatchStorage addAttribute: NSBackgroundColorAttributeName
value: [NSColor colorWithCalibratedWhite:shade alpha:1.0]
range: captureRange];
}
}
NSString *tempString = [[NSString alloc]initWithFormat:@"%d",
[currentCapture endPosition]];
[overlappingCapturesSet addObject:tempString];
[tempString autorelease];
lowestInSet = [overlappingCapturesSet lowestInt];
}
}
[overlappingCapturesSet release];
}
[textToMatchStorage endEditing];
// Prepare the replaceResultTextView.
[[replaceResultText textStorage] beginEditing];
if (replaceRegex != nil) {
int length = [[replaceResultText textStorage] length];
[replaceResultText replaceCharactersInRange: NSMakeRange(0,length) withString: @""];
BOOL underlineReplace = [[NSUserDefaults standardUserDefaults] boolForKey: @"underlineReplace"];
NSMutableAttributedString *tempString;
int beginPosition = 0;
for (matchNumber = 1; matchNumber <= maxMatchNumber; matchNumber++) {
// Get the part after the previous match and before the current match.
tempString = [[NSMutableAttributedString alloc] initWithString:
[[textToMatch string] substringWithRange: NSMakeRange(beginPosition,[[regExResults matchNumber: matchNumber] beginPosition] - beginPosition)]];
[tempString addAttribute: NSFontAttributeName
value: MAIN_FONT
range: NSMakeRange(0,[tempString length])];
[[replaceResultText textStorage] appendAttributedString:tempString];
[tempString release];
// Bump the beginPosition one place along
beginPosition = [[regExResults matchNumber:matchNumber]endPosition];
// Get the current replacementString.
tempString = [[NSMutableAttributedString alloc]initWithString:[[regExResults matchNumber:matchNumber]replacementText]];
[tempString addAttribute: NSFontAttributeName
value: MAIN_FONT
range: NSMakeRange(0,[tempString length])];
[tempString addAttribute: NSForegroundColorAttributeName
value: replaceColour
range: NSMakeRange(0,[tempString length])];
if (underlineReplace) {
[tempString addAttribute: NSUnderlineColorAttributeName
value: replaceColour
range: NSMakeRange(0,[tempString length])];
[tempString addAttribute: NSUnderlineStyleAttributeName
value: [NSNumber numberWithInt:1]
range: NSMakeRange(0,[tempString length])];
}
[[replaceResultText textStorage] appendAttributedString:tempString];
[tempString release];
}
// Finish of with the string from the end of the last match to the end of the text.
beginPosition = [[regExResults matchNumber:--matchNumber]endPosition];
tempString = [[NSMutableAttributedString alloc]initWithString:[[textToMatch string]substringWithRange: NSMakeRange(beginPosition,[[textToMatch string]length] - beginPosition)]];
[tempString addAttribute: NSFontAttributeName
value: MAIN_FONT
range: NSMakeRange(0,[tempString length])];
[[replaceResultText textStorage] appendAttributedString:tempString];
[tempString release];
}
[[replaceResultText textStorage] endEditing];
// When done, force de detailsOutline to update.
[detailsOutlineView reloadData];
// If necessary, do the same for the splitresults table.
if (resultReplacing == splitResultTableView) {
[resultReplacing reloadData];
}
[self finishDrawing];
}
- (void) finishDrawing
// Finish up after drawing.
{
if (forceMatchDrawing) {
forceMatchDrawing = FALSE; // Turn off forceMatchDrawing if it has been turned on.
}
[self setMatchInProgress: FALSE];
[self runProgressIndicator: FALSE];
if (![self liveMatching]) {
[toggleMatchingButton setEnabled:TRUE];
[toggleMatchingButton setTitle: NSLocalizedString(@"Match", nil)];
}
}
#pragma mark-
#pragma mark Other methods
- (int) firstMatchToDraw: (int) firstVisibleCharacter lowerLimit: (int) lowerLimit upperLimit: (int) upperLimit
// To speed up RegExhibit, this method determines the first match that should be drawwn, based on the first visible character in the window.
// It does a binary search, until it distance between the possible starting matches is less than 10 and than does a linear search.
{
if (upperLimit <= lowerLimit) {
return 1;
} else if (upperLimit - lowerLimit < 10) {
while ((lowerLimit <= upperLimit) && ([[regExResults matchNumber: lowerLimit] range].location < firstVisibleCharacter)) {
lowerLimit++;
}
return lowerLimit;
} else {
int midWay = lowerLimit + (upperLimit - lowerLimit) / 2;
if ([[regExResults matchNumber: midWay] range].location < firstVisibleCharacter) {
return [self firstMatchToDraw: firstVisibleCharacter lowerLimit: midWay upperLimit: upperLimit];
} else {
return [self firstMatchToDraw: firstVisibleCharacter lowerLimit: lowerLimit upperLimit: midWay];
}
}
}
- (void) runProgressIndicator: (BOOL) animateProgressIndicator
{
if (animateProgressIndicator) {
[regExProgressIndicator startAnimation: self];
} else {
[regExProgressIndicator stopAnimation: self];
}
}
- (void) regExInvalid: (BOOL) regExInvalid
{
if ([self matchAborted]) {