Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
636 views
in Technique[技术] by (71.8m points)

macos - Swift 3: Set Finder label color

I'm trying to set the colored labels shown by the finder. The only function I know is setResourceValue. But this needs localized names!

I could image my mother language and english as well, but all others I don't know. I can't believe, that this should be the way.

Is the are translation function, which takes a standard parameter like an enum or int and delivers the localized color name?

I have an running part, but only for two languages (German and English):

let colorNamesEN = [ "None", "Gray", "Green", "Purple", "Blue", "Yellow", "Red", "Orange" ]
let colorNamesDE = [ "",     "Grau", "Grün",  "Lila",   "Blau", "Gelb",   "Rot", "Orange" ]

public enum TagColors : Int8 {
    case None = -1, Gray, Green, Purple, Blue, Yellow, Red, Orange, Max
}

//let theURL : NSURL = NSURL.fileURLWithPath("/Users/dirk/Documents/MyLOG.txt")

extension NSURL {
    // e.g.  theURL.setColors(0b01010101)
    func tagColorValue(tagcolor : TagColors) -> UInt16 {
        return 1 << UInt16(tagcolor.rawValue)
    }

    func addTagColor(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = tagColorValue(tagcolor) | self.getTagColors()
        return setTagColors(bits)
    }

    func remTagColor(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = ~tagColorValue(tagcolor) & self.getTagColors()
        return setTagColors(bits)
    }

    func setColors(tagcolor : TagColors) -> Bool {
        let bits : UInt16 = tagColorValue(tagcolor)
        return setTagColors(bits)
    }

    func setTagColors(colorMask : UInt16) -> Bool {
        // get string for all available and requested bits
        let arr = colorBitsToStrings(colorMask & (tagColorValue(TagColors.Max)-1))

        do {
            try self.setResourceValue(arr, forKey: NSURLTagNamesKey)
            return true
        }
        catch {
            print("Could not write to file (self.absoluteURL)")
            return false
        }
    }

    func getTagColors() -> UInt16 {
        return getAllTagColors(self.absoluteURL)
    }
}


// let initialBits: UInt8 = 0b00001111
func colorBitsToStrings(colorMask : UInt16) -> NSArray {
    // translate bits to (localized!) color names
    let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!

    // I don't know how to automate it for all languages possible!!!!
    let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN

    var tagArray = [String]()
    var bitNumber : Int = -1   // ignore first loop
    for colorName in colorNames {
        if bitNumber >= 0 {
            if colorMask & UInt16(1<<bitNumber) > 0 {
                tagArray.append(colorName)
            }
        }
        bitNumber += 1
    }
    return tagArray
}


func getAllTagColors(file : NSURL) -> UInt16 {
    var colorMask : UInt16 = 0

    // translate (localized!) color names to bits
    let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!
    // I don't know how to automate it for all languages possible!!!!
    let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN
    var bitNumber : Int = -1   // ignore first loop

    var tags : AnyObject?

    do {
        try file.getResourceValue(&tags, forKey: NSURLTagNamesKey)
        if tags != nil {
            let tagArray = tags as! [String]

            for colorName in colorNames {
                if bitNumber >= 0 {
                    // color name listed?
                    if tagArray.filter( { $0 == colorName } ).count > 0 {
                        colorMask |= UInt16(1<<bitNumber)
                    }
                }
                bitNumber += 1
            }
        }
    } catch {
        // process the error here
    }

    return colorMask
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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
1"
        case .green: return "Green
2"
        case .purple: return "Purple
3"
        case .blue: return "Blue
4"
        case .yellow: return "Yellow
5"
        case .red: return "Red
6"
        case .orange: return "Orange
7"
        }
    }

    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)")
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...