Swift 3: Set Finder Label Color

Swift 3: Set Finder label color

To set a single color, the setResourceValue API call is indeed what you should use. However, the resource key you should use is NSURLLabelNumberKey, or URLResourceKey.labelNumberKey in Swift 3 (not NSURLTagNamesKey):

enum LabelNumber: Int {
case none
case grey
case green
case purple
case blue
case yellow
case red
case orange
}

do {
// casting to NSURL here as the equivalent API in the URL value type appears borked:
// setResourceValue(_, forKey:) is not available there,
// and setResourceValues(URLResourceValues) appears broken at least as of Xcode 8.1…
// fix-it for setResourceValues(URLResourceValues) is saying to use [URLResourceKey: AnyObject],
// and the dictionary equivalent also gives an opposite compiler error. Looks like an SDK / compiler bug.
try (fileURL as NSURL).setResourceValue(LabelNumber.purple.rawValue, forKey: .labelNumberKey)
}
catch {
print("Error when setting the label number: \(error)")
}

(This is a Swift 3 port of an answer to a related Objective-C question. Tested with Xcode 8.1, macOS Sierra 10.12.1)

To set multiple colors, you can either use the API you've used with setting resource values with the label key. The distinction between these two encodings is described here: http://arstechnica.com/apple/2013/10/os-x-10-9/9/ – basically the label key is internally setting the extended attribute "com.apple.metadata:_kMDItemUserTags" which stores an array of those label strings as a binary plist, whereas the single colour option shown above is setting the 10th byte of 32 byte long extended attribute value "com.apple.FinderInfo".

The "localized" in that key name is a bit confusing in the sense that what is actually being set with it is the set of labels chosen by the user, amongst the label names set by the user. Those label values are indeed localized, but only to the extent where they are set according to the localisation setting when you initially created your account. To demonstrate, these are the label values used by Finder on my system, which I'd set to Finnish localization as a test and restarted Finder, rebooted machine etc:

➜  defaults read com.apple.Finder FavoriteTagNames
(
"",
Red,
Orange,
Yellow,
Green,
Blue,
Purple,
Gray
)

The way the data is encoded in that binary plist value is simply the favourite tag name followed by its index in the array (which is fixed to be of length 8, with actual values starting from 1, i.e. matching the seven colours in the order Red, Orange, Yellow, Green, Blue, Purple, Gray). For example:

xattr -p com.apple.metadata:_kMDItemUserTags foobar.png | xxd -r -p | plutil -convert xml1 - -o -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>Gray
1</string>
<string>Purple
3</string>
<string>Green
2</string>
<string>Red
6</string>
</array>
</plist>

So, the system localisation is not taken into account, and in fact setting the tag with any string followed by a linefeed, followed by a number between 1–7 will show up in Finder with the colour indicated by the tag's index. However, to know the correct current values to apply to get the tags to be applied from the set of favorite tags (such that both colour and the label match up) you would need to read that key from Finder preferences (key 'FavoriteTagNames' from domain 'com.apple.Finder' which encodes an array of those favourite tag names as shown above).

Ignoring the above complication in case you want to get the label name and colour correct, requiring reading from Finder preferences domain (which you may or may not be able to do, depending on whether your app is sandboxed or not), should you wish to use multiple colours, here's an example solution that sets the colour using extended attribute values directly (I used SOExtendedAttributes to avoid having to touch the unwieldy xattr C APIs):

enum LabelNumber: Int {
case none
case gray
case green
case purple
case blue
case yellow
case red
case orange

// using an enum here is really for illustrative purposes:
// to know the correct values to apply you would need to read Finder preferences (see body of my response for more detail).
var label:String? {
switch self {
case .none: return nil
case .gray: return "Gray\n1"
case .green: return "Green\n2"
case .purple: return "Purple\n3"
case .blue: return "Blue\n4"
case .yellow: return "Yellow\n5"
case .red: return "Red\n6"
case .orange: return "Orange\n7"
}
}

static func propertyListData(labels: [LabelNumber]) throws -> Data {
let labelStrings = labels.flatMap { $0.label }
let propData = try! PropertyListSerialization.data(fromPropertyList: labelStrings,
format: PropertyListSerialization.PropertyListFormat.binary,
options: 0)
return propData
}
}

do {
try (fileURL as NSURL).setExtendedAttributeData(LabelNumber.propertyListData(labels: [.gray, .green]),
name: "com.apple.metadata:_kMDItemUserTags")
}
catch {
print("Error when setting the label number: \(error)")
}

How can I conditionally color files and folders in the OS X Finder?

Unfortunately there is no public API for that. You need to inject the code inside Finder and patch it.

Before 10.6, it was quite easy to inject codes into Cocoa app by just using InputManagers. This is no longer true but you can do that using OSAX, see this blog post. SIMBL does that automatically.

But you have to figure out what's going on inside Finder to see how to patch things.
To explore the inside of Finder, F-Script anywhere will help you.

Have fun and good luck!

