diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index fd8dea0..304bc2d 100644 --- a/MTMR.xcodeproj/project.pbxproj +++ b/MTMR.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 36C2ECE0207CB1B0003CDA33 /* defaultPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */; }; 6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; }; 6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; }; + B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0008E542080286C003AD4DD /* SupportHelpers.swift */; }; B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D621205E03F5006E6B86 /* TouchBarController.swift */; }; B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */; }; B059D62D205F11E8006E6B86 /* DFRFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B059D62C205F11E8006E6B86 /* DFRFoundation.framework */; }; @@ -57,6 +58,7 @@ 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultPreset.json; sourceTree = ""; }; 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = ""; }; 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = ""; }; + B0008E542080286C003AD4DD /* SupportHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportHelpers.swift; sourceTree = ""; }; B059D621205E03F5006E6B86 /* TouchBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarController.swift; sourceTree = ""; }; B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButtonTouchBarItem.swift; sourceTree = ""; }; B059D629205E13E5006E6B86 /* TouchBarPrivateApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarPrivateApi.h; sourceTree = ""; }; @@ -150,6 +152,7 @@ 36C2ECD6207B6DAE003CDA33 /* TimeTouchBarItem.swift */, B059D621205E03F5006E6B86 /* TouchBarController.swift */, 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */, + B0008E542080286C003AD4DD /* SupportHelpers.swift */, B082B258205C7D8000BC04DC /* Main.storyboard */, B082B25B205C7D8000BC04DC /* Info.plist */, B082B25C205C7D8000BC04DC /* MTMR.entitlements */, @@ -317,6 +320,7 @@ B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */, 36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */, B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */, + B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */, B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */, B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */, 6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */, diff --git a/MTMR/AppleScriptTouchBarItem.swift b/MTMR/AppleScriptTouchBarItem.swift index 9c01521..1e71b04 100644 --- a/MTMR/AppleScriptTouchBarItem.swift +++ b/MTMR/AppleScriptTouchBarItem.swift @@ -5,7 +5,7 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem { private let interval: TimeInterval private var forceHideConstraint: NSLayoutConstraint! - init?(identifier: NSTouchBarItem.Identifier, source: Source, interval: TimeInterval, onTap: @escaping ()->()) { + init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval, onTap: @escaping ()->()) { self.interval = interval super.init(identifier: identifier, title: "compile", onTap: onTap) self.forceHideConstraint = self.view.widthAnchor.constraint(equalToConstant: 0) @@ -56,7 +56,7 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem { } -extension Source { +extension SourceProtocol { var appleScript: NSAppleScript? { guard let source = self.string else { return nil } return NSAppleScript(source: source) diff --git a/MTMR/CustomButtonTouchBarItem.swift b/MTMR/CustomButtonTouchBarItem.swift index 0a91d83..d3ba783 100644 --- a/MTMR/CustomButtonTouchBarItem.swift +++ b/MTMR/CustomButtonTouchBarItem.swift @@ -12,15 +12,10 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem { let tapClosure: () -> () private(set) var button: NSButton! - init(identifier: NSTouchBarItem.Identifier, title: String, onTap callback: @escaping () -> (), image: NSImage? = nil) { + init(identifier: NSTouchBarItem.Identifier, title: String, onTap callback: @escaping () -> ()) { self.tapClosure = callback super.init(identifier: identifier) - if let image = image { - button = NSButton(title: title, image: image, target: self, action: #selector(didTapped)) - button.bezelColor = .clear - } else { - button = NSButton(title: title, target: self, action: #selector(didTapped)) - } + button = NSButton(title: title, target: self, action: #selector(didTapped)) self.view = button } diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index a32d6de..3b6a773 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -30,8 +30,8 @@ struct BarItemDefinition: Decodable { let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type) let additionalParameters = try GeneralParameters(from: decoder).parameters if let result = try? parametersDecoder(decoder), - case let (itemType, action) = result { - self.init(type: itemType, action: action, additionalParameters: additionalParameters) + case let (itemType, action, parameters) = result { + self.init(type: itemType, action: action, additionalParameters: additionalParameters + parameters) } else { self.init(type: .staticButton(title: "unknown"), action: .none, additionalParameters: additionalParameters) } @@ -40,23 +40,38 @@ struct BarItemDefinition: Decodable { } class SupportedTypesHolder { - typealias ParametersDecoder = (Decoder) throws ->(item: ItemType, action: ActionType) + typealias ParametersDecoder = (Decoder) throws ->(item: ItemType, action: ActionType, parameters: [GeneralParameter]) private var supportedTypes: [String: ParametersDecoder] = [ - "escape": { _ in return (item: .staticButton(title: "esc"), action: .keyPress(keycode: 53)) }, - "brightnessUp": { _ in return (item: .staticButton(title: "🔆"), action: .keyPress(keycode: 113)) }, - "brightnessDown": { _ in return (item: .staticButton(title: "🔅"), action: .keyPress(keycode: 107)) }, - "volumeDown": { _ in return (item: .staticImageButton(title: "", image: NSImage(named: .touchBarVolumeDownTemplate)!), action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN)) }, - "volumeUp": { _ in return (item: .staticImageButton(title: "", image: NSImage(named: .touchBarVolumeUpTemplate)!), action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP)) }, - "previous": { _ in return (item: .staticImageButton(title: "", image: NSImage(named: .touchBarRewindTemplate)!), action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS)) }, - "play": { _ in return (item: .staticImageButton(title: "", image: NSImage(named: .touchBarPlayPauseTemplate)!), action: .hidKey(keycode: NX_KEYTYPE_PLAY)) }, - "next": { _ in return (item: .staticImageButton(title: "", image: NSImage(named: .touchBarFastForwardTemplate)!), action: .hidKey(keycode: NX_KEYTYPE_NEXT)) }, + "escape": { _ in return (item: .staticButton(title: "esc"), action: .keyPress(keycode: 53), parameters: []) }, + "brightnessUp": { _ in return (item: .staticButton(title: "🔆"), action: .keyPress(keycode: 113), parameters: []) }, + "brightnessDown": { _ in return (item: .staticButton(title: "🔅"), action: .keyPress(keycode: 107), parameters: []) }, + "volumeDown": { _ in + let imageParameter = GeneralParameter.image(source: NSImage(named: .touchBarVolumeDownTemplate)!) + return (item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN), parameters: [imageParameter]) + }, + "volumeUp": { _ in + let imageParameter = GeneralParameter.image(source: NSImage(named: .touchBarVolumeUpTemplate)!) + return (item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP), parameters: [imageParameter]) + }, + "previous": { _ in + let imageParameter = GeneralParameter.image(source: NSImage(named: .touchBarRewindTemplate)!) + return (item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS), parameters: [imageParameter]) + }, + "play": { _ in + let imageParameter = GeneralParameter.image(source: NSImage(named: .touchBarPlayPauseTemplate)!) + return (item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_PLAY), parameters: [imageParameter]) + }, + "next": { _ in + let imageParameter = GeneralParameter.image(source: NSImage(named: .touchBarFastForwardTemplate)!) + return (item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_NEXT), parameters: [imageParameter]) + }, "weather": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval } let container = try decoder.container(keyedBy: CodingKeys.self) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) let scriptPath = Bundle.main.path(forResource: "Weather", ofType: "scpt")! let item = ItemType.appleScriptTitledButton(source: Source(filePath: scriptPath), refreshInterval: interval ?? 1800.0) - return (item: item, action: .none) + return (item: item, action: .none, parameters: []) }, "battery": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval } @@ -64,17 +79,17 @@ class SupportedTypesHolder { let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) let scriptPath = Bundle.main.path(forResource: "Battery", ofType: "scpt")! let item = ItemType.appleScriptTitledButton(source: Source(filePath: scriptPath), refreshInterval: interval ?? 1800.0) - return (item: item, action: .none) + return (item: item, action: .none, parameters: []) }, - "sleep": { _ in return (item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]) ) }, - "displaySleep": { _ in return (item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]) ) }, + "sleep": { _ in return (item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]), parameters: []) }, + "displaySleep": { _ in return (item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]), parameters: []) }, ] static let sharedInstance = SupportedTypesHolder() func lookup(by type: String) -> ParametersDecoder { return supportedTypes[type] ?? { decoder in - return (item: try ItemType(from: decoder), action: try ActionType(from: decoder)) + return (item: try ItemType(from: decoder), action: try ActionType(from: decoder), parameters: []) } } @@ -84,15 +99,14 @@ class SupportedTypesHolder { func register(typename: String, item: ItemType, action: ActionType) { register(typename: typename) { _ in - return (item: item, action: action) + return (item: item, action: action, parameters: []) } } } enum ItemType: Decodable { case staticButton(title: String) - case staticImageButton(title: String, image: NSImage) - case appleScriptTitledButton(source: Source, refreshInterval: Double) + case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case timeButton(formatTemplate: String) case flexSpace() case volume() @@ -109,7 +123,6 @@ enum ItemType: Decodable { enum ItemTypeRaw: String, Decodable { case staticButton - case staticImageButton case appleScriptTitledButton case timeButton case flexSpace @@ -125,15 +138,6 @@ enum ItemType: Decodable { let source = try container.decode(Source.self, forKey: .source) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0 self = .appleScriptTitledButton(source: source, refreshInterval: interval) - case .staticImageButton: - let title = try container.decode(String.self, forKey: .title) - let imageRaw = try container.decode(String.self, forKey: .image) - if let decodedImageData = Data(base64Encoded: imageRaw, options: .ignoreUnknownCharacters) { - let decImage = NSImage(data: decodedImageData)! - self = .staticImageButton(title: title, image: decImage) - } else { - self = .staticButton(title: title) - } case .staticButton: let title = try container.decode(String.self, forKey: .title) self = .staticButton(title: title) @@ -154,7 +158,7 @@ enum ActionType: Decodable { case none case hidKey(keycode: Int) case keyPress(keycode: Int) - case appleSctipt(source: Source) + case appleSctipt(source: SourceProtocol) case shellScript(executable: String, parameters: [String]) case custom(closure: ()->()) @@ -230,6 +234,7 @@ func ==(lhs: ActionType, rhs: ActionType) -> Bool { enum GeneralParameter { case width(_: CGFloat) + case image(source: SourceProtocol) } struct GeneralParameters: Decodable { @@ -237,6 +242,7 @@ struct GeneralParameters: Decodable { fileprivate enum CodingKeys: String, CodingKey { case width + case image } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -244,20 +250,36 @@ struct GeneralParameters: Decodable { if let value = try container.decodeIfPresent(CGFloat.self, forKey: .width) { result.append(.width(value)) } + if let imageSource = try container.decodeIfPresent(Source.self, forKey: .image) { + result.append(.image(source: imageSource)) + } parameters = result } } - -struct Source: Decodable { +protocol SourceProtocol { + var data: Data? { get } + var string: String? { get } + var image: NSImage? { get } +} +struct Source: Decodable, SourceProtocol { let filePath: String? let base64: String? let inline: String? - + + private enum CodingKeys: String, CodingKey { + case filePath + case base64 + case inline + } + var data: Data? { return base64?.base64Data ?? inline?.data(using: .utf8) ?? filePath?.fileData } var string: String? { - return inline ?? self.data?.base64Content + return inline ?? self.data?.utf8string + } + var image: NSImage? { + return data?.image } private init(filePath: String?, base64: String?, inline: String?) { @@ -269,8 +291,19 @@ struct Source: Decodable { self.init(filePath: filePath, base64: nil, inline: nil) } } -extension Source: Equatable {} -func ==(left: Source, right: Source) -> Bool { +extension NSImage: SourceProtocol { + var data: Data? { + return nil + } + var string: String? { + return nil + } + var image: NSImage? { + return self + } +} +extension SourceProtocol where Self: Equatable {} +func ==(left: SourceProtocol, right: SourceProtocol) -> Bool { return left.data == right.data } @@ -283,7 +316,10 @@ extension String { } } extension Data { - var base64Content: String? { + var utf8string: String? { return String(data: self, encoding: .utf8) } + var image: NSImage? { + return NSImage(data: self)?.resize(maxSize: NSSize(width: 24, height: 24)) + } } diff --git a/MTMR/SupportHelpers.swift b/MTMR/SupportHelpers.swift new file mode 100644 index 0000000..76ba761 --- /dev/null +++ b/MTMR/SupportHelpers.swift @@ -0,0 +1,52 @@ +// +// SupportHelpers.swift +// MTMR +// +// Created by Anton Palgunov on 13/04/2018. +// Copyright © 2018 Anton Palgunov. All rights reserved. +// + +import Foundation + +extension String { + func trim() -> String { + return self.trimmingCharacters(in: NSCharacterSet.whitespaces) + } +} + +extension NSImage { + func resize(maxSize:NSSize) -> NSImage { + var ratio:Float = 0.0 + let imageWidth = Float(self.size.width) + let imageHeight = Float(self.size.height) + let maxWidth = Float(maxSize.width) + let maxHeight = Float(maxSize.height) + + // Get ratio (landscape or portrait) + if (imageWidth > imageHeight) { + // Landscape + ratio = maxWidth / imageWidth; + } + else { + // Portrait + ratio = maxHeight / imageHeight; + } + + // Calculate new size based on the ratio + let newWidth = imageWidth * ratio + let newHeight = imageHeight * ratio + + // Create a new NSSize object with the newly calculated size + let newSize:NSSize = NSSize(width: Int(newWidth), height: Int(newHeight)) + + // Cast the NSImage to a CGImage + var imageRect:NSRect = NSMakeRect(0, 0, self.size.width, self.size.height) + let imageRef = self.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) + + // Create NSImage from the CGImage using the new size + let imageWithNewSize = NSImage(cgImage: imageRef!, size: newSize) + + // Return the new image + return imageWithNewSize + } +} diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index 6e42b9e..0eee90d 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -14,13 +14,11 @@ struct ExactItem { } extension ItemType { - + var identifierBase: String { switch self { case .staticButton(title: _): return "com.toxblh.mtmr.staticButton." - case .staticImageButton(title: _): - return "com.toxblh.mtmr.staticImageButton." case .appleScriptTitledButton(source: _): return "com.toxblh.mtmr.appleScriptButton." case .timeButton(formatTemplate: _): @@ -33,7 +31,7 @@ extension ItemType { return "com.toxblh.mtmr.brightness" } } - + } extension NSTouchBarItem.Identifier { @@ -43,23 +41,23 @@ extension NSTouchBarItem.Identifier { class TouchBarController: NSObject, NSTouchBarDelegate { static let shared = TouchBarController() - + let touchBar = NSTouchBar() - + var items: [NSTouchBarItem.Identifier: BarItemDefinition] = [:] - + private override init() { super.init() SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() })) - + loadItems() - + touchBar.delegate = self self.presentTouchBar() } - + func loadItems() { let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!.appending("/MTMR") let presetPath = appSupportDirectory.appending("/items.json") @@ -70,7 +68,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate { } let jsonData = presetPath.fileData let jsonItems = jsonData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, additionalParameters: [])] - + for item in jsonItems { let identifierString = item.type.identifierBase.appending(UUID().uuidString) let identifier = item.type == ItemType.flexSpace() @@ -88,30 +86,29 @@ class TouchBarController: NSObject, NSTouchBarDelegate { NSTouchBarItem.addSystemTrayItem(item) DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, true) } - + func updateControlStripPresence() { DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, true) } - + @objc private func presentTouchBar() { NSTouchBar.presentSystemModalFunctionBar(touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem) } - + @objc private func dismissTouchBar() { NSTouchBar.minimizeSystemModalFunctionBar(touchBar) } - + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { guard let item = self.items[identifier] else { return nil } let action = self.action(forItem: item) + var barItem: NSTouchBarItem! switch item.type { case .staticButton(title: let title): barItem = CustomButtonTouchBarItem(identifier: identifier, title: title, onTap: action) - case .staticImageButton(title: let title, image: let image): - barItem = CustomButtonTouchBarItem(identifier: identifier, title: title, onTap: action, image: image) case .appleScriptTitledButton(source: let source, refreshInterval: let interval): barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, onTap: action) case .timeButton(formatTemplate: let template): @@ -127,11 +124,17 @@ class TouchBarController: NSObject, NSTouchBarDelegate { if case .width(let value) = parameter, let widthBarItem = barItem as? CanSetWidth { widthBarItem.setWidth(value: value) } + if case .image(let source) = parameter, let item = barItem as? CustomButtonTouchBarItem { + let button = item.button! + button.image = source.image + button.imagePosition = .imageLeading + button.imageHugsTitle = true + button.bezelColor = .clear + } } return barItem } - - + func action(forItem item: BarItemDefinition) -> ()->() { switch item.action { case .hidKey(keycode: let keycode): diff --git a/README.md b/README.md index 4f17f31..19952fc 100644 --- a/README.md +++ b/README.md @@ -82,13 +82,6 @@ File for customize your preset for MTMR: `open ~/Library/Application Support/MTM "title": "esc", ``` -- `staticImageButton` -```json - "type": "staticImageButton", - "image": "StringInbase64" - "title": "Finder", -``` - - `appleScriptTitledButton` ```js "type": "appleScriptTitledButton", @@ -175,9 +168,8 @@ File for customize your preset for MTMR: `open ~/Library/Application Support/MTM "refreshInterval": 1 }, { - "type": "staticImageButton", - "title": "Finder", - "image": "%base64Finder%", + "type": "staticButton", + "image": { "base64" : "%base64Finder%"}, "action": "appleScript", "actionAppleScript": { "inline": "tell application \"Finder\"\rmake new Finder window\rset target of front window to path to home folder as string\ractivate\rend tell"