Started rework of inspector layout using separate view controllers for attribute editiors

This commit is contained in:
Michael Starke
2021-10-15 14:54:22 +02:00
parent 6cd443278f
commit f28bcb527a
11 changed files with 331 additions and 22 deletions

View File

@@ -0,0 +1,24 @@
//
// MPEntryAttributeViewController.h
// MacPass
//
// Created by Michael Starke on 14.10.21.
// Copyright © 2021 HicknHack Software GmbH. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <HNHUi/HNHUi.h>
#import "MPInspectorEditor.h"
NS_ASSUME_NONNULL_BEGIN
/// View controller to show and edit KPKAttributes of KPKEntries.
/// Set the represented object to the KPKAttribute the editor shoudl show/edit
/// The editor can be set to edit or view
@interface MPEntryAttributeViewController : NSViewController <MPInspectorEditor, HNHUITextFieldDelegate>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,151 @@
//
// MPEntryAttributeViewController.m
// MacPass
//
// Created by Michael Starke on 14.10.21.
// Copyright © 2021 HicknHack Software GmbH. All rights reserved.
//
#import "MPEntryAttributeViewController.h"
#import <HNHUi/HNHUi.h>
#import <KeePassKit/KeePassKit.h>
#import "MPPasteBoardController.h"
@interface MPEntryAttributeViewController () {
BOOL _isDefaultAttribute;
}
@property (strong) IBOutlet NSTextField *keyTextField;
@property (strong) IBOutlet HNHUITextField *valueTextField;
@property (readonly, nullable, strong) KPKAttribute *representedAttribute;
@end
@implementation MPEntryAttributeViewController
@synthesize isEditor = _isEditor;
- (instancetype)initWithNibName:(NSNibName)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if(self) {
_isEditor = NO;
_isDefaultAttribute = NO;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
// set editor to false?
return self;
}
- (void)viewDidLoad {
NSString *placeHolder = NSLocalizedString(@"NONE", "Placeholder text for input fields if no entry or group is selected");
self.keyTextField.placeholderString = placeHolder;
self.valueTextField.placeholderString = placeHolder;
[super viewDidLoad];
[self _updateValues];
[self _updateEditing];
__weak MPEntryAttributeViewController *welf = self;
self.valueTextField.copyActionBlock = ^void(NSTextField *tf) {
NSText *text = [welf.view.window fieldEditor:NO forObject:welf.valueTextField];
if([text isKindOfClass:NSTextView.class]) {
[welf textField:welf.valueTextField textView:(NSTextView *)text performAction:@selector(copy:)];
}
};
}
- (KPKAttribute *)representedAttribute {
if([self.representedObject isKindOfClass:KPKAttribute.class]) {
return (KPKAttribute *)self.representedObject;
}
return nil;
}
- (void)setIsEditor:(BOOL)isEditor {
_isEditor = isEditor;
[self _updateEditing];
}
- (void)setRepresentedObject:(id)representedObject {
if(self.representedAttribute) {
[NSNotificationCenter.defaultCenter removeObserver:self name:KPKWillChangeAttributeNotification object:self.representedObject];
[NSNotificationCenter.defaultCenter removeObserver:self name:KPKDidChangeAttributeNotification object:self.representedObject];
}
super.representedObject = representedObject;
if(self.representedAttribute) {
[NSNotificationCenter.defaultCenter addObserver:self
selector:(@selector(_willChangeAttribute:))
name:KPKWillChangeAttributeNotification
object:self.representedAttribute];
[NSNotificationCenter.defaultCenter addObserver:self
selector:(@selector(_didChangeAttribute:))
name:KPKDidChangeAttributeNotification
object:self.representedAttribute];
}
_isDefaultAttribute = self.representedAttribute.isDefault;
[self _updateValues];
}
- (BOOL)textField:(NSTextField *)textField textView:(NSTextView *)textView performAction:(SEL)action {
if(action != @selector(copy:)) {
return YES;
}
// Only copy action
MPPasteboardOverlayInfoType info = MPPasteboardOverlayInfoCustom;
NSMutableString *selectedValue = [[NSMutableString alloc] init];
for(NSValue *rangeValue in textView.selectedRanges) {
[selectedValue appendString:[textView.string substringWithRange:rangeValue.rangeValue]];
}
if(selectedValue.length == 0) {
[selectedValue setString:textView.string];
}
NSString *name = @"";
if([self.representedAttribute.key isEqual:kKPKUsernameKey]) {
info = MPPasteboardOverlayInfoUsername;
}
else if([self.representedAttribute.key isEqual:kKPKPasswordKey]) {
info = MPPasteboardOverlayInfoPassword;
}
else if([self.representedAttribute.key isEqual:kKPKURLKey]) {
info = MPPasteboardOverlayInfoURL;
}
else if([self.representedAttribute.key isEqual:kKPKTitleKey]) {
name = NSLocalizedString(@"TITLE", "Displayed name when title field was copied");
}
else {
name = self.representedAttribute.key;
}
[MPPasteBoardController.defaultController copyObject:selectedValue overlayInfo:info name:name atView:self.view];
return NO;
}
- (void)_willChangeAttribute:(NSNotification *)notification {
// nothing to d
}
- (void)_didChangeAttribute:(NSNotification *)notification {
[self _updateValues];
}
- (void)_updateValues {
self.view.hidden = (!self.isEditor && self.representedAttribute.value.length == 0);
self.keyTextField.stringValue = self.representedAttribute.key ? self.representedAttribute.key : @"";
self.valueTextField.stringValue = self.representedAttribute.value ? self.representedAttribute.value : @"";
}
- (void)_updateEditing {
self.keyTextField.editable = !_isDefaultAttribute && self.isEditor;
self.valueTextField.editable = self.isEditor;
self.keyTextField.selectable = YES;
self.valueTextField.selectable = YES;
}
@end

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MPEntryAttributeViewController">
<connections>
<outlet property="keyTextField" destination="m8q-FN-S8D" id="HzJ-cd-ifA"/>
<outlet property="valueTextField" destination="HZM-H4-dB4" id="56C-eA-J7G"/>
<outlet property="view" destination="Hz6-mo-xeY" id="0bl-1N-x8E"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="Hz6-mo-xeY">
<rect key="frame" x="0.0" y="0.0" width="251" height="43"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="No7-P9-4cl">
<rect key="frame" x="0.0" y="0.0" width="251" height="43"/>
<subviews>
<textField horizontalHuggingPriority="249" verticalHuggingPriority="750" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="m8q-FN-S8D">
<rect key="frame" x="-2" y="29" width="255" height="14"/>
<textFieldCell key="cell" controlSize="small" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="AttributeName" id="MQc-TE-UjE">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="disabledControlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="249" verticalHuggingPriority="750" horizontalCompressionResistancePriority="249" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="HZM-H4-dB4" customClass="HNHUITextField">
<rect key="frame" x="0.0" y="0.0" width="251" height="21"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" truncatesLastVisibleLine="YES" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" drawsBackground="YES" usesSingleLineMode="YES" id="sO3-xr-VwO">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<outlet property="delegate" destination="-2" id="Dm6-8t-HmN"/>
</connections>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="No7-P9-4cl" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="0cj-uh-lAy"/>
<constraint firstAttribute="trailing" secondItem="No7-P9-4cl" secondAttribute="trailing" id="Awb-Of-fpP"/>
<constraint firstAttribute="bottom" secondItem="No7-P9-4cl" secondAttribute="bottom" id="MX2-as-4EI"/>
<constraint firstItem="No7-P9-4cl" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="mbE-gq-jDF"/>
</constraints>
<point key="canvasLocation" x="108" y="202"/>
</customView>
</objects>
</document>

