Sequential selection among multiple NSTextViews - objective-c

I have a bunch of NSTextViews that I would like to share a single selection. I basically want this to behave like selecting text on a web page, where there are multiple text views but you can drag to sequentially select text among them.
I found this document which states that it is possible to have multiple NSTextContainer objects sharing a single NSLayoutManager and thus share the selection. This is halfway to what I want, except for the fact that one NSLayoutManager can only have a single NSTextStorage object. I want each text view to have its own NSTextStorage so that each text view can have its own text, but I still want to be able to select text in multiple text views with one drag. Is this possible?

There's no easy way to solve this problem (as I tried to find by asking this question). It involves all the mouse event handling and text selection calculations you'd expect, so I wrote the code and have open sourced it as INDSequentialTextSelectionManager.

To make this separate text containers work, you would calculate the drawn size of each part of the string and limit the NSTextView to that size:
NSLayoutManager * layout = [[NSLayoutManager alloc] init];
NSString * storedString = #"A\nquick\nBrown\nFox";
NSTextStorage * storage = [[NSTextStorage alloc] initWithString:storedString];
[storage addLayoutManager:layout];
//I assume you have a parent view to add the text views
NSView * view;
//Assuming you want to split up into separate view by line break
NSArray * paragraphs = [storedString componentsSeparatedByString:#"\n"];
for (NSString * paragraph in paragraphs)
{
NSSize paragraphSize = [paragraph sizeWithAttributes:#{}];
//Create a text container only big enough for the string to be displayed by the text view
NSTextContainer * paragraphContainer = [[NSTextContainer alloc] initWithContainerSize:paragraphSize];
[layout addTextContainer:paragraphContainer];
//Use autolayout or calculate size/placement as you go along
NSRect lazyRectWithoutSizeOrPlacement = NSMakeRect(0, 0, 0, 0);
NSTextView * textView = [[NSTextView alloc] initWithFrame:lazyRectWithoutSizeOrPlacement
textContainer:paragraphContainer];
[view addSubview:textView];
}
You can add a delegate to the NSLayoutManager to watch your text container usage:
- (void)layoutManager:(NSLayoutManager *)aLayoutManager
didCompleteLayoutForTextContainer:(NSTextContainer *)aTextContainer
atEnd:(BOOL)flag
{
if (aTextContainer == nil)
{
//All text was unable to be displayed in existing containers. A new NSTextContainer is needed.
}
}

Related

Programmatically drawn UIView tags (or something alike)

I'm drawing a view that contains an xib, multiple times (same uiview) and then update the outlets each time it gets drawn displaying an nsmutablearray with different strings on the outlets
Clicking the view opens a viewcontroller that should be displaying the data from the view (+more), this works perfectly fine except it doesn't know which index of the array it's supposed to be displaying.
I'm trying to figure out a way to manage them in a way that I can see which view is being pressed so I can pass that 'id' onto the new vc (something to uniquely identify the view even though it's the same view being drawn)
Here's how the view is being drawn multiple times
- (void) populateUpcoming:(int)events {
[self resetVariables];
upcomingEventsCenterPos = self.view.frame.size.width / 2 - 159;
for (int i = 0; i < events; i++) {
upcomingEventsY2 = 175 * upcomingEvents2;
UIView *firstViewUIView = [[[NSBundle mainBundle] loadNibNamed:#"UpcomingEventFull" owner:self options:nil] firstObject];
[_scrollView addSubview:firstViewUIView];
CGRect frame = firstViewUIView.frame;
frame.origin.y = 9 + upcomingEventsY2;
frame.origin.x = upcomingEventsCenterPos + upcomingEventsX2;
firstViewUIView.frame= frame;
upcomingEvents2++;
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapUpcomingEvent:)];
[firstViewUIView addGestureRecognizer:singleFingerTap];
}
[self setupScroll];
}
I think you can use UITableView and custom UITableViewCell for this purpose. On click of "Add More", add one entry to NSMutableArray and just reload tableview.
And no need to manage index separately, you can get it directly by indexPath.row
Also you don't need to manage scroll view size as tableview automatically adjust is content height according to row.
If you are using same kind of view,then go with this approach, as its easy and will save your time.

iOS Segmented Control Set Segments After Creation

In a custom uitableviewcell, the segmented control is created when the row is created. Each row is a question in a form. Depending on the type of question, the segmented control can have different answers. In the UISegmentedControl reference it only lists the init method as allowing setting all segments at once. Is there a better way than using remove and insert to update the segment to have the pertinent segments?
In the custom UITableView cell's init it has
_answerSegmented = [[UISegmentedControl alloc] init];
[_answerSegmented addTarget:self action:#selector(answerChanged:) forControlEvents:UIControlEventValueChanged];
_answerSegmented.backgroundColor = [UIColor columnHeaderBackground];
[self addSubview:_answerSegmented];
It's not until later that it knows what the segments should be
NSMutableArray *answers = [NSMutableArray arrayWithObjects:#"IN", #"OUT", nil];
if (_question.noItem.boolValue) {
[answers addObject:#"N/O"];
}
if (_question.naItem.boolValue) {
[answers addObject:#"N/A"];
}
_answerSegmented.segments = answers; // <---- this line gives a compile error
No, there is no better way. If all segmented controls have the same number of segments then create the control with that number of segments. Then simply use setTitle:forSegmentAtIndex: to change the title of each segment, one at a time.
If the number can change, use removeAllSegments and insertSegmentWithTitle:atIndex:animated:.
Or simply create a new segmented control and remove the old one each time.

Which one of the two NSTextViews has been edited? doCommandBySelector is always returns the first one

I'm desperate to find the answer, so I opened TextLayoutDemo sample project from Apple. The point is that: I have two NSTextViews for column view. Everything works fine, text I enter is successfully laying out in those two text views via single layout manager:
NSLayoutManager *twoColumnLayoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *firstColumnTextContainer = [[NSTextContainer alloc] init];
NSTextContainer *secondColumnTextContainer = [[NSTextContainer alloc] init];
NSTextView *firstColumnTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 240, 360) textContainer:firstColumnTextContainer];
firstColumnTextView.delegate = self;
NSTextView *secondColumnTextView = [[NSTextView alloc] initWithFrame:NSMakeRect(240, 0, 240, 360) textContainer:secondColumnTextContainer];
secondColumnTextView.delegate = self;
[firstColumnTextContainer setContainerSize:NSMakeSize(240, 360)];
[secondColumnTextContainer setContainerSize:NSMakeSize(240, 360)];
[twoColumnLayoutManager addTextContainer:firstColumnTextContainer];
[twoColumnLayoutManager addTextContainer:secondColumnTextContainer];
[twoColumnLayoutManager replaceTextStorage:[firstTextView textStorage]];
[[secondWindow contentView] addSubview:firstColumnTextView];
[[secondWindow contentView] addSubview:secondColumnTextView];
But my goal is to get to know in which one text views the user edits a text. If it's the left one, I need to call one method, but if it's the right one, I want to call another method. And it seems impossible to recognize the correct text view, because delegate always get notified by the first text view.
- (BOOL) textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
NSLog(#"edit: %#", textView);
return NO;
}
This method is always prints the first text view, even if I change text in the second one. And I see it's going according to docs, where Apple says there always will be just the first NSTextView in series.
But how can I solve my problem then?
Just tell me, if this solution is the one I am looking for. Because, in fact, it works just fine. The one thing I don't understand is why Cocoa text system is so tricky where it is not necessary?
- (void)textStorageDidProcessEditing:(NSNotification *)aNotification {
// that's the active text view
NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
NSTextView *activeTextView = (NSTextView *)[keyWindow firstResponder];
NSLog(#"%p", activeTextView);
}
UPDATE: this works only if user clicked the mouse button. Arrow keys do not update window's first responder:(

UITextField -- Adding UIView for leftView - Can't get Focus

The UITextFields in my app have placeholder text defined (in Interface Builder), and I cannot cause these fields to acquire focus (i.e. show the keyboard and allow editing) when I tap on the area occupied by the placeholder text. If I tap on the textfields in an area just outside the that of placeholder text (though still within the bounds of the textfiled itself), it acts as normal (i.e. the keyboard pops up and I can edit the content of the textfield). How can I fix this?
Thanks.
EDIT 1
Ok, I think I've got it. I'm also setting a blank view to the "leftView" property of these UITextFields. If I remove this, you can touch the UITextFields in the area of the placeholder text and it reacts as expected; I need this view for the leftView though. If you change the background color of this spacer view to red, you can see that it doesn't get in the way at all, so I don't know what's going wrong.
Why does this code cause this problem?
Thanks.
+(UIView*)getTextFieldLeftSpacerViewWithBackgroundColor:(UIColor*)backgroundColor andHeight:(CGFloat)height
{
UIView *leftWrapper = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 8.0f, height)];
leftWrapper.autoresizingMask = UIViewAutoresizingNone;
[leftWrapper setOpaque:YES];
if(backgroundColor){leftWrapper.backgroundColor = backgroundColor;}
else{leftWrapper.backgroundColor = [UIColor clearColor];}
return [leftWrapper autorelease];
}
+(void)setTextFieldLeftSpacerForTextFieled:(UITextField*)textField
{
if(textField)
{
UIView *spacer = [MYViewController getTextFieldLeftSpacerViewWithBackgroundColor:nil andHeight:textField.bounds.size.height];
textField.leftView = spacer;
textField.leftViewMode = UITextFieldViewModeAlways;
}
}
Just ran into the same problem and didn't want to subclass, just had to use :
leftWrapper.userInteractionEnabled = NO;
I abandoned this approach. Instead of using an invisible view to offset the text, I opted to subclass UITextField and provide offset CGRects for the bounds of the text within theUITextField. The following SO post was very helpful:
Indent the text in a UITextField

IOS: fill a NSArray with UIImageView

In my project I created 30 small UIImageView where inside I put a different background. This is my group of UIImageView without background
and this is the group of UIImageView with different background:
I put these group of UImageView inside a UITableViewCell and I use them to define a timeline, but it's not important.
I declared in .h these UIImageView but now I must insert theme in an NSArray, or NSMutableArray? Because in my code (depending on the case) I must set different background for each UIImageView. How can I organize them in my code?
You don't want to put them all in an array. What you actually want to do is assign 'tags' to each view. Then, you can use -[UIView viewWithTag:(int)]; to get a pointer to the image view you want to 'talk' to at the time. Keep in mind not to use negative numbers or 0 when tagging your views.
Although I would suggest drawing your timeline view using a single UIView, dynamically creating and storing views in an array is simple:
NSMutableArray *views = [NSMutableArray array];
NSUInteger viewCount = 30;
NSUInteger index;
for (index = 0; index < viewCount; index++) {
UIImageView *newView = [[[UIImageView alloc] initWithImage:anImage] autorelease];
[newView setFrame:CGRectMake(index * width, 0.0, width, height);
[[self someView] addSubview:newView];
[views addObject:newView];
}
Remember to retain or copy views at some point.
Then, to modify a view:
[[views objectAtIndex:8] setImage:anotherImage];