├── .gitignore ├── .jazzy.yaml ├── .swiftlint.yml ├── Demo ├── Bin │ ├── Demo.zip │ └── SampleApp.zip ├── Demo.xcodeproj │ └── project.pbxproj ├── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ ├── Contents.json │ │ ├── apple.imageset │ │ │ ├── Contents.json │ │ │ └── apple.png │ │ ├── file_doc.imageset │ │ │ ├── Contents.json │ │ │ └── file_doc.png │ │ ├── icoWarning.imageset │ │ │ ├── Contents.json │ │ │ └── icoWarning.pdf │ │ ├── ladyindark.imageset │ │ │ ├── Contents.json │ │ │ └── ladyindark.png │ │ ├── moon.imageset │ │ │ ├── Contents.json │ │ │ ├── moon.png │ │ │ └── moon@2x.png │ │ ├── paper.imageset │ │ │ ├── Contents.json │ │ │ ├── paper.png │ │ │ └── paper_@2X.png │ │ ├── saturday_warmth.imageset │ │ │ ├── Contents.json │ │ │ └── saturday_warmth.png │ │ └── sun.imageset │ │ │ ├── Contents.json │ │ │ ├── sun.png │ │ │ └── sun@2x.png │ ├── Base.lproj │ │ └── Main.storyboard │ ├── ContentViewController.swift │ ├── DarkTheme+DemoSwift.swift │ ├── DetailsView.swift │ ├── DetailsViewController.swift │ ├── Info.plist │ ├── LightTheme+DemoSwift.swift │ ├── Note.swift │ ├── NotificationName+Note.swift │ ├── PaperTheme.swift │ ├── SidebarViewController+Actions.swift │ ├── SidebarViewController.swift │ ├── ThemeColor+DemoSwift.swift │ ├── ThemeGradient+DemoSwift.swift │ ├── ThemeImage+DemoSwift.swift │ ├── TitleBarOverlayView.swift │ ├── TitleView.swift │ └── WindowController.swift └── Themes │ ├── LadyInDark.theme │ ├── PurpleGreen.theme │ └── SaturdayWarmth.theme ├── Docs ├── Classes │ ├── DarkTheme.html │ ├── LightTheme.html │ ├── SystemTheme.html │ ├── ThemeColor.html │ ├── ThemeGradient.html │ ├── ThemeImage.html │ ├── ThemeManager.html │ ├── ThemeManager │ │ └── WindowThemePolicy.html │ └── UserTheme.html ├── Extensions.html ├── Extensions │ ├── NSColor.html │ ├── NSWindow.html │ ├── Notification.html │ └── Notification │ │ └── Name.html ├── Protocols │ └── Theme.html ├── Theme Assets.html ├── ThemeKit.html ├── Themes.html ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── ThemeKit.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes │ │ │ │ ├── DarkTheme.html │ │ │ │ ├── LightTheme.html │ │ │ │ ├── SystemTheme.html │ │ │ │ ├── ThemeColor.html │ │ │ │ ├── ThemeGradient.html │ │ │ │ ├── ThemeImage.html │ │ │ │ ├── ThemeManager.html │ │ │ │ ├── ThemeManager │ │ │ │ │ └── WindowThemePolicy.html │ │ │ │ └── UserTheme.html │ │ │ ├── Extensions.html │ │ │ ├── Extensions │ │ │ │ ├── NSColor.html │ │ │ │ ├── NSWindow.html │ │ │ │ ├── Notification.html │ │ │ │ └── Notification │ │ │ │ │ └── Name.html │ │ │ ├── Protocols │ │ │ │ └── Theme.html │ │ │ ├── Theme Assets.html │ │ │ ├── ThemeKit.html │ │ │ ├── Themes.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ ├── gh.png │ │ │ │ └── spinner.gif │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ ├── jazzy.search.js │ │ │ │ ├── jquery.min.js │ │ │ │ ├── lunr.min.js │ │ │ │ └── typeahead.jquery.js │ │ │ └── search.json │ │ │ └── docSet.dsidx │ ├── ThemeKit.tgz │ └── ThemeKit.xml ├── img │ ├── carat.png │ ├── dash.png │ ├── gh.png │ └── spinner.gif ├── index.html ├── js │ ├── jazzy.js │ ├── jazzy.search.js │ ├── jquery.min.js │ ├── lunr.min.js │ └── typeahead.jquery.js ├── search.json └── undocumented.json ├── Imgs ├── ThemeKit.gif ├── ThemeKit.png └── ThemeKit@2x.png ├── LICENSE ├── README.md ├── Sources ├── Common.swift ├── DarkTheme.swift ├── Info.plist ├── LightTheme.swift ├── NSColor+ThemeKit.swift ├── NSDictionary+UserTheme.swift ├── NSImage+ThemeKit.swift ├── NSObject+ThemeKit.swift ├── NSView+ThemeKit.swift ├── NSWindow+ThemeKit.swift ├── NotificationName+ThemeKit.swift ├── String+Substring.swift ├── SystemTheme.swift ├── Theme.swift ├── ThemeColor.swift ├── ThemeGradient.swift ├── ThemeImage.swift ├── ThemeKit.h ├── ThemeManager+ObjectiveC.swift ├── ThemeManager.swift ├── Thread+ThemeKit.swift └── UserTheme.swift ├── ThemeKit.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── ThemeKit.xcscheme └── macOSThemeKit.podspec /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | *.xcworkspace 6 | xcuserdata/ 7 | project.xcworkspace/ 8 | C.gitignore & Swift.gitignore 9 | 10 | # Cocoapods 11 | Pods/ 12 | 13 | ## Build generated 14 | build/ 15 | DerivedData/ 16 | 17 | ## Various settings 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | xcuserdata/ 27 | 28 | ## Other 29 | *.moved-aside 30 | *.xcuserstate 31 | 32 | # Swift Package Manager 33 | Packages/ 34 | .build/ 35 | 36 | # Carthage 37 | Carthage/Checkouts 38 | Carthage/Build 39 | 40 | Demo/Index 41 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | # About 2 | module: ThemeKit 3 | author: Paw & Nuno Grilo 4 | author_url: https://nunogrilo.com 5 | readme: README.md 6 | copyright: '© 2016-2017 [Paw](https://paw.cloud) & [Nuno Grilo](http://nunogrilo.com) under [MIT License](https://github.com/luckymarmot/ThemeKit/blob/master/LICENSE).' 7 | 8 | # URLs 9 | github_url: https://github.com/luckymarmot/ThemeKit 10 | root_url: http://themekit.nunogrilo.com 11 | github_file_prefix: https://github.com/luckymarmot/ThemeKit/blob/master 12 | 13 | # Generation 14 | clean: true 15 | output: Docs 16 | min_acl: public 17 | hide_documentation_coverage: true 18 | skip_undocumented: true 19 | exclude: 20 | - "Demo*/*.*" 21 | - "**/*ObjC.swift" 22 | - "**/*ObjectiveC.swift" 23 | - "**/NSDictionary+UserTheme.swift" 24 | 25 | # Navigation 26 | custom_categories: 27 | - name: ThemeKit 28 | children: 29 | - ThemeManager 30 | - Notification 31 | - name: Themes 32 | children: 33 | - Theme 34 | - LightTheme 35 | - DarkTheme 36 | - SystemTheme 37 | - UserTheme 38 | - name: Theme Assets 39 | children: 40 | - ThemeColor 41 | - ThemeGradient 42 | - ThemeImage 43 | - name: Extensions 44 | children: 45 | - NSWindow 46 | - NSColor 47 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - block_based_kvo 3 | - identifier_name 4 | - cyclomatic_complexity 5 | - function_body_length 6 | - file_length 7 | - type_body_length 8 | - function_parameter_count 9 | - discarded_notification_center_observer 10 | - line_length 11 | - large_tuple 12 | -------------------------------------------------------------------------------- /Demo/Bin/Demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Bin/Demo.zip -------------------------------------------------------------------------------- /Demo/Bin/SampleApp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Bin/SampleApp.zip -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 29/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | // MARK: - 16 | // MARK: App Launch 17 | 18 | func applicationWillFinishLaunching(_ notification: Notification) { 19 | 20 | /* 0. Enable/disable theming capabilities. */ 21 | // ThemeManager.shared.isEnabled = false 22 | 23 | /* 1. Simpler usage: switch between light and dark theme directly. */ 24 | 25 | // ThemeManager.lightTheme.apply() 26 | // ThemeManager.darkTheme.apply() 27 | // ThemeManager.systemTheme.apply() 28 | 29 | /* 2. Advanced usage: define window theme policy & enable user themes. 30 | * Themes will be selected using popup bound to user defaults. */ 31 | 32 | // 2.1 Setup window theme policy 33 | ThemeManager.shared.windowThemePolicy = .themeAllWindows 34 | // ThemeManager.shared.windowThemePolicy = .themeSomeWindows(windowClasses: [MyCustomWindow.self]) 35 | // ThemeManager.shared.windowThemePolicy = .doNotThemeSomeWindows(windowClasses: [NSPanel.self]) 36 | // ThemeManager.shared.windowThemePolicy = .doNotThemeWindows 37 | 38 | // 2.2 User themes folder 39 | if let bundleResourcePath = Bundle.main.resourcePath { 40 | ThemeManager.shared.userThemesFolderURL = URL(fileURLWithPath: bundleResourcePath) 41 | NSLog("ThemeManager.shared.userThemesFolderURL: %@", ThemeManager.shared.userThemesFolderURL?.path ?? "-") 42 | } 43 | 44 | // 2.3 You can define the default light and dark theme, used for `ThemeManager.systemTheme` 45 | // if let paperTheme = ThemeManager.shared.theme(withIdentifier: PaperTheme.identifier) { 46 | // ThemeManager.lightTheme = paperTheme 47 | // } 48 | // if let purpleGreenTheme = ThemeManager.shared.theme(withIdentifier: "com.luckymarmot.ThemeKit.PurpleGreen") { 49 | // ThemeManager.darkTheme = purpleGreenTheme 50 | // } 51 | 52 | // 2.4 Set default theme (default: macOS theme `ThemeManager.systemTheme`) 53 | ThemeManager.defaultTheme = ThemeManager.lightTheme 54 | 55 | // 2.5 Animate theme transitions? (default = true) 56 | // ThemeManager.shared.animateThemeTransitions = false 57 | 58 | // 2.6 Apply last applied theme, or the default one 59 | ThemeManager.shared.applyLastOrDefaultTheme() 60 | 61 | } 62 | 63 | func applicationDidFinishLaunching(_ aNotification: Notification) { 64 | 65 | // Watch for theme changes and update Theme menu 66 | NotificationCenter.default.addObserver(self, selector: #selector(updateThemeMenu(_:)), name: .didChangeTheme, object: nil) 67 | 68 | // Build theme menu 69 | updateThemeMenu() 70 | } 71 | 72 | // MARK: - 73 | // MARK: Theme related 74 | 75 | @objc private func updateThemeMenu(_ notification: Notification? = nil) { 76 | // Clean menu 77 | themeMenu.removeAllItems() 78 | 79 | // Add themes 80 | var counter = 1 81 | for theme in ThemeManager.shared.themes { 82 | let item = NSMenuItem(title: theme.shortDisplayName, action: #selector(switchTheme(_:)), keyEquivalent: String(counter)) 83 | item.representedObject = theme 84 | item.state = (ThemeManager.shared.theme.identifier == theme.identifier) ? .on : .off 85 | themeMenu.addItem(item) 86 | counter += 1 87 | } 88 | 89 | // Add separator 90 | themeMenu.addItem(NSMenuItem.separator()) 91 | 92 | // Add slideshow 93 | let slideshowItem = NSMenuItem(title: "Slideshow", action: #selector(startStopThemeSlideshow), keyEquivalent: "0") 94 | slideshowItem.state = slideshowTimerOn ? .on : .off 95 | themeMenu.addItem(slideshowItem) 96 | } 97 | 98 | @IBAction func switchTheme(_ menuItem: NSMenuItem) { 99 | guard menuItem.representedObject != nil else { return } 100 | 101 | if let theme = menuItem.representedObject as? Theme { 102 | ThemeManager.shared.theme = theme 103 | updateThemeMenu() 104 | } 105 | } 106 | 107 | // MARK: - 108 | // MARK: Theme Slideshow 109 | 110 | /// Slideshow timer. 111 | private var slideshowTimer: Timer? 112 | 113 | /// Slideshow state. 114 | private var slideshowTimerOn: Bool { 115 | return (slideshowTimer?.isValid ?? false) 116 | } 117 | 118 | /// Themes used on slideshow. Indices from `ThemeManager.shared.themes`. 119 | private static let slideshowAllThemeIndices = [0, 1, 3, 4, 5, 6] 120 | private static let slideshowAnimatedGIFThemeIndices = [0, 1, 3, 5] 121 | private static let slideshowThemeIndices = AppDelegate.slideshowAllThemeIndices 122 | 123 | /// Start/stop theme slideshow. 124 | @objc func startStopThemeSlideshow() { 125 | if slideshowTimerOn { 126 | slideshowTimer?.invalidate() 127 | slideshowTimer = nil 128 | } else { 129 | slideshowTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(nextSlideshowTheme), userInfo: nil, repeats: true) 130 | } 131 | } 132 | 133 | /// Apply next slideshow theme. 134 | @objc func nextSlideshowTheme() { 135 | let themes = ThemeManager.shared.themes 136 | guard themes.count > 0 else { 137 | startStopThemeSlideshow() 138 | return 139 | } 140 | 141 | // Determine next theme 142 | var nextIndex = AppDelegate.slideshowThemeIndices[0] 143 | if let currentThemeIndex = themes.index(where: { $0 === ThemeManager.shared.theme }), 144 | let currentSlideshowIndex = AppDelegate.slideshowThemeIndices.index(of: currentThemeIndex), 145 | currentSlideshowIndex + 1 < AppDelegate.slideshowThemeIndices.count { 146 | nextIndex = AppDelegate.slideshowThemeIndices[currentSlideshowIndex + 1] 147 | } 148 | 149 | // Apply theme 150 | if nextIndex >= themes.count { 151 | nextIndex = 0 152 | } 153 | ThemeManager.shared.theme = themes[nextIndex] 154 | updateThemeMenu() 155 | } 156 | 157 | // As a bonus, a way of theming window titlebar is shown on 158 | // `WindowController` for illustrative purposes. 159 | 160 | // MARK: - 161 | // MARK: App Termination 162 | 163 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 164 | return true 165 | } 166 | 167 | func applicationWillTerminate(_ aNotification: Notification) { 168 | // Insert code here to tear down your application 169 | } 170 | 171 | // MARK: - 172 | // MARK: Tabs 173 | 174 | private var tabs: [NSWindowController] = [] 175 | 176 | @IBAction func newWindowForTab(_ sender: Any?) { 177 | let storyBoard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 178 | if let windowController = storyBoard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("WindowController")) as? NSWindowController { 179 | windowController.showWindow(self) 180 | } 181 | } 182 | 183 | // MARK: - 184 | // MARK: Notes actions 185 | 186 | @IBAction func addNote(_ sender: NSButton) { 187 | sidebarViewController?.addNote(sender) 188 | } 189 | 190 | @IBAction func deleteNote(_ sender: NSButton) { 191 | sidebarViewController?.deleteNote(sender) 192 | } 193 | 194 | @IBAction func resetNotes(_ sender: Any) { 195 | sidebarViewController?.resetNotes(sender) 196 | } 197 | 198 | // MARK: - 199 | // MARK: Others 200 | 201 | // Weak control references 202 | @IBOutlet weak var themeMenu: NSMenu! 203 | 204 | // Strong controllers references 205 | @objc weak var sidebarViewController: SidebarViewController? 206 | 207 | } 208 | 209 | extension NSApplication { 210 | 211 | @objc static var sidebarViewController: SidebarViewController? { 212 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 213 | return appDelegate.sidebarViewController 214 | } 215 | return nil 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/apple.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "apple.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/apple.imageset/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/apple.imageset/apple.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/file_doc.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "file_doc.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/file_doc.imageset/file_doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/file_doc.imageset/file_doc.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/icoWarning.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "icoWarning.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/icoWarning.imageset/icoWarning.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/icoWarning.imageset/icoWarning.pdf -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/ladyindark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "ladyindark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/ladyindark.imageset/ladyindark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/ladyindark.imageset/ladyindark.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/moon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "moon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "moon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/moon.imageset/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/moon.imageset/moon.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/moon.imageset/moon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/moon.imageset/moon@2x.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/paper.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "paper.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "paper_@2X.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/paper.imageset/paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/paper.imageset/paper.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/paper.imageset/paper_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/paper.imageset/paper_@2X.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/saturday_warmth.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "saturday_warmth.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/saturday_warmth.imageset/saturday_warmth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/saturday_warmth.imageset/saturday_warmth.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/sun.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sun.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "sun@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/sun.imageset/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/sun.imageset/sun.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/sun.imageset/sun@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Demo/Demo/Assets.xcassets/sun.imageset/sun@2x.png -------------------------------------------------------------------------------- /Demo/Demo/ContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentViewController.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 29/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class ContentViewController: NSViewController, NSTextDelegate { 13 | 14 | /// Our content view 15 | @IBOutlet var contentView: NSView! 16 | 17 | /// Our text view 18 | @IBOutlet var contentTextView: NSTextView! 19 | 20 | /// Our placeholder view when no note is selected. 21 | @IBOutlet var noSelectionPlaceholder: NSView! 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Setup our Text View 27 | contentTextView.textColor = ThemeColor.contentTextColor 28 | contentTextView.backgroundColor = ThemeColor.contentBackgroundColor 29 | contentTextView.drawsBackground = true 30 | contentTextView.textContainer?.lineFragmentPadding = 16 31 | var font = NSFont(name: "GillSans-Light", size: 16) 32 | if font == nil { 33 | font = NSFont.systemFont(ofSize: 14) 34 | } 35 | contentTextView.font = font 36 | let paragraphStyle = NSMutableParagraphStyle() 37 | paragraphStyle.lineSpacing = 4.4 38 | contentTextView.defaultParagraphStyle = paragraphStyle 39 | 40 | // Setup our clipping view background color (text view parent) 41 | if let clipView = contentTextView.enclosingScrollView?.contentView { 42 | clipView.backgroundColor = ThemeColor.contentBackgroundColor 43 | clipView.drawsBackground = true 44 | } 45 | 46 | // Observe note selection change notifications 47 | NotificationCenter.default.addObserver(forName: .didChangeNoteSelection, object: nil, queue: nil) { (notification) in 48 | let obj = notification.object 49 | if let viewController = obj as? NSViewController, 50 | viewController.view.window == self.view.window { 51 | self.representedObject = notification.userInfo?["note"] 52 | } 53 | } 54 | 55 | // Fix white scrollbar view when in dark theme and scrollbars are set to 56 | // always be shown on *System Preferences > General*. 57 | if let scrollView = contentTextView.enclosingScrollView { 58 | scrollView.backgroundColor = ThemeColor.contentBackgroundColor 59 | scrollView.wantsLayer = true 60 | } 61 | 62 | // Observe theme changes 63 | NotificationCenter.default.addObserver(self, selector: #selector(didChangeTheme(_:)), name: .didChangeTheme, object: nil) 64 | } 65 | 66 | override var representedObject: Any? { 67 | didSet { 68 | var subview: NSView 69 | 70 | if let note = representedObject as? Note { 71 | contentTextView.string = note.text 72 | contentTextView.scrollToBeginningOfDocument(self) 73 | noSelectionPlaceholder.removeFromSuperview() 74 | subview = contentView 75 | } else { 76 | contentTextView.string = "" 77 | contentView.removeFromSuperview() 78 | subview = noSelectionPlaceholder 79 | } 80 | 81 | self.view.addSubview(subview) 82 | subview.translatesAutoresizingMaskIntoConstraints = true 83 | subview.autoresizingMask = [NSView.AutoresizingMask.width, NSView.AutoresizingMask.height] 84 | subview.frame = view.bounds 85 | } 86 | } 87 | 88 | // MARK: - 89 | // MARK: Theme related 90 | 91 | @objc private func didChangeTheme(_ notification: Notification? = nil) { 92 | // Update `NSScroller` background color based on current theme 93 | DispatchQueue.main.async { 94 | self.contentTextView.enclosingScrollView?.verticalScroller?.layer?.backgroundColor = ThemeColor.contentBackgroundColor.cgColor 95 | } 96 | 97 | // If in fullscreen, need to re-focus current window 98 | if let window = view.window { 99 | let isWindowInFullScreen = window.styleMask.contains(NSWindow.StyleMask.fullScreen) || window.className == "NSToolbarFullScreenWindow" 100 | if isWindowInFullScreen { 101 | DispatchQueue.main.async { 102 | window.makeKey() 103 | } 104 | } 105 | } 106 | } 107 | 108 | // MARK: - 109 | // MARK: NSTextDelegate 110 | 111 | public func textDidChange(_ notification: Notification) { 112 | if let note = representedObject as? Note { 113 | note.text = contentTextView.string 114 | note.lastModified = Date() 115 | NotificationCenter.default.post(name: .didEditNoteText, object: self, userInfo: ["note": note]) 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /Demo/Demo/DarkTheme+DemoSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarkTheme+DemoSwift.swift 3 | // DemoSwift 4 | // 5 | // Created by Nuno Grilo on 09/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ThemeKit 11 | 12 | extension DarkTheme { 13 | 14 | // MARK: CONTENT 15 | 16 | /// Notes content title text color 17 | @objc var contentTitleColor: NSColor { 18 | return NSColor(calibratedWhite: 0.95, alpha: 1.0) 19 | } 20 | 21 | /// Notes text foreground color 22 | @objc var contentTextColor: NSColor { 23 | return NSColor.lightGray 24 | } 25 | 26 | /// Notes text background color 27 | @objc var contentBackgroundColor: NSColor { 28 | return NSColor.black 29 | } 30 | 31 | /// Rainbow gradient (used between title and text) 32 | @objc var rainbowGradient: NSGradient? { 33 | return NSGradient(starting: NSColor.green, ending: NSColor.blue) 34 | } 35 | 36 | // MARK: DETAILS 37 | 38 | /// Notes details title text color 39 | @objc var detailsTitleColor: NSColor { 40 | return NSColor(red: 0.43, green: 0.43, blue: 0.43, alpha: 1.0) 41 | } 42 | 43 | /// Notes details background color 44 | @objc var detailsBackgroundColor: NSColor { 45 | return NSColor(calibratedWhite: 1.0, alpha: 0.0) 46 | } 47 | 48 | /// Notes details image 49 | @objc var detailsImage: NSImage? { 50 | return NSImage(named: NSImage.Name("moon")) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Demo/Demo/DetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsView.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 30/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class DetailsView: NSView { 13 | 14 | /// Drawing code 15 | override func draw(_ dirtyRect: NSRect) { 16 | // Fill with details background 17 | ThemeColor.detailsBackgroundColor.set() 18 | NSBezierPath(rect: bounds).fill() 19 | 20 | // Draw theme details image 21 | let imageSize = NSSize(width: 48, height: 48) 22 | ThemeImage.detailsImage.draw(in: NSRect(x: (bounds.width - imageSize.width) / 2, y: bounds.height - 56, width: imageSize.width, height: imageSize.height)) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Demo/Demo/DetailsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsViewController.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 30/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class DetailsViewController: NSViewController { 13 | 14 | // IBOutlets 15 | @IBOutlet weak var containerView: NSView! 16 | @IBOutlet weak var detailsTitleTxt: NSTextField! 17 | @IBOutlet weak var fakeControlsTitleTxt: NSTextField! 18 | @IBOutlet weak var wordCountTxt: NSTextField! 19 | @IBOutlet weak var characterCount: NSTextField! 20 | @IBOutlet weak var lastModifiedTxt: NSTextField! 21 | 22 | @objc var dateFormatter: DateFormatter? 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | // Setup colors 28 | detailsTitleTxt.textColor = ThemeColor.detailsTitleColor 29 | fakeControlsTitleTxt.textColor = ThemeColor.detailsTitleColor 30 | 31 | // Setup date formatter 32 | dateFormatter = DateFormatter() 33 | dateFormatter?.dateStyle = .medium 34 | dateFormatter?.timeStyle = .none 35 | 36 | // Hide stuff until we have a selected note 37 | containerView.isHidden = true 38 | 39 | // Observe note selection change notifications 40 | NotificationCenter.default.addObserver(forName: .didChangeNoteSelection, object: nil, queue: nil) { (notification) in 41 | let obj = notification.object 42 | if let viewController = obj as? NSViewController, 43 | viewController.view.window == self.view.window { 44 | self.representedObject = notification.userInfo?["note"] 45 | } 46 | } 47 | 48 | // Observe note text edit notifications 49 | NotificationCenter.default.addObserver(forName: .didEditNoteText, object: nil, queue: nil) { (notification) in 50 | let obj = notification.object 51 | if let viewController = obj as? NSViewController, 52 | viewController.view.window == self.view.window { 53 | self.representedObject = notification.userInfo?["note"] 54 | } 55 | } 56 | } 57 | 58 | override var representedObject: Any? { 59 | didSet { 60 | if let note = representedObject as? Note { 61 | containerView.isHidden = false 62 | wordCountTxt.stringValue = String(note.textWordCount) 63 | characterCount.stringValue = String(note.textCharacterCount) 64 | lastModifiedTxt.stringValue = dateFormatter?.string(from: note.lastModified) ?? "-" 65 | } else { 66 | containerView.isHidden = true 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Demo/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.4.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2016-2018 Paw & Nuno Grilo. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /Demo/Demo/LightTheme+DemoSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LightTheme+DemoSwift.swift 3 | // DemoSwift 4 | // 5 | // Created by Nuno Grilo on 09/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ThemeKit 11 | 12 | extension LightTheme { 13 | 14 | // MARK: CONTENT 15 | 16 | /// Notes content title text color 17 | @objc var contentTitleColor: NSColor { 18 | return NSColor(calibratedRed: 0.15, green: 0.15, blue: 0.15, alpha: 1.0) 19 | } 20 | 21 | /// Notes text foreground color 22 | @objc var contentTextColor: NSColor { 23 | return NSColor(calibratedRed: 0.1, green: 0.1, blue: 0.1, alpha: 1.0) 24 | } 25 | 26 | /// Notes text background color 27 | @objc var contentBackgroundColor: NSColor { 28 | return NSColor(red: 1.0, green: 0.99, blue: 0.98, alpha: 1.0) 29 | } 30 | 31 | /// Rainbow gradient (used between title and text) 32 | @objc var rainbowGradient: NSGradient? { 33 | return NSGradient(starting: NSColor(calibratedRed: 0.0, green: 0.66, blue: 1.0, alpha: 1.0), ending: ThemeColor.contentBackgroundColor) 34 | } 35 | 36 | // MARK: DETAILS 37 | 38 | /// Notes details title text color 39 | @objc var detailsTitleColor: NSColor { 40 | return NSColor(red: 0.43, green: 0.43, blue: 0.43, alpha: 1.0) 41 | } 42 | 43 | /// Notes details background color 44 | @objc var detailsBackgroundColor: NSColor { 45 | return NSColor(calibratedWhite: 1.0, alpha: 0.0) 46 | } 47 | 48 | /// Notes details image 49 | @objc var detailsImage: NSImage? { 50 | return NSImage(named: NSImage.Name("sun")) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Demo/Demo/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 29/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Our basic Note class. 12 | class Note: NSObject, NSCoding { 13 | 14 | /// Note title. 15 | @objc var title: String = "" 16 | 17 | /// Note text. 18 | @objc var text: String = "" 19 | 20 | /// Last modified date. 21 | @objc var lastModified: Date = Date() 22 | 23 | // Character count. 24 | @objc var textCharacterCount: Int { 25 | return text.count 26 | } 27 | 28 | // Word count. 29 | @objc var textWordCount: Int { 30 | return text.components(separatedBy: " ").count 31 | } 32 | 33 | /// Initializer 34 | @objc init(title: String = "Untitled Note", text: String = "") { 35 | self.title = title 36 | self.text = text 37 | self.lastModified = Date() 38 | } 39 | 40 | // MARK: - 41 | // MARK: NSCoder 42 | 43 | public func encode(with aCoder: NSCoder) { 44 | aCoder.encode(title, forKey: "title") 45 | aCoder.encode(text, forKey: "text") 46 | aCoder.encode(lastModified, forKey: "lastModified") 47 | } 48 | 49 | public required init?(coder aDecoder: NSCoder) { 50 | if let title = aDecoder.decodeObject(forKey: "title") as? String { 51 | self.title = title 52 | } 53 | if let text = aDecoder.decodeObject(forKey: "text") as? String { 54 | self.text = text 55 | } 56 | if let lastModified = aDecoder.decodeObject(forKey: "lastModified") as? Date { 57 | self.lastModified = lastModified 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Demo/Demo/NotificationName+Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Demo.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 29/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Notification.Name { 12 | 13 | /// Notification sent when note selection changed. 14 | public static let didChangeNoteSelection = Notification.Name("ThemeKitDemoDidChangeNoteSelectionNotification") 15 | 16 | /// Notification sent when note title did edit. 17 | public static let didEditNoteTitle = Notification.Name("ThemeKitDemoDidEditNoteTitleNotification") 18 | 19 | /// Notification sent when note text did edit. 20 | public static let didEditNoteText = Notification.Name("ThemeKitDemoDidEditNoteTextNotification") 21 | } 22 | -------------------------------------------------------------------------------- /Demo/Demo/PaperTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaperTheme.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 01/10/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class PaperTheme: NSObject, Theme { 13 | 14 | /// Light theme identifier (static). 15 | @objc public static var identifier: String = "com.luckymarmot.ThemeKit.PaperTheme" 16 | 17 | /// Unique theme identifier. 18 | public var identifier: String = PaperTheme.identifier 19 | 20 | /// Theme display name. 21 | public var displayName: String = "Paper Theme" 22 | 23 | /// Theme short display name. 24 | public var shortDisplayName: String = "Paper" 25 | 26 | /// Is this a dark theme? 27 | public var isDarkTheme: Bool = false 28 | 29 | /// Description. 30 | public override var description: String { 31 | return "<\(PaperTheme.self): \(themeDescription(self))>" 32 | } 33 | 34 | // MARK: - 35 | // MARK: Theme Assets 36 | 37 | // MARK: WINDOW 38 | 39 | @objc var windowTitleBarActiveColor: NSColor { 40 | if _windowTitleBarActiveColor == nil, 41 | let paperImage = self.paperImage { 42 | // darken paper image 43 | paperImage.lockFocus() 44 | NSColor.init(white: 0.0, alpha: 0.05).setFill() 45 | NSBezierPath(rect: NSRect(x: 0, y: 0, width: paperImage.size.width, height: paperImage.size.height)).fill() 46 | paperImage.unlockFocus() 47 | 48 | _windowTitleBarActiveColor = NSColor(patternImage: paperImage) 49 | } 50 | 51 | return _windowTitleBarActiveColor ?? defaultFallbackBackgroundColor 52 | } 53 | private var _windowTitleBarActiveColor: NSColor? 54 | 55 | @objc var windowTitleBarInactiveColor: NSColor { 56 | guard let paperImage = self.paperImage else { 57 | return defaultFallbackBackgroundColor 58 | } 59 | return NSColor(patternImage: paperImage) 60 | } 61 | 62 | // MARK: CONTENT 63 | 64 | /// Notes content title text color 65 | @objc var contentTitleColor: NSColor { 66 | return NSColor(calibratedRed: 0.15, green: 0.15, blue: 0.35, alpha: 1.0) 67 | } 68 | 69 | /// Notes text foreground color 70 | @objc var contentTextColor: NSColor { 71 | return NSColor(calibratedRed: 0.1, green: 0.1, blue: 0.3, alpha: 1.0) 72 | } 73 | 74 | /// Notes text background color 75 | @objc var contentBackgroundColor: NSColor { 76 | if _contentBackgroundColor == nil, 77 | let paperImage = self.paperImage { 78 | // lighten paper image 79 | paperImage.lockFocus() 80 | NSColor.init(white: 1.0, alpha: 0.5).setFill() 81 | NSBezierPath(rect: NSRect(x: 0, y: 0, width: paperImage.size.width, height: paperImage.size.height)).fill() 82 | paperImage.unlockFocus() 83 | 84 | _contentBackgroundColor = NSColor(patternImage: paperImage) 85 | } 86 | 87 | return _contentBackgroundColor ?? defaultFallbackBackgroundColor 88 | } 89 | private var _contentBackgroundColor: NSColor? 90 | 91 | /// Rainbow gradient (used between title and text) 92 | @objc var rainbowGradient: NSGradient? { 93 | let blue = NSColor(calibratedRed: 0.18, green: 0.45, blue: 0.88, alpha: 1.0) 94 | let clear = NSColor(calibratedWhite: 1.0, alpha: 0.0) 95 | return NSGradient(colorsAndLocations: (blue, 0.0), (clear, 0.66)) 96 | } 97 | 98 | // MARK: DETAILS 99 | 100 | /// Notes details title text color 101 | @objc var detailsTitleColor: NSColor { 102 | return contentTitleColor 103 | } 104 | 105 | /// Notes details background color 106 | @objc var detailsBackgroundColor: NSColor { 107 | guard let paperImage = self.paperImage else { 108 | return defaultFallbackBackgroundColor 109 | } 110 | return NSColor(patternImage: paperImage) 111 | } 112 | 113 | /// Notes details image 114 | @objc var detailsImage: NSImage? { 115 | return NSImage(named: NSImage.Name("file_doc")) 116 | } 117 | 118 | // MARK: SYSTEM OVERRIDE 119 | 120 | @objc var secondaryLabelColor: NSColor { 121 | return NSColor(calibratedRed: 0.18, green: 0.45, blue: 0.88, alpha: 1.0) 122 | } 123 | 124 | // MARK: SHARED 125 | 126 | @objc var paperImage: NSImage? { 127 | return Bundle.main.image(forResource: NSImage.Name("paper")) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Demo/Demo/SidebarViewController+Actions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSViewController+GlobalActions.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 01/10/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSViewController { 12 | 13 | @IBAction func addNote(_ sender: NSButton) { 14 | NSApplication.sidebarViewController?.addNote(sender) 15 | } 16 | 17 | @IBAction func deleteNote(_ sender: NSButton) { 18 | NSApplication.sidebarViewController?.deleteNote(sender) 19 | } 20 | 21 | @IBAction func resetNotes(_ sender: Any) { 22 | NSApplication.sidebarViewController?.resetNotes(sender) 23 | } 24 | 25 | } 26 | 27 | extension NSWindowController { 28 | 29 | @IBAction func addNote(_ sender: NSButton) { 30 | NSApplication.sidebarViewController?.addNote(sender) 31 | } 32 | 33 | @IBAction func deleteNote(_ sender: NSButton) { 34 | NSApplication.sidebarViewController?.deleteNote(sender) 35 | } 36 | 37 | @IBAction func resetNotes(_ sender: Any) { 38 | NSApplication.sidebarViewController?.resetNotes(sender) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Demo/Demo/ThemeColor+DemoSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeColor+DemoSwift.swift 3 | // DemoSwift 4 | // 5 | // Created by Nuno Grilo on 08/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ThemeKit 11 | 12 | extension ThemeColor { 13 | 14 | // MARK: WINDOW 15 | 16 | /// Active window title bar color 17 | @objc static var windowTitleBarActiveColor: ThemeColor { 18 | return ThemeColor.color(with: #function) 19 | } 20 | 21 | /// Inactive window title bar color 22 | @objc static var windowTitleBarInactiveColor: ThemeColor { 23 | return ThemeColor.color(with: #function) 24 | } 25 | 26 | // MARK: CONTENT 27 | 28 | /// Notes content title text color 29 | @objc static var contentTitleColor: ThemeColor { 30 | return ThemeColor.color(with: #function) 31 | } 32 | 33 | /// Notes text foreground color 34 | @objc static var contentTextColor: ThemeColor { 35 | return ThemeColor.color(with: #function) 36 | } 37 | 38 | /// Notes text background color 39 | @objc static var contentBackgroundColor: ThemeColor { 40 | return ThemeColor.color(with: #function) 41 | } 42 | 43 | // MARK: DETAILS 44 | 45 | /// Notes details title text color 46 | @objc static var detailsTitleColor: ThemeColor { 47 | return ThemeColor.color(with: #function) 48 | } 49 | 50 | /// Notes details background color 51 | @objc static var detailsBackgroundColor: ThemeColor { 52 | return ThemeColor.color(with: #function) 53 | } 54 | 55 | // MARK: SYSTEM OVERRIDE 56 | 57 | override open class var labelColor: ThemeColor { 58 | return ThemeColor.color(with: #function) 59 | } 60 | 61 | override open class var secondaryLabelColor: ThemeColor { 62 | return ThemeColor.color(with: #function) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Demo/Demo/ThemeGradient+DemoSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeGradient+DemoSwift.swift 3 | // DemoSwift 4 | // 5 | // Created by Nuno Grilo on 08/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ThemeKit 11 | 12 | extension ThemeGradient { 13 | 14 | // MARK: CONTENT 15 | 16 | /// Rainbow gradient (used between title and text) 17 | @objc static var rainbowGradient: ThemeGradient? { 18 | return ThemeGradient.gradient(with: #function) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Demo/Demo/ThemeImage+DemoSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeFont+DemoSwift.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 03/10/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import ThemeKit 11 | 12 | extension ThemeImage { 13 | 14 | // MARK: DETAILS 15 | 16 | /// Notes details image 17 | @objc static var detailsImage: ThemeImage { 18 | return ThemeImage.image(with: #function) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Demo/Demo/TitleBarOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleBarOverlayView.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 11/07/2017. 6 | // Copyright © 2017 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class TitleBarOverlayView: NSView { 13 | 14 | /// Drawing code 15 | override func draw(_ dirtyRect: NSRect) { 16 | let theme = ThemeManager.shared.theme 17 | let windowIsActive = window?.isKeyWindow ?? false 18 | let isWindowInFullScreen = window?.styleMask.contains(NSWindow.StyleMask.fullScreen) ?? false 19 | || (window?.className ?? "") == "NSToolbarFullScreenWindow" 20 | 21 | if (windowIsActive || isWindowInFullScreen), 22 | let color = theme.themeAsset("windowTitleBarActiveColor") as? NSColor { 23 | // Fill with 'active' color 24 | color.set() 25 | NSBezierPath(rect: bounds).fill() 26 | } else if !windowIsActive, 27 | let color = theme.themeAsset("windowTitleBarInactiveColor") as? NSColor { 28 | // Fill with 'inactive' color 29 | color.set() 30 | NSBezierPath(rect: bounds).fill() 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo/TitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientView.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 30/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class TitleView: NSView, NSTextFieldDelegate { 13 | 14 | /// Our text field 15 | @IBOutlet weak var titleTextField: NSTextField! 16 | 17 | /// Weak reference to selected Note 18 | @objc weak var note: Note? 19 | 20 | override func awakeFromNib() { 21 | // Setup title text 22 | titleTextField.textColor = ThemeColor.contentTitleColor 23 | titleTextField.backgroundColor = ThemeColor.contentBackgroundColor 24 | titleTextField.drawsBackground = true 25 | titleTextField.delegate = self 26 | var font = NSFont(name: "GillSans-Light", size: 24) 27 | if font == nil { 28 | font = NSFont.systemFont(ofSize: 24) 29 | } 30 | titleTextField.font = font 31 | 32 | // Observe note selection change notifications 33 | NotificationCenter.default.addObserver(forName: .didChangeNoteSelection, object: nil, queue: nil) { (notification) in 34 | let obj = notification.object 35 | if let viewController = obj as? NSViewController, 36 | viewController.view.window == self.window, 37 | let note = notification.userInfo?["note"] as? Note { 38 | 39 | self.note = note 40 | self.titleTextField.stringValue = note.title 41 | } 42 | } 43 | 44 | // Observe note title change notifications 45 | NotificationCenter.default.addObserver(forName: .didEditNoteTitle, object: nil, queue: nil) { (_) in 46 | if let note = self.note { 47 | self.titleTextField.stringValue = note.title 48 | } 49 | } 50 | } 51 | 52 | /// Drawing code 53 | override func draw(_ dirtyRect: NSRect) { 54 | guard note?.title != nil else { 55 | return 56 | } 57 | 58 | // Fill with content background 59 | ThemeColor.contentBackgroundColor.set() 60 | NSBezierPath(rect: bounds).fill() 61 | 62 | // Draw gradient 63 | let gradientFrame = NSRect(x: 4, y: 0, width: bounds.width - 8, height: 4) 64 | if let gradient = ThemeGradient.rainbowGradient { 65 | gradient.draw(in: gradientFrame, angle: 0) 66 | } 67 | 68 | } 69 | 70 | // MARK: - 71 | // MARK: NSTextDelegate 72 | 73 | func controlTextDidChange(_ obj: Notification) { 74 | guard let note = self.note else { 75 | return 76 | } 77 | note.title = titleTextField.stringValue 78 | note.lastModified = Date() 79 | NotificationCenter.default.post(name: .didEditNoteTitle, object: self, userInfo: ["note": note]) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Demo/Demo/WindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowController.swift 3 | // Demo 4 | // 5 | // Created by Nuno Grilo on 29/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import ThemeKit 11 | 12 | class WindowController: NSWindowController { 13 | 14 | @objc public var themeKit: ThemeManager = ThemeManager.shared 15 | 16 | override func windowDidLoad() { 17 | // Observe note selection change notifications 18 | NotificationCenter.default.addObserver(forName: .didChangeNoteSelection, object: nil, queue: nil) { (notification) in 19 | let obj = notification.object 20 | if let note = notification.userInfo?["note"] as? Note, 21 | let viewController = obj as? NSViewController, 22 | viewController.view.window == self.window { 23 | self.updateTitle(note) 24 | } 25 | } 26 | 27 | // Observe note text edit notifications 28 | NotificationCenter.default.addObserver(forName: .didEditNoteText, object: nil, queue: nil) { (notification) in 29 | let obj = notification.object 30 | if let note = notification.userInfo?["note"] as? Note, 31 | let viewController = obj as? NSViewController, 32 | viewController.view.window == self.window { 33 | self.updateTitle(note) 34 | } 35 | } 36 | 37 | // Observe theme change notifications 38 | NotificationCenter.default.addObserver(forName: .didChangeTheme, object: nil, queue: nil) { (_) in 39 | // update KVO property 40 | self.willChangeValue(forKey: "canEditTheme") 41 | self.didChangeValue(forKey: "canEditTheme") 42 | } 43 | } 44 | 45 | /// Add titlebar overlay view. 46 | override func awakeFromNib() { 47 | super.awakeFromNib() 48 | 49 | if let titlebarView = self.titlebarView { 50 | // create titlebar background overlay view 51 | let overlayView = TitleBarOverlayView(frame: NSRect(x: 0, y: 0, width: 100, height: 100)) 52 | overlayView.translatesAutoresizingMaskIntoConstraints = false 53 | 54 | // add overlay view below everything else 55 | titlebarView.addSubview(overlayView, positioned: .below, relativeTo: nil) 56 | 57 | // add constraints 58 | let constraintViews = ["view": overlayView] 59 | titlebarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]-0-|", options: [NSLayoutConstraint.FormatOptions.directionLeadingToTrailing], metrics: nil, views: constraintViews)) 60 | titlebarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]-0-|", options: [NSLayoutConstraint.FormatOptions.directionLeadingToTrailing], metrics: nil, views: constraintViews)) 61 | 62 | // refresh it when key flag changes 63 | NotificationCenter.default.addObserver(forName: NSWindow.didBecomeKeyNotification, object: window, queue: nil, using: { _ in 64 | overlayView.needsDisplay = true 65 | }) 66 | NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: window, queue: nil, using: { _ in 67 | overlayView.needsDisplay = true 68 | }) 69 | } 70 | } 71 | 72 | /// Find `NSTitlebarContainerView` view. 73 | private var titlebarView: NSView? { 74 | if let themeFrame = self.window?.contentView?.superview { 75 | for subview in themeFrame.subviews where subview.className == "NSTitlebarContainerView" { 76 | return subview.subviews.first { $0.className == "NSTitlebarView" } 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | /// Update window title with current note title. 83 | @objc func updateTitle(_ note: Note) { 84 | self.window?.title = "\(note.title) - ThemeKit Demo" 85 | } 86 | 87 | /// Can edit current theme (must be a `UserTheme`). 88 | @objc var canEditTheme: Bool { 89 | return ThemeManager.shared.theme.isUserTheme 90 | } 91 | 92 | /// Edit current (`UserTheme`) theme. 93 | @IBAction func editTheme(_ sender: Any) { 94 | if ThemeManager.shared.theme.isUserTheme, 95 | let userTheme = ThemeManager.shared.theme as? UserTheme, 96 | let userThemeURL = userTheme.fileURL { 97 | 98 | guard FileManager.default.isWritableFile(atPath: userThemeURL.path) else { 99 | let alert = NSAlert() 100 | alert.messageText = "Theme file is not writable." 101 | alert.informativeText = "If you're lunching Demo from the Downloads folder, move it to another place and try again." 102 | alert.alertStyle = NSAlert.Style.critical 103 | alert.addButton(withTitle: "OK") 104 | alert.runModal() 105 | return 106 | } 107 | 108 | // check if there is any app associted with `.theme` extension 109 | let userThemeCFURL: CFURL = userThemeURL as CFURL 110 | if LSCopyDefaultApplicationURLForURL(userThemeCFURL, .editor, nil) != nil { 111 | NSWorkspace.shared.open(userThemeURL) 112 | } else { 113 | // otherwise open with TextEdit 114 | NSWorkspace.shared.openFile(userThemeURL.path, withApplication: "TextEdit", andDeactivate: true) 115 | } 116 | 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /Demo/Themes/LadyInDark.theme: -------------------------------------------------------------------------------- 1 | // ************************** Credits *************************** // 2 | // - "Lady in dark" pattern 3 | // Author: http://www.colourlovers.com/lover/Tenshiko/loveNote 4 | // URL: http://www.colourlovers.com/pattern/1758170/Lady_in_dark 5 | 6 | // ************************* Theme Info ************************* // 7 | displayName = Lady in Dark Theme 8 | shortDisplayName = Lady in Dark 9 | identifier = com.luckymarmot.ThemeKit.LadyInDark 10 | darkTheme = true 11 | 12 | // *************************** Window ************************** // 13 | windowTitleBarActiveColor = $sexy.latex 14 | windowTitleBarInactiveColor = $intellektuelle.black 15 | 16 | // *************************** Content ************************** // 17 | contentTitleColor = $text.lightgray 18 | contentTextColor = $text.gray 19 | contentBackgroundColor = $take.a.bow 20 | rainbowGradient = linear-gradient($text.darkgray, $sexy.latex) 21 | 22 | // *************************** Details ************************** // 23 | detailsTitleColor = $text.gray 24 | detailsBackgroundColor = pattern(named:ladyindark) 25 | detailsImage = image(named:moon) 26 | 27 | // *********************** System Override ********************** // 28 | secondaryLabelColor = $text.darkgray 29 | 30 | // ************** 'Lady in dark' Pattern Colors ***************** // 31 | sexy.latex = rgb(15, 15, 15) 32 | take.a.bow = rgb(23, 23, 23) 33 | dislike.black = rgb(28, 28, 28) 34 | intellektuelle.black = rgb(36, 36, 36) 35 | coal.mine = rgb(44, 44, 44) 36 | 37 | // *********************** Common Colors ************************ // 38 | text.darkgray = rgb(85, 85, 85) 39 | text.gray = rgb(107, 107, 107) 40 | text.lightgray = rgb(187, 187, 187) 41 | 42 | // ********************** Fallback Assets *********************** // 43 | fallbackForegroundColor = $text.lightgray 44 | fallbackGradient = $rainbowGradient 45 | -------------------------------------------------------------------------------- /Demo/Themes/PurpleGreen.theme: -------------------------------------------------------------------------------- 1 | // ************************* Theme Info ************************* // 2 | displayName = Purple Green Theme 3 | shortDisplayName = Purple Green 4 | identifier = com.luckymarmot.ThemeKit.PurpleGreen 5 | darkTheme = true 6 | 7 | // *************************** Window ************************** // 8 | windowTitleBarActiveColor= $purple2 9 | windowTitleBarInactiveColor = $purple3 10 | 11 | // *************************** Content ************************** // 12 | contentTitleColor = $vivid.green 13 | contentTextColor = $dark.green 14 | contentBackgroundColor = $black1 15 | //contentBackgroundColor = pattern(named:paper) 16 | rainbowGradient = linear-gradient($purple, $vivid.green) 17 | 18 | // *************************** Details ************************** // 19 | detailsTitleColor = $dark.green 20 | detailsBackgroundColor = $purple2 21 | detailsImage = image(named:apple) 22 | //detailsImage2 = image(file:../some/path/some-file.jpg) 23 | 24 | // *********************** System Override ********************** // 25 | labelColor = $vivid.green 26 | secondaryLabelColor = $dark.green 27 | 28 | // *********************** Common Colors ************************ // 29 | blue = rgb(0, 170, 255) 30 | vivid.green = rgb(230, 255, 20) 31 | vivid.darker.green = rgb(200, 225, 0) 32 | dark.green = rgb(130, 155, 80) 33 | lime.green = rgb(250, 190, 45, .5) 34 | black1 = rgb(5, 25, 5) 35 | purple = rgb(175, 27, 230) 36 | purple2 = rgb(175, 27, 230, 0.25) 37 | purple3 = rgb(175, 27, 230, 0.15) 38 | lime.green2 = rgb(120, 233, 47) 39 | clear = rgb(0,0,0,0) 40 | 41 | // ********************** Fallback Assets *********************** // 42 | fallbackForegroundColor = rgb(255, 10, 90, 1.0) 43 | fallbackGradient = linear-gradient($blue, rgba(200, 140, 60, 1.0)) 44 | 45 | -------------------------------------------------------------------------------- /Demo/Themes/SaturdayWarmth.theme: -------------------------------------------------------------------------------- 1 | // ************************** Credits *************************** // 2 | // - "saturday warmth" pattern 3 | // Author: http://www.colourlovers.com/lover/logochic/loveNote 4 | // URL: http://www.colourlovers.com/pattern/582552/saturday_warmth 5 | 6 | // ************************* Theme Info ************************* // 7 | displayName = Saturday Warmth Theme 8 | shortDisplayName = Saturday Warmth 9 | identifier = com.luckymarmot.ThemeKit.SaturdayWarmth 10 | darkTheme = false 11 | 12 | // *************************** Window ************************** // 13 | windowTitleBarActiveColor = $black.tea 14 | windowTitleBarInactiveColor = rgba(252, 235, 182, 0.60) 15 | 16 | // *************************** Content ************************** // 17 | contentTitleColor = $history 18 | contentTextColor = $history 19 | contentBackgroundColor = $white 20 | rainbowGradient = linear-gradient($full.orange, $tweaked.turqouise) 21 | 22 | // *************************** Details ************************** // 23 | detailsTitleColor = $tweaked.turqouise 24 | detailsBackgroundColor = $black.tea 25 | detailsImage = image(named:saturday_warmth) 26 | 27 | // *********************** Colors ******************************* // 28 | black.tea = rgb(252, 235, 182) 29 | history = rgb(94, 65, 47) 30 | full.orange = rgb(240, 120, 24) 31 | tweaked.turqouise = rgb(120, 192, 168) 32 | joystiq.gold = rgb(240, 168, 48) 33 | clear = rgba(0,0,0,0) 34 | white = rgb(255,255,255) 35 | 36 | // ********************** Fallback Assets *********************** // 37 | fallbackForegroundColor = $history 38 | fallbackGradient = linear-gradient($history, $full.orange) 39 | -------------------------------------------------------------------------------- /Docs/Extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extensions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