View File

@@ -33,6 +33,7 @@
#import "MPReferenceBuilderViewController.h"
#import "MPTOTPViewController.h"
#import "MPTOTPSetupViewController.h"
#import "MPEntryAttributeViewController.h"
#import "MPPrettyPasswordTransformer.h"
#import "NSString+MPPasswordCreation.h"
@@ -75,6 +76,7 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
MPWindowTitleComboBoxDelegate *_windowTitleMenuDelegate;
MPTagsTokenFieldDelegate *_tagTokenFieldDelegate;
MPAddCustomFieldContextMenuDelegate *_addCustomFieldContextMenuDelegate;
NSMutableArray<MPEntryAttributeViewController *> *_attributeEditorViewControllers;
}
@property (nonatomic, assign) BOOL showPassword;
@@ -111,6 +113,7 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
_customFieldTableDelegate.viewController = self;
_addCustomFieldContextMenuDelegate.viewController = self;
_attributeEditorViewControllers = [[NSMutableArray alloc] init];
_activeTab = MPEntryTabGeneral;
}
return self;
@@ -130,6 +133,9 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
}
super.representedObject = representedObject;
self.totpViewController.representedObject = self.representedObject;
//[self _updateAttributeEditors];
/* only register for a single entry! */
if(self.representedEntry) {
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(_willChangeEntry:) name:KPKWillChangeEntryNotification object:self.representedEntry];
@@ -195,9 +201,12 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
self.tagsTokenField.delegate = _tagTokenFieldDelegate;
[self _setupTOPTView];
//[self _setupAttributeEditors];
//[self _updateAttributeEditors];
[self _setupCustomFieldsButton];
[self _setupViewBindings];
[self _updateFieldVisibilty];
}
- (void)registerNotificationsForDocument:(MPDocument *)document {
@@ -607,8 +616,20 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
//[self.addCustomFieldButton setEnabled:NO forSegment:MPContextButtonSegmentContextButton];
}
- (void)_updateFieldVisibilty {
- (void)_setupAttributeEditors {
for(NSUInteger index = 0; index < kKPKDefaultEntryKeysCount; index++) {
MPEntryAttributeViewController *vc = [[MPEntryAttributeViewController alloc] init];
vc.isEditor = NO;
[_attributeEditorViewControllers addObject:vc];
[self.fieldsStackView addArrangedSubview:vc.view];
}
}
- (void)_updateAttributeEditors {
_attributeEditorViewControllers[0].representedObject = [self.representedEntry attributeWithKey:kKPKTitleKey];
_attributeEditorViewControllers[1].representedObject = [self.representedEntry attributeWithKey:kKPKUsernameKey];
_attributeEditorViewControllers[2].representedObject = [self.representedEntry attributeWithKey:kKPKPasswordKey];
_attributeEditorViewControllers[3].representedObject = [self.representedEntry attributeWithKey:kKPKURLKey];
}
- (void)_setupTOPTView {
@@ -716,7 +737,11 @@ typedef NS_ENUM(NSUInteger, MPEntryTab) {
}
- (void)_didChangeEntry:(NSNotification *)notification {
NSLog(@"didChangeEntry");
if(notification.object != self.representedObject) {
NSLog(@"Skipping: didChangeEntry:%@, we do not need this change!", notification.object);
return;
}
NSLog(@"didChangeEntry:%@", notification.object);
[self _updateEntryValues];
}

View File

@@ -0,0 +1,23 @@
//
// MPInspectorEditor.h
// MacPass
//
// Created by Michael Starke on 14.10.21.
// Copyright © 2021 HicknHack Software GmbH. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// The InpsectorEditor protocoll that should be implemented by editors used in the inspector
/// Individual editors shoudl adopt different APIs to accomodate their needs
/// The preferred way to set model data is to use the representedObject
@protocol MPInspectorEditor <NSObject>
@required
@property (nonatomic) BOOL isEditor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -21,6 +21,8 @@
@property (strong) IBOutlet NSGridView *gridView;
@property (strong) IBOutlet NSPopUpButton *typePopUpButton;
@property (nonatomic, readonly) KPKEntry *representedEntry;
@property NSInteger timeSlice;
@end
@@ -43,19 +45,17 @@ typedef NS_ENUM(NSUInteger, MPOTPType) {
@implementation MPTOTPSetupViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSAssert([self.representedObject isKindOfClass:KPKEntry.class], @"represented object needs to be a KPKEntry");
[self _setupView];
[self _updateView:MPOTPUpdateSourceEntry];
- (KPKEntry *)representedEntry {
if([self.representedObject isKindOfClass:KPKEntry.class]) {
return (KPKEntry *)self.representedObject;
}
return nil;
}
- (IBAction)toggleDisclosure:(id)sender {
for(NSInteger row = 1; row < self.gridView.numberOfRows; row++) {
NSGridRow *gridRow = [self.gridView rowAtIndex:row];
gridRow.hidden = !gridRow.hidden;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self _setupView];
[self _updateView:MPOTPUpdateSourceEntry];
}
- (IBAction)changeType:(id)sender {
@@ -131,6 +131,14 @@ typedef NS_ENUM(NSUInteger, MPOTPType) {
[self.timeStepTextField bind:NSValueBinding toObject:self withKeyPath:NSStringFromSelector(@selector(timeSlice)) options:nil];
[self.timeStepStepper bind:NSValueBinding toObject:self withKeyPath:NSStringFromSelector(@selector(timeSlice)) options:nil];
KPKEntry *entry = self.representedEntry;
if(entry.hasTimeOTP) {
KPKTimeOTPGenerator *generator = [[KPKTimeOTPGenerator alloc] initWithAttributes:self.representedEntry.attributes];
if(generator.isRFC6238) {
[self.typePopUpButton selectItemWithTag:MPOTPTypeRFC];
}
}
}
- (void)_updateView:(MPOTPUpdateSource)source {
@@ -151,12 +159,15 @@ typedef NS_ENUM(NSUInteger, MPOTPType) {
NSString *qrCodeString = self.qrCodeImageView.image.QRCodeString;
NSURL *otpURL = [NSURL URLWithString:qrCodeString];
self.urlTextField.stringValue = otpURL.absoluteString;
// fallthroug
generator = [[KPKTimeOTPGenerator alloc] initWithURL:self.urlTextField.stringValue];
break;
}
case MPOTPUpdateSourceURL:
generator = [[KPKTimeOTPGenerator alloc] initWithURL:self.urlTextField.stringValue];
break;
case MPOTPUpdateSourceSecret:
generator.key = [NSData dataWithBase32EncodedString:self.secretTextField.stringValue];
break;
case MPOTPUpdateSourceAlgorithm:
break;

View File

@@ -61,7 +61,6 @@
return NO;
}
return YES;
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -94,7 +94,7 @@
<constraint firstItem="Lz5-aM-o4b" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="y4i-uB-HlK"/>
<constraint firstItem="Lz5-aM-o4b" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="zFP-JZ-Qtu"/>
</constraints>
<point key="canvasLocation" x="-354" y="115"/>
<point key="canvasLocation" x="-629" y="-23"/>
</customView>
</objects>
<resources>