setResourceValue NSURLTagNamesKey error

Solution:

1 - The first argument for setResourceValue has to be an NSArray

2 - Shocking, but... the color name has to be the localized one!

This example fixes your error 8050 but doesn't actually set a color tag if your system language isn't English:

var error: NSError?
let theURL:NSURL = NSURL(fileURLWithPath: "/Users/me/tests/z.png")!
let tag: AnyObject = NSWorkspace.sharedWorkspace().fileLabels[4] // "Blue" tag
let arr = NSArray(object: tag)
theURL.setResourceValue(arr, forKey: NSURLTagNamesKey, error: &error)

On my system (French), this doesn't set an actual blue color label tag, only a text tag containing the word "Blue".

To set the proper color tag, you have to give the localized color name literally:

var error: NSError?
let theURL:NSURL = NSURL(fileURLWithPath: "/Users/me/tests/z.png")!
let arr = NSArray(object: "Bleu") // "Blue" translated to French
theURL.setResourceValue(arr, forKey: NSURLTagNamesKey, error: &error)

Tagging files with colors in OS X Finder from shell scripts

This shell script takes the file or folder name as its first argument and the label index (0 for no label, 1 for red, ..., 7 for gray) as its second argument.

#!/bin/sh
osascript -e "tell application \"Finder\" to set label index of alias POSIX file \"`cd -P -- "$(dirname -- "$1")" && printf '%s\n' "$(pwd -P)/$(basename -- "$1")"`\" to $2"

More directly, if $filename is a shell variable with the absolute path name of the file or folder to be labeled and $label is a shell variable with the label index number,

osascript -e "tell application \"Finder\" to set label index of alias POSIX file \"$filename\" to $label"

is a shell command to assign the label to the file or folder.

How to invert text colour of selected NSTableView row

You don't say what view or view hierarchy you're using for your cells. You also don't say how or where you're setting the text fields' color or to what color, specifically.

When a row is selected, the row automatically computes its interiorBackgroundStyle. It also sets the backgroundStyle of the cell view if it responds to -setBackgroundStyle: or is an NSControl with a cell which responds to that.

If your cell view is an instance of NSTableCellView, it forwards the background style to all of its subviews which meet the same criteria. If you use a different container view as your cell view and you want the background style forwarded along like this, you would have to implement that yourself in your view class.

An NSTextField's cell (an NSTextFieldCell) responds to -setBackgroundStyle: and so has its background style set automatically by the above mechanisms. The text field cell will automatically change its text color to white if its textColor is one of the standard control colors (e.g. NSColor.controlTextColor()), but won't do so if you assign a non-standard color. So, if you're setting a specific color for your text, you are responsible for changing that when the background style changes.

You can use a subclass of NSTableCellView and add a property observer (didSet) for the backgroundStyle property. That can change the text field's textColor depending on the style that was set. For example, you can use your custom color if the background style is not .Dark or use the normal text field color NSColor.controlTextColor() if it is .Dark (so that the text field will actually display it as white).

You could also use a subclass of NSTextFieldCell for your text field and do the same sort of thing. Or override drawInteriorWithFrame(_:inView:) to draw with a different text color depending on the background style.

How do I retrieve all available Finder tags?

NSWorkspace.shared().fileLabels only returns the system tags that were available when the user account was created (the default system tags).

There's unfortunately no API in macOS to retrieve the tags that you have created yourself in the Finder.

The solution is to parse the ~/Library/SyncedPreferences/com.apple.finder.plist:

func allTagLabels() -> [String] {
// this doesn't work if the app is Sandboxed:
// the users would have to point to the file themselves with NSOpenPanel
let url = URL(fileURLWithPath: "\(NSHomeDirectory())/Library/SyncedPreferences/com.apple.finder.plist")
let keyPath = "values.FinderTagDict.value.FinderTags"
if let d = try? Data(contentsOf: url) {
if let plist = try? PropertyListSerialization.propertyList(from: d, options: [], format: nil),
let pdict = plist as? NSDictionary,
let ftags = pdict.value(forKeyPath: keyPath) as? [[AnyHashable: Any]]
{
return ftags.flatMap { $0["n"] as? String }
}
}
return []
}

let all = allTagLabels()
print(all)

This gets all Finder tags labels.

You can also select only the custom tags (ignore the system ones):

func customTagLabels() -> [String] {
let url = URL(fileURLWithPath: "\(NSHomeDirectory())/Library/SyncedPreferences/com.apple.finder.plist")
let keyPath = "values.FinderTagDict.value.FinderTags"
if let d = try? Data(contentsOf: url) {
if let plist = try? PropertyListSerialization.propertyList(from: d, options: [], format: nil),
let pdict = plist as? NSDictionary,
let ftags = pdict.value(forKeyPath: keyPath) as? [[AnyHashable: Any]]
{
return ftags.flatMap { tag in
if let n = tag["n"] as? String,
tag.values.count != 2
{
return n
}
return nil
}
}
}
return []
}


Related Topics



Leave a reply



Submit