Extensions

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | NSWindow 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    NSWindow ThemeKit extension.

    127 | 128 | See more 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public extension NSWindow
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
  • 142 |
    143 | 144 | 145 | 146 | NSColor 147 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |

    NSColor ThemeKit extension to help on when overriding colors in ThemeColor extensions.

    155 | 156 | See more 157 |
    158 |
    159 |

    Declaration

    160 |
    161 |

    Swift

    162 |
    extension NSColor
    163 | 164 |
    165 |
    166 |
    167 |
    168 |
  • 169 |
170 |
171 |
172 |
173 | 177 |
178 |
179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /Docs/Extensions/NSColor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NSColor Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

NSColor

108 |
109 |
110 | 111 |
extension NSColor
112 | 113 |
114 |
115 |

NSColor ThemeKit extension to help on when overriding colors in ThemeColor extensions.

116 | 117 |
118 |
119 |
120 |
121 | 122 | 123 |
124 | 125 |

Color override

126 |

127 |
128 |
129 |
    130 |
  • 131 |
    132 | 133 | 134 | 135 | isThemeOverriden 136 | 137 |
    138 |
    139 |
    140 |
    141 |
    142 |
    143 |

    Check if color is being overriden in a ThemeColor extension.

    144 | 145 |
    146 |
    147 |

    Declaration

    148 |
    149 |

    Swift

    150 |
    @objc
    151 | public var isThemeOverriden: Bool { get }
    152 | 153 |
    154 |
    155 |
    156 | Show on GitHub 157 |
    158 |
    159 |
    160 |
  • 161 |
  • 162 |
    163 | 164 | 165 | 166 | colorMethodNames() 167 | 168 |
    169 |
    170 |
    171 |
    172 |
    173 |
    174 |

    Get all NSColor color methods. 175 | Overridable class methods (can be overriden in ThemeColor extension).

    176 | 177 |
    178 |
    179 |

    Declaration

    180 |
    181 |

    Swift

    182 |
    @objc
    183 | public class func colorMethodNames() -> [String]
    184 | 185 |
    186 |
    187 |
    188 | Show on GitHub 189 |
    190 |
    191 |
    192 |
  • 193 |
