feat: add panel to set user preferences (closes #8)
This commit is contained in:
1 parent
5ddd2f95fd
commit
a99482525b
12 files changed
+487
-254
No files matched your search
@@ -7,18 +7,24 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */; };
|
||||
D04BA20D4A240843293B3B52 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA56355579F78776E6D51 /* Cell.swift */; };
|
||||
D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */; };
|
||||
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA90C6C36DB1D65BC2B66 /* Application.swift */; };
|
||||
D04BA57A871B7269BEBAFF84 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA35456DA0DDA74F9687E /* Keyboard.swift */; };
|
||||
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA02F476DE30C4647886C /* PreferencesPanel.swift */; };
|
||||
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */; };
|
||||
D04BA86F2E687F573F920D93 /* HighInterpolationImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE70D6AFD2E52A71701D /* HighInterpolationImageView.swift */; };
|
||||
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */; };
|
||||
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */; };
|
||||
D04BA960DDD1D32A3019C835 /* CollectionViewCenterFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */; };
|
||||
D04BAE1A64F5572B19F9E2B1 /* preferences.json in Resources */ = {isa = PBXBuildFile; fileRef = D04BA8924E1644C932B8CBAA /* preferences.json */; };
|
||||
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */; };
|
||||
D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
D04BA02F476DE30C4647886C /* PreferencesPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPanel.swift; sourceTree = "<group>"; };
|
||||
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = "<group>"; };
|
||||
D04BA0CE87BE264C52987ED1 /* 7 windows - 2 lines - wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - wide window.jpg"; sourceTree = "<group>"; };
|
||||
D04BA0E1C5DBC07108AC2F54 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
D04BA0F5EB832B8E142B654B /* 4 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "4 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
@@ -27,9 +33,11 @@
|
||||
D04BA1FC9022590D7AA02486 /* 1 window - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1 window - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA27584274BC8C2D9809F /* 3 windows - 1 line.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "3 windows - 1 line.png"; sourceTree = "<group>"; };
|
||||
D04BA2C7B51F68651B3C60E2 /* 6 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "6 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemPermissions.swift; sourceTree = "<group>"; };
|
||||
D04BA3202A2C22C347E849B3 /* CollectionViewCenterFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewCenterFlowLayout.swift; sourceTree = "<group>"; };
|
||||
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA35456DA0DDA74F9687E /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = "<group>"; };
|
||||
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
|
||||
D04BA4336B6004A0A99849AD /* package.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = package.json; sourceTree = "<group>"; };
|
||||
D04BA4B5292629AA6B560216 /* package_release.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = package_release.sh; sourceTree = "<group>"; };
|
||||
D04BA4F23325560BC0BCDDB7 /* 7 windows - 2 lines - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - tall window.jpg"; sourceTree = "<group>"; };
|
||||
@@ -41,7 +49,6 @@
|
||||
D04BA7B6AAB0812631BBC7A2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = "<group>"; };
|
||||
D04BA7ECCE728582D9ECA613 /* determine_version.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = determine_version.sh; sourceTree = "<group>"; };
|
||||
D04BA82F792DF53958D92572 /* alt-tab-macos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "alt-tab-macos.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D04BA8924E1644C932B8CBAA /* preferences.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = preferences.json; sourceTree = "<group>"; };
|
||||
D04BA8DDB96B0A60C3394351 /* dark-background.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dark-background.png"; sourceTree = "<group>"; };
|
||||
D04BA90C6C36DB1D65BC2B66 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
D04BA92541D46EA4F6943A72 /* package-lock.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "package-lock.json"; sourceTree = "<group>"; };
|
||||
@@ -57,6 +64,7 @@
|
||||
D04BAD82F652C4ED5D8BF7AE /* 3 windows - 1 line - tall window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line - tall window.jpg"; sourceTree = "<group>"; };
|
||||
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = "<group>"; };
|
||||
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = "<group>"; };
|
||||
D04BAE70D6AFD2E52A71701D /* HighInterpolationImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighInterpolationImageView.swift; sourceTree = "<group>"; };
|
||||
D04BAF076A30A1BAFEDBEA66 /* 5 windows - 2 lines.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "5 windows - 2 lines.jpg"; sourceTree = "<group>"; };
|
||||
D04BAFA277EAE3BDDDB61110 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
|
||||
@@ -91,14 +99,6 @@
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D04BA18D1EDDD56CA89604B6 /* resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D04BA8924E1644C932B8CBAA /* preferences.json */,
|
||||
);
|
||||
path = resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D04BA1A461BBB5A9BE15E015 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -155,6 +155,8 @@
|
||||
D04BA35456DA0DDA74F9687E /* Keyboard.swift */,
|
||||
D04BAD1BED44EAEB77FED8A4 /* WindowManager.swift */,
|
||||
D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */,
|
||||
D04BA3F15EAE8D8C39B6F2CF /* Screen.swift */,
|
||||
D04BA2D2AD6B1CCA3F3A4DD7 /* SystemPermissions.swift */,
|
||||
);
|
||||
path = logic;
|
||||
sourceTree = "<group>";
|
||||
@@ -166,6 +168,9 @@
|
||||
D04BAE70D6AFD2E52A71701D /* HighInterpolationImageView.swift */,
|
||||
D04BA56355579F78776E6D51 /* Cell.swift */,
|
||||
D04BA90C6C36DB1D65BC2B66 /* Application.swift */,
|
||||
D04BA02F476DE30C4647886C /* PreferencesPanel.swift */,
|
||||
D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */,
|
||||
D04BA0AF7C5DCF367FBB663C /* StatusItem.swift */,
|
||||
);
|
||||
path = ui;
|
||||
sourceTree = "<group>";
|
||||
@@ -195,7 +200,6 @@
|
||||
D04BA7B6AAB0812631BBC7A2 /* Info.plist */,
|
||||
D04BAA44C837F3A67403B9DB /* main.swift */,
|
||||
D04BAA1C553891551B903DA7 /* logic */,
|
||||
D04BA18D1EDDD56CA89604B6 /* resources */,
|
||||
);
|
||||
path = "alt-tab-macos";
|
||||
sourceTree = "<group>";
|
||||
@@ -251,7 +255,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D04BAE1A64F5572B19F9E2B1 /* preferences.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -270,6 +273,11 @@
|
||||
D04BA278D9EFA568C8D18A4C /* WindowManager.swift in Sources */,
|
||||
D04BA3261C7DA5F48310E654 /* Application.swift in Sources */,
|
||||
D04BA70FF7262BF5F9E6E13B /* Preferences.swift in Sources */,
|
||||
D04BA6368E681BE3A408AC99 /* PreferencesPanel.swift in Sources */,
|
||||
D04BA9119E2329DB5A35B3C7 /* ThumbnailsPanel.swift in Sources */,
|
||||
D04BA8EBC0365A019A27C7EA /* Screen.swift in Sources */,
|
||||
D04BA9CCE02D30C8164A552A /* SystemPermissions.swift in Sources */,
|
||||
D04BA02DD4152997C32CF50B /* StatusItem.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -284,7 +292,7 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = "alt-tab-macos/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
@@ -299,7 +307,7 @@
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = "alt-tab-macos/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.lwouis.alt-tab-macos";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import Cocoa
|
||||
|
||||
class Keyboard {
|
||||
static func listenToGlobalEvents(_ delegate: Application) {
|
||||
listenToGlobalKeyboardEvents(delegate)
|
||||
}
|
||||
}
|
||||
|
||||
var eventTap: CFMachPort?
|
||||
|
||||
func listenToGlobalKeyboardEvents(_ delegate: Application) {
|
||||
@@ -22,7 +28,7 @@ func listenToGlobalKeyboardEvents(_ delegate: Application) {
|
||||
|
||||
func keyboardHandler(_ cgEvent: CGEvent, _ delegate: Application) -> Unmanaged<CGEvent>? {
|
||||
if cgEvent.type == .keyDown || cgEvent.type == .keyUp || cgEvent.type == .flagsChanged {
|
||||
if let event = NSEvent.init(cgEvent: cgEvent) {
|
||||
if let event = NSEvent(cgEvent: cgEvent) {
|
||||
let keyDown = event.type == .keyDown
|
||||
let optionKeyEvent = event.keyCode == Preferences.metaKey
|
||||
let tabKeyEvent = event.keyCode == Preferences.tabKey
|
||||
|
||||
@@ -1,90 +1,173 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
struct PreferencesDecodable: Decodable {
|
||||
let maxScreenUsage: CGFloat?
|
||||
let windowPadding: CGFloat?
|
||||
let cellPadding: CGFloat?
|
||||
let cellBorderWidth: CGFloat?
|
||||
let maxThumbnailsPerRow: CGFloat?
|
||||
let thumbnailMaxWidth: CGFloat?
|
||||
let thumbnailMaxHeight: CGFloat?
|
||||
let iconSize: CGFloat?
|
||||
let fontHeight: CGFloat?
|
||||
let interItemPadding: CGFloat?
|
||||
let tabKey: UInt16?
|
||||
let metaKey: UInt16?
|
||||
let metaModifierFlagInt: UInt?
|
||||
let highlightColorString : String?
|
||||
let thumbnailQuality : UInt?
|
||||
let windowDisplayDelay: Int?
|
||||
}
|
||||
let defaults = [
|
||||
"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",
|
||||
]
|
||||
|
||||
func fileLines(_ url: URL) throws -> Int {
|
||||
try String(contentsOf: url, encoding: .utf8).split(separator: "\n").count
|
||||
}
|
||||
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)
|
||||
|
||||
class Preferences: Decodable {
|
||||
private static let decoded: PreferencesDecodable? = decodeFromJson()
|
||||
private static let defaultsFile = fileFromPreferencesFolder("alt-tab-macos-defaults.json")
|
||||
private static let userFile = fileFromPreferencesFolder("alt-tab-macos.json")
|
||||
|
||||
private static func decodeFromJson() -> PreferencesDecodable? {
|
||||
static func loadFromDiskAndUpdateValues() {
|
||||
do {
|
||||
let defaultFile = Bundle.main.url(forResource: "preferences", withExtension: "json")!
|
||||
let userFile = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)
|
||||
.first!
|
||||
.appendingPathComponent("Preferences", isDirectory: true)
|
||||
.appendingPathComponent("alt-tab-macos.json")
|
||||
try handleNoFileOrOldFile(userFile: userFile, defaultFile: defaultFile)
|
||||
return try JSONDecoder().decode(PreferencesDecodable.self, from: Data(contentsOf: userFile))
|
||||
rawValues = try loadFromDisk()
|
||||
try rawValues.forEach {
|
||||
try updateAndValidateValue($0.key, $0.value)
|
||||
}
|
||||
} catch {
|
||||
debugPrint("Error parsing preferences.json", error)
|
||||
debugPrint("Error loading preferences from JSON file on disk", error)
|
||||
NSApp.terminate(NSApplication.shared)
|
||||
}
|
||||
}
|
||||
|
||||
static func updateAndValidateValue(_ 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()
|
||||
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())!
|
||||
case "windowDisplayDelay":
|
||||
windowDisplayDelay = DispatchTimeInterval.milliseconds(try Int(value).orThrow())
|
||||
case "font":
|
||||
font = NSFont.systemFont(ofSize: fontHeight)
|
||||
default:
|
||||
throw "Tried to update an unknown preference"
|
||||
}
|
||||
rawValues[valueName] = value
|
||||
}
|
||||
|
||||
static func saveRawToDisk() throws {
|
||||
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 loadFromDisk() throws -> [String: String] {
|
||||
try handleNoFileOrOldFile(userFile)
|
||||
return try JSONDecoder().decode([String: String].self, from: Data(contentsOf: userFile))
|
||||
}
|
||||
|
||||
private static func handleNoFileOrOldFile(_ userFile: URL) throws {
|
||||
if !FileManager.default.fileExists(atPath: defaultsFile.path) {
|
||||
try saveDefaultsToDisk()
|
||||
}
|
||||
if !FileManager.default.fileExists(atPath: userFile.path) {
|
||||
try FileManager.default.copyItem(at: defaultsFile, to: userFile)
|
||||
} else {
|
||||
if try fileLines(defaultsFile) > fileLines(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func saveDefaultsToDisk() throws {
|
||||
try saveToDisk(defaults, defaultsFile)
|
||||
}
|
||||
|
||||
private static func saveToDisk(_ values: [String: String], _ path: URL) throws {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
try encoder
|
||||
.encode(values)
|
||||
.write(to: path)
|
||||
}
|
||||
|
||||
private static func fileFromPreferencesFolder(_ fileName: String) -> URL {
|
||||
FileManager.default
|
||||
.urls(for: .libraryDirectory, in: .userDomainMask)
|
||||
.first!
|
||||
.appendingPathComponent("Preferences", isDirectory: true)
|
||||
.appendingPathComponent(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
// add CGFloat constructor from String
|
||||
extension CGFloat {
|
||||
init?(_ string: String) {
|
||||
guard let number = NumberFormatter().number(from: string) else {
|
||||
return nil
|
||||
}
|
||||
self.init(number.floatValue)
|
||||
}
|
||||
|
||||
private static func handleNoFileOrOldFile(userFile: URL, defaultFile: URL) throws {
|
||||
if !FileManager.default.fileExists(atPath: userFile.path) {
|
||||
try FileManager.default.copyItem(at: defaultFile, to: userFile)
|
||||
} else if try fileLines(defaultFile) > fileLines(userFile) {
|
||||
// TODO: handle upgrades in a smarter way (e.g. merge files)
|
||||
try FileManager.default.removeItem(at: userFile)
|
||||
try FileManager.default.copyItem(at: defaultFile, to: userFile)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum width/height of the main window, in percentage of screen width/height
|
||||
static let maxScreenUsage = decoded?.maxScreenUsage ?? 0.8
|
||||
// padding in the main window
|
||||
static let windowPadding = decoded?.windowPadding ?? 20
|
||||
// padding in each cell
|
||||
static let cellPadding = decoded?.cellPadding ?? 6
|
||||
// border width of each cell
|
||||
static let cellBorderWidth = decoded?.cellBorderWidth ?? 2
|
||||
// maximum number of thumbnails on each row
|
||||
static let maxThumbnailsPerRow = decoded?.maxThumbnailsPerRow ?? 4
|
||||
// maximum width of each thumbnail
|
||||
static var thumbnailMaxWidth = decoded?.thumbnailMaxWidth ?? 0
|
||||
// maximum height of eacg thumbnail
|
||||
static var thumbnailMaxHeight = decoded?.thumbnailMaxHeight ?? 0
|
||||
// width/height for each cell app icon
|
||||
static let iconSize = decoded?.iconSize ?? 32
|
||||
// font height for each cell title
|
||||
static let fontHeight = decoded?.fontHeight ?? 15
|
||||
// padding between cells within the main window
|
||||
static let interItemPadding = decoded?.interItemPadding ?? 4
|
||||
// NSEvent.keyCode (e.g. leftCommand=55, leftOption=58, leftControl=59, rightCommand=54, rightOption=61)
|
||||
static let tabKey = decoded?.tabKey ?? 48
|
||||
// NSEvent.keyCode (e.g. leftCommand=55, leftOption=58, leftControl=59, rightCommand=54, rightOption=61)
|
||||
static let metaKey = decoded?.metaKey ?? 59
|
||||
// NSImageInterpolation (e.g. none=1, low=2, medium=4, high=3)
|
||||
static let thumbnailQuality = NSImageInterpolation.init(rawValue: decoded?.thumbnailQuality ?? 3) ?? .high
|
||||
// NSEvent.ModifierFlags (e.g. control=262144, command=1048576, shift=131072, option=524288)
|
||||
static let metaModifierFlag = NSEvent.ModifierFlags(rawValue: decoded?.metaModifierFlagInt ?? 262144)
|
||||
// delay (in ms) before the window is displayed after the shortcut is pressed
|
||||
static let windowDisplayDelay = DispatchTimeInterval.milliseconds(decoded?.windowDisplayDelay ?? 200)
|
||||
// color for the currently selected cell
|
||||
static let highlightColor = NSColor.white
|
||||
// derived properties
|
||||
static let font: NSFont = .systemFont(ofSize: Preferences.fontHeight)
|
||||
}
|
||||
|
||||
// add throw-on-nil method on Optional
|
||||
extension Optional {
|
||||
func orThrow() throws -> Wrapped {
|
||||
switch self {
|
||||
case .some(let value):
|
||||
return value
|
||||
case .none:
|
||||
throw "Optional contained nil"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allow String to be treated as Error (e.g. throw "explanation")
|
||||
extension String: Error {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Screen {
|
||||
static func listenToChanges() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSApplication.didChangeScreenParametersNotification,
|
||||
object: NSApplication.shared,
|
||||
queue: OperationQueue.main
|
||||
) { notification -> Void in
|
||||
updateThumbnailMaxSize()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add String constructor from CGFloat that round up at 1 decimal
|
||||
extension String {
|
||||
init?(_ cgFloat: CGFloat) {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.maximumFractionDigits = 1
|
||||
guard let string = formatter.string(from: cgFloat as NSNumber) else {
|
||||
return nil
|
||||
}
|
||||
self.init(string)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class SystemPermissions {
|
||||
static func ensureAccessibilityCheckboxIsChecked() {
|
||||
if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) {
|
||||
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.\nPlease authorize and re-launch.\nSee https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx")
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
|
||||
static func ensureScreenRecordingCheckboxIsChecked() {
|
||||
let firstWindow = cgWindows()[0]
|
||||
let windowImage = CGWindowListCreateImage(.null, .optionIncludingWindow, firstWindow[kCGWindowNumber] as! CGWindowID, [.boundsIgnoreFraming, .bestResolution])
|
||||
if windowImage == nil {
|
||||
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.\nPlease authorize and re-launch.\nSee https://dropshare.zendesk.com/hc/en-us/articles/360033453434-Enabling-Screen-Recording-Permission-on-macOS-Catalina-10-15-")
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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,
|
||||
"metaModifierFlagInt": 262144,
|
||||
"highlightColorString": "white",
|
||||
"thumbnailQuality": 4,
|
||||
"windowDisplayDelay": 200
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
let cellId = NSUserInterfaceItemIdentifier("Cell")
|
||||
|
||||
func updateThumbnailMaxSize() -> Void {
|
||||
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)
|
||||
}
|
||||
|
||||
class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout {
|
||||
var item: NSStatusItem?
|
||||
class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
static let name = "AltTab"
|
||||
var statusItem: NSStatusItem?
|
||||
var backgroundView: NSVisualEffectView?
|
||||
var collectionView_: NSCollectionView?
|
||||
var window: NSPanel?
|
||||
var thumbnailsPanel: ThumbnailsPanel?
|
||||
var preferencesPanel: PreferencesPanel?
|
||||
var selectedOpenWindow: Int = 0
|
||||
var numberOfColumns: Int = 0
|
||||
var openWindows: [OpenWindow] = []
|
||||
@@ -31,94 +24,25 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCol
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
ensureScreenRecordingCheckboxIsChecked()
|
||||
ensureAccessibilityCheckboxIsChecked()
|
||||
updateThumbnailMaxSize()
|
||||
makeStatusBarItem()
|
||||
makeWindow()
|
||||
makeCollectionView()
|
||||
backgroundView = NSVisualEffectView()
|
||||
backgroundView!.blendingMode = .behindWindow
|
||||
backgroundView!.material = .dark
|
||||
backgroundView!.state = .active
|
||||
backgroundView!.addSubview(collectionView_!)
|
||||
window!.contentView = backgroundView
|
||||
listenToGlobalKeyboardEvents(self)
|
||||
listenToScreenChanges()
|
||||
SystemPermissions.ensureScreenRecordingCheckboxIsChecked()
|
||||
SystemPermissions.ensureAccessibilityCheckboxIsChecked()
|
||||
Preferences.loadFromDiskAndUpdateValues()
|
||||
Screen.updateThumbnailMaxSize()
|
||||
statusItem = StatusItem.make(self)
|
||||
thumbnailsPanel = ThumbnailsPanel(self)
|
||||
preferencesPanel = PreferencesPanel()
|
||||
Keyboard.listenToGlobalEvents(self)
|
||||
Screen.listenToChanges()
|
||||
}
|
||||
|
||||
@objc func showCenteredPreferencesPanel() {
|
||||
showCenteredPanel(preferencesPanel!)
|
||||
}
|
||||
|
||||
func showCenteredPanel(_ panel: NSPanel) {
|
||||
panel.center()
|
||||
panel.makeKeyAndOrderFront(nil)
|
||||
Application.shared.activate(ignoringOtherApps: true)
|
||||
Application.shared.runModal(for: window!)
|
||||
Application.shared.hide(nil)
|
||||
}
|
||||
|
||||
func listenToScreenChanges() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSApplication.didChangeScreenParametersNotification,
|
||||
object: NSApplication.shared,
|
||||
queue: OperationQueue.main
|
||||
) { notification -> Void in
|
||||
updateThumbnailMaxSize()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
if !appIsBeingUsed {
|
||||
focusSelectedWindow(currentlySelectedWindow())
|
||||
}
|
||||
}
|
||||
|
||||
func makeCollectionView() {
|
||||
let layout = CollectionViewCenterFlowLayout()
|
||||
layout.estimatedItemSize = NSSize(width: Preferences.thumbnailMaxWidth, height: Preferences.thumbnailMaxHeight)
|
||||
layout.minimumInteritemSpacing = 5
|
||||
layout.minimumLineSpacing = 5
|
||||
collectionView_ = NSCollectionView()
|
||||
collectionView_!.dataSource = self
|
||||
collectionView_!.delegate = self
|
||||
collectionView_!.collectionViewLayout = layout
|
||||
collectionView_!.backgroundColors = [.clear]
|
||||
collectionView_!.isSelectable = true
|
||||
collectionView_!.allowsMultipleSelection = false
|
||||
collectionView_!.register(Cell.self, forItemWithIdentifier: cellId)
|
||||
}
|
||||
|
||||
func makeWindow() {
|
||||
window = NSPanel()
|
||||
window!.styleMask = [.borderless]
|
||||
window!.level = .floating
|
||||
window!.animationBehavior = NSWindow.AnimationBehavior.none
|
||||
window!.backgroundColor = .clear
|
||||
window!.setIsVisible(true)
|
||||
window!.delegate = self
|
||||
}
|
||||
|
||||
func makeStatusBarItem() {
|
||||
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
item!.button!.title = "AltTab"
|
||||
item!.menu = NSMenu()
|
||||
item!.menu!.addItem(
|
||||
withTitle: "Version #VERSION#",
|
||||
action: nil,
|
||||
keyEquivalent: "")
|
||||
item!.menu!.addItem(
|
||||
withTitle: "Quit \(ProcessInfo.processInfo.processName)",
|
||||
action: #selector(NSApplication.terminate(_:)),
|
||||
keyEquivalent: "q")
|
||||
}
|
||||
|
||||
func ensureAccessibilityCheckboxIsChecked() {
|
||||
if !AXIsProcessTrustedWithOptions(["AXTrustedCheckOptionPrompt": true] as CFDictionary) {
|
||||
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Accessibility.\nPlease authorize and re-launch.\nSee https://help.rescuetime.com/article/59-how-do-i-enable-accessibility-permissions-on-mac-osx")
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureScreenRecordingCheckboxIsChecked() {
|
||||
let firstWindow = cgWindows()[0]
|
||||
let windowImage = CGWindowListCreateImage(.null, .optionIncludingWindow, firstWindow[kCGWindowNumber] as! CGWindowID, [.boundsIgnoreFraming, .bestResolution])
|
||||
if windowImage == nil {
|
||||
debugPrint("Before using this app, you need to give permission in System Preferences > Security & Privacy > Privacy > Screen Recording.\nPlease authorize and re-launch.\nSee https://dropshare.zendesk.com/hc/en-us/articles/360033453434-Enabling-Screen-Recording-Permission-on-macOS-Catalina-10-15-")
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
|
||||
func computeOpenWindows() {
|
||||
@@ -141,44 +65,6 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCol
|
||||
}
|
||||
}
|
||||
|
||||
func highlightThumbnail(_ step: Int) {
|
||||
collectionView_!.selectItems(at: [IndexPath(item: selectedOpenWindow, section: 0)], scrollPosition: .top)
|
||||
collectionView_!.deselectItems(at: [IndexPath(item: cellWithStep(-step), section: 0)])
|
||||
}
|
||||
|
||||
func computeThumbnails() {
|
||||
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()
|
||||
collectionView_!.layoutSubtreeIfNeeded()
|
||||
window!.setContentSize(NSSize(width: collectionView_!.frame.size.width + Preferences.windowPadding * 2, height: collectionView_!.frame.size.height + Preferences.windowPadding * 2))
|
||||
backgroundView!.setFrameSize(window!.frame.size)
|
||||
collectionView_!.setFrameOrigin(NSPoint(x: Preferences.windowPadding, y: Preferences.windowPadding))
|
||||
window!.center()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
// debugPrint("collectionView: count items", openWindows.count)
|
||||
return openWindows.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
// debugPrint("collectionView: make item", indexPath.item)
|
||||
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell
|
||||
item.updateWithNewContent(openWindows[indexPath.item], self.focusSelectedWindow)
|
||||
return item
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
|
||||
// debugPrint("collectionView: item size")
|
||||
if indexPath.item < openWindows.count {
|
||||
let (width, height) = computeDownscaledSize(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 .zero
|
||||
}
|
||||
|
||||
func cellWithStep(_ step: Int) -> Int {
|
||||
selectedOpenWindow + step < 0 ? openWindows.count - 1 : (selectedOpenWindow + step) % openWindows.count
|
||||
}
|
||||
@@ -191,14 +77,14 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCol
|
||||
isFirstSummon = false
|
||||
let workItem = DispatchWorkItem {
|
||||
self.computeOpenWindows()
|
||||
self.computeThumbnails()
|
||||
self.highlightThumbnail(step)
|
||||
Application.shared.unhideWithoutActivation()
|
||||
self.thumbnailsPanel!.computeThumbnails()
|
||||
self.thumbnailsPanel!.highlightThumbnail(step)
|
||||
self.showCenteredPanel(self.thumbnailsPanel!)
|
||||
}
|
||||
workItems.append(workItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay, execute: workItem)
|
||||
} else {
|
||||
highlightThumbnail(step)
|
||||
self.thumbnailsPanel!.highlightThumbnail(step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +97,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate, NSCol
|
||||
workItems.forEach({ $0.cancel() })
|
||||
workItems.removeAll()
|
||||
window?.focus()
|
||||
Application.shared.hide(nil)
|
||||
thumbnailsPanel!.orderOut(nil)
|
||||
appIsBeingUsed = false
|
||||
isFirstSummon = true
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class Cell: NSCollectionViewItem {
|
||||
textContainer.maximumNumberOfLines = 1
|
||||
textContainer.lineFragmentPadding = 0
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
text = NSTextView.init(frame: NSRect.zero, textContainer: textContainer)
|
||||
text = NSTextView(frame: NSRect.zero, textContainer: textContainer)
|
||||
text.drawsBackground = true
|
||||
text.backgroundColor = .clear
|
||||
text.isSelectable = false
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import Cocoa
|
||||
|
||||
class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
var inputsMap = [NSTextView: String]()
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
let rows = makeLabelsAndInputs()
|
||||
let gridView = makeGridView(rows)
|
||||
makeAndAddWarningLabel(gridView)
|
||||
title = Application.name + " Preferences"
|
||||
hidesOnDeactivate = false
|
||||
contentView = gridView
|
||||
}
|
||||
|
||||
private func makeLabelsAndInputs() -> [[NSTextView]] {
|
||||
var rows = [[NSTextView]]()
|
||||
[
|
||||
("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
|
||||
}
|
||||
|
||||
private func makeGridView(_ rows: [[NSTextView]]) -> NSGridView {
|
||||
let gridView = NSGridView(views: rows)
|
||||
gridView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
gridView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 580).isActive = true
|
||||
return gridView
|
||||
}
|
||||
|
||||
private func makeAndAddWarningLabel(_ gridView: NSGridView) {
|
||||
let warningLabel = makeLabel("Some settings require restarting the app to apply")
|
||||
warningLabel.textColor = .systemRed
|
||||
warningLabel.alignment = .center
|
||||
gridView.addRow(with: [warningLabel, NSGridCell.emptyContentView])
|
||||
gridView.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: gridView.numberOfRows - 1, length: 1))
|
||||
}
|
||||
|
||||
private func makePreference(_ labelText: String, _ initialValue: 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
|
||||
return [label, input]
|
||||
}
|
||||
|
||||
private func makeLabel(_ text: String) -> NSTextView {
|
||||
let label = NSTextView()
|
||||
label.drawsBackground = true
|
||||
label.backgroundColor = .clear
|
||||
label.isSelectable = false
|
||||
label.isEditable = false
|
||||
label.font = Preferences.font
|
||||
label.string = text
|
||||
label.enabledTextCheckingTypes = 0
|
||||
label.heightAnchor.constraint(greaterThanOrEqualToConstant: Preferences.fontHeight + Preferences.interItemPadding).isActive = true
|
||||
return label
|
||||
}
|
||||
|
||||
func textDidEndEditing(_ notification: Notification) {
|
||||
if let textView = notification.object as? NSTextView {
|
||||
let key = inputsMap[textView]!
|
||||
do {
|
||||
try Preferences.updateAndValidateValue(key, textView.string)
|
||||
try Preferences.saveRawToDisk()
|
||||
} catch {
|
||||
textView.string = Preferences.rawValues[key]!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Cocoa
|
||||
|
||||
class StatusItem {
|
||||
static func make(_ application: Application) -> NSStatusItem {
|
||||
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
item.button!.title = Application.name
|
||||
item.menu = NSMenu()
|
||||
item.menu!.addItem(
|
||||
withTitle: "Version #VERSION#",
|
||||
action: nil,
|
||||
keyEquivalent: "")
|
||||
item.menu!.addItem(
|
||||
withTitle: "Preferences…",
|
||||
action: #selector(application.showCenteredPreferencesPanel),
|
||||
keyEquivalent: ",")
|
||||
item.menu!.addItem(
|
||||
withTitle: "Quit \(ProcessInfo.processInfo.processName)",
|
||||
action: #selector(NSApplication.terminate(_:)),
|
||||
keyEquivalent: "q")
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import Cocoa
|
||||
|
||||
class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout {
|
||||
var backgroundView: NSVisualEffectView?
|
||||
var collectionView_: NSCollectionView?
|
||||
var application: Application?
|
||||
let cellId = NSUserInterfaceItemIdentifier("Cell")
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
}
|
||||
|
||||
convenience init(_ application: Application) {
|
||||
self.init()
|
||||
self.application = application
|
||||
styleMask = [.borderless]
|
||||
isFloatingPanel = true
|
||||
animationBehavior = NSWindow.AnimationBehavior.none
|
||||
backgroundColor = .clear
|
||||
hidesOnDeactivate = false
|
||||
makeThumbnailsPanelBackgroundView()
|
||||
contentView = backgroundView!
|
||||
}
|
||||
|
||||
private func makeThumbnailsPanelBackgroundView() {
|
||||
backgroundView = NSVisualEffectView()
|
||||
backgroundView!.blendingMode = .behindWindow
|
||||
backgroundView!.material = .dark
|
||||
backgroundView!.state = .active
|
||||
makeCollectionView()
|
||||
backgroundView!.addSubview(collectionView_!)
|
||||
}
|
||||
|
||||
func makeCollectionView() {
|
||||
collectionView_ = NSCollectionView()
|
||||
collectionView_!.dataSource = self
|
||||
collectionView_!.delegate = self
|
||||
collectionView_!.collectionViewLayout = makeLayout()
|
||||
collectionView_!.backgroundColors = [.clear]
|
||||
collectionView_!.isSelectable = true
|
||||
collectionView_!.allowsMultipleSelection = false
|
||||
collectionView_!.register(Cell.self, forItemWithIdentifier: cellId)
|
||||
}
|
||||
|
||||
func makeLayout() -> CollectionViewCenterFlowLayout {
|
||||
let layout = CollectionViewCenterFlowLayout()
|
||||
layout.estimatedItemSize = NSSize(width: Preferences.thumbnailMaxWidth, height: Preferences.thumbnailMaxHeight)
|
||||
layout.minimumInteritemSpacing = 5
|
||||
layout.minimumLineSpacing = 5
|
||||
return layout
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
// debugPrint("collectionView: count items", openWindows.count)
|
||||
application!.openWindows.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
// debugPrint("collectionView: make item", indexPath.item)
|
||||
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell
|
||||
item.updateWithNewContent(application!.openWindows[indexPath.item], application!.focusSelectedWindow)
|
||||
return item
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
|
||||
// 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 .zero
|
||||
}
|
||||
|
||||
func highlightThumbnail(_ step: Int) {
|
||||
collectionView_!.selectItems(at: [IndexPath(item: application!.selectedOpenWindow, section: 0)], scrollPosition: .top)
|
||||
collectionView_!.deselectItems(at: [IndexPath(item: application!.cellWithStep(-step), section: 0)])
|
||||
}
|
||||
|
||||
func computeThumbnails() {
|
||||
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()
|
||||
collectionView_!.layoutSubtreeIfNeeded()
|
||||
setContentSize(NSSize(width: collectionView_!.frame.size.width + Preferences.windowPadding * 2, height: collectionView_!.frame.size.height + Preferences.windowPadding * 2))
|
||||
backgroundView!.setFrameSize(frame.size)
|
||||
collectionView_!.setFrameOrigin(NSPoint(x: Preferences.windowPadding, y: Preferences.windowPadding))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@ set -ex
|
||||
|
||||
version="$(cat VERSION.txt)"
|
||||
# set the version for the app menubar menu text
|
||||
sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/ui/Application.swift
|
||||
sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/ui/StatusItem.swift
|
||||
# set the version in the app meta-data for the AppStore and app "Get Info" panel
|
||||
sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/Info.plist
|
||||
Reference in new issue
Block a user