feat: add mac theme and improve preferences (closes #21)

This commit is contained in:
Louis Pontoise committed 2019-10-24 23:40:21 +09:00
1 parent 0a164c731c
commit 4a5bbe9d1c
7 files changed
+162 -132

No files matched your search

+4 -4
View File
@@ -30,15 +30,15 @@ func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged<C
if cgEvent.type == .keyDown || cgEvent.type == .keyUp || cgEvent.type == .flagsChanged {
if let event = NSEvent(cgEvent: cgEvent) {
let keyDown = event.type == .keyDown
let optionKeyEvent = event.keyCode == Preferences.metaKey
let optionKeyEvent = event.keyCode == Preferences.metaKeyCode
let tabKeyEvent = event.keyCode == Preferences.tabKey
if optionKeyEvent && event.modifiersDown([Preferences.metaModifierFlag]) {
if optionKeyEvent && event.modifiersDown([Preferences.metaModifierFlag!]) {
delegate.keyDownMeta()
} else if tabKeyEvent && event.modifiersDown([Preferences.metaModifierFlag]) && keyDown {
} else if tabKeyEvent && event.modifiersDown([Preferences.metaModifierFlag!]) && keyDown {
delegate.keyDownMetaTab()
// focused app will not receive the event (will not press tab key in that app)
return nil
} else if tabKeyEvent && event.modifiersDown([Preferences.metaModifierFlag, .shift]) && keyDown {
} else if tabKeyEvent && event.modifiersDown([Preferences.metaModifierFlag!, .shift]) && keyDown {
delegate.keyDownMetaShiftTab()
// focused app will not receive the event (will not press tab key in that app)
return nil
+67 -65
View File
@@ -2,97 +2,99 @@ import Foundation
import Cocoa
let defaults = [
"version": "1", // bump this anytime the dictionary is changed
"maxScreenUsage": "0.8",
"windowPadding": "20",
"cellPadding": "6",
"cellBorderWidth": "2",
"maxThumbnailsPerRow": "4",
"thumbnailMaxWidth": "0",
"thumbnailMaxHeight": "0",
"iconSize": "32",
"fontHeight": "15",
"interItemPadding": "4",
"tabKey": "48",
"metaKey": "59",
"metaModifierFlag": "262144",
"highlightColor": "white",
"thumbnailQuality": "4",
"windowDisplayDelay": "200",
"metaKey": Preferences.metaKeyArray[0],
"windowDisplayDelay": "0",
"theme": Preferences.themeArray[0]
]
class Preferences {
static var rawValues = [String: String]()
static var maxScreenUsage: CGFloat = 0
static var windowPadding: CGFloat = 0
static var cellPadding: CGFloat = 0
static var cellBorderWidth: CGFloat = 0
static var maxThumbnailsPerRow: CGFloat = 0
static var thumbnailMaxWidth: CGFloat = 0
static var thumbnailMaxHeight: CGFloat = 0
static var iconSize: CGFloat = 0
static var fontHeight: CGFloat = 0
static var interItemPadding: CGFloat = 0
static var tabKey: UInt16 = 0
static var metaKey: UInt16 = 0
static var metaModifierFlag: NSEvent.ModifierFlags = .control
static var highlightColor: NSColor = .white
static var thumbnailQuality: NSImageInterpolation = .high
static var windowDisplayDelay: DispatchTimeInterval = .milliseconds(200)
static var font: NSFont = .systemFont(ofSize: 15)
static var thumbnailMaxWidth: CGFloat = 200
static var thumbnailMaxHeight: CGFloat = 200
static var fontColor: NSColor = .white
static var windowMaterial: NSVisualEffectView.Material = .dark
static var windowPadding: CGFloat = 23
static var interItemPadding: CGFloat = 4
static var cellPadding: CGFloat = 6
static var cellBorderWidth: CGFloat?
static var cellCornerRadius: CGFloat?
static var maxScreenUsage: CGFloat?
static var maxThumbnailsPerRow: CGFloat?
static var iconSize: CGFloat?
static var fontHeight: CGFloat?
static var tabKey: UInt16?
static var highlightBorderColor: NSColor?
static var highlightBackgroundColor: NSColor?
static var metaKeyCode: UInt16?
static var metaModifierFlag: NSEvent.ModifierFlags?
static var windowDisplayDelay: DispatchTimeInterval?
static var windowCornerRadius: CGFloat?
static var font: NSFont?
static var themeArray = [" macOS", "❖ Windows 10"]
static var metaKeyArray = ["⌥ option", "⌃ control", "⌘ command", "⇪ caps lock", "fn"]
static var metaKeyMap: [String: (UInt16, NSEvent.ModifierFlags)] = [
metaKeyArray[0]: (58, .option),
metaKeyArray[1]: (59, .control),
metaKeyArray[2]: (55, .command),
metaKeyArray[3]: (57, .capsLock),
metaKeyArray[4]: (63, .function),
]
private static let defaultsFile = fileFromPreferencesFolder("alt-tab-macos-defaults.json")
private static let userFile = fileFromPreferencesFolder("alt-tab-macos.json")
static func loadFromDiskAndUpdateValues() {
do {
rawValues = try loadFromDisk()
try rawValues.forEach {
try updateAndValidateValue($0.key, $0.value)
}
try handleNoFileOrOldFile(userFile)
rawValues = try loadFromDisk(userFile)
try rawValues
.filter {
$0.key != "version"
}
.forEach {
try updateAndValidateFromString($0.key, $0.value)
}
} catch {
debugPrint("Error loading preferences from JSON file on disk", error)
debugPrint("Error loading preferences", error)
NSApp.terminate(NSApplication.shared)
}
}
static func updateAndValidateValue(_ valueName: String, _ value: String) throws {
static func updateAndValidateFromString(_ valueName: String, _ value: String) throws {
switch valueName {
case "maxScreenUsage":
maxScreenUsage = try CGFloat(value).orThrow()
case "windowPadding":
windowPadding = try CGFloat(value).orThrow()
case "cellPadding":
cellPadding = try CGFloat(value).orThrow()
case "cellBorderWidth":
cellBorderWidth = try CGFloat(value).orThrow()
case "maxThumbnailsPerRow":
maxThumbnailsPerRow = try CGFloat(value).orThrow()
case "thumbnailMaxWidth":
thumbnailMaxWidth = try CGFloat(value).orThrow()
case "thumbnailMaxHeight":
thumbnailMaxHeight = try CGFloat(value).orThrow()
case "iconSize":
iconSize = try CGFloat(value).orThrow()
case "fontHeight":
fontHeight = try CGFloat(value).orThrow()
case "interItemPadding":
interItemPadding = try CGFloat(value).orThrow()
font = NSFont.systemFont(ofSize: fontHeight!)
case "tabKey":
tabKey = try UInt16(value).orThrow()
case "metaKey":
metaKey = try UInt16(value).orThrow()
case "metaModifierFlag":
metaModifierFlag = NSEvent.ModifierFlags(rawValue: try UInt(value).orThrow())
case "highlightColor":
highlightColor = NSColor.white
case "thumbnailQuality":
thumbnailQuality = NSImageInterpolation(rawValue: try UInt(value).orThrow())!
let (keyCode, modifierFlag) = try metaKeyMap[value].orThrow()
metaKeyCode = keyCode
metaModifierFlag = modifierFlag
case "theme":
let isMac = value == themeArray[0]
cellBorderWidth = isMac ? 0 : 2
cellCornerRadius = isMac ? 5 : 0
highlightBorderColor = isMac ? .clear : .white
highlightBackgroundColor = isMac ? NSColor(red: 0, green: 0, blue: 0, alpha: 0.15) : .clear
windowCornerRadius = isMac ? 20 : 0
case "windowDisplayDelay":
windowDisplayDelay = DispatchTimeInterval.milliseconds(try Int(value).orThrow())
case "font":
font = NSFont.systemFont(ofSize: fontHeight)
default:
throw "Tried to update an unknown preference"
throw "Tried to update an unknown preference: '\(valueName)' = '\(value)'"
}
rawValues[valueName] = value
}
@@ -101,23 +103,20 @@ class Preferences {
try saveToDisk(rawValues, userFile)
}
private static func fileLines(_ url: URL) throws -> Int {
try String(contentsOf: url, encoding: .utf8).split(separator: "\n").count
private static func preferencesVersion(_ url: URL) throws -> Int {
try Int(loadFromDisk(url)["version"] ?? "0").orThrow()
}
private static func loadFromDisk() throws -> [String: String] {
try handleNoFileOrOldFile(userFile)
return try JSONDecoder().decode([String: String].self, from: Data(contentsOf: userFile))
private static func loadFromDisk(_ url: URL) throws -> [String: String] {
try JSONDecoder().decode([String: String].self, from: Data(contentsOf: url))
}
private static func handleNoFileOrOldFile(_ userFile: URL) throws {
if !FileManager.default.fileExists(atPath: defaultsFile.path) {
try saveDefaultsToDisk()
}
try saveDefaultsToDisk()
if !FileManager.default.fileExists(atPath: userFile.path) {
try FileManager.default.copyItem(at: defaultsFile, to: userFile)
} else {
if try fileLines(defaultsFile) > fileLines(userFile) {
if try preferencesVersion(defaultsFile) > preferencesVersion(userFile) {
// TODO: handle upgrades in a smarter way (e.g. merge files)
try FileManager.default.removeItem(at: userFile)
try FileManager.default.copyItem(at: defaultsFile, to: userFile)
@@ -163,6 +162,9 @@ extension Optional {
case .some(let value):
return value
case .none:
Thread.callStackSymbols.forEach {
print($0)
}
throw "Optional contained nil"
}
}
+4 -12
View File
@@ -13,18 +13,10 @@ class Screen {
}
static func updateThumbnailMaxSize() -> Void {
if Preferences.thumbnailMaxWidth == 0 && Preferences.thumbnailMaxHeight == 0 {
do {
let main = NSScreen.main!.frame
try Preferences.updateAndValidateValue("thumbnailMaxWidth",
String((NSScreen.main!.frame.size.width * Preferences.maxScreenUsage - Preferences.windowPadding * 2) / Preferences.maxThumbnailsPerRow - Preferences.interItemPadding).orThrow())
try Preferences.updateAndValidateValue("thumbnailMaxHeight",
String(Preferences.thumbnailMaxWidth * (main.height / main.width)).orThrow())
try Preferences.saveRawToDisk()
} catch {
debugPrint("Error updating preferences based on screen size", error)
NSApp.terminate(NSApplication.shared)
}
if Preferences.thumbnailMaxWidth == 200 && Preferences.thumbnailMaxHeight == 200 {
let main = NSScreen.main!.frame
Preferences.thumbnailMaxWidth = (NSScreen.main!.frame.size.width * Preferences.maxScreenUsage! - Preferences.windowPadding * 2) / Preferences.maxThumbnailsPerRow! - Preferences.interItemPadding
Preferences.thumbnailMaxHeight = Preferences.thumbnailMaxWidth * (main.height / main.width)
}
}
}
+1 -1
View File
@@ -82,7 +82,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
self.showCenteredPanel(self.thumbnailsPanel!)
}
workItems.append(workItem)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay, execute: workItem)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay!, execute: workItem)
} else {
self.thumbnailsPanel!.highlightThumbnail(step)
}
+14 -11
View File
@@ -14,7 +14,9 @@ class Cell: NSCollectionViewItem {
override func loadView() {
let vStackView = NSStackView()
vStackView.wantsLayer = true
vStackView.layer!.borderWidth = Preferences.cellBorderWidth
vStackView.layer!.backgroundColor = .clear
vStackView.layer!.cornerRadius = Preferences.cellCornerRadius!
vStackView.layer!.borderWidth = Preferences.cellBorderWidth!
vStackView.layer!.borderColor = .clear
vStackView.edgeInsets = NSEdgeInsets(top: Preferences.cellPadding, left: Preferences.cellPadding, bottom: Preferences.cellPadding, right: Preferences.cellPadding)
vStackView.orientation = .vertical
@@ -33,8 +35,8 @@ class Cell: NSCollectionViewItem {
text.backgroundColor = .clear
text.isSelectable = false
text.isEditable = false
text.font = Preferences.font
text.textColor = Preferences.highlightColor
text.font = Preferences.font!
text.textColor = Preferences.fontColor
let shadow = NSShadow()
shadow.shadowColor = .darkGray
shadow.shadowOffset = NSMakeSize(0, 0)
@@ -43,11 +45,11 @@ class Cell: NSCollectionViewItem {
text.enabledTextCheckingTypes = 0
let paragraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.maximumLineHeight = Preferences.fontHeight
paragraphStyle.minimumLineHeight = Preferences.fontHeight
paragraphStyle.maximumLineHeight = Preferences.fontHeight!
paragraphStyle.minimumLineHeight = Preferences.fontHeight!
paragraphStyle.allowsDefaultTighteningForTruncation = false
text.defaultParagraphStyle = paragraphStyle
text.heightAnchor.constraint(equalToConstant: Preferences.fontHeight).isActive = true
text.heightAnchor.constraint(equalToConstant: Preferences.fontHeight!).isActive = true
let shadow2 = NSShadow()
shadow2.shadowColor = .gray
shadow2.shadowOffset = NSMakeSize(0, 0)
@@ -63,7 +65,8 @@ class Cell: NSCollectionViewItem {
override var isSelected: Bool {
didSet {
view.layer!.borderColor = isSelected ? Preferences.highlightColor.cgColor : .clear
view.layer!.backgroundColor = isSelected ? Preferences.highlightBackgroundColor!.cgColor : .clear
view.layer!.borderColor = isSelected ? Preferences.highlightBorderColor!.cgColor : .clear
}
}
@@ -74,12 +77,12 @@ class Cell: NSCollectionViewItem {
thumbnail.image!.size = NSSize(width: width, height: height)
thumbnail.frame.size = NSSize(width: width, height: height)
icon.image = element.icon
icon.image!.size = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
icon.frame.size = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
icon.image!.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
icon.frame.size = NSSize(width: Preferences.iconSize!, height: Preferences.iconSize!)
text.string = element.cgTitle
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
text.font = Preferences.font
text.textContainer!.size.width = thumbnail.frame.size.width - Preferences.iconSize - Preferences.interItemPadding
text.font = Preferences.font!
text.textContainer!.size.width = thumbnail.frame.size.width - Preferences.iconSize! - Preferences.interItemPadding
mouseDownCallback = fn
}
}
+60 -32
View File
@@ -1,6 +1,20 @@
import Cocoa
class PreferencesPanel: NSPanel, NSTextViewDelegate {
var maxScreenUsage: NSTextView?
var windowPadding: NSTextView?
var cellPadding: NSTextView?
var cellBorderWidth: NSTextView?
var maxThumbnailsPerRow: NSTextView?
var thumbnailMaxWidth: NSTextView?
var thumbnailMaxHeight: NSTextView?
var iconSize: NSTextView?
var fontHeight: NSTextView?
var interItemPadding: NSTextView?
var tabKey: NSTextView?
var windowDisplayDelay: NSTextView?
var metaKey: NSPopUpButton?
var theme: NSPopUpButton?
var inputsMap = [NSTextView: String]()
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
@@ -13,38 +27,24 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
contentView = gridView
}
private func makeLabelsAndInputs() -> [[NSTextView]] {
var rows = [[NSTextView]]()
private func makeLabelsAndInputs() -> [[NSView]] {
[
("maxScreenUsage", "Maximum size of the main window, in percentage of screen size"),
("windowPadding", "Padding in the main window"),
("cellPadding", "Padding in each cell"),
("cellBorderWidth", "Border width of each cell"),
("maxThumbnailsPerRow", "Maximum number of thumbnails on each row"),
("thumbnailMaxWidth", "Maximum width of each thumbnail"),
("thumbnailMaxHeight", "Maximum height of each thumbnail"),
("iconSize", "Width/height for each cell app icon"),
("fontHeight", "Font height for each cell title"),
("interItemPadding", "Padding between cells within the main window"),
("tabKey", "Tab key (NSEvent.keyCode)"),
("metaKey", "Meta key (NSEvent.keyCode)"),
("metaModifierFlag", "Meta key (NSEvent.ModifierFlags)"),
// ("highlightColor", "Color for the currently selected cell"),
("thumbnailQuality", "NSImageInterpolation (e.g. none=1, low=2, medium=4, high=3)"),
("windowDisplayDelay", "Delay in ms before the window is displayed after pressing the shortcut"),
].forEach {
let p = makePreference($1, Preferences.rawValues[$0]!)
rows.append(p)
inputsMap[p[1]] = $0
}
return rows
makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeArray),
makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyArray),
makePreference(\PreferencesPanel.tabKey, "Tab key (NSEvent.keyCode)", "tabKey"),
makePreference(\PreferencesPanel.maxScreenUsage, "Max window size (screen %)", "maxScreenUsage"),
makePreference(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", "maxThumbnailsPerRow"),
makePreference(\PreferencesPanel.iconSize, "Apps icon size (px)", "iconSize"),
makePreference(\PreferencesPanel.fontHeight, "Font size (px)", "fontHeight"),
makePreference(\PreferencesPanel.windowDisplayDelay, "Window apparition delay (ms)", "windowDisplayDelay"),
]
}
private func makeGridView(_ rows: [[NSTextView]]) -> NSGridView {
private func makeGridView(_ rows: [[NSView]]) -> NSGridView {
let gridView = NSGridView(views: rows)
gridView.setContentHuggingPriority(.defaultLow, for: .horizontal)
gridView.setContentHuggingPriority(.defaultLow, for: .vertical)
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 580).isActive = true
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 360).isActive = true
return gridView
}
@@ -56,18 +56,45 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
gridView.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: gridView.numberOfRows - 1, length: 1))
}
private func makePreference(_ labelText: String, _ initialValue: String) -> [NSTextView] {
private func makePreference(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSTextView?>, _ labelText: String, _ rawName: String) -> [NSTextView] {
let label = makeLabel(labelText)
label.alignment = .right
let input = NSTextView()
input.delegate = self
input.font = Preferences.font
input.string = initialValue
input.widthAnchor.constraint(equalToConstant: 65).isActive = true
input.string = Preferences.rawValues[rawName]!
input.widthAnchor.constraint(equalToConstant: 32).isActive = true
self[keyPath: keyPath] = input
inputsMap[input] = rawName
return [label, input]
}
private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath<PreferencesPanel, NSPopUpButton?>, _ labelText: String, _ rawName: String, _ values: [String]) -> [NSView] {
let label = makeLabel(labelText)
label.alignment = .right
let input = NSPopUpButton()
input.addItems(withTitles: values)
input.selectItem(withTitle: Preferences.rawValues[rawName]!)
input.action = #selector(dropdownDidChange)
input.target = self
self[keyPath: keyPath] = input
return [label, input]
}
@objc func dropdownDidChange(sender: AnyObject) throws {
if let popUpButton = sender as? NSPopUpButton {
switch popUpButton {
case theme:
try! Preferences.updateAndValidateFromString("theme", popUpButton.titleOfSelectedItem!)
case metaKey:
try! Preferences.updateAndValidateFromString("metaKey", popUpButton.titleOfSelectedItem!)
default:
throw "Tried to update an unknown popUpButton: '\(popUpButton)' = '\(popUpButton.titleOfSelectedItem!)'"
}
try! Preferences.saveRawToDisk()
}
}
private func makeLabel(_ text: String) -> NSTextView {
let label = NSTextView()
label.drawsBackground = true
@@ -77,7 +104,7 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
label.font = Preferences.font
label.string = text
label.enabledTextCheckingTypes = 0
label.heightAnchor.constraint(greaterThanOrEqualToConstant: Preferences.fontHeight + Preferences.interItemPadding).isActive = true
label.heightAnchor.constraint(greaterThanOrEqualToConstant: Preferences.fontHeight! + Preferences.interItemPadding).isActive = true
return label
}
@@ -85,9 +112,10 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
if let textView = notification.object as? NSTextView {
let key = inputsMap[textView]!
do {
try Preferences.updateAndValidateValue(key, textView.string)
try Preferences.updateAndValidateFromString(key, textView.string)
try Preferences.saveRawToDisk()
} catch {
debugPrint(key, error)
textView.string = Preferences.rawValues[key]!
}
}
+12 -7
View File
@@ -13,20 +13,25 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
convenience init(_ application: Application) {
self.init()
self.application = application
styleMask = [.borderless]
isFloatingPanel = true
animationBehavior = NSWindow.AnimationBehavior.none
backgroundColor = .clear
hidesOnDeactivate = false
hasShadow = false
titleVisibility = .hidden
styleMask.remove(.titled)
backgroundColor = .clear
makeThumbnailsPanelBackgroundView()
contentView = backgroundView!
contentView!.addSubview(backgroundView!)
}
private func makeThumbnailsPanelBackgroundView() {
backgroundView = NSVisualEffectView()
backgroundView!.blendingMode = .behindWindow
backgroundView!.material = .dark
backgroundView!.translatesAutoresizingMaskIntoConstraints = false
backgroundView!.material = Preferences.windowMaterial
backgroundView!.state = .active
backgroundView!.wantsLayer = true
backgroundView!.layer!.cornerRadius = Preferences.windowCornerRadius!
makeCollectionView()
backgroundView!.addSubview(collectionView_!)
}
@@ -66,7 +71,7 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
// debugPrint("collectionView: item size")
if indexPath.item < application!.openWindows.count {
let (width, height) = computeDownscaledSize(application!.openWindows[indexPath.item].thumbnail)
return NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight, Preferences.iconSize) + Preferences.interItemPadding + Preferences.cellPadding * 2)
return NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight!, Preferences.iconSize!) + Preferences.interItemPadding + Preferences.cellPadding * 2)
}
return .zero
}
@@ -77,7 +82,7 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
}
func computeThumbnails() {
let maxSize = NSSize(width: NSScreen.main!.frame.width * Preferences.maxScreenUsage, height: NSScreen.main!.frame.height * Preferences.maxScreenUsage)
let maxSize = NSSize(width: NSScreen.main!.frame.width * Preferences.maxScreenUsage!, height: NSScreen.main!.frame.height * Preferences.maxScreenUsage!)
collectionView_!.setFrameSize(maxSize)
collectionView_!.collectionViewLayout!.invalidateLayout()
collectionView_!.reloadData()