Files
baRSS/baRSS/NSCategories/NSView+Ext.m
2022-10-01 17:37:37 +02:00

360 lines
13 KiB
Objective-C

#import "NSView+Ext.h"
@implementation NSView (Ext)
#pragma mark - UI: TextFields -
/// Create label with non-editable text. Ensures uniform fontsize and text color. @c 17px height.
+ (NSTextField*)label:(NSString*)text {
NSTextField *label = [NSTextField labelWithString:text];
[label setFrameSize: NSMakeSize(0, HEIGHT_LABEL)];
label.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
label.textColor = [NSColor controlTextColor];
label.lineBreakMode = NSLineBreakByTruncatingTail;
// label.backgroundColor = [NSColor yellowColor];
// label.drawsBackground = YES;
return [label sizeWidthToFit];
}
/// Create input text field with placeholder text. @c 21px height.
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w {
NSTextField *input = [NSTextField textFieldWithString:@""];
[input setFrameSize: NSMakeSize(w, HEIGHT_INPUTFIELD)];
input.alignment = NSTextAlignmentJustified;
input.placeholderString = placeholder;
input.font = [NSFont systemFontOfSize: NSFont.systemFontSize];
input.textColor = [NSColor controlTextColor];
return input;
}
/// Create view with @c NSTextField subviews with right-aligned and row-centered text from @c labels.
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad {
CGFloat w = 0, y = 0;
CGFloat off = (h - HEIGHT_LABEL) / 2;
NSView *parent = [[NSView alloc] init];
for (NSUInteger i = 0; i < labels.count; i++) {
NSTextField *lbl = [[NSView label:labels[i]] placeIn:parent xRight:0 yTop:y + off];
w = Max(w, NSWidth(lbl.frame));
y += h + pad;
}
[parent setFrameSize: NSMakeSize(w, y - pad)];
return parent;
}
#pragma mark - UI: Buttons -
/// Create button. @c 21px height.
+ (NSButton*)button:(NSString*)text {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_BUTTON)];
btn.font = [NSFont systemFontOfSize:NSFont.systemFontSize];
btn.bezelStyle = NSBezelStyleRounded;
btn.title = text;
return [btn sizeWidthToFit];
}
/// Create @c NSBezelStyleSmallSquare image button. @c 25x21px
+ (NSButton*)buttonImageSquare:(nonnull NSImageName)name {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 25, HEIGHT_BUTTON)];
btn.bezelStyle = NSBezelStyleSmallSquare;
btn.image = [NSImage imageNamed:name];
if (!btn.image) btn.title = name; // fallback to text
return btn;
}
/// Create pure image button with no border.
+ (NSButton*)buttonIcon:(nonnull NSImageName)name size:(CGFloat)size {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
btn.bezelStyle = NSBezelStyleRounded;
btn.bordered = NO;
btn.image = [NSImage imageNamed:name];
return btn;
}
/// Create round button with question mark. @c 21x21px
+ (NSButton*)helpButton {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 21, 21)];
btn.bezelStyle = NSBezelStyleHelpButton;
btn.title = @"";
return btn;
}
/// Create gray inline button with rounded corners. @c 16px height.
+ (NSButton*)inlineButton:(NSString*)text {
NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_INLINEBUTTON)];
btn.font = [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightBold];
btn.bezelStyle = NSBezelStyleInline;
btn.controlSize = NSControlSizeSmall;
btn.title = text;
return [btn sizeWidthToFit];
}
/// Create empty drop down button. @c 21px height.
+ (NSPopUpButton*)popupButton:(CGFloat)w {
return [[NSPopUpButton alloc] initWithFrame: NSMakeRect(0, 0, w, HEIGHT_POPUP) pullsDown:NO];
}
#pragma mark - UI: Others -
/// Create @c ImageView with square @c size
+ (NSImageView*)imageView:(nullable NSImageName)name size:(CGFloat)size {
NSImageView *imgView = [[NSImageView alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
if (name) imgView.image = [NSImage imageNamed:name];
return imgView;
}
/// Create checkbox. @c 14px height.
+ (NSButton*)checkbox:(BOOL)flag {
NSButton *check = [NSButton checkboxWithTitle:@"" target:nil action:nil];
check.title = @""; // needed, otherwise will print "Button"
check.frame = NSMakeRect(0, 0, HEIGHT_CHECKBOX, HEIGHT_CHECKBOX);
check.state = (flag? NSControlStateValueOn : NSControlStateValueOff);
return check;
}
/// Create progress spinner. @c 16px size.
+ (NSProgressIndicator*)activitySpinner {
NSProgressIndicator *spin = [[NSProgressIndicator alloc] initWithFrame: NSMakeRect(0, 0, HEIGHT_SPINNER, HEIGHT_SPINNER)];
spin.indeterminate = YES;
spin.displayedWhenStopped = NO;
spin.style = NSProgressIndicatorStyleSpinning;
spin.controlSize = NSControlSizeSmall;
return spin;
}
/// Create grouping view with vertically, left-aligned radio buttons. Action is identical for all buttons (grouping).
+ (nullable NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action {
if (entries.count == 0)
return nil;
CGFloat w = 0, h = 0;
NSView *parent = [[NSView alloc] init];
for (NSUInteger i = entries.count; i > 0; i--) {
NSButton *btn = [NSButton radioButtonWithTitle:entries[i-1] target:target action:action];
btn.tag = (NSInteger)i-1;
if (btn.tag == 0)
btn.state = NSControlStateValueOn;
w = Max(w, NSWidth(btn.frame)); // find max width (before alignmentRect:)
[btn placeIn:parent x:0 y:h];
h += NSHeight([btn alignmentRectForFrame:btn.frame]) + PAD_XS;
}
[parent setFrameSize: NSMakeSize(w, h - PAD_XS)];
return parent;
}
/// Same as @c radioGroup:target:action: but using dummy action to ignore radio button click events.
+ (nullable NSView*)radioGroup:(NSArray<NSString*>*)entries {
return [self radioGroup:entries target:self action:@selector(donothing)];
}
/// Solely used to group radio buttons
+ (void)donothing {}
#pragma mark - UI: Enclosing Container -
/// Create transient popover with initial view controller and view @c size
+ (NSPopover*)popover:(NSSize)size {
NSPopover *pop = [[NSPopover alloc] init];
pop.behavior = NSPopoverBehaviorTransient;
pop.contentViewController = [[NSViewController alloc] init];
pop.contentViewController.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, size.width, size.height)];
return pop;
}
/// Insert @c scrollView, remove @c self from current view and set as @c documentView for the newly created scroll view.
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect {
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
scroll.borderType = NSBezelBorder;
scroll.hasVerticalScroller = YES;
scroll.horizontalScrollElasticity = NSScrollElasticityNone;
[self addSubview:scroll];
if (content.superview) [content removeFromSuperview]; // remove if added already (e.g., helper methods above)
content.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
scroll.documentView = content;
return scroll;
}
/// Create view with @c NSTextField label in front of the view.
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad {
NSView *parent = [[NSView alloc] initWithFrame: NSZeroRect];
NSTextField *label = [NSView label:str];
[label placeIn:parent x:pad yTop:pad];
[other placeIn:parent x:pad + NSWidth(label.frame) yTop:pad];
[parent setFrameSize: NSMakeSize(NSMaxX(other.frame), NSHeight(other.frame) + 2 * pad)];
return parent;
}
#pragma mark - Insert UI elements in parent view -
/**
Set frame origin and insert @c self in @c parent view with @c frameForAlignmentRect:.
You may use @c CENTER to automatically calculate midpoint in parent view.
The @c autoresizingMask will be set accordingly.
*/
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y {
if (x == CENTER) {
x = (NSWidth(parent.frame) - NSWidth(self.frame)) / 2;
self.autoresizingMask |= NSViewMinXMargin | NSViewMaxXMargin;
}
if (y == CENTER) {
y = (NSHeight(parent.frame) - NSHeight(self.frame)) / 2;
self.autoresizingMask |= NSViewMinYMargin | NSViewMaxYMargin;
}
[self setFrameOrigin: NSMakePoint(x, y)];
self.frame = [self frameForAlignmentRect:self.frame];
[parent addSubview:self];
return self;
}
/// Same as @c placeIn:x:y: but measure position from top instead of bottom. Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x yTop:(CGFloat)y {
return [[self placeIn:parent x:x y:NSHeight(parent.frame) - NSHeight(self.frame) - y] alignTop];
}
/// Same as @c placeIn:x:y: but measure position from right instead of left. Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y {
return [[self placeIn:parent x:NSWidth(parent.frame) - NSWidth(self.frame) - x y:y] alignRight];
}
/// Set origin by measuring from top right (@c CENTER is not allowed here). Also sets @c autoresizingMask.
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y {
[self setFrameOrigin: NSMakePoint(NSWidth(parent.frame) - NSWidth(self.frame) - x,
NSHeight(parent.frame) - NSHeight(self.frame) - y)];
self.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
self.frame = [self frameForAlignmentRect:self.frame];
[parent addSubview:self];
return self;
}
#pragma mark - Modify existing UI elements -
// Aligned Frame Origins
// pad - view.alignmentRectInsets.left;
// pad - view.alignmentRectInsets.bottom;
// NSWidth(view.superview.frame) - NSWidth(view.frame) - pad + view.alignmentRectInsets.right;
// NSHeight(view.superview.frame) - NSHeight(view.frame) - pad + view.alignmentRectInsets.top;
/// Modify @c .autoresizingMask; Clear @c NSViewMaxYMargin flag and set @c NSViewMinYMargin
- (instancetype)alignTop { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxYMargin) | NSViewMinYMargin; return self; }
/// Modify @c .autoresizingMask; Clear @c NSViewMaxXMargin flag and set @c NSViewMinXMargin
- (instancetype)alignRight { self.autoresizingMask = (self.autoresizingMask & ~NSViewMaxXMargin) | NSViewMinXMargin; return self; }
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable @c | @c NSViewHeightSizable flags
- (instancetype)sizableWidthAndHeight { self.autoresizingMask |= NSViewWidthSizable | NSViewHeightSizable; return self; }
/// Extend frame in its @c superview and stick to right with padding. Adds @c NSViewWidthSizable to @c autoresizingMask
- (instancetype)sizeToRight:(CGFloat)rightPadding {
SetFrameWidth(self, NSWidth(self.superview.frame) - NSMinX(self.frame) - rightPadding + self.alignmentRectInsets.right);
self.autoresizingMask |= NSViewWidthSizable;
return self;
}
/// Set @c width to @c fittingSize.width but keep original height.
- (instancetype)sizeWidthToFit {
SetFrameWidth(self, self.fittingSize.width);
return self;
}
/// Set @c tooltip and @c accessibilityTitle of view and return self
- (instancetype)tooltip:(NSString*)tt {
self.toolTip = tt;
if (self.accessibilityLabel.length == 0)
self.accessibilityLabel = tt;
else
self.accessibilityValueDescription = tt;
return self;
}
/// Helper method to set frame width and keep same height
static inline void SetFrameWidth(NSView *view, CGFloat w) {
[view setFrameSize: NSMakeSize(w, NSHeight(view.frame))];
}
#pragma mark - Debugging -
/// Set background color on @c .layer
- (instancetype)colorLayer:(NSColor*)color {
self.layer = [CALayer layer];
self.layer.backgroundColor = color.CGColor;
return self;
}
+ (NSView*)redCube:(CGFloat)size {
return [[[NSView alloc] initWithFrame: NSMakeRect(0, 0, size, size)] colorLayer:NSColor.redColor];
}
@end
#pragma mark - NSControl specific -
@implementation NSControl (Ext)
/// Set @c target and @c action simultaneously
- (instancetype)action:(SEL)selector target:(nullable id)target {
self.action = selector;
self.target = target;
return self;
}
/// Set system font with current @c pointSize @c + @c 2. A label will be @c 19px height.
- (instancetype)large { SetFontAndResize(self, [NSFont systemFontOfSize: self.font.pointSize + 2]); return self; }
/// Set system font with @c smallSystemFontSize and perform @c sizeToFit. A label will be @c 14px height.
- (instancetype)small { SetFontAndResize(self, [NSFont systemFontOfSize: NSFont.smallSystemFontSize]); return self; }
/// Set monospaced font with @c labelFontSize regular and perform @c sizeToFit. A label will be @c 13px height.
- (instancetype)tiny { SetFontAndResize(self, [NSFont monospacedDigitSystemFontOfSize: NSFont.labelFontSize weight: NSFontWeightRegular]); return self; }
/// Set system bold font with current @c pointSize
- (instancetype)bold { SetFontAndResize(self, [NSFont boldSystemFontOfSize: self.font.pointSize]); return self; }
/// Set @c .alignment to @c NSTextAlignmentRight
- (instancetype)textRight { self.alignment = NSTextAlignmentRight; return self; }
/// Set @c .alignment to @c NSTextAlignmentCenter
- (instancetype)textCenter { self.alignment = NSTextAlignmentCenter; return self; }
/// Helper method to set new font, subsequently run @c sizeToFit
static inline void SetFontAndResize(NSControl *control, NSFont *font) {
control.font = font; [control sizeToFit];
}
@end
@implementation NSTextField (Ext)
/// Set text color to @c systemGrayColor
- (instancetype)gray { self.textColor = [NSColor systemGrayColor]; return self; }
/// Set @c .selectable to @c YES
- (instancetype)selectable { self.selectable = YES; return self; }
/// Set @c .maximumNumberOfLines @c = @c 7 and @c preferredMaxLayoutWidth.
- (instancetype)multiline:(NSSize)size {
[self setFrameSize:size];
self.preferredMaxLayoutWidth = size.width;
self.lineBreakMode = NSLineBreakByWordWrapping;
self.usesSingleLineMode = NO;
self.maximumNumberOfLines = 7; // used in ModalFeedEditView
return self;
}
@end