How do I make NSTextView grow with text? - objective-c

I am creating a simple word processor where it's possible for the user to add a text box to an NSView, similar to the function in Pages. The problem is that when it's added it will stay the same size no matter how much text the user inputs. I want it to grow as the user inputs text, I have tried with this GitHub project but when I use it the text field only expands when I have deleted all the text, as if the code doesn't react before the textDidEndEditing method. After working a bit with NSTextView I found that it would be more suitable for the task, but I still can't make it work. I'm running Mavericks and Xcode 5.0.1.
Hope someone can help me!

The following uses an NSTextView subclass that must be created in code. For reasons of its own Xcode won't allow you to instantiate an NSTextView in a nib without an enclosing NSScrollView instance.
This class lets the only defines the text view intrinsic height - the width is left undefined which allows the view to grow with its enclosing view. I used this in an NSStackView and it seemed to work well. Trying to bludgeon NSTextField so that it could wrap multiline text, edit and support Auto Layout was too messy.
Note we have support for a focus ring as I wanted my class to act like and Uber text field. Also note that we have no support for a border. In my actual usage I create a compound view that wraps the custom text view. This wrapper view draws a border as required.
#interface BPTextViewField : NSTextView
// primitives
#property (assign, nonatomic) CGFloat borderOffsetX;
#property (assign, nonatomic) CGFloat borderOffsetY;
#end
#implementation BPTextViewField
#pragma mark -
#pragma mark Life cycle
- (instancetype)initWithFrame:(NSRect)frameRect textContainer:(nullable NSTextContainer *)container
{
self = [super initWithFrame:frameRect textContainer:container];
if (self) {
[self commonInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
_borderOffsetX = 1;
_borderOffsetY = 3;
self.usesFontPanel = NO;
self.usesFindPanel = NO;
}
#pragma mark -
#pragma mark Auto layout
- (NSSize)intrinsicContentSize
{
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
NSSize size = [layoutManager usedRectForTextContainer: textContainer].size;
return NSMakeSize(NSViewNoIntrinsicMetric, size.height);
}
#pragma mark -
#pragma mark Accessors
- (void)setString:(NSString *)string
{
[super setString:string];
[self invalidateIntrinsicContentSize];
}
#pragma mark -
#pragma mark Text change notifications
- (void)didChangeText
{
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
#pragma mark -
#pragma mark Drawing
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect];
}
#pragma mark -
#pragma mark Focus ring
- (void)drawFocusRingMask
{
if (self.editable) {
NSRectFill(self.focusRingMaskBounds);
}
}
- (NSRect)focusRingMaskBounds {
NSRect r = [self bounds];
return NSMakeRect(r.origin.x - self.borderOffsetX, r.origin.y - self.borderOffsetY, r.size.width + self.borderOffsetX * 2, r.size.height + self.borderOffsetY * 2);
}
#end

This example fixed it for me: https://developer.apple.com/library/mac/documentation/cocoa/conceptual/TextUILayer/Tasks/TextInScrollView.html
You have to put it into a NSScrollView. The behavior is not the same as in UITextView (if you have a iOS background).

Related

Custom selection style for view based "Source List" NSOutlineView

I'm using a view based NSOutlineView that has it's selectionHighlightStyle set to NSTableViewSelectionHighlightStyleSourceList.
I want to overwrite the selection style (background) for certain rows and draw a different color/gradient.
What I tried so far is creating a custom NSTableRowView and returning it via outlineView:rowViewForItem:.
I verified that my custom row views are created and returned by the outline view delegate.
However, none of the methods I'm overwriting in the custom row view are being called.
I tried to overwrite drawBackgroundInRect:, drawSelectionInRect:, drawSeparatorInRect: and even drawRect:. None of those are called, ever.
I'm suspecting the outline view to be doing some custom "magic" when it's set to the source list style, but I've not found anything in the documentation that indicates that a custom NSTableRowView wouldn't be honored at all in this case.
AppKit adds separate NSVisualEffectView with custom material to row view for drawing background when using NSTableViewSelectionHighlightStyleSourceList. I've come up with the following workaround which uses zero private APIs, but can break later if Apple implements some other way of highlighting rows.
#class CustomHighlightRowSelectionView;
#interface CustomHighlightRowView : NSTableRowView
#property (nonatomic, strong) CustomHighlightRowSelectionView *selectionView;
#end
#interface CustomHighlightRowSelectionView : NSView
#property (nonatomic, getter=isEmphasized) BOOL emphasized;
#property (nonatomic, getter=isSelected) BOOL selected;
#end
#implementation CustomHighlightRowView
- (CustomHighlightRowSelectionView *)selectionView
{
if (!_selectionView)
{
_selectionView = [[CustomHighlightRowSelectionView alloc] initWithFrame:NSZeroRect];
}
return _selectionView;
}
- (void)setEmphasized:(BOOL)emphasized
{
[super setEmphasized:emphasized];
self.selectionView.emphasized = emphasized;
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
self.selectionView.selected = selected;
}
- (void)addSubview:(NSView *)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView *)otherView
{
if (![aView isKindOfClass:[NSVisualEffectView class]])
{
[super addSubview:aView positioned:place relativeTo:otherView];
}
else
{
if (!self.selectionView.superview)
{
[super addSubview:self.selectionView positioned:place relativeTo:otherView];
self.selectionView.frame = self.bounds;
}
}
}
- (void)setFrame:(NSRect)frame
{
[super setFrame:frame];
self.selectionView.frame = self.bounds;
}
- (void)setBounds:(NSRect)bounds
{
[super setBounds:bounds];
self.selectionView.frame = self.bounds;
}
#end
#implementation CustomHighlightRowSelectionView
- (void)setEmphasized:(BOOL)emphasized
{
_emphasized = emphasized;
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)selected
{
_selected = selected;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
if (!self.selected)
{
return;
}
NSColor *fillColor = self.emphasized ? [NSColor alternateSelectedControlColor] : [NSColor secondarySelectedControlColor];
[fillColor setFill];
NSRectFill(dirtyRect);
}
#end
Are you using Yosemite?
From Apple's document Adopting Advanced Features of the new UI in Yosemite
When selectionHighlightStyle ==
NSTableViewSelectionHighlightStyleSourceList • Selection is now a
special blue material that does behind window blending
- The material size and drawing can not be customized
If you set it to NSTableViewSelectionHighlightStyleRegular and override the drawRect, it should work.
You'll need to overwrite -selectionHighlightStyle in your NSTableRowView subclass:
- (NSTableViewSelectionHighlightStyle)selectionHighlightStyle
{
return NSTableViewSelectionHighlightStyleRegular;
}
That way, the table view can be used in source list style but with a customized row selection. I wanted to have the source list under Yosemite in my project but with the user-selected color from the System Preferences.
Edit: I just noticed doing it this way causes text fields and image views inside the cell view to have an artifact like border looking very odd and ugly.

