interpreting text storage of a NSTextView

Hi Community,
I am using a NSTextView as a text field in an application I am making. I allow the user to use an NSColorWheel to change the color of the text. When I use the text to interact with different applications, I am going to need to know which characters are a different color, and what color that is. Obviously I am going to need to use textStorage(), but after logging the text storage, I found myself overwhelmed with all of the data. I have no idea how I would pull values, but the simplest way I can think of getting the data in the end would be splitting up all the text in the NSTextView into items of an array (where the items will each be of an individual color), and then another array of the colors (corresponding to the text of the original array). Is something like this possible? Thanks,

~Josh Fletcher

Josh,

I think that this is possible, but whether it’s the best way to go is hard to tell from your description of your project. It would be helpful to know a little more about the work flow and what you mean by “When I use the text to interact with different applications”. Is the user typing in this text, or is the text coming back from these different apps?

NSTextStorage has a method attributeRuns(), which might do what you want. If the user can only change the color attribute of the text, and not other things like font, size, etc., then that method will break up the text into blocks each of which is all the text in a row of a particular color. One thing for sure though, is that you won’t be able to use a color wheel, because each time you try to pick the same color from it, it would be slightly different, and thus be seen by attributeRuns() as a different block – it would be much better to have a discreet selection of colors to choose from. Try logging tv’s textStorage()'s attributeRuns() (where tv is the IBOutlet for your text view) – you will still get a lot of data, but the first thing in each item of the array will be the text of that block.

Ric

Here is a little program that shows how to use the attributeRuns() method that I mentioned in my last post. Here, I extract just the string and color from the array you get from attributeRuns(), and put that in a new array called textBlocks. Then I created some predicates for the colors black, blue, green and red and use those to filter the textBlocks array to group all the text of a particular color into its own array. In the applicationWillFinishLaunching method I added a setTextColor method to set the original color to the same black as you get by picking black from the color picker (the default black is in a different color space, so it wouldn’t be seen as the same black as the one picked from the color picker). I used the third choice from the left in the color picker with the default apple palette of colors to choose different colors when testing this code.

on applicationWillFinishLaunching_(aNotification)
		tv's setTextColor_(current application's NSColor's colorWithCalibratedRed_green_blue_alpha_(0, 0, 0, 1))
	end applicationWillFinishLaunching_
	
	on splitTextByColor_(sender)
		set textBlocks to current application's NSMutableArray's array()
		set textArray to tv's textStorage()'s attributeRuns()
		repeat with anItem in textArray
			textBlocks's addObject_({theText:anItem's |string|(), theColor:anItem's foregroundColor})
		end repeat
		--log textBlocks
		set blackPred to current application's NSPredicate's predicateWithFormat_("theColor == %@", current application's NSColor's colorWithCalibratedRed_green_blue_alpha_(0, 0, 0, 1))
		set bluePred to current application's NSPredicate's predicateWithFormat_("theColor == %@", current application's NSColor's colorWithCalibratedRed_green_blue_alpha_(0, 0, 1, 1))
		set greenPred to current application's NSPredicate's predicateWithFormat_("theColor == %@", current application's NSColor's colorWithCalibratedRed_green_blue_alpha_(0, 1, 0, 1))
		set redPred to current application's NSPredicate's predicateWithFormat_("theColor == %@", current application's NSColor's colorWithCalibratedRed_green_blue_alpha_(1, 0, 0, 1))
		set blackText to textBlocks's filteredArrayUsingPredicate_(blackPred)'s valueForKey_("theText")
		set blueText to textBlocks's filteredArrayUsingPredicate_(bluePred)'s valueForKey_("theText")
		set greenText to textBlocks's filteredArrayUsingPredicate_(greenPred)'s valueForKey_("theText")
		set redText to textBlocks's filteredArrayUsingPredicate_(redPred)'s valueForKey_("theText")
		log blackText
		log blueText
		log greenText
		log redText
	end splitTextByColor_

I think to use an approach like this successfully, you would have to restrict the users ability to access the font panel that you get automatically with the text view so they couldn’t change anything other than the color. You would also probably have to bring up the color picker yourself (rather than the one you get through the font panel), and have it restricted to a set palette of colors so you can define a predicate for each one.

Ric

To go more in depth, the man I am making the app for needs to send emails out to over 700 people at once, so he puts the subject in an NSTextField, and the body in an NSTextView. The application then reads the text from both, and makes multiple emails with twenty contacts in each and sends them off. Putting text in a mail message works a bit like appendAttributedString_() in ASOC, you append the specified text with a specified color. Right now, the app reads the last change to the color wheel, converts it to RGB (mail needs RGB), and then sends off the message. I convert to RGB with this code:

