3 * Copyright (c) 2016 Apple Inc. All rights reserved.
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 #import "CNBrowseDomainsController.h"
19 #import "CNDomainBrowserViewController.h"
20 #import "CNDomainBrowserPathUtils.h"
21 #import "BonjourSCStore.h"
22 #import <AssertMacros.h>
24 #define kTag_AddBrowse 100
25 #define kTag_AddManual 101
27 const NSString * _CNBrowseDomainKey_fullname = (NSString *)SC_DYNDNS_DOMAIN_KEY;
28 const NSString * _CNBrowseDomainKey_enabled = (NSString *)SC_DYNDNS_ENABLED_KEY;
30 #define LocalizedStringFromMyBundle(key, comment ) \
31 NSLocalizedStringFromTableInBundle(key, @"Localizable", [NSBundle bundleForClass: [self class]], comment )
33 @interface CNPathPopoverViewController : UIViewController
35 @property (strong) UILabel * label;
36 @property (copy) NSArray * pathArray;
37 @property (assign) CGPoint offset;
41 @implementation CNPathPopoverViewController
43 - (instancetype)initWithPathArray:(NSArray *)pathArray
45 if (self = [self initWithNibName: nil bundle: nil] )
47 _pathArray = pathArray;
48 NSMutableParagraphStyle * paragraphStyle = [[NSMutableParagraphStyle alloc] init];
49 [paragraphStyle setLineSpacing: [UIFont labelFontSize] * 0.3];
51 NSMutableAttributedString * itemStr = [[NSMutableAttributedString alloc] init];
53 for (NSString * next in _pathArray )
55 NSString * nextLine = [NSString stringWithFormat: @"%@%@", itemStr.length ? @"\n" : @"", next];
56 UIColor * nextColor = ((count++ == 0) || (count == _pathArray.count)) ? [UIColor grayColor] : [UIColor blackColor];
57 [itemStr appendAttributedString:
58 [[NSMutableAttributedString alloc] initWithString: nextLine
59 attributes: @{ NSForegroundColorAttributeName: nextColor,
60 NSParagraphStyleAttributeName: paragraphStyle
64 _label = [[UILabel alloc] initWithFrame: CGRectZero];
65 _label.numberOfLines = 0;
66 _label.attributedText = itemStr;
69 _offset = CGPointMake(25, 15 );
70 CGRect rect = _label.frame;
71 rect.origin.x += _offset.x;
72 rect.origin.y += _offset.y;
73 [_label setFrame: rect];
81 [self.view addSubview: _label];
84 - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
86 [super willTransitionToTraitCollection: newCollection withTransitionCoordinator: coordinator];
87 [self.presentingViewController dismissViewControllerAnimated: NO completion: nil];
92 @interface CNPathAccessoryView : UIView
94 @property (strong, readonly) UIButton * elipsisButton;
95 @property (strong, readonly) UIButton * pathLabel;
96 @property (weak) UITableView * tableView;
97 @property (weak) UITableViewCell * cell;
98 @property (strong, readonly) NSArray * pathArray;
102 @implementation CNPathAccessoryView
104 - (instancetype) initWithFrame:(CGRect)frame pathArray:(NSArray *)pathArray
106 self = [super initWithFrame:frame];
108 _pathArray = pathArray;
111 _pathLabel = [UIButton buttonWithType: UIButtonTypeCustom];
112 [_pathLabel setTitle: _pathArray[_pathArray.count-1] forState: UIControlStateNormal];
113 [_pathLabel setTitleColor: [UIColor blackColor] forState: UIControlStateNormal];
114 _pathLabel.titleLabel.font = [UIFont systemFontOfSize: [UIFont labelFontSize]];
115 [self addSubview: _pathLabel];
117 _pathLabel.translatesAutoresizingMaskIntoConstraints = NO;
118 [_pathLabel sizeToFit];
119 [_pathLabel.widthAnchor constraintEqualToConstant: _pathLabel.frame.size.width].active = YES;
120 [_pathLabel.trailingAnchor constraintEqualToAnchor: self.trailingAnchor].active = YES;
121 [_pathLabel.topAnchor constraintEqualToAnchor: self.topAnchor].active = YES;
122 [_pathLabel.bottomAnchor constraintEqualToAnchor: self.bottomAnchor].active = YES;
124 if (_pathArray.count > 2 )
126 _elipsisButton = [UIButton buttonWithType: UIButtonTypeCustom];
127 [_elipsisButton setTitle: @"…" forState: UIControlStateNormal];
128 [_elipsisButton setTitleColor: _elipsisButton.tintColor forState: UIControlStateNormal];
129 [_elipsisButton setTitleColor: [UIColor grayColor] forState: UIControlStateHighlighted];
130 _elipsisButton.titleLabel.font = [UIFont boldSystemFontOfSize: [UIFont labelFontSize]];
131 [_elipsisButton addTarget: self action: @selector(pathButtonPressed:withEvent:) forControlEvents: UIControlEventTouchUpInside];
132 _elipsisButton.userInteractionEnabled = YES;
133 [self addSubview: _elipsisButton];
135 _elipsisButton.translatesAutoresizingMaskIntoConstraints = NO;
136 [_elipsisButton sizeToFit];
137 [_elipsisButton.widthAnchor constraintEqualToConstant: _elipsisButton.frame.size.width].active = YES;
138 [_elipsisButton.trailingAnchor constraintEqualToAnchor: _pathLabel.leadingAnchor].active = YES;
139 [_elipsisButton.topAnchor constraintGreaterThanOrEqualToAnchor: self.topAnchor].active = YES;
140 [_elipsisButton.bottomAnchor constraintLessThanOrEqualToAnchor: self.bottomAnchor].active = YES;
146 - (BOOL) canBecomeFirstResponder
151 - (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection
153 (void)controller; // Unused
154 (void)traitCollection; // Unused
155 return UIModalPresentationNone;
158 - (void) pathButtonPressed:(UIControl *)button withEvent:(UIEvent *)event
160 (void)button; // Unused
162 if (!self.cell.showingDeleteConfirmation )
164 [self becomeFirstResponder];
166 UIView *buttonView = [[event.allTouches anyObject] view];
167 CGRect buttonFrame = [buttonView convertRect: buttonView.frame toView: self];
169 CNPathPopoverViewController * controller = [[CNPathPopoverViewController alloc] initWithPathArray: self.pathArray];
170 controller.modalPresentationStyle = UIModalPresentationPopover;
171 controller.preferredContentSize = CGSizeMake(controller.label.frame.size.width + controller.offset.x * 2, controller.label.frame.size.height + controller.offset.y * 2 );
173 UIPopoverPresentationController *popover = controller.popoverPresentationController;
174 popover.delegate = (id<UIPopoverPresentationControllerDelegate>)self;
175 popover.sourceView = buttonView;
176 popover.sourceRect = buttonFrame;
177 popover.permittedArrowDirections = UIPopoverArrowDirectionLeft | UIPopoverArrowDirectionRight;
179 [(UIViewController *)self.tableView.delegate presentViewController: controller
185 - (CGSize) sizeThatFits:(CGSize)size
187 (void)size; // Unused
188 CGSize ret = CGSizeZero;
190 CGFloat maxHeight = 0.0;
191 for (UIView *view in [self subviews] )
193 CGRect frame = view.frame;
194 CGFloat frameMaxX = CGRectGetMaxX(frame);
195 CGFloat frameHeight = CGRectGetHeight(frame);
197 if (frameHeight > maxHeight )
199 maxHeight = frameHeight;
203 ret.height = maxHeight;
209 @interface CNManualDomainViewController : UITableViewController
211 @property (weak) CNBrowseDomainsController * delegate;
212 @property (weak) UITextField * textField;
216 #define kTag_EditTextField 103
218 @implementation CNManualDomainViewController
224 self.tableView.dataSource = (id<UITableViewDataSource>)self;
225 self.tableView.delegate = (id<UITableViewDelegate>)self;
227 self.title = LocalizedStringFromMyBundle(@"_dnsBrowser.manualdomain.title", nil );
228 self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: LocalizedStringFromMyBundle(@"_dnsBrowser.manualdomain.cancel", nil )
229 style: UIBarButtonItemStylePlain
231 action: @selector(cancelAction:)];
233 self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: LocalizedStringFromMyBundle(@"_dnsBrowser.manualdomain.add", nil )
234 style: UIBarButtonItemStylePlain
236 action: @selector(addAction:)];
237 self.navigationItem.rightBarButtonItem.enabled = NO;
240 - (void)viewWillDisappear:(BOOL)animated
242 [super viewWillDisappear: animated];
243 if (self.isMovingFromParentViewController )
245 [[NSNotificationCenter defaultCenter] removeObserver: self];
249 - (void)viewDidAppear:(BOOL)animated
251 [super viewDidAppear: animated];
252 if (self.isMovingToParentViewController )
254 [self.textField becomeFirstResponder];
258 - (IBAction)addAction:(id)sender
260 (void)sender; // Unused
261 [self.navigationController dismissViewControllerAnimated: YES completion: ^{
262 [self.delegate addBrowseDomain: self.textField.text];
266 - (IBAction)cancelAction:(id)sender
268 (void)sender; // Unused
269 [self.navigationController dismissViewControllerAnimated: YES completion: nil];
272 #pragma mark - Notifications
274 - (void)textFieldDidChange:(NSNotification *)notification
276 UITextField * textField = (UITextField *)notification.object;
277 self.navigationItem.rightBarButtonItem.enabled = (textField.text.length > 0);
280 #pragma mark - TableView Delegates
282 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
284 (void)tableView; // Unused
285 (void)indexPath; // Unused
286 return UITableViewAutomaticDimension;
289 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
291 (void)tableView; // Unused
292 NSInteger result = 0;
294 if (tableView == self.tableView ) result = 1;
299 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
301 (void)section; // Unused
302 NSInteger result = 0;
304 if (tableView == self.tableView && section == 0 ) result = 1;
309 - (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
311 NSString * result = nil;
313 if (tableView == self.tableView && section == 0 )
315 result = LocalizedStringFromMyBundle(@"_dnsBrowser.manualdomain.footer", nil );
321 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
323 UITableViewCell *cell = nil;
325 if (tableView == self.tableView )
327 if (indexPath.section == 0 && indexPath.row == 0 )
329 static NSString *MyIdentifier = @"manual_domain_cell_id";
330 cell = [tableView dequeueReusableCellWithIdentifier: MyIdentifier];
333 cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier: MyIdentifier];
336 cell.textLabel.hidden = YES;
337 cell.detailTextLabel.hidden = YES;
339 [[cell viewWithTag: kTag_EditTextField] removeFromSuperview];
340 UITextField * textField = [[UITextField alloc] initWithFrame:CGRectZero];
341 textField.tag = kTag_EditTextField;
342 textField.adjustsFontSizeToFitWidth = YES;
343 textField.placeholder = LocalizedStringFromMyBundle(@"_dnsBrowser.manualdomain.defaultValue", nil );
344 textField.keyboardType = UIKeyboardTypeURL;
345 textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
346 textField.autocorrectionType = UITextAutocorrectionTypeNo;
347 textField.enablesReturnKeyAutomatically = YES;
348 textField.clearButtonMode = UITextFieldViewModeAlways;
349 [cell.contentView addSubview: textField];
351 textField.translatesAutoresizingMaskIntoConstraints = NO;
352 [textField.leadingAnchor constraintEqualToAnchor: cell.layoutMarginsGuide.leadingAnchor].active = YES;
353 [textField.trailingAnchor constraintEqualToAnchor: cell.layoutMarginsGuide.trailingAnchor].active = YES;
354 [textField.topAnchor constraintEqualToAnchor: cell.layoutMarginsGuide.topAnchor].active = YES;
355 [textField.bottomAnchor constraintEqualToAnchor: cell.layoutMarginsGuide.bottomAnchor].active = YES;
357 [[NSNotificationCenter defaultCenter] addObserver: self
358 selector: @selector(textFieldDidChange:)
359 name: UITextFieldTextDidChangeNotification
361 self.textField = textField;
368 - (nullable NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
370 (void)tableView; // Unused
371 (void)indexPath; // Unused
377 @interface CNBrowseDomainsController ()
379 @property (strong) NSDictionary * selectedInstance;
380 @property (strong) NSMutableDictionary * instanceInfoStrings;
381 @property (strong) NSMutableDictionary * instanceStatusViews;
383 @property (strong) CNDomainBrowserViewController * browseController;
387 @implementation CNBrowseDomainsController
389 - (instancetype)initWithStyle:(UITableViewStyle)style
391 if (self = [super initWithStyle: style] )
398 - (void)contentViewsInit
400 self.tableView.allowsMultipleSelectionDuringEditing = NO;
405 self.instanceInfoStrings = [NSMutableDictionary dictionary];
406 self.instanceStatusViews = [NSMutableDictionary dictionary];
407 self.browseController = [[CNDomainBrowserViewController alloc] initWithStyle: UITableViewStylePlain];
408 _browseController.delegate = (id<CNDomainBrowserViewControllerDelegate>)self;
409 _browseController.ignoreLocal = YES;
415 [self contentViewsInit];
417 self.tableView.dataSource = (id<UITableViewDataSource>)self;
418 self.tableView.delegate = (id<UITableViewDelegate>)self;
421 - (void)viewWillAppear:(BOOL)animated
423 [super viewWillAppear: animated];
424 if (self.isMovingToParentViewController)
426 [_browseController startBrowse];
430 - (void)viewWillDisappear:(BOOL)animated
432 [super viewWillDisappear: animated];
433 if (self.isMovingFromParentViewController )
435 [self savePreferences];
436 [_browseController stopBrowse];
440 -(void)savePreferences
442 [BonjourSCStore setObject: _browseDomainsA.count ? _browseDomainsA : nil forKey: (NSString *)SC_DYNDNS_BROWSEDOMAINS_KEY];
445 - (NSDictionary *)browseDomainForRow:(NSInteger)row
447 NSDictionary * result = nil;
448 NSInteger curRow = 0;
450 for (NSDictionary * nextDomain in self.browseDomainsA )
463 - (NSInteger)indexOfDomainInList:(NSString *)domainString
466 if (_browseDomainsA ) {
467 NSDictionary *domainDict;
468 NSString *domainName;
469 NSEnumerator *arrayEnumerator = [_browseDomainsA objectEnumerator];
470 while ((domainDict = [arrayEnumerator nextObject]) != NULL)
472 domainName = [domainDict objectForKey:_CNBrowseDomainKey_fullname];
473 if ([domainString caseInsensitiveCompare:domainName] == NSOrderedSame) return index;
480 - (void)addBrowseDomain:(NSString *)fullPath
482 NSString * trimmedPath = TrimCharactersFromDNSDomain(fullPath );
483 NSInteger index = [self indexOfDomainInList: trimmedPath];
484 NSMutableArray * domains = [NSMutableArray arrayWithArray: self.browseDomainsA];
486 if (index == NSNotFound )
488 [domains addObject: @{
489 _CNBrowseDomainKey_fullname: trimmedPath,
490 _CNBrowseDomainKey_enabled: @YES
493 self.browseDomainsA = [NSArray arrayWithArray: domains];
494 [self.tableView insertRowsAtIndexPaths: @[[NSIndexPath indexPathForRow: domains.count-1 inSection: 0]] withRowAnimation: UITableViewRowAnimationAutomatic];
498 NSDictionary * domain = domains[index];
500 _CNBrowseDomainKey_fullname: domain[_CNBrowseDomainKey_fullname],
501 _CNBrowseDomainKey_enabled: @YES
503 self.browseDomainsA = [NSArray arrayWithArray: domains];
504 NSIndexPath * indexPath = [NSIndexPath indexPathForRow: index inSection: 0];
505 [CATransaction begin];
506 [CATransaction setCompletionBlock:^{
507 [self.tableView deselectRowAtIndexPath: indexPath animated: YES];
509 [self.tableView reloadRowsAtIndexPaths: @[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
510 [self.tableView selectRowAtIndexPath: indexPath animated: YES scrollPosition: UITableViewScrollPositionMiddle];
511 [CATransaction commit];
516 #pragma mark - CNDomainBrowserViewControllerDelegate
518 - (void)domainBrowserDomainSelected:(NSString *)domain
520 [self newPathSelected: domain];
523 - (void)bonjourBrowserDomainUpdate:(NSString *)defaultDomain
525 (void)defaultDomain; // Unused
526 [self.tableView reloadData];
529 #pragma mark - TableView Delegates
531 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
533 (void)tableView; // Unused
534 (void)indexPath; // Unused
535 return UITableViewAutomaticDimension;
538 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
540 (void)tableView; // Unused
541 NSInteger result = 0;
543 if (tableView == self.tableView ) result = (self.browseController.flattenedDNSDomains.count > 0) ? 3 : 2;
549 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
551 (void)section; // Unused
552 NSInteger result = 0;
554 if (tableView == self.tableView )
558 result = self.browseDomainsA.count;
569 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
571 UITableViewCell *cell = nil;
573 if (tableView == self.tableView )
575 if (indexPath.section == 0 )
577 NSDictionary * record = [self browseDomainForRow: indexPath.row];
580 static NSString *MyIdentifier = @"browse_cell_id";
581 cell = [tableView dequeueReusableCellWithIdentifier: MyIdentifier];
584 cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle reuseIdentifier: MyIdentifier];
586 NSArray * pathArray = DNSDomainToDomainPath(record[_CNBrowseDomainKey_fullname] );
587 cell.textLabel.text = pathArray[pathArray.count-1];
588 cell.textLabel.textColor = nil;
589 cell.textLabel.numberOfLines = 0;
590 cell.accessoryType = UITableViewCellAccessoryNone;
591 cell.accessoryView = nil;
592 cell.imageView.image = [[UIImage imageNamed: @"UIPreferencesBlueCheck.png"
593 inBundle: [NSBundle bundleForClass: [self class]]
594 compatibleWithTraitCollection: nil] imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
595 cell.imageView.hidden = ![record[_CNBrowseDomainKey_enabled] boolValue];
597 NSArray * accPathArray = (pathArray.count > 2) ?
598 [[pathArray reverseObjectEnumerator] allObjects] :
599 ((pathArray.count > 1) ? @[pathArray[0]] : nil);
602 CNPathAccessoryView * accView = [[CNPathAccessoryView alloc] initWithFrame: CGRectZero
603 pathArray: accPathArray];
604 accView.tableView = tableView;
607 cell.accessoryView = accView;
613 static NSString *MyIdentifier = @"button_cell_id";
614 cell = [tableView dequeueReusableCellWithIdentifier: MyIdentifier];
617 cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: MyIdentifier];
620 if (self.browseController.flattenedDNSDomains.count > 0 && indexPath.section == 1 )
622 cell.textLabel.textColor = self.view.tintColor;
623 cell.textLabel.text = LocalizedStringFromMyBundle(@"_dnsBrowser.domains.selectdomain", nil );
624 cell.accessoryType = UITableViewCellAccessoryNone;
625 cell.tag = kTag_AddBrowse;
629 cell.textLabel.textColor = self.view.tintColor;
630 cell.textLabel.text = LocalizedStringFromMyBundle(@"_dnsBrowser.domains.selectmanual", nil );
631 cell.accessoryType = UITableViewCellAccessoryNone;
632 cell.tag = kTag_AddManual;
640 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
644 if (tableView == self.tableView && indexPath.section == 0 ) result = YES;
649 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
651 if (editingStyle == UITableViewCellEditingStyleDelete )
653 NSMutableArray * domains = [NSMutableArray arrayWithArray: self.browseDomainsA];
654 [domains removeObjectAtIndex: indexPath.row];
655 self.browseDomainsA = [NSArray arrayWithArray: domains];
656 [tableView deleteRowsAtIndexPaths: @[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
660 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
662 UITableViewCell * cell = [tableView cellForRowAtIndexPath: indexPath];
663 if (tableView == self.tableView )
665 if (indexPath.section == 0 )
666 { // Toggle check and enable bit
667 NSMutableArray * domains = [NSMutableArray arrayWithArray: self.browseDomainsA];
668 NSMutableDictionary * selection = [NSMutableDictionary dictionaryWithDictionary: domains[indexPath.row]];
669 selection[_CNBrowseDomainKey_enabled] = [NSNumber numberWithBool: ![selection[_CNBrowseDomainKey_enabled] boolValue]];
670 domains[indexPath.row] = selection;
671 self.browseDomainsA = [NSArray arrayWithArray: domains];
672 [tableView reloadRowsAtIndexPaths: @[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
676 if (cell.tag == kTag_AddBrowse )
678 CNDomainBrowserViewController *c = _browseController;
679 c.title = LocalizedStringFromMyBundle(@"_bonjour.browse.name", nil);
680 c.clearsSelectionOnViewWillAppear = NO;
681 c.modalPresentationStyle = UIModalPresentationFormSheet;
682 UINavigationController * nv = [[UINavigationController alloc] initWithRootViewController: c];
683 [self presentViewController: nv animated: YES completion: nil];
685 else if (cell.tag == kTag_AddManual )
687 CNManualDomainViewController * c = [[CNManualDomainViewController alloc] initWithStyle: UITableViewStyleGrouped];
689 c.modalPresentationStyle = UIModalPresentationFormSheet;
690 UINavigationController * nv = [[UINavigationController alloc] initWithRootViewController: c];
691 [self presentViewController: nv animated: YES completion: nil];
695 [tableView deselectRowAtIndexPath: indexPath animated: YES];
698 #pragma mark - Commands
700 - (void)newPathSelected:(NSString *)fullPath
702 [CATransaction begin];
703 [CATransaction setCompletionBlock:^{
704 [self addBrowseDomain: fullPath];
706 [self.navigationController popToViewController: self animated: YES];
707 [CATransaction commit];