How do I get NSTextFinder to show up

I have a mac cocoa app with a webview that contains some text. I would like to search through that text using the default find bar provided by NSTextFinder. As easy as this may seem reading through the NSTextFinder class reference, I cannot get the find bar to show up. What am I missing?
As a sidenote:
- Yes, I tried setting findBarContainer to a different view, same thing. I reverted back to the scroll view to eliminate complexity in debugging
- performTextFinderAction is called to perform the find operation
**App Delegate:**
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.textFinderController = [[NSTextFinder alloc] init];
self.webView = [[STEWebView alloc] initWithFrame:CGRectMake(0, 0, self.window.frame.size.width, 200)];
[[self.window contentView] addSubview:self.webView];
[self.textFinderController setClient:self.webView];
[self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
[[self.webView mainFrame] loadHTMLString:#"sample string" baseURL:NULL];
}
- (IBAction)performTextFinderAction:(id)sender {
[self.textFinderController performAction:[sender tag]];
}
**STEWebView**
#interface STEWebView : WebView <NSTextFinderClient>
#end
#implementation STEWebView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (NSUInteger) stringLength {
return [[self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"] length];
}
- (NSString *)string {
return [self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"];
}
In my tests, WebView.enclosingScrollView was null.
// [self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
NSLog(#"%#", self.webView.enclosingScrollView);
Using the following category on NSView, it is possible to find the nested subview that extends NSScrollView, and set that as the container, allowing the NSTextFinder to display beautifully within a WebView
#interface NSView (ScrollView)
- (NSScrollView *) scrollView;
#end
#implementation NSView (ScrollView)
- (NSScrollView *) scrollView {
if ([self isKindOfClass:[NSScrollView class]]) {
return (NSScrollView *)self;
}
if ([self.subviews count] == 0) {
return nil;
}
for (NSView *subview in self.subviews) {
NSView *scrollView = [subview scrollView];
if (scrollView != nil) {
return (NSScrollView *)scrollView;
}
}
return nil;
}
#end
And in your applicationDidFinishLaunching:aNotification:
[self.textFinderController setFindBarContainer:[self scrollView]];
To get the Find Bar to appear (as opposed to the default Find Panel), you simply have to use the setUsesFindBar: method.
In your case, you'll want to do (in your applicationDidFinishLaunching:aNotification method):
[textFinderController setUsesFindBar:YES];
//Optionally, incremental searching is a nice feature
[textFinderController setIncrementalSearchingEnabled:YES];
Finally got this to show up.
First set your NSTextFinder instances' client to a class implementing the <NSTextFinderClient> protocol:
self.textFinder.client = self.textFinderController;
Next, make sure your NSTextFinder has a findBarContainer set to the webView category described by Michael Robinson, or get the scrollview within the webView yourself:
self.textFinder.findBarContainer = [self.webView scrollView];
Set the find bar position above the content (or wherever you wish):
[self.webView scrollView].findBarPosition = NSScrollViewFindBarPositionAboveContent;
Finally, tell it to show up:
[self.textFinder performAction:NSTextFinderActionShowFindInterface];
It should show up in your webView:
Also, not sure if it makes a difference, but I have the NSTextFinder in the XIB, with a referencing outlet:
#property (strong) IBOutlet NSTextFinder *textFinder;
You may also be able to get it by simply initing it like normal: self.textFinder = [[NSTextFinder alloc] init];

Why isn't my subclassed UItableView scrolling?

I'm writing an iOS 5 app (in Xcode 4.3, using Storyboards and ARC) that has some table cells that need to respond to horizontal pans. I had a table setup that worked really well but then I needed to implement the same behavior on another scene. I figured the best-practices way would be to abstract out the gesture-recognizing and -handling code into subclasses. But now the tableView won't scroll, and the solution I had for this problem under the old method doesn't help.
I have a RestaurantViewController which inherits from UIViewController and has a property ULPanningTableView *tableView. Some of the table's cells are MenuItemCells and inherit from ULPanningTableViewCell. The table's delegate and data source are the RestaurantViewController.
ULPanningTableViewCell inherits from UITableViewCell and is pretty close to the original, the only difference being that it has properties to keep track of the cell's front and back views, and the custom backgrounds.
ULPanningTableView is a bit more complicated, since it has to set up the recognition and handling.
ULPanningTableView.h:
#import <UIKit/UIKit.h>
#interface ULPanningTableView : UITableView <UIGestureRecognizerDelegate>
#property (nonatomic) float openCellLastTX;
#property (nonatomic, strong) NSIndexPath *openCellIndexPath;
- (id)dequeueReusablePanningCellWithIdentifier:(NSString *)identifier;
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer;
// ... some helpers for handlePan:
#end
and ULPanningTableView.m:
#import "ULPanningTableView.h"
#import "ULPanningTableViewCell.h"
#implementation ULPanningTableView
#synthesize openCellIndexPath=_openCellIndexPath, openCellLastTX=_openCellLastTX;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
#pragma mark - Table View Helpers
- (id)dequeueReusablePanningCellWithIdentifier:(NSString *)identifier
{
ULPanningTableViewCell *cell = (ULPanningTableViewCell *)[self dequeueReusableCellWithIdentifier:identifier];
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[panGestureRecognizer setDelegate:self];
[cell addGestureRecognizer:panGestureRecognizer];
return cell;
}
#pragma mark - UIGestureRecognizerDelegate protocol
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// for testing: only allow UIScrollViewPanGestureRecognizers to begin
NSString *gr = NSStringFromClass([gestureRecognizer class]);
if ([gr isEqualToString:#"UIScrollViewPanGestureRecognizer"]) {
return YES;
} else {
return NO;
}
}
#pragma mark - panhandling
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer
{
// ...
}
// ... some helpers for handlePan:
#end
I've played around with gestureRecognizerShouldBegin:, because that was how I solved this problem back when these weren't separate classes (ULPanningTableView stuff was implemented inside RestaurantViewController and ULPanningTableViewCell was stuff was implemented in MenuItemCell. I would essentially return NO for gestures where the translationInView was more vertical than horizontal). Anyway, I can't get the table to scroll! I can get the pan gestures to be recognized if I return YES from gestureRecognizerShouldBegin:, or if I remove the UIGestureRecognizerDelegate implementation entirely.
I'm still a beginner in iOS, and in Objective-C, so I only have hunches based on things I've read, and I'm under the impression from a similar problem that the culprit is UIScrollViewPanGestureRecognizer doing voodoo with the responder chain...
I would greatly appreciate any light you can shed on this!
Ok, so I feel really silly. -handlePan: is already a method! I changed it to -handleCustomPan: and it will handle other pans normally. I'm not sure why it wasn't crashing, but there it is. Oh, and I had to keep the UIScrollViewPanGestureRecognizer edge case in -gestureRecognizerDidBegin::
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
NSString *gr = NSStringFromClass([gestureRecognizer class]);
if ([gr isEqualToString:#"UIScrollViewPanGestureRecognizer"]) {
// allow scroll to begin
return YES;
} else if ([gr isEqualToString:#"UIPanGestureRecognizer"]){
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *)gestureRecognizer;
// allow horizontal pans to begin
ULPanningTableViewCell *cell = (ULPanningTableViewCell *)[panGR view];
CGPoint translation = [panGR translationInView:[cell superview]];
BOOL should = (fabs(translation.x) / fabs(translation.y) > 1) ? YES : NO;
if (!should) {
[self closeOpenCellAnimated:YES];
}
return should;
} else {
NSLog(#"%#",gestureRecognizer);
return NO;
}
}

NSControl flickers on NSView attached to a NSView in full screen mode

The setup:
My application has background views that are set into full screen mode using enterFullScreenMode:withOptions:.
To these background views I move a content view as a subview using removeFromSuperview and addSubview:.
Further, there is a preferences view that I add to the content view as described before.
The preferences view comes from a XIB and contains NSControls like checkboxes, circular slides, combo box, ...
The whole setup is much more complicated which makes it very difficult to post code here. Nevertheless, I will add some parts if you request them.
The problem:
When I select any of the controls on the preferences view the elements flicker. That means the checkbox for example disappears and reappears.
I already tried to solve the problem running setNeedsDisplay:, setNeedsLayout or makeKeyAndOrderFront: in the viewDidMoveToSuperview method of the view. Though, nothing helped. What can I do?
Edit 1:
I took a screen capture of the application so you can see what happens.
Edit 2:
Here is the PreferencesViewController.h:
// PreferencesViewController.h
#import <Cocoa/Cocoa.h>
#import "SlideShowModelProtocol.h"
#import "DisplayInfoController.h"
#interface PreferencesViewController : NSViewController {
id<SlideShowModelProtocol> m_localModel;
DisplayInfoController* m_displayInfoController;
}
#property (readonly) id<SlideShowModelProtocol> localModel;
#property (readwrite, assign) IBOutlet DisplayInfoController* displayInfoController;
- (void)moveViewToSuperview:(NSView*)superview;
- (void)showOnView:(NSView*)superview;
- (void)removeViewFromSuperview;
- (BOOL)viewHasSuperview;
- (void)updateModelSettings;
- (IBAction)cancelView:(id)sender;
- (IBAction)confirmView:(id)sender;
#end
... and the implementation PreferencesViewController.m:
// PreferencesViewController.m
#import "PreferencesViewController.h"
#import "ApplicationController.h"
#import "DisplayInfo.h"
#interface PreferencesViewController()
- (void)centerViewOnSuperview;
- (void)loadModel;
#end
#implementation PreferencesViewController
- (id)init {
self = [super initWithNibName:#"PreferencesView" bundle:nil];
if (self != nil) {
m_localModel = nil;
m_displayInfoController = nil; // Assigned in Interface Builder.
}
return self;
}
#synthesize localModel = m_localModel;
#synthesize displayInfoController = m_displayInfoController;
- (void)loadModel {
[self willChangeValueForKey:#"localModel"];
// Retrieve deep copy of the model.
m_localModel = [[[ApplicationController sharedController] model] copyWithZone:nil];
// Reset the table view selection as saved in the model.
NSIndexSet* selectionIndices = [NSIndexSet indexSetWithIndex:[[m_localModel selectedScreenIndex] unsignedIntegerValue]];
[[m_displayInfoController displayInfoTableView] selectRowIndexes:selectionIndices byExtendingSelection:NO];
[self didChangeValueForKey:#"localModel"];
}
/**
Moves and positions the view on the given superview (aka another screen).
#param superview A superview.
*/
- (void)showOnView:(NSView*)superview {
[self moveViewToSuperview:superview];
[self centerViewOnSuperview];
}
/**
Moves the view on the given superview.
#param superview A superview (In full screen mode a background view).
*/
- (void)moveViewToSuperview:(NSView*)superview {
if ([[self view] superview] == superview) {
return;
}
[self loadModel];
[[self view] removeFromSuperview];
[superview addSubview:[self view]];
}
- (void)removeViewFromSuperview {
[[self view] removeFromSuperview];
}
- (void)centerViewOnSuperview {
NSRect superviewFrame = self.view.superview.frame;
NSRect viewFrame = self.view.frame;
float viewFrameWidth = viewFrame.size.width;
float viewFrameHeight = viewFrame.size.height;
float xPos = 0.5f * superviewFrame.size.width - 0.5f * viewFrameWidth;
float yPos = 0.5f * superviewFrame.size.height - 0.5f * viewFrameHeight;
NSRect frame = NSMakeRect(xPos, yPos, viewFrameWidth, viewFrameHeight);
[self.view setFrame:frame];
}
- (BOOL)viewHasSuperview {
return [[self view] superview] != nil;
}
- (void)updateModelSettings {
id<SlideShowModelProtocol> globalModel = [[ApplicationController sharedController] model];
[globalModel setFadeDuration:[m_localModel fadeDuration]];
[globalModel setStayDuration:[m_localModel stayDuration]];
[globalModel setStartWithFirst:[m_localModel startWithFirst]];
[globalModel setSortingMode:[m_localModel sortingMode]];
[globalModel setAnimationIsActive:[m_localModel animationIsActive]];
[globalModel setSelectedScreenIndex:[m_localModel selectedScreenIndex]];
[globalModel setPanAmount:[m_localModel panAmount]];
[globalModel setZoomAmount:[m_localModel zoomAmount]];
[globalModel setZoomFactor:[m_localModel zoomFactor]];
}
#pragma mark -
#pragma mark UserInterface
- (IBAction)cancelView:(id)sender {
[[ApplicationController sharedController] hidePreferencesViewModelSettingsUpdated:NO];
}
- (IBAction)confirmView:(id)sender {
ApplicationController* applicationController = [ApplicationController sharedController];
if ([[applicationController model] isEqualToModel:m_localModel]) {
[self cancelView:sender];
return;
}
[self updateModelSettings];
[applicationController hidePreferencesViewModelSettingsUpdated:YES];
}
#end
As already told in the comments to my post I created a new PreferencesViewTest.xib and added the GUI controls step by step. I also added the bindings, controllers and so on. Everything worked fine - no flickering. Then I assigned RETURN as the key equivalent for the OK button in Interface Builder. I started the application once again to check the behavior .... it flickers! I double checked the phenomenon with the former PreferencesView.xib by removing the key equivalent from the OK button .... it does not flicker! - Crazy shit. But why..?

Hot to create custom NSSlider like "Start screen saver:" slider in System Preferences

How to create custom NSSlider that is working exactly as slider in System Preferences -> Desktop & Screen Saver -> Screen Saver -> Start screen saver: ?
I tried to subclass NSSliderCell with overridden continueTracking: but it don't work as expected.
I played around for a bit and at least got off to a pretty good start with an NSSliderCell subclass.
MDSliderCell.h:
#import <Cocoa/Cocoa.h>
#interface MDSliderCell : NSSliderCell {
BOOL tracking;
}
#end
MDSliderCell.m:
#import "MDSliderCell.h"
#implementation MDSliderCell
- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView {
if ([self numberOfTickMarks] > 0) tracking = YES;
return [super startTrackingAt:startPoint inView:controlView];
}
#define MD_SNAPPING 10.0
- (BOOL)continueTracking:(NSPoint)lastPoint at:(NSPoint)currentPoint
inView:(NSView *)controlView {
if (tracking) {
NSUInteger count = [self numberOfTickMarks];
for (NSUInteger i = 0; i < count; i++) {
NSRect tickMarkRect = [self rectOfTickMarkAtIndex:i];
if (ABS(tickMarkRect.origin.x - currentPoint.x) <= MD_SNAPPING) {
[self setAllowsTickMarkValuesOnly:YES];
} else if (ABS(tickMarkRect.origin.x - currentPoint.x) >= MD_SNAPPING &&
ABS(tickMarkRect.origin.x - currentPoint.x) <= MD_SNAPPING *2) {
[self setAllowsTickMarkValuesOnly:NO];
}
}
}
return [super continueTracking:lastPoint at:currentPoint inView:controlView];
}
- (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint
inView:(NSView *)controlView mouseIsUp:(BOOL)flag {
[super stopTracking:lastPoint at:stopPoint inView:controlView mouseIsUp:flag];
}
#end
Basically, during the -continueTracking:at:inView:, it checks to see how close it is to a tick mark, and if it's close enough, it turns on the option to only allow tick mark values. That causes it to snap to the tick mark, then once you get far enough away, you turn the "tick mark-only" option off until you get close enough to another tick mark.