set theColor to (colorWheel's |color|) set someColor to theColor set temp1 to theColor's redComponent() set temp2 to theColor's greenComponent() set temp3 to theColor's blueComponent() set theColor to {(temp1 * 65535), (temp2 * 65535), (temp3 * 65535)}
I’ve written the code so that when he changes the color wheel, it will only change the color of the text he has selected, but as you can see, I’ve run into a trouble with finding which text that is. What I was hoping for with this was something like:
if I have a string “Hi my name is Josh” and “Hi” and “Josh” are red, then i would get two arrays, {“Hi”, “my name is”, “Josh”} and {“Red”, “Black”, “Red”}
I feel like there should be an easy way to do something like this without having to limit the user’s choices.

Also, I’m sorry for my ignorance, but how do I access the data of attributedRuns() and textStorage()? I can see everything when i log them, but they are obviously not strings, and not any simple type of array, so I can’t figure out how to extract their data. Thank you so much for helping me with this so far,

~Josh Fletcher

This is what I am doing with the repeat loop below. You get the text from anItem’s |string|() and the color from anItem’s foregroundColor(). If you log textBlocks, you’ll see just the text and the color.

 set textBlocks to current application's NSMutableArray's array()
       set textArray to tv's textStorage()'s attributeRuns()
       repeat with anItem in textArray
           textBlocks's addObject_({theText:anItem's |string|(), theColor:anItem's foregroundColor})
       end repeat

I don’t think there is an easy way if you allow the user to pick from a color wheel, because, what is red? The red color in the palette list is “1,0,0,1” in red green blue alpha terms. But if you pick from the wheel it might be “.99,.01,.03,1” or something like that. Those would be seen as different colors by attributeRuns(). You would have to go through an array like the textBlock array that I created, look at all the color values, and if they were close enough to “1,0,0,1” call them “red”. It could be done, but I’m not sure I would call it simple. You could allow the user to create their own custom palette with a limited set of colors, so they would still have a limited choice.

Ok, that is exactly what I am looking for. My only question is how do I convert from the “NSCalibrated” color to RGB? (it looks like there are different types too, ie. NSCalibratedWhiteColorSpace and NSCalibratedRGBColor space, why is that?).

I’m sorry if I was unclear, but “Red” and “Black”, I meant the RGB equivalents to whatever shade was chosen. However, there is obviously no reason to make two arrays if I use the one that you make with that repeat block, that definitely makes it easier. Thank you!

EDIT:
I got it all working and converted to RGB using colorUsingColorSpaceName to convert the color space. Now that I’ve got that, everything is running smoothly. Thank you so much for all the help, I’m sorry if I was more than a little hard to work with :P. Here’s the code I used in the end:

set textBlocks to current application's NSMutableArray's array() set textArray to bodyField's textStorage()'s attributeRuns() repeat with anItem in textArray textBlocks's addObject_({theText:anItem's |string|(), theColor:anItem's foregroundColor}) end repeat set theColor to theColor of item 2 of textBlocks set theColor to theColor's colorUsingColorSpaceName_(current application's NSCalibratedRGBColorSpace) set temp1 to theColor's redComponent() set temp2 to theColor's greenComponent() set temp3 to theColor's blueComponent() set theColor to {(temp1 * 65535), (temp2 * 65535), (temp3 * 65535)}

Cocoa has several different color spaces. The default black color you get in a text view is in the NSCalibratedWhiteColorSpace whereas all the colors (including black) that you get from the color picker (at least the default one) are in the NSCalibratedRGBColor space. That’s why I include the line " tv’s setTextColor_(current application’s NSColor’s colorWithCalibratedRed_green_blue_alpha_(0, 0, 0, 1))" in the applicationWillFinishLaunching method, so that if just start typing without changing color, it types in the NSCalibratedRGBColor space version of black, rather than the default one. If you include that line, then all the colors should be in the same NSCalibratedRGBColor space, and then you don’t need the line “set theColor to theColor’s colorUsingColorSpaceName_(current application’s NSCalibratedRGBColorSpace)” in your code.

So how do you get the content into your mail message? Are you adding it one piece at a time, each with its own color. I haven’t used Mail much, so I don’t know how to set the content other than with something like:

set newMessage to make new outgoing message with properties {subject:"Test", content:theText}

which sets the content all at once.
Ric