194 |
195 |
196 |
197 | 201 |
202 |
203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /Docs/Extensions/Notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Notification Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

Notification

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | Name 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    Notifications defined by ThemeKit.

    127 | 128 | See more 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public extension Notification.Name
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
142 |
143 |
144 |
145 | 149 |
150 |
151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /Docs/ThemeKit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ThemeKit Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

ThemeKit

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | ThemeManager 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    Use ThemeManager shared instance to perform app-wide theming related operations, 127 | such as:

    128 | 129 |
      130 |
    • Get information about current theme/appearance
    • 131 |
    • Change current theme (can also be changed from NSUserDefaults)
    • 132 |
    • List available themes
    • 133 |
    • Define ThemeKit behaviour
    • 134 |
    135 | 136 | See more 137 |
    138 |
    139 |

    Declaration

    140 |
    141 |

    Swift

    142 |
    @objc(TKThemeManager)
    143 | public class ThemeManager : NSObject
    144 | 145 |
    146 |
    147 |
    148 | Show on GitHub 149 |
    150 |
    151 |
    152 |
  • 153 |
  • 154 |
    155 | 156 | 157 | 158 | Notification 159 | 160 |
    161 |
    162 |
    163 |
    164 |
    165 |
    166 | 167 | See more 168 |
    169 |
    170 |
    171 |
  • 172 |
173 |
174 |
175 |
176 | 180 |
181 |
182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /Docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.themekit 7 | CFBundleName 8 | ThemeKit 9 | DocSetPlatformFamily 10 | themekit 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | DashDocSetFallbackURL 20 | http://themekit.nunogrilo.com/ 21 | 22 | 23 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/Extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extensions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

Extensions

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | NSWindow 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    NSWindow ThemeKit extension.

    127 | 128 | See more 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public extension NSWindow
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
  • 142 |
    143 | 144 | 145 | 146 | NSColor 147 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |

    NSColor ThemeKit extension to help on when overriding colors in ThemeColor extensions.

    155 | 156 | See more 157 |
    158 |
    159 |

    Declaration

    160 |
    161 |

    Swift

    162 |
    extension NSColor
    163 | 164 |
    165 |
    166 |
    167 |
    168 |
  • 169 |
170 |
171 |
172 |
173 | 177 |
178 |
179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/Extensions/NSColor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NSColor Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

NSColor

108 |
109 |
110 | 111 |
extension NSColor
112 | 113 |
114 |
115 |

NSColor ThemeKit extension to help on when overriding colors in ThemeColor extensions.

116 | 117 |
118 |
119 |
120 |
121 | 122 | 123 |
124 | 125 |

Color override

126 |

127 |
128 |
129 |
    130 |
  • 131 |
    132 | 133 | 134 | 135 | isThemeOverriden 136 | 137 |
    138 |
    139 |
    140 |
    141 |
    142 |
    143 |

    Check if color is being overriden in a ThemeColor extension.

    144 | 145 |
    146 |
    147 |

    Declaration

    148 |
    149 |

    Swift

    150 |
    @objc
    151 | public var isThemeOverriden: Bool { get }
    152 | 153 |
    154 |
    155 |
    156 | Show on GitHub 157 |
    158 |
    159 |
    160 |
  • 161 |
  • 162 |
    163 | 164 | 165 | 166 | colorMethodNames() 167 | 168 |
    169 |
    170 |
    171 |
    172 |
    173 |
    174 |

    Get all NSColor color methods. 175 | Overridable class methods (can be overriden in ThemeColor extension).

    176 | 177 |
    178 |
    179 |

    Declaration

    180 |
    181 |

    Swift

    182 |
    @objc
    183 | public class func colorMethodNames() -> [String]
    184 | 185 |
    186 |
    187 |
    188 | Show on GitHub 189 |
    190 |
    191 |
    192 |
  • 193 |
194 |
195 |
196 |
197 | 201 |
202 |
203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/Extensions/Notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Notification Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

Notification

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | Name 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    Notifications defined by ThemeKit.

    127 | 128 | See more 129 |
    130 |
    131 |

    Declaration

    132 |
    133 |

    Swift

    134 |
    public extension Notification.Name
    135 | 136 |
    137 |
    138 |
    139 |
    140 |
  • 141 |
142 |
143 |
144 |
145 | 149 |
150 |
151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/ThemeKit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ThemeKit Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

ThemeKit 1.4.0 Docs

21 |

View on GitHub

22 |

Install in Dash

23 |

24 |

25 | 26 |
27 |

28 |
29 |
30 |
31 | 36 |
37 |
38 | 104 |
105 |
106 |
107 |

ThemeKit

108 | 109 |
110 |
111 |
112 |
    113 |
  • 114 |
    115 | 116 | 117 | 118 | ThemeManager 119 | 120 |
    121 |
    122 |
    123 |
    124 |
    125 |
    126 |

    Use ThemeManager shared instance to perform app-wide theming related operations, 127 | such as:

    128 | 129 |
      130 |
    • Get information about current theme/appearance
    • 131 |
    • Change current theme (can also be changed from NSUserDefaults)
    • 132 |
    • List available themes
    • 133 |
    • Define ThemeKit behaviour
    • 134 |
    135 | 136 | See more 137 |
    138 |
    139 |

    Declaration

    140 |
    141 |

    Swift

    142 |
    @objc(TKThemeManager)
    143 | public class ThemeManager : NSObject
    144 | 145 |
    146 |
    147 |
    148 | Show on GitHub 149 |
    150 |
    151 |
    152 |
  • 153 |
  • 154 |
    155 | 156 | 157 | 158 | Notification 159 | 160 |
    161 |
    162 |
    163 |
    164 |
    165 |
    166 | 167 | See more 168 |
    169 |
    170 |
    171 |
  • 172 |
173 |
174 |
175 |
176 | 180 |
181 |
182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/img/spinner.gif -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/Documents/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/docsets/ThemeKit.tgz -------------------------------------------------------------------------------- /Docs/docsets/ThemeKit.xml: -------------------------------------------------------------------------------- 1 | 1.4.0http://themekit.nunogrilo.com/docsets/ThemeKit.tgz 2 | -------------------------------------------------------------------------------- /Docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/img/carat.png -------------------------------------------------------------------------------- /Docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/img/dash.png -------------------------------------------------------------------------------- /Docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/img/gh.png -------------------------------------------------------------------------------- /Docs/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Docs/img/spinner.gif -------------------------------------------------------------------------------- /Docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /Docs/js/jazzy.search.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var $typeahead = $('[data-typeahead]'); 3 | var $form = $typeahead.parents('form'); 4 | var searchURL = $form.attr('action'); 5 | 6 | function displayTemplate(result) { 7 | return result.name; 8 | } 9 | 10 | function suggestionTemplate(result) { 11 | var t = '
'; 12 | t += '' + result.name + ''; 13 | if (result.parent_name) { 14 | t += '' + result.parent_name + ''; 15 | } 16 | t += '
'; 17 | return t; 18 | } 19 | 20 | $typeahead.one('focus', function() { 21 | $form.addClass('loading'); 22 | 23 | $.getJSON(searchURL).then(function(searchData) { 24 | const searchIndex = lunr(function() { 25 | this.ref('url'); 26 | this.field('name'); 27 | this.field('abstract'); 28 | for (const [url, doc] of Object.entries(searchData)) { 29 | this.add({url: url, name: doc.name, abstract: doc.abstract}); 30 | } 31 | }); 32 | 33 | $typeahead.typeahead( 34 | { 35 | highlight: true, 36 | minLength: 3, 37 | autoselect: true 38 | }, 39 | { 40 | limit: 10, 41 | display: displayTemplate, 42 | templates: { suggestion: suggestionTemplate }, 43 | source: function(query, sync) { 44 | const lcSearch = query.toLowerCase(); 45 | const results = searchIndex.query(function(q) { 46 | q.term(lcSearch, { boost: 100 }); 47 | q.term(lcSearch, { 48 | boost: 10, 49 | wildcard: lunr.Query.wildcard.TRAILING 50 | }); 51 | }).map(function(result) { 52 | var doc = searchData[result.ref]; 53 | doc.url = result.ref; 54 | return doc; 55 | }); 56 | sync(results); 57 | } 58 | } 59 | ); 60 | $form.removeClass('loading'); 61 | $typeahead.trigger('focus'); 62 | }); 63 | }); 64 | 65 | var baseURL = searchURL.slice(0, -"search.json".length); 66 | 67 | $typeahead.on('typeahead:select', function(e, result) { 68 | window.location = baseURL + result.url; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /Imgs/ThemeKit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Imgs/ThemeKit.gif -------------------------------------------------------------------------------- /Imgs/ThemeKit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Imgs/ThemeKit.png -------------------------------------------------------------------------------- /Imgs/ThemeKit@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckymarmot/ThemeKit/36df64ff1d5d2ee33562a0cc8cb27c371deb79e3/Imgs/ThemeKit@2x.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Paw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 14/10/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | func CacheKey(selector: Selector) -> NSNumber { 10 | return CacheKey(selector: selector, colorSpace: nil, theme: nil) 11 | } 12 | 13 | func CacheKey(selector: Selector, colorSpace: NSColorSpace?) -> NSNumber { 14 | return CacheKey(selector: selector, colorSpace: colorSpace, theme: nil) 15 | } 16 | 17 | func CacheKey(selector: Selector, theme: Theme?) -> NSNumber { 18 | return CacheKey(selector: selector, colorSpace: nil, theme: theme) 19 | } 20 | 21 | func CacheKey(selector: Selector, colorSpace: NSColorSpace?, theme: Theme?) -> NSNumber { 22 | let hashValue = selector.hashValue ^ (colorSpace == nil ? 0 : (colorSpace!.hashValue << 4)) ^ (theme == nil ? 0 : (theme!.hash << 8)) 23 | return NSNumber(value: hashValue) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DarkTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarkTheme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Dark theme. 12 | @objc(TKDarkTheme) 13 | public class DarkTheme: NSObject, Theme { 14 | 15 | /// Dark theme identifier (static). 16 | @objc public static var identifier: String = "com.luckymarmot.ThemeKit.DarkTheme" 17 | 18 | /// Unique theme identifier. 19 | public var identifier: String = DarkTheme.identifier 20 | 21 | /// Theme display name. 22 | public var displayName: String = "Dark Theme" 23 | 24 | /// Theme short display name. 25 | public var shortDisplayName: String = "Dark" 26 | 27 | /// Is this a dark theme? 28 | public var isDarkTheme: Bool = true 29 | 30 | /// Calling `init()` is not allowed outside this library. 31 | /// Use `ThemeManager.darkTheme` instead. 32 | internal override init() { 33 | super.init() 34 | } 35 | 36 | override public var description: String { 37 | return "<\(DarkTheme.self): \(themeDescription(self))>" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.4.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2016-2018 Paw & Nuno Grilo. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/LightTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LightTheme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Light theme (default). 12 | @objc(TKLightTheme) 13 | public class LightTheme: NSObject, Theme { 14 | 15 | /// Light theme identifier (static). 16 | @objc public static var identifier: String = "com.luckymarmot.ThemeKit.LightTheme" 17 | 18 | /// Unique theme identifier. 19 | public var identifier: String = LightTheme.identifier 20 | 21 | /// Theme display name. 22 | public var displayName: String = "Light Theme" 23 | 24 | /// Theme short display name. 25 | public var shortDisplayName: String = "Light" 26 | 27 | /// Is this a dark theme? 28 | public var isDarkTheme: Bool = false 29 | 30 | /// Calling `init()` is not allowed outside this library. 31 | /// Use `ThemeManager.lightTheme` instead. 32 | internal override init() { 33 | super.init() 34 | } 35 | 36 | override public var description: String { 37 | return "<\(LightTheme.self): \(themeDescription(self))>" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/NSColor+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 24/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | `NSColor` ThemeKit extension to help on when overriding colors in `ThemeColor` extensions. 13 | */ 14 | extension NSColor { 15 | 16 | // MARK: - 17 | // MARK: Color override 18 | 19 | /// Swizzle NSColor in case we are replacing system colors by themable colors. 20 | @objc static func swizzleNSColor() { 21 | swizzleNSColorOnce 22 | } 23 | 24 | /// Swizzle NSColor in case we are replacing system colors by themable colors. 25 | /// This code is executed *only once* (even if invoked multiple times). 26 | private static let swizzleNSColorOnce: Void = { 27 | // swizzle only if needed 28 | guard needsSwizzling else { return } 29 | 30 | // swizzle NSColor methods 31 | swizzleInstanceMethod(cls: NSClassFromString("NSDynamicSystemColor"), selector: #selector(set), withSelector: #selector(themeKitSet)) 32 | swizzleInstanceMethod(cls: NSClassFromString("NSDynamicSystemColor"), selector: #selector(setFill), withSelector: #selector(themeKitSetFill)) 33 | swizzleInstanceMethod(cls: NSClassFromString("NSDynamicSystemColor"), selector: #selector(setStroke), withSelector: #selector(themeKitSetStroke)) 34 | }() 35 | 36 | /// Check if color is being overriden in a ThemeColor extension. 37 | @objc public var isThemeOverriden: Bool { 38 | 39 | // check if `NSColor` provides this color 40 | let selector = Selector(colorNameComponent) 41 | let nsColorMethod = class_getClassMethod(NSColor.classForCoder(), selector) 42 | guard nsColorMethod != nil else { 43 | return false 44 | } 45 | 46 | // get current theme 47 | let theme = ThemeManager.shared.effectiveTheme 48 | 49 | // `UserTheme`: check `hasThemeAsset(_:)` method 50 | if let userTheme = theme as? UserTheme { 51 | return userTheme.hasThemeAsset(colorNameComponent) 52 | } 53 | 54 | // native themes: look up for an instance method 55 | else { 56 | let themeClass: AnyClass = object_getClass(theme)! 57 | let themeColorMethod = class_getInstanceMethod(themeClass, selector) 58 | return themeColorMethod != nil && nsColorMethod != themeColorMethod 59 | } 60 | } 61 | 62 | /// Get all `NSColor` color methods. 63 | /// Overridable class methods (can be overriden in `ThemeColor` extension). 64 | @objc public class func colorMethodNames() -> [String] { 65 | let nsColorMethods = NSObject.classMethodNames(for: NSColor.classForCoder()).filter { (methodName) -> Bool in 66 | return methodName.hasSuffix("Color") 67 | } 68 | return nsColorMethods 69 | } 70 | 71 | // MARK: - Private 72 | 73 | /// Check if we need to swizzle NSDynamicSystemColor class. 74 | private class var needsSwizzling: Bool { 75 | let themeColorMethods = classMethodNames(for: ThemeColor.classForCoder()).filter { (methodName) -> Bool in 76 | return methodName.hasSuffix("Color") 77 | } 78 | let nsColorMethods = classMethodNames(for: NSColor.classForCoder()).filter { (methodName) -> Bool in 79 | return methodName.hasSuffix("Color") 80 | } 81 | 82 | // checks if NSColor `*Color` class methods are being overriden 83 | for colorMethod in themeColorMethods { 84 | if nsColorMethods.contains(colorMethod) { 85 | // theme color with `colorMethod` selector is overriding a `NSColor` method -> swizzling needed. 86 | return true 87 | } 88 | } 89 | 90 | return false 91 | } 92 | 93 | // ThemeKit.set() replacement to use theme-aware color 94 | @objc public func themeKitSet() { 95 | // call original .set() function 96 | themeKitSet() 97 | 98 | // check if the user provides an alternative color 99 | if ThemeManager.shared.isEnabled && isThemeOverriden { 100 | // call ThemeColor.set() function 101 | ThemeColor.color(with: Selector(colorNameComponent)).set() 102 | } 103 | } 104 | 105 | // ThemeKit.setFill() replacement to use theme-aware color 106 | @objc public func themeKitSetFill() { 107 | // call original .setFill() function 108 | themeKitSetFill() 109 | 110 | // check if the user provides an alternative color 111 | if ThemeManager.shared.isEnabled && isThemeOverriden { 112 | // call ThemeColor.setFill() function 113 | ThemeColor.color(with: Selector(colorNameComponent)).setFill() 114 | } 115 | } 116 | 117 | // ThemeKit.setStroke() replacement to use theme-aware color 118 | @objc public func themeKitSetStroke() { 119 | // call original .setStroke() function 120 | themeKitSetStroke() 121 | 122 | // check if the user provides an alternative color 123 | if ThemeManager.shared.isEnabled && isThemeOverriden { 124 | // call ThemeColor.setStroke() function 125 | ThemeColor.color(with: Selector(colorNameComponent)).setStroke() 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Sources/NSDictionary+UserTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+UserTheme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSDictionary { 12 | 13 | // MARK: `NSRegularExpression` Initialization 14 | 15 | /// Regular expression for user theme variables ($var) 16 | @objc static var varsRegExpr: NSRegularExpression? { 17 | do { 18 | return try NSRegularExpression(pattern: "(\\$[a-zA-Z0-9_\\-\\.]+)+", options: .caseInsensitive) 19 | } catch let error { 20 | print(error) 21 | return nil 22 | } 23 | } 24 | 25 | /// Regular expression for user theme colors 26 | @objc static var colorRegExpr: NSRegularExpression? { 27 | do { 28 | return try NSRegularExpression(pattern: "(?:rgba?)?[\\s]?[\\(]?[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?([0-1]?(?:\\.\\d+)?)", options: .caseInsensitive) 29 | } catch let error { 30 | print(error) 31 | return nil 32 | } 33 | } 34 | 35 | /// Regular expression for user theme gradients 36 | @objc static var linearGradRegExpr: NSRegularExpression? { 37 | do { 38 | return try NSRegularExpression(pattern: "linear-gradient\\(\\s*((?:rgba?)?[\\s]?[\\(]?[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]*[\\s+]?([0-1]?(?:\\.\\d+)?)\\))\\s*,\\s*((?:rgba?)?[\\s]?[\\(]?[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]+[\\s+]?(\\d+)[(\\s)|(,)]*[\\s+]?([0-1]?(?:\\.\\d+)?)\\))\\s*\\)", options: .caseInsensitive) 39 | } catch let error { 40 | print(error) 41 | return nil 42 | } 43 | } 44 | 45 | /// Regular expression for user theme pattern images (NSColor) 46 | @objc static var patternRegExpr: NSRegularExpression? { 47 | do { 48 | return try NSRegularExpression(pattern: "pattern\\(((named):[\\s]*([\\w-\\. ]+)|(file):[\\s]*([\\w-\\. \\/]+))\\)", options: .caseInsensitive) 49 | } catch let error { 50 | print(error) 51 | return nil 52 | } 53 | } 54 | 55 | /// Regular expression for user theme images 56 | @objc static var imageRegExpr: NSRegularExpression? { 57 | do { 58 | return try NSRegularExpression(pattern: "image\\(((named):[\\s]*([\\w-\\. ]+)|(file):[\\s]*([\\w-\\. \\/]+))\\)", options: .caseInsensitive) 59 | } catch let error { 60 | print(error) 61 | return nil 62 | } 63 | } 64 | 65 | // MARK: Evaluation 66 | 67 | /// Evaluate object for the specified key as theme asset (`NSColor`, `NSGradient`, `NSImage`, ...). 68 | @objc func evaluatedObject(key: String) -> AnyObject? { 69 | // Resolve any variables 70 | let stringValue = evaluatedString(key: key) 71 | 72 | // Evaluate as theme asset (NSColor, NSGradient, NSImage, ...) 73 | return evaluatedObjectAsThemeAsset(value: stringValue as AnyObject) 74 | } 75 | 76 | // MARK: Internal evaluation functions 77 | 78 | /// Evaluate object for the specified key as string. 79 | private func evaluatedString(key: String) -> String? { 80 | guard let stringValue = self[key] as? String else { 81 | return nil 82 | } 83 | 84 | // Resolve any variables 85 | var evaluatedStringValue = stringValue 86 | var rangeOffset = 0 87 | NSDictionary.varsRegExpr?.enumerateMatches(in: stringValue, 88 | options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), 89 | range: NSRange(location: 0, length: stringValue.count), 90 | using: { (match, _, _) in 91 | if let matchRange = match?.range(at: 1) { 92 | var range = matchRange 93 | range.location += rangeOffset 94 | 95 | // Extract variable 96 | let start = range.location + 1 97 | let end = start + range.length - 2 98 | guard start < evaluatedStringValue.count && end < evaluatedStringValue.count else { return } 99 | let variable = evaluatedStringValue[start.. AnyObject? { 119 | guard let stringValue = value as? String else { 120 | // value is already evaluated as a non-string object 121 | return value 122 | } 123 | 124 | var evaluatedObject: AnyObject? = value 125 | 126 | // linear-gradient(color1, color2) 127 | if let match = NSDictionary.linearGradRegExpr?.firstMatch(in: stringValue, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSRange(location: 0, length: stringValue.count)), 128 | match.numberOfRanges == 11 { 129 | 130 | // Starting color 131 | let red1 = (Float(stringValue.substring(withNSRange: match.range(at: 2))) ?? 255) / 255 132 | let green1 = (Float(stringValue.substring(withNSRange: match.range(at: 3))) ?? 255) / 255 133 | let blue1 = (Float(stringValue.substring(withNSRange: match.range(at: 4))) ?? 255) / 255 134 | let alpha1 = Float(stringValue.substring(withNSRange: match.range(at: 5))) ?? 1.0 135 | let color1 = NSColor(red: CGFloat(red1), green: CGFloat(green1), blue: CGFloat(blue1), alpha: CGFloat(alpha1)) 136 | 137 | // Ending color 138 | let red2 = (Float(stringValue.substring(withNSRange: match.range(at: 7))) ?? 255) / 255 139 | let green2 = (Float(stringValue.substring(withNSRange: match.range(at: 8))) ?? 255) / 255 140 | let blue2 = (Float(stringValue.substring(withNSRange: match.range(at: 9))) ?? 255) / 255 141 | let alpha2 = Float(stringValue.substring(withNSRange: match.range(at: 10))) ?? 1.0 142 | let color2 = NSColor(red: CGFloat(red2), green: CGFloat(green2), blue: CGFloat(blue2), alpha: CGFloat(alpha2)) 143 | 144 | // Gradient 145 | evaluatedObject = NSGradient(starting: color1, ending: color2) 146 | } 147 | 148 | // rgb/rgba color 149 | if evaluatedObject is String, 150 | let match = NSDictionary.colorRegExpr?.firstMatch(in: stringValue, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSRange(location: 0, length: stringValue.count)), 151 | match.numberOfRanges == 5 { 152 | 153 | let red = (Float(stringValue.substring(withNSRange: match.range(at: 1))) ?? 255) / 255 154 | let green = (Float(stringValue.substring(withNSRange: match.range(at: 2))) ?? 255) / 255 155 | let blue = (Float(stringValue.substring(withNSRange: match.range(at: 3))) ?? 255) / 255 156 | let alpha = Float(stringValue.substring(withNSRange: match.range(at: 4))) ?? 1.0 157 | 158 | // Color 159 | evaluatedObject = NSColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha)) 160 | } 161 | 162 | // pattern 163 | if evaluatedObject is String, 164 | let match = NSDictionary.patternRegExpr?.firstMatch(in: stringValue, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSRange(location: 0, length: stringValue.count)), 165 | match.numberOfRanges == 6 { 166 | 167 | let isNamedType = stringValue.substring(withNSRange: match.range(at: 2)) == "named" 168 | let imageName = stringValue.substring(withNSRange: match.range(at: 3)) 169 | let isFileType = stringValue.substring(withNSRange: match.range(at: 4)) == "file" 170 | let imageFileName = stringValue.substring(withNSRange: match.range(at: 5)) 171 | 172 | // Pattern image 173 | var pattern: NSImage 174 | if isNamedType { 175 | pattern = NSImage(named: NSImage.Name(imageName)) ?? NSImage(size: NSSize.zero) 176 | } else if isFileType, let imageURL = ThemeManager.shared.userThemesFolderURL?.appendingPathComponent(imageFileName) { 177 | pattern = NSImage(contentsOf: imageURL) ?? NSImage(size: NSSize.zero) 178 | } else { 179 | pattern = NSImage(size: NSSize.zero) 180 | } 181 | evaluatedObject = NSColor(patternImage: pattern) 182 | } 183 | 184 | // image 185 | if evaluatedObject is String, 186 | let match = NSDictionary.imageRegExpr?.firstMatch(in: stringValue, options: NSRegularExpression.MatchingOptions(rawValue: UInt(0)), range: NSRange(location: 0, length: stringValue.count)), 187 | match.numberOfRanges == 6 { 188 | 189 | let isNamedType = stringValue.substring(withNSRange: match.range(at: 2)) == "named" 190 | let imageName = stringValue.substring(withNSRange: match.range(at: 3)) 191 | let isFileType = stringValue.substring(withNSRange: match.range(at: 4)) == "file" 192 | let imageFileName = stringValue.substring(withNSRange: match.range(at: 5)) 193 | 194 | // Image 195 | if isNamedType { 196 | evaluatedObject = NSImage(named: NSImage.Name(imageName)) ?? NSImage(size: NSSize.zero) 197 | } else if isFileType, let imageURL = ThemeManager.shared.userThemesFolderURL?.appendingPathComponent(imageFileName) { 198 | evaluatedObject = NSImage(contentsOf: imageURL) ?? NSImage(size: NSSize.zero) 199 | } else { 200 | evaluatedObject = NSImage(size: NSSize.zero) 201 | } 202 | } 203 | 204 | return evaluatedObject 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /Sources/NSImage+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Converted to Swift 3 by Nuno Grilo on 02/10/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | /* 10 | Based on original work from Mircea "Bobby" Georgescu from: 11 | http://www.bobbygeorgescu.com/2011/08/finding-average-color-of-uiimage/ 12 | 13 | UIImage+AverageColor.m 14 | 15 | Copyright (c) 2010, Mircea "Bobby" Georgescu 16 | All rights reserved. 17 | 18 | Redistribution and use in source and binary forms, with or without 19 | modification, are permitted provided that the following conditions are met: 20 | * Redistributions of source code must retain the above copyright 21 | notice, this list of conditions and the following disclaimer. 22 | * Redistributions in binary form must reproduce the above copyright 23 | notice, this list of conditions and the following disclaimer in the 24 | documentation and/or other materials provided with the distribution. 25 | * Neither the name of the Mircea "Bobby" Georgescu nor the 26 | names of its contributors may be used to endorse or promote products 27 | derived from this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 30 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL Mircea "Bobby" Georgescu BE LIABLE FOR ANY 33 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 34 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 35 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 36 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | */ 40 | 41 | import Cocoa 42 | 43 | extension NSImage { 44 | 45 | /// Find the average color in image. 46 | /// Not the most accurate algorithm, but probably got enough for the purpose. 47 | @objc internal func averageColor() -> NSColor { 48 | // setup a single-pixel image 49 | let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() 50 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) 51 | guard let bitmapData = malloc(4), 52 | let context = CGContext(data: bitmapData, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue), 53 | let cgImage = self.cgImage(forProposedRect: nil, context: NSGraphicsContext(cgContext: context, flipped: false), hints: nil) else { 54 | return NSColor.white 55 | } 56 | 57 | // draw the image into a 1x1 image 58 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: 1, height: 1)) 59 | 60 | // extract byte colors from single-pixel image 61 | let red = bitmapData.load(fromByteOffset: 0, as: UInt8.self) 62 | let green = bitmapData.load(fromByteOffset: 1, as: UInt8.self) 63 | let blue = bitmapData.load(fromByteOffset: 2, as: UInt8.self) 64 | let alpha = bitmapData.load(fromByteOffset: 3, as: UInt8.self) 65 | 66 | // build "average color" 67 | let modifier = alpha > 0 ? CGFloat(alpha) / 255.0 : 1.0 68 | let redFloat: CGFloat = CGFloat(red) * modifier / 255.0 69 | let greenFloat: CGFloat = CGFloat(green) * modifier / 255.0 70 | let blueFloat: CGFloat = CGFloat(blue) * modifier / 255.0 71 | let alphaFloat: CGFloat = CGFloat(alpha) / 255.0 72 | 73 | return NSColor(red: redFloat, green: greenFloat, blue: blueFloat, alpha: alphaFloat) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/NSObject+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 24/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSObject { 12 | 13 | /// Swizzle instance methods. 14 | @objc internal class func swizzleInstanceMethod(cls: AnyClass?, selector originalSelector: Selector, withSelector swizzledSelector: Selector) { 15 | guard cls != nil else { 16 | print("Unable to swizzle \(originalSelector): dynamic system color override will not be available.") 17 | return 18 | } 19 | 20 | // methods 21 | let originalMethod = class_getInstanceMethod(cls, originalSelector) 22 | let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector) 23 | 24 | // add new method 25 | let didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!)) 26 | 27 | // switch implementations 28 | if didAddMethod { 29 | class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!)) 30 | } else { 31 | method_exchangeImplementations(originalMethod!, swizzledMethod!) 32 | } 33 | } 34 | 35 | /// Returns class method names. 36 | @objc internal class func classMethodNames(for cls: AnyClass?) -> [String] { 37 | var results: [String] = [] 38 | 39 | // retrieve class method list 40 | var count: UInt32 = 0 41 | if let methods: UnsafeMutablePointer = class_copyMethodList(object_getClass(cls), &count) { 42 | 43 | // iterate class methods 44 | for i in 0.. [AnyClass] { 58 | var results: [AnyClass] = [] 59 | 60 | // class count 61 | let expectedCount: Int32 = objc_getClassList(nil, 0) 62 | 63 | // retrieve class list 64 | let buffer = UnsafeMutablePointer.allocate(capacity: Int(expectedCount)) 65 | let realCount: Int32 = objc_getClassList(AutoreleasingUnsafeMutablePointer(buffer), expectedCount) 66 | 67 | // iterate classes 68 | for i in 0.. [AnyClass] { 82 | let classes = classList() 83 | var results = [AnyClass]() 84 | 85 | // iterate classes 86 | for cls in classes { 87 | if class_conformsToProtocol(cls, aProtocol) { 88 | results.append(cls) 89 | } 90 | } 91 | 92 | return results 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Sources/NSView+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 22/09/2016. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSView { 12 | 13 | /// Returns the first subview that matches specified class. 14 | @objc func deepSubview(withClassName className: String) -> NSView? { 15 | 16 | // search level below (view subviews) 17 | for subview: NSView in self.subviews where subview.className == className { 18 | return subview 19 | } 20 | 21 | // search deeper 22 | for subview: NSView in self.subviews { 23 | if let foundView = subview.deepSubview(withClassName: className) { 24 | return foundView 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/NotificationName+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationName+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 07/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Notifications defined by `ThemeKit`. 13 | */ 14 | public extension Notification.Name { 15 | 16 | /// ThemeKit notification sent when current theme is about to change. 17 | static let willChangeTheme = Notification.Name("ThemeKitWillChangeThemeNotification") 18 | 19 | /// ThemeKit notification sent when current theme did change. 20 | static let didChangeTheme = Notification.Name("ThemeKitDidChangeThemeNotification") 21 | 22 | /// ThemeKit notification sent when system theme did change (System Preference > General > Appearance). 23 | static let didChangeSystemTheme = Notification.Name("ThemeKitDidChangeSystemThemeNotification") 24 | 25 | /// Convenience property for the system notification sent when System Preference for dark mode changes. 26 | static let didChangeAppleInterfaceTheme = Notification.Name("AppleInterfaceThemeChangedNotification") 27 | } 28 | -------------------------------------------------------------------------------- /Sources/String+Substring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Substring.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | /// Convenience subscript with Int to get character as Character. 14 | internal subscript (i: Int) -> Character { 15 | return self[self.index(self.startIndex, offsetBy: i)] 16 | } 17 | 18 | /// Convenience subscript with Int to get character as String. 19 | internal subscript (i: Int) -> String { 20 | return String(self[i] as Character) 21 | } 22 | 23 | /// Convenience function to get substring Range instead of Range. 24 | internal subscript (r: Range) -> String { 25 | let start = self.index(self.startIndex, offsetBy: r.lowerBound) 26 | let end = self.index(self.startIndex, offsetBy: r.upperBound) 27 | 28 | return String(self[start...end]) 29 | } 30 | 31 | /// Convenience function to get substring with NSRange. 32 | internal func substring(withNSRange: NSRange) -> String { 33 | guard withNSRange.location < self.count else { return "" } 34 | let start = self.index(self.startIndex, offsetBy: withNSRange.location) 35 | let end = self.index(start, offsetBy: withNSRange.length) 36 | let range = Range(uncheckedBounds: (lower: start, upper: end)) 37 | return String(self[range]) 38 | } 39 | 40 | /// Convenience function to replace characters with NSRange. 41 | internal func replacingCharacters(inNSRange: NSRange, with: String) -> String { 42 | guard inNSRange.location < self.count else { return "" } 43 | let start = self.index(self.startIndex, offsetBy: inNSRange.location) 44 | let end = self.index(start, offsetBy: inNSRange.length) 45 | let range = Range(uncheckedBounds: (lower: start, upper: end)) 46 | return self.replacingCharacters(in: range, with: with) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SystemTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemTheme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// System theme. 12 | /// 13 | /// Will dynamically resolve to either `ThemeManager.lightTheme` or `ThemeManager.darkTheme`, 14 | /// depending on the macOS preference at **System Preferences > General > Appearance**. 15 | @objc(TKSystemTheme) 16 | public class SystemTheme: NSObject, Theme { 17 | 18 | /// System theme identifier (static). 19 | @objc public static var identifier: String = "com.luckymarmot.ThemeKit.SystemTheme" 20 | 21 | /// Unique theme identifier. 22 | public var identifier: String = SystemTheme.identifier 23 | 24 | /// Theme display name. 25 | public var displayName: String { 26 | let systemVersion = OperatingSystemVersion(majorVersion: 10, minorVersion: 12, patchVersion: 0) 27 | return ProcessInfo.processInfo.isOperatingSystemAtLeast(systemVersion) ? "macOS Theme" : "OS X Theme" 28 | } 29 | 30 | /// Theme short display name. 31 | public var shortDisplayName: String { 32 | return displayName 33 | } 34 | 35 | /// Is this a dark theme? 36 | public var isDarkTheme: Bool = SystemTheme.isAppleInterfaceThemeDark 37 | 38 | /// Checks if Apple UI theme is set to dark, as set on **System Preferences > General > Appearance**. 39 | @objc public static var isAppleInterfaceThemeDark: Bool = SystemTheme.isAppleInterfaceThemeDarkOnUserDefaults() 40 | 41 | /// Calling `init()` is not allowed outside this library. 42 | /// Use `ThemeManager.systemTheme` instead. 43 | internal override init() { 44 | super.init() 45 | 46 | // Observe macOS Apple Interface Theme 47 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(appleInterfaceThemeDidChange(_:)), name: .didChangeAppleInterfaceTheme, object: nil) 48 | } 49 | 50 | /// Apple UI Theme has changed. 51 | @objc func appleInterfaceThemeDidChange(_ notification: Notification) { 52 | isDarkTheme = SystemTheme.isAppleInterfaceThemeDarkOnUserDefaults() 53 | SystemTheme.isAppleInterfaceThemeDark = isDarkTheme 54 | NotificationCenter.default.post(name: .didChangeSystemTheme, object: nil) 55 | } 56 | 57 | /// Read Apple Interface Theme preference from User Defaults. 58 | private static func isAppleInterfaceThemeDarkOnUserDefaults() -> Bool { 59 | return UserDefaults.standard.string(forKey: "AppleInterfaceStyle") != nil 60 | } 61 | 62 | override public var description: String { 63 | return "<\(SystemTheme.self): \(themeDescription(self))>" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Theme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Theme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Theme protocol: the base of all themes. 13 | 14 | *ThemeKit* makes available, without any further coding: 15 | 16 | - a `LightTheme` (the default macOS theme) 17 | - a `DarkTheme` (the dark macOS theme, using `NSAppearanceNameVibrantDark`) 18 | - a `SystemTheme` (which dynamically resolve to either `LightTheme` or `DarkTheme 19 | depending on the macOS preference at **System Preferences > General > Appearance**) 20 | 21 | You can choose wheter or not to use these, and you can also implement your custom 22 | themes by: 23 | 24 | - implementing native `Theme` classes conforming to this protocol and `NSObject` 25 | - provide user themes (`UserTheme`) with `.theme` files 26 | 27 | Please check the provided *Demo.app* project for sample implementations of both. 28 | 29 | */ 30 | @objc(TKTheme) 31 | public protocol Theme: NSObjectProtocol { 32 | // MARK: Required Properties 33 | 34 | /// Unique theme identifier. 35 | var identifier: String { get } 36 | 37 | /// Theme display name. 38 | var displayName: String { get } 39 | 40 | /// Theme short display name. 41 | var shortDisplayName: String { get } 42 | 43 | /// Is this a dark theme? 44 | var isDarkTheme: Bool { get } 45 | 46 | // MARK: Optional Methods/Properties 47 | 48 | /// Optional: foreground color to be used on when a foreground color is not provided 49 | /// by the theme. 50 | @objc optional var fallbackForegroundColor: NSColor? { get } 51 | 52 | /// Optional: background color to be used on when a background color (a color which 53 | /// contains `Background` in its name) is not provided by the theme. 54 | @objc optional var fallbackBackgroundColor: NSColor? { get } 55 | 56 | /// Optional: gradient to be used on when a gradient is not provided by the theme. 57 | @objc optional var fallbackGradient: NSGradient? { get } 58 | 59 | /// Optional: image to be used on when an image is not provided by the theme. 60 | @objc optional var fallbackImage: NSImage? { get } 61 | } 62 | 63 | /// Theme protocol extension. 64 | /// 65 | /// These functions are available for all `Theme`s. 66 | public extension Theme { 67 | 68 | // MARK: Convenient Methods/Properties (Swift only) 69 | 70 | /// Is this a light theme? 71 | /// 72 | /// This method is not available from Objective-C. Alternative code: 73 | /// 74 | /// ```objc 75 | /// !aTheme.isDarkTheme 76 | /// ``` 77 | var isLightTheme: Bool { 78 | return !isDarkTheme 79 | } 80 | 81 | /// Is this the system theme? If true, theme automatically resolve to 82 | /// `ThemeManager.lightTheme` or `ThemeManager.darkTheme`, accordingly to 83 | /// **System Preferences > General > Appearance**. 84 | /// 85 | /// This method is not available from Objective-C. Alternative code: 86 | /// 87 | /// ```objc 88 | /// [aTheme.identifier isEqualToString:TKSystemTheme.identifier] 89 | /// ``` 90 | var isSystemTheme: Bool { 91 | return identifier == SystemTheme.identifier 92 | } 93 | 94 | /// Is this a user theme? 95 | /// 96 | /// This method is not available from Objective-C. Alternative code: 97 | /// 98 | /// ```objc 99 | /// [aTheme isKindOfClass:[TKUserTheme class]] 100 | /// ``` 101 | var isUserTheme: Bool { 102 | return self is UserTheme 103 | } 104 | 105 | /// Apply theme (make it the current one). 106 | /// 107 | /// This method is not available from Objective-C. Alternative code: 108 | /// 109 | /// ```objc 110 | /// [[TKThemeManager sharedManager] setTheme:aTheme] 111 | /// ``` 112 | func apply() { 113 | ThemeManager.shared.theme = self 114 | } 115 | 116 | /// Theme asset for the specified key. Supported assets are `NSColor`, `NSGradient`, `NSImage` and `NSString`. 117 | /// 118 | /// This function is overriden by `UserTheme`. 119 | /// 120 | /// This method is not available from Objective-C. 121 | /// 122 | /// - parameter key: A color name, gradient name, image name or a theme string 123 | /// 124 | /// - returns: The theme value for the specified key. 125 | func themeAsset(_ key: String) -> Any? { 126 | // Because `Theme` is an @objc protocol, we cannot define this method on 127 | // the protocol and a provide a default implementation on this extension, 128 | // plus another on `UserTheme`. This is a workaround to accomplish it. 129 | if let userTheme = self as? UserTheme { 130 | return userTheme.themeAsset(key) 131 | } 132 | 133 | let selector = NSSelectorFromString(key) 134 | if let theme = self as? NSObject, 135 | theme.responds(to: selector) { 136 | return theme.perform(selector).takeUnretainedValue() 137 | } 138 | 139 | return nil 140 | } 141 | 142 | /// Checks if a theme asset is provided for the given key. 143 | /// 144 | /// This function is overriden by `UserTheme`. 145 | /// 146 | /// This method is not available from Objective-C. 147 | /// 148 | /// - parameter key: A color name, gradient name, image name or a theme string 149 | /// 150 | /// - returns: `true` if theme provides an asset for the given key; `false` otherwise. 151 | func hasThemeAsset(_ key: String) -> Bool { 152 | return themeAsset(key) != nil 153 | } 154 | 155 | /// Default foreground color to be used on fallback situations when 156 | /// no `fallbackForegroundColor` was specified by the theme. 157 | /// 158 | /// This method is not available from Objective-C. Alternative code: 159 | /// 160 | /// ```objc 161 | /// aTheme.isDarkTheme ? NSColor.whiteColor : NSColor.blackColor 162 | /// ``` 163 | var defaultFallbackForegroundColor: NSColor { 164 | return isLightTheme ? NSColor.black : NSColor.white 165 | } 166 | 167 | /// Default background color to be used on fallback situations when 168 | /// no `fallbackBackgroundColor` was specified by the theme (background color 169 | /// is a color method that contains `Background` in its name). 170 | /// 171 | /// This method is not available from Objective-C. Alternative code: 172 | /// 173 | /// ```objc 174 | /// aTheme.isDarkTheme ? NSColor.blackColor : NSColor.whiteColor 175 | /// ``` 176 | var defaultFallbackBackgroundColor: NSColor { 177 | return isLightTheme ? NSColor.white : NSColor.black 178 | } 179 | 180 | /// Default gradient to be used on fallback situations when 181 | /// no `fallbackForegroundColor` was specified by the theme. 182 | /// 183 | /// This method is not available from Objective-C. Alternative code: 184 | /// 185 | /// ```objc 186 | /// [[NSGradient alloc] initWithStartingColor:(aTheme.isDarkTheme ? NSColor.blackColor : NSColor.whiteColor) endingColor:(aTheme.isDarkTheme ? NSColor.whiteColor : NSColor.blackColor)] 187 | /// ``` 188 | var defaultFallbackGradient: NSGradient? { 189 | return NSGradient(starting: defaultFallbackBackgroundColor, ending: defaultFallbackBackgroundColor) 190 | } 191 | 192 | /// Default image to be used on fallback situations when 193 | /// no image was specified by the theme. 194 | /// 195 | /// This method is not available from Objective-C. Alternative code: 196 | /// 197 | /// ```objc 198 | /// [[NSImage alloc] initWithSize:NSZeroSize] 199 | /// ``` 200 | var defaultFallbackImage: NSImage { 201 | return NSImage(size: NSSize.zero) 202 | } 203 | 204 | /// Effective theme, which can be different from itself if it represents the 205 | /// system theme, respecting **System Preferences > General > Appearance** 206 | /// (in that case it will be either `ThemeManager.lightTheme` or `ThemeManager.darkTheme`). 207 | /// 208 | /// This method is not available from Objective-C. Alternative code: 209 | /// 210 | /// ```objc 211 | /// [aTheme.identifier isEqualToString:TKSystemTheme.identifier] ? (aTheme.isDarkTheme ? TKThemeManager.darkTheme : TKThemeManager.lightTheme) : aTheme; 212 | /// ``` 213 | var effectiveTheme: Theme { 214 | if isSystemTheme { 215 | return isDarkTheme ? ThemeManager.darkTheme : ThemeManager.lightTheme 216 | } else { 217 | return self 218 | } 219 | } 220 | 221 | /// Theme description. 222 | func themeDescription(_ theme: Theme) -> String { 223 | return "\"\(displayName)\" [\(identifier)]\(isDarkTheme ? " (Dark)" : "")" 224 | } 225 | } 226 | 227 | /// Check if themes are the same. 228 | func == (lhs: Theme, rhs: Theme) -> Bool { 229 | return lhs.identifier == rhs.identifier 230 | } 231 | 232 | /// Check if themes are different. 233 | func != (lhs: Theme, rhs: Theme) -> Bool { 234 | return lhs.identifier != rhs.identifier 235 | } 236 | -------------------------------------------------------------------------------- /Sources/ThemeKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeKit.h 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 21/09/2016. 6 | // Copyright © 2016-2017 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ThemeKit. 12 | FOUNDATION_EXPORT double ThemeKitVersionNumber; 13 | 14 | //! Project version string for ThemeKit. 15 | FOUNDATION_EXPORT const unsigned char ThemeKitVersionString[]; 16 | -------------------------------------------------------------------------------- /Sources/ThemeManager+ObjectiveC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeManager+ObjectiveC.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 09/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension ThemeManager { 12 | 13 | /// Window theme policies that define which windows should be automatically themed, if any (Objective-C variant, only). 14 | @objc(TKThemeManagerWindowThemePolicy) 15 | enum TKThemeManagerWindowThemePolicy: Int { 16 | /// Theme all application windows (default). 17 | case themeAllWindows 18 | /// Only theme windows of the specified classes. 19 | case themeSomeWindows 20 | /// Do not theme windows of the specified classes. 21 | case doNotThemeSomeWindows 22 | /// Do not theme any window. 23 | case doNotThemeWindows 24 | } 25 | 26 | /// Current window theme policy. 27 | @objc(windowThemePolicy) 28 | var objc_windowThemePolicy: TKThemeManagerWindowThemePolicy { 29 | get { 30 | switch windowThemePolicy { 31 | 32 | case .themeAllWindows: 33 | return .themeAllWindows 34 | 35 | case .themeSomeWindows: 36 | return .themeSomeWindows 37 | 38 | case .doNotThemeSomeWindows: 39 | return .doNotThemeSomeWindows 40 | 41 | case .doNotThemeWindows: 42 | return .doNotThemeWindows 43 | } 44 | } 45 | set(value) { 46 | switch value { 47 | case .themeAllWindows: 48 | windowThemePolicy = .themeAllWindows 49 | case .themeSomeWindows: 50 | windowThemePolicy = .themeSomeWindows(windowClasses: themableWindowClasses ?? []) 51 | case .doNotThemeSomeWindows: 52 | windowThemePolicy = .doNotThemeSomeWindows(windowClasses: notThemableWindowClasses ?? []) 53 | case .doNotThemeWindows: 54 | windowThemePolicy = .doNotThemeWindows 55 | } 56 | } 57 | } 58 | 59 | /// Windows classes to be excluded from theming with the `TKThemeManagerWindowThemePolicyDoNotThemeSomeWindows`. 60 | @objc(notThemableWindowClasses) 61 | var notThemableWindowClasses: [AnyClass]? { 62 | get { 63 | switch windowThemePolicy { 64 | 65 | case .themeAllWindows: 66 | return nil 67 | 68 | case .themeSomeWindows: 69 | return [] 70 | 71 | case .doNotThemeSomeWindows(let windowClasses): 72 | return windowClasses 73 | 74 | case .doNotThemeWindows: 75 | return [] 76 | } 77 | } 78 | set(value) { 79 | if let newValue = value { 80 | if newValue.count > 0 { 81 | // theme some if value > 0 82 | windowThemePolicy = .doNotThemeSomeWindows(windowClasses: newValue) 83 | } else { 84 | // theme none if value is 0 85 | windowThemePolicy = .doNotThemeWindows 86 | } 87 | } else { 88 | // theme all windows if value is nil 89 | windowThemePolicy = .themeAllWindows 90 | } 91 | } 92 | } 93 | 94 | /// Windows classes to be themed with the `TKThemeManagerWindowThemePolicyThemeSomeWindows`. 95 | @objc(themableWindowClasses) 96 | var themableWindowClasses: [AnyClass]? { 97 | get { 98 | switch windowThemePolicy { 99 | 100 | case .themeAllWindows: 101 | return nil 102 | 103 | case .themeSomeWindows(let windowClasses): 104 | return windowClasses 105 | 106 | case .doNotThemeSomeWindows: 107 | return [] 108 | 109 | case .doNotThemeWindows: 110 | return [] 111 | } 112 | } 113 | set(value) { 114 | if let newValue = value { 115 | if newValue.count > 0 { 116 | // theme some if value > 0 117 | windowThemePolicy = .themeSomeWindows(windowClasses: newValue) 118 | } else { 119 | // theme none if value is 0 120 | windowThemePolicy = .doNotThemeWindows 121 | } 122 | } else { 123 | // theme all windows if value is nil 124 | windowThemePolicy = .themeAllWindows 125 | } 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Sources/Thread+ThemeKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Thread+ThemeKit.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 08/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Thread { 12 | 13 | /// Make sure code block is executed on main thread. 14 | @objc class func onMain(block: @escaping () -> Void) { 15 | if Thread.isMainThread { 16 | block() 17 | } else { 18 | DispatchQueue.main.async { 19 | block() 20 | } 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/UserTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTheme.swift 3 | // ThemeKit 4 | // 5 | // Created by Nuno Grilo on 06/09/16. 6 | // Copyright © 2016 Paw & Nuno Grilo. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | A `Theme` class wrapping a user provided theme file (`.theme`). 13 | 14 | To enable user themes, set theme folder on `ThemeManager.userThemesFolderURL`. 15 | 16 | Notes about `.theme` files: 17 | 18 | - lines starting with `#` or `//` will be treated as comments, thus, ignored; 19 | - non-comment lines consists on simple variable/value assignments (eg, `variable = value`); 20 | - `variable` name can contain characters `[a-zA-Z0-9_-.]+`; 21 | - custom variables can be specified (eg, `myBackgroundColor = ...`); 22 | - theming properties match the class methods of `ThemeColor`, `ThemeGradient` and `ThemeImage` (eg, `labelColor`); 23 | - variables can be referenced by prefixing them with `$` (eg, `mainBorderColor = $commonBorderColor`); 24 | - colors are defined using `rgb(255, 255, 255)` or `rgba(255, 255, 255, 1.0)` (case insensitive); 25 | - gradients are defined using `linear-gradient(color1, color2)` (where colors are defined as above; case insensitive); 26 | - pattern images are defined using `pattern(named:xxxx)` (named images) or `pattern(file:../dddd/xxxx.yyy)` (filesystem images); 27 | - images are defined using `image(named:xxxx)` (named images) or `image(file:../dddd/xxxx.yyy)` (filesystem images); 28 | - `ThemeManager.themes` property is automatically updated when there are changes on the user themes folder; 29 | - file changes are applied on-the-fly, if it corresponds to the currently applied theme. 30 | 31 | Example `.theme` file: 32 | 33 | ```ruby 34 | // ************************* Theme Info ************************* // 35 | displayName = My Theme 1 36 | identifier = com.luckymarmot.ThemeKit.MyTheme1 37 | darkTheme = true 38 | 39 | // ********************* Colors & Gradients ********************* // 40 | # define color for `ThemeColor.brandColor` 41 | brandColor = $blue 42 | # define a new color for `NSColor.labelColor` (overriding) 43 | labelColor = rgb(11, 220, 111) 44 | # define gradient for `ThemeGradient.brandGradient` 45 | brandGradient = linear-gradient($orange.sky, rgba(200, 140, 60, 1.0)) 46 | 47 | // ********************* Images & Patterns ********************** // 48 | # define pattern image from named image "paper" for color `ThemeColor.contentBackgroundColor` 49 | contentBackgroundColor = pattern(named:paper) 50 | # define pattern image from filesystem (relative to user themes folder) for color `ThemeColor.bottomBackgroundColor` 51 | bottomBackgroundColor = pattern(file:../some/path/some-file.png) 52 | # use named image "apple" 53 | namedImage = image(named:apple) 54 | # use image from filesystem (relative to user themes folder) 55 | fileImage = image(file:../some/path/some-file.jpg) 56 | 57 | // *********************** Common Colors ************************ // 58 | blue = rgb(0, 170, 255) 59 | orange.sky = rgb(160, 90, 45, .5) 60 | 61 | // ********************** Fallback Assets *********************** // 62 | fallbackForegroundColor = rgb(255, 10, 90, 1.0) 63 | fallbackBackgroundColor = rgb(255, 200, 190) 64 | fallbackGradient = linear-gradient($blue, rgba(200, 140, 60, 1.0)) 65 | ``` 66 | 67 | With the exception of system overrided named colors (e.g., `labelColor`), which 68 | defaults to the original system provided named color, unimplemented properties 69 | on theme file will default to `-fallbackForegroundColor`, `-fallbackBackgroundColor`, 70 | `-fallbackGradient` and `-fallbackImage`, for foreground color, background color, 71 | gradients and images, respectively. 72 | 73 | 74 | */ 75 | @objc(TKUserTheme) 76 | public class UserTheme: NSObject, Theme { 77 | /// Unique theme identifier. 78 | public var identifier: String = "{Theme-Not-Loaded}" 79 | 80 | /// Theme display name. 81 | public var displayName: String = "Theme Not Loaded" 82 | 83 | /// Theme short display name. 84 | public var shortDisplayName: String = "Not Loaded" 85 | 86 | /// Is this a dark theme? 87 | public var isDarkTheme: Bool = false 88 | 89 | /// File URL. 90 | @objc public var fileURL: URL? 91 | 92 | /// Dictionary with key/values pairs read from the .theme file 93 | private var _keyValues: NSMutableDictionary = NSMutableDictionary() 94 | 95 | /// Dictionary with evaluated key/values pairs read from the .theme file 96 | private var _evaluatedKeyValues: NSMutableDictionary = NSMutableDictionary() 97 | 98 | // MARK: - 99 | // MARK: Initialization 100 | 101 | /// `init()` is disabled. 102 | private override init() { 103 | super.init() 104 | } 105 | 106 | /// Calling `init(_:)` is not allowed outside this library. 107 | /// Use `ThemeManager.shared.theme(:_)` instead. 108 | /// 109 | /// - parameter themeFileURL: A theme file (`.theme`) URL. 110 | /// 111 | /// - returns: An instance of `UserTheme`. 112 | @objc internal init(_ themeFileURL: URL) { 113 | super.init() 114 | 115 | // Load file 116 | fileURL = themeFileURL 117 | loadThemeFile(from: themeFileURL) 118 | } 119 | 120 | /// Reloads user theme from file. 121 | @objc public func reload() { 122 | if let url = fileURL { 123 | _keyValues.removeAllObjects() 124 | _evaluatedKeyValues.removeAllObjects() 125 | loadThemeFile(from: url) 126 | } 127 | } 128 | 129 | // MARK: - 130 | // MARK: Theme Assets 131 | 132 | /// Theme asset for the specified key. Supported assets are `NSColor`, `NSGradient`, `NSImage` and `NSString`. 133 | /// 134 | /// - parameter key: A color name, gradient name, image name or a theme string 135 | /// 136 | /// - returns: The theme value for the specified key. 137 | @objc public func themeAsset(_ key: String) -> Any? { 138 | var value = _evaluatedKeyValues[key] 139 | if value == nil, 140 | let evaluatedValue = _keyValues.evaluatedObject(key: key) { 141 | value = evaluatedValue 142 | _evaluatedKeyValues.setObject(evaluatedValue, forKey: key as NSString) 143 | } 144 | return value 145 | } 146 | 147 | /// Checks if a theme asset is provided for the given key. 148 | /// 149 | /// Do not check for theme asset availability with `themeAsset(_:)`, use 150 | /// this method instead, which is much faster. 151 | /// 152 | /// - parameter key: A color name, gradient name, image name or a theme string 153 | /// 154 | /// - returns: `true` if theme provides an asset for the given key; `false` otherwise. 155 | @objc public func hasThemeAsset(_ key: String) -> Bool { 156 | return _keyValues[key] != nil 157 | } 158 | 159 | // MARK: - 160 | // MARK: File 161 | 162 | /// Load theme file 163 | /// 164 | /// - parameter from: A theme file (`.theme`) URL. 165 | private func loadThemeFile(from: URL) { 166 | // Load contents from theme file 167 | if let themeContents = try? String(contentsOf: from, encoding: String.Encoding.utf8) { 168 | 169 | // Split content into lines 170 | var lineCharset = CharacterSet(charactersIn: ";") 171 | lineCharset.formUnion(CharacterSet.newlines) 172 | let lines: [String] = themeContents.components(separatedBy: lineCharset) 173 | 174 | // Parse lines 175 | for line in lines { 176 | // Trim 177 | let trimmedLine = line.trimmingCharacters(in: CharacterSet.whitespaces) 178 | 179 | // Skip comments 180 | if trimmedLine.hasPrefix("#") || trimmedLine.hasPrefix("//") { 181 | continue 182 | } 183 | 184 | // Assign theme key-values (lazy evaluation) 185 | let assignment = trimmedLine.components(separatedBy: "=") 186 | if assignment.count == 2 { 187 | let key = assignment[0].trimmingCharacters(in: CharacterSet.whitespaces) 188 | let value = assignment[1].trimmingCharacters(in: CharacterSet.whitespaces) 189 | _keyValues.setObject(value, forKey: key as NSString) 190 | } 191 | } 192 | 193 | // Initialize properties with evaluated values from file 194 | 195 | // Identifier 196 | if let identifierString = themeAsset("identifier") as? String { 197 | identifier = identifierString 198 | } else { 199 | identifier = "{identifier: is mising}" 200 | } 201 | 202 | // Display Name 203 | if let displayNameString = themeAsset("displayName") as? String { 204 | displayName = displayNameString 205 | } else { 206 | displayName = "{displayName: is mising}" 207 | } 208 | 209 | // Short Display Name 210 | if let shortDisplayNameString = themeAsset("shortDisplayName") as? String { 211 | shortDisplayName = shortDisplayNameString 212 | } else { 213 | shortDisplayName = "{shortDisplayName: is mising}" 214 | } 215 | 216 | // Dark? 217 | if let isDarkThemeString = themeAsset("darkTheme") as? String { 218 | isDarkTheme = NSString(string: isDarkThemeString).boolValue 219 | } else { 220 | isDarkTheme = false 221 | } 222 | } 223 | } 224 | 225 | override public var description: String { 226 | return "<\(UserTheme.self): \(themeDescription(self))>" 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /ThemeKit.xcodeproj/xcshareddata/xcschemes/ThemeKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /macOSThemeKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'macOSThemeKit' 3 | s.version = '1.4.0' 4 | s.license = { :type => 'MIT', :file => 'LICENSE' } 5 | s.summary = 'macOS Theming Framework' 6 | s.homepage = 'https://github.com/luckymarmot/ThemeKit' 7 | s.screenshots = 'https://github.com/luckymarmot/ThemeKit/raw/master/Imgs/ThemeKit.gif' 8 | s.authors = { 'Paw' => 'https://paw.cloud', 9 | 'Nuno Grilo' => 'http://nunogrilo.com' } 10 | s.source = { :git => 'https://github.com/luckymarmot/ThemeKit.git', 11 | :tag => s.version } 12 | s.documentation_url = 'http://themekit.nunogrilo.com' 13 | 14 | s.platform = :osx, '10.10' 15 | s.requires_arc = true 16 | s.swift_version = '5.4.1' 17 | 18 | s.source_files = 'Sources/**/*.swift' 19 | end 20 | --------------------------------------------------------------------------------