No more keyboard shortcuts sounds in the popover.

This commit is contained in:
2025-01-15 15:10:11 -08:00
parent 94a2c217ef
commit e1146840f6
16 changed files with 257 additions and 173 deletions

4
.gitignore vendored
View File

@@ -2,3 +2,7 @@
arm64 arm64
x86_64 x86_64
build build
CmdBar
CmdBar.app
cmdbar_updater
src/updater

View File

@@ -5,7 +5,7 @@
// Created by Igor Kolokolnikov on 8/16/23. // Created by Igor Kolokolnikov on 8/16/23.
// //
import Cocoa import AppKit
// MARK: - Constants // MARK: - Constants
fileprivate enum AboutLinks { fileprivate enum AboutLinks {
@@ -24,12 +24,6 @@ enum Strings {
static let activating = "Activating..." static let activating = "Activating..."
} }
fileprivate enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
}
// MARK: - Controller // MARK: - Controller
class AboutViewController: NSViewController, NSTextFieldDelegate { class AboutViewController: NSViewController, NSTextFieldDelegate {
// MARK: - Views // MARK: - Views

View File

@@ -1,4 +1,4 @@
import Cocoa import AppKit
import ServiceManagement import ServiceManagement
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {

View File

@@ -28,7 +28,7 @@
// from the coverage of the license being applied to the rest of the code. // from the coverage of the license being applied to the rest of the code.
// //
import Cocoa import AppKit
import os import os
final class CBMenuBarItem: NSObject, NSWindowDelegate { final class CBMenuBarItem: NSObject, NSWindowDelegate {

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict/> <dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist> </plist>

View File

@@ -1,14 +1,12 @@
import Foundation import AppKit
import Cocoa
// MARK: - CmdFile
class CmdFile { class CmdFile {
// MARK: - State
private var timer: DispatchSourceTimer? private var timer: DispatchSourceTimer?
private(set) var lastExec = Int(Date().timeIntervalSince1970) private(set) var lastExec = Int(Date().timeIntervalSince1970)
var untilNextExec: Int { var untilNextExec: Int {
get { get {
let res = reloadTime - (Int(Date().timeIntervalSince1970) - lastExec) let res = reloadTime -
(Int(Date().timeIntervalSince1970) - lastExec)
if res <= 0 { return reloadTime } if res <= 0 { return reloadTime }
return res return res
} }
@@ -16,11 +14,10 @@ class CmdFile {
private(set) var url: URL private(set) var url: URL
private(set) var reloadTime: Int private(set) var reloadTime: Int
private var statusItem: CBMenuBarItem = CBMenuBarItem() private(set) var statusItem: CBMenuBarItem = CBMenuBarItem()
private var shouldWait = false private var shouldWait = false
// MARK: - Initializers
init(url: URL, reloadTime: Int) { init(url: URL, reloadTime: Int) {
self.url = url self.url = url
self.reloadTime = reloadTime self.reloadTime = reloadTime
@@ -37,7 +34,6 @@ class CmdFile {
cancelTimer() cancelTimer()
} }
// MARK: - Reload Widget
private func reloadWidget() { private func reloadWidget() {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in DispatchQueue.global(qos: .userInitiated).async { [weak self] in
if let url = self?.url { if let url = self?.url {
@@ -45,10 +41,13 @@ class CmdFile {
let execResult = execute(atPath: url) let execResult = execute(atPath: url)
DispatchQueue.main.async { DispatchQueue.main.async {
if execResult.title.isEmpty { if execResult.title.isEmpty {
self?.statusItem.setImage(title: "exclamationmark.triangle.fill", description: "Warning") self?.statusItem.setImage(
title: "exclamationmark.triangle.fill",
description: "Warning")
self?.statusItem.setTitle("") self?.statusItem.setTitle("")
} else { } else {
self?.statusItem.setImage(title: nil, description: "") self?.statusItem.setImage(title: nil,
description: "")
self?.statusItem.setTitle(execResult.0) self?.statusItem.setTitle(execResult.0)
} }
self?.statusItem.setContents(to: execResult.body) self?.statusItem.setContents(to: execResult.body)
@@ -58,7 +57,6 @@ class CmdFile {
} }
} }
// MARK: - Timer
private func setupTimer() { private func setupTimer() {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer?.schedule(deadline: .now(), repeating: .seconds(reloadTime)) timer?.schedule(deadline: .now(), repeating: .seconds(reloadTime))
@@ -91,15 +89,12 @@ class CmdFile {
} }
} }
// MARK: - CmdManager
class CmdManager { class CmdManager {
//MARK: - State
static let standard = CmdManager() static let standard = CmdManager()
var paths: [CmdFile] = [] var paths: [CmdFile] = []
private var defaultStatusItem: NSStatusItem? private var defaultStatusItem: NSStatusItem?
// MARK: - Configure
func configure() { func configure() {
getPaths() getPaths()
if paths.isEmpty { if paths.isEmpty {
@@ -111,23 +106,31 @@ class CmdManager {
private func getPaths() { private func getPaths() {
let manager = FileManager.default let manager = FileManager.default
let path = manager.homeDirectoryForCurrentUser.appending(path: ".cmdbar") let path = manager.homeDirectoryForCurrentUser
if let contents = try? manager.contentsOfDirectory(atPath: path.path()) { .appending(path: ".cmdbar")
if let contents = try? manager
.contentsOfDirectory(atPath: path.path()) {
for content in contents { for content in contents {
let filePath = path.appending(path: content) let filePath = path.appending(path: content)
let domains = filePath.path().components(separatedBy: ".") let domains = filePath.path().components(separatedBy: ".")
if (isValidTimeFormat(domains[domains.count - 1])) { if (isValidTimeFormat(domains[domains.count - 1])) {
paths.append(CmdFile(url: filePath, reloadTime: intervalToSeconds(from: domains[domains.count - 1]))) paths.append(CmdFile(url: filePath,
} else if (domains.last == "sh" && isValidTimeFormat(domains[domains.count - 2])) { reloadTime: intervalToSeconds(
paths.append(CmdFile(url: filePath, reloadTime: intervalToSeconds(from: domains[domains.count - 2]))) from: domains[domains.count - 1])))
} else if domains.last == "sh" &&
isValidTimeFormat(domains[domains.count - 2]) {
paths.append(CmdFile(url: filePath,
reloadTime: intervalToSeconds(
from: domains[domains.count - 2])))
} }
} }
} }
} }
private func setupMenu() { private func setupMenu() {
defaultStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) defaultStatusItem = NSStatusBar.system
.statusItem(withLength: NSStatusItem.variableLength)
if let btn = defaultStatusItem?.button { if let btn = defaultStatusItem?.button {
let image = NSApp.applicationIconImage let image = NSApp.applicationIconImage
image!.size = NSSize(width: 22, height: 22) image!.size = NSSize(width: 22, height: 22)
@@ -135,13 +138,21 @@ class CmdManager {
} }
guard defaultStatusItem != nil else { return } guard defaultStatusItem != nil else { return }
let menu = NSMenu() let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Reload", action: #selector(reloadWidgets), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Reload",
action: #selector(reloadWidgets),
keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "About",
action: #selector(showAbout),
keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Check for Updates...", action: #selector(checkForUpdates), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "Check for Updates...",
action: #selector(checkForUpdates),
keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "Quit",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q"))
defaultStatusItem!.menu = menu defaultStatusItem!.menu = menu
} }
@@ -164,7 +175,6 @@ class CmdManager {
reloadItems() reloadItems()
} }
// MARK: - NSMenu Functions
@objc private func showAbout() { @objc private func showAbout() {
delegate.showAbout() delegate.showAbout()
} }

View File

@@ -1,22 +1,17 @@
import Cocoa import AppKit
import Carbon
import ServiceManagement import ServiceManagement
import OSLog
fileprivate enum Metrics { fileprivate enum Metrics {
static let contentMinWidth = 400.0 static let contentMinWidth = 400.0
static let contentMaxWidth = /*600.0*/ NSScreen.main!.visibleFrame.size.width * 0.7 // REVIEW: Under what circumstances can NSScreen return nil? static let contentMaxWidth = /*600.0*/ NSScreen.main!.visibleFrame.size.width * 0.7 // WARNING: Under what circumstances can NSScreen return nil?
static let contentMinHeight = 160.0 static let contentMinHeight = 160.0
static let contentMaxHeight = /*800.0*/ NSScreen.main!.visibleFrame.size.height * 0.8 // REVIEW: Under what circumstances can NSScreen return nil? static let contentMaxHeight = /*800.0*/ NSScreen.main!.visibleFrame.size.height * 0.8 // WARNING: Under what circumstances can NSScreen return nil?
static let padding = 10.0 static let padding = 10.0
static let buttonSpacing = 2.0 static let buttonSpacing = 2.0
} }
class CmdViewController: NSViewController { class CmdViewController: NSViewController {
fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: CmdViewController.self)
)
private weak var cmdFile: CmdFile? private weak var cmdFile: CmdFile?
private var timer: DispatchSourceTimer? private var timer: DispatchSourceTimer?
private var keyboardEvents: EventMonitor? private var keyboardEvents: EventMonitor?
@@ -332,7 +327,10 @@ class CmdViewController: NSViewController {
} }
private func resetReloadButton() { private func resetReloadButton() {
reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemBlue])) reloadButton.image = systemImage("arrow.clockwise.circle.fill",
.title2, .large,
.init(paletteColors: [.white,
.systemBlue]))
reloadButton.action = #selector(reloadWidget) reloadButton.action = #selector(reloadWidget)
reloadButton.toolTip = "Reload Current" reloadButton.toolTip = "Reload Current"
} }
@@ -351,51 +349,51 @@ class CmdViewController: NSViewController {
} }
private func setupKeyEvents() { private func setupKeyEvents() {
keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown], handler: { [weak self] event in keyboardEvents = LocalEventMonitor(mask: [.flagsChanged, .keyDown],
handler:
{ [weak self] event in
let modifiers = event.modifierFlags.rawValue let modifiers = event.modifierFlags.rawValue
let command = NSEvent.ModifierFlags.command.rawValue let key = event.keyCode
let shift = NSEvent.ModifierFlags.shift.rawValue
let control = NSEvent.ModifierFlags.control.rawValue
let option = NSEvent.ModifierFlags.option.rawValue
if event.modifierFlags.contains(.shift) { if modsContains(keys: OSShift, in: modifiers) {
self?.reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemCyan])) self?.reloadButton.image =
systemImage("arrow.clockwise.circle.fill", .title2,
.large,.init(paletteColors: [.white,
.systemCyan]))
self?.reloadButton.action = #selector(self?.reloadWidgets) self?.reloadButton.action = #selector(self?.reloadWidgets)
self?.reloadButton.toolTip = "Reload All" self?.reloadButton.toolTip = "Reload All"
} else { } else {
self?.reloadButton.image = systemImage("arrow.clockwise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemBlue])) self?.reloadButton.image =
systemImage("arrow.clockwise.circle.fill", .title2,
.large, .init(paletteColors: [.white,
.systemBlue]))
self?.reloadButton.action = #selector(self?.reloadWidget) self?.reloadButton.action = #selector(self?.reloadWidget)
self?.reloadButton.toolTip = "Reload Current" self?.reloadButton.toolTip = "Reload Current"
} }
// NOTE: Standalone keys should go last! if modsContains(keys: OSCmd, in: modifiers) {
if (modifiers & command) == command, if key == kVK_ANSI_Q {
(modifiers & (control | shift | option)) == 0, self?.terminateApp()
event.keyCode == 12 // Q } else if key == kVK_ANSI_W {
{ self?.view.superview?.window?.resignKey()
self?.terminateApp() } else if key == kVK_ANSI_R {
} else if (modifiers & command) == command, self?.reloadWidget()
(modifiers & (control | shift | option)) == 0, } else if key == kVK_ANSI_C {
event.keyCode == 13 // W self?.textLabel.currentEditor()?.copy(nil)
{ }
self?.view.superview?.window?.resignKey() } else if modsContains(keys: OSCmd | OSShift, in: modifiers) {
} else if (modifiers & command) == command, if key == kVK_ANSI_R {
(modifiers & (control | shift | option)) == 0, // NOTE: We need to resign the window in order to
event.keyCode == 15 // R // prevent the program from intercepting the
{ // events, which result in sounds beign made for
self?.reloadWidget() // missing performKeyEquivalent.
} else if (modifiers & (command & shift)) == command & shift, self?.view.window?.resignKey()
(modifiers & (control | option)) == 0, self?.reloadWidgets()
event.keyCode == 15 // R }
{ } else if modsContainsNone(in: modifiers) {
self?.reloadWidgets() if key == kVK_ANSI_Q || key == kVK_Escape {
} else if (modifiers & command) == command, self?.view.superview?.window?.resignKey()
(modifiers & (control | shift | option)) == 0, }
event.keyCode == 8 // C
{
self?.textLabel.currentEditor()?.copy(nil)
} else if event.keyCode == 12 || event.keyCode == 53 { // Q, ESC
self?.view.superview?.window?.resignKey()
} }
return event return event

View File

@@ -1,32 +1,44 @@
import Cocoa import AppKit
import Carbon
final class EditableNSTextField: NSTextField { final class EditableNSTextField: NSTextField {
private let commandKey = NSEvent.ModifierFlags.command.rawValue
private let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
override func performKeyEquivalent(with event: NSEvent) -> Bool { override func performKeyEquivalent(with event: NSEvent) -> Bool {
let modifiers = event.modifierFlags.rawValue
let key = event.keyCode
if event.type == NSEvent.EventType.keyDown { if event.type == NSEvent.EventType.keyDown {
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey { if modsContains(keys: OSCmd, in: modifiers) {
switch event.charactersIgnoringModifiers! { if key == kVK_ANSI_X {
case "x": if NSApp.sendAction(#selector(NSText.cut(_:)),
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true } to: nil, from: self)
case "c": { return true }
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true } } else if key == kVK_ANSI_C {
case "v": if NSApp.sendAction(#selector(NSText.copy(_:)),
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true } to: nil, from: self)
case "z": { return true }
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true } } else if key == kVK_ANSI_V {
case "a": if NSApp.sendAction(#selector(NSText.paste(_:)),
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true } to: nil, from: self)
default: { return true }
break } else if key == kVK_ANSI_Z {
if NSApp.sendAction(Selector(("undo:")),
to: nil, from: self)
{ return true }
} else if key == kVK_ANSI_A {
if NSApp.sendAction(
#selector(NSResponder.selectAll(_:)),
to: nil, from: self)
{ return true }
} }
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey { } else if modsContains(keys: OSCmd | OSShift, in: modifiers) {
if event.charactersIgnoringModifiers == "Z" { if key == kVK_ANSI_Z {
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true } if NSApp.sendAction(Selector(("redo:")),
to: nil, from: self)
{ return true }
} }
} }
} }
return super.performKeyEquivalent(with: event) return super.performKeyEquivalent(with: event)
} }
} }

View File

@@ -1,4 +1,4 @@
import Cocoa import AppKit
class EventMonitor { class EventMonitor {
fileprivate let mask: NSEvent.EventTypeMask fileprivate let mask: NSEvent.EventTypeMask

View File

@@ -1,6 +1,32 @@
import AppKit import AppKit
import Carbon
let OSCtrl = NSEvent.ModifierFlags.control.rawValue
let OSCmd = NSEvent.ModifierFlags.command.rawValue
let OSOpt = NSEvent.ModifierFlags.option.rawValue
let OSShift = NSEvent.ModifierFlags.shift.rawValue
let OSMods = UInt(OSCtrl | OSCmd | OSOpt | OSShift)
func modsContains(keys: UInt, in modifiers: UInt) -> Bool {
return (modifiers & keys) == keys && ((modifiers ^ keys) & OSMods) == 0
}
func modsContainsNone(in modifiers: UInt) -> Bool {
return (modifiers & OSMods) == 0
}
enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing5: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing15: CGFloat = 15
static let spacing20: CGFloat = 20
static let spacing25: CGFloat = 25
static let spacing30: CGFloat = 30
static let spacing35: CGFloat = 35
static let spacing40: CGFloat = 40
}
// MARK: - Tracking Notifications
fileprivate extension Notification.Name { fileprivate extension Notification.Name {
static let beginMenuTracking = Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification") static let beginMenuTracking = Notification.Name("com.apple.HIToolbox.beginMenuTrackingNotification")
static let endMenuTracking = Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification") static let endMenuTracking = Notification.Name("com.apple.HIToolbox.endMenuTrackingNotification")
@@ -9,10 +35,10 @@ fileprivate extension Notification.Name {
#if(DEBUG) #if(DEBUG)
struct EndpointConstants { struct EndpointConstants {
static var versionURL = "http://localhost:8081/version" static var versionURL = "http://localhost:8081/version"
static var appURL = "http://localhost:8081/download" static var appURL = "http://localhost:8081/download"
static var appURLZip = "http://localhost:8081/static/app/CmdBar.zip" static var appURLZip = "http://localhost:8081/static/app/CmdBar.zip"
static var changelog = "http://localhost:8081/changelog" static var changelog = "http://localhost:8081/changelog"
static var validateURL = "http://localhost:8081/validate" static var validateURL = "http://localhost:8081/validate"
} }
#else #else
struct EndpointConstants { struct EndpointConstants {
@@ -24,7 +50,6 @@ struct EndpointConstants {
} }
#endif #endif
//
enum Resulting<Success, Failure> { enum Resulting<Success, Failure> {
case success case success
case failure case failure
@@ -156,7 +181,10 @@ func getColors(_ light: NSColor, _ dark: NSColor, for appearance: NSAppearance)
} }
// ex: systemImage("sunrise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange])) // ex: systemImage("sunrise.circle.fill", .title2, .large, .init(paletteColors: [.white, .systemOrange]))
func systemImage(_ name: String, _ size: NSFont.TextStyle, _ scale: NSImage.SymbolScale, _ configuration: NSImage.SymbolConfiguration) -> NSImage? { func systemImage(_ name: String, _ size: NSFont.TextStyle,
_ scale: NSImage.SymbolScale,
_ configuration: NSImage.SymbolConfiguration) -> NSImage?
{
return NSImage(systemSymbolName: name, accessibilityDescription: nil)? return NSImage(systemSymbolName: name, accessibilityDescription: nil)?
.withSymbolConfiguration( .withSymbolConfiguration(
NSImage.SymbolConfiguration(textStyle: size, scale: scale) NSImage.SymbolConfiguration(textStyle: size, scale: scale)

View File

@@ -5,7 +5,7 @@
// Created by Igor Kolokolnikov on 8/26/23. // Created by Igor Kolokolnikov on 8/26/23.
// //
import Cocoa import AppKit
import CryptoKit import CryptoKit
fileprivate struct LicenseModel: Codable { fileprivate struct LicenseModel: Codable {

View File

@@ -86,7 +86,11 @@ $(EXEC).app: $(EXEC)
cp resources/AppIcon.icns $@/Contents/Resources/ && \ cp resources/AppIcon.icns $@/Contents/Resources/ && \
cp $(EXEC) $@/Contents/MacOS/ && \ cp $(EXEC) $@/Contents/MacOS/ && \
cp cmdbar_updater $@/Contents/MacOS/ && \ cp cmdbar_updater $@/Contents/MacOS/ && \
codesign -s ${DEVELOPER_ID} -f --timestamp -o runtime $(EXEC).app $(if $(DEBUG), \
codesign --entitlements Grapp.entitlements \
-s ${APPLE_DEVELOPMENT} -f --timestamp -o runtime $(EXEC).app, \
codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp \
-o runtime $(EXEC).app)
all: zip cmdbar_updater $(EXEC).app all: zip cmdbar_updater $(EXEC).app

View File

@@ -1,4 +1,4 @@
import Cocoa import AppKit
class MenulessWindow: NSWindow { class MenulessWindow: NSWindow {
init(viewController: NSViewController) { init(viewController: NSViewController) {

View File

@@ -1,4 +1,4 @@
import Cocoa import AppKit
class PopoverPanel: NSPanel { class PopoverPanel: NSPanel {
override var canBecomeKey: Bool { true } override var canBecomeKey: Bool { true }
@@ -6,7 +6,8 @@ class PopoverPanel: NSPanel {
init(viewController: NSViewController) { init(viewController: NSViewController) {
super.init( super.init(
contentRect: CGRect(x: 0, y: 0, width: 100, height: 100), contentRect: CGRect(x: 0, y: 0, width: 100, height: 100),
styleMask: [.titled, .nonactivatingPanel, .utilityWindow, .fullSizeContentView], styleMask: [.titled, .nonactivatingPanel, .utilityWindow,
.fullSizeContentView],
backing: .buffered, backing: .buffered,
defer: false defer: false
) )
@@ -22,7 +23,8 @@ class PopoverPanel: NSPanel {
titlebarAppearsTransparent = true titlebarAppearsTransparent = true
animationBehavior = .none animationBehavior = .none
collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .transient] collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary,
.transient]
isReleasedWhenClosed = false isReleasedWhenClosed = false
hidesOnDeactivate = false hidesOnDeactivate = false

View File

@@ -1,28 +1,14 @@
//
// UpdateViewController.swift
// CmdBar
//
// Created by Igor Kolokolnikov on 5/23/24.
//
import AppKit import AppKit
fileprivate enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
static let spacing40: CGFloat = 40
}
fileprivate enum UpdateStatus: String { fileprivate enum UpdateStatus: String {
case check = "Check for updates..." case check = "Check for updates..."
case latest = "You're up-to-date!" case latest = "You're up-to-date!"
case checking = "Checking for udpates..." case checking = "Checking for udpates..."
case available = "A new version is available!" case available = "A new version is available!"
case downloading = "Downloading update..." case downloading = "Downloading update..."
case extracting = "Extracting update..." case extracting = "Extracting update..."
case install = "Ready to Install" case install = "Ready to Install"
case checkFailed = "Failed to Check for Updates" case checkFailed = "Failed to Check for Updates"
case installFailed = "Failed to Install Update" case installFailed = "Failed to Install Update"
} }
@@ -30,9 +16,9 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
private weak var updaterDelegate: UpdateManagerDelegate? private weak var updaterDelegate: UpdateManagerDelegate?
private var appIconImage: NSImageView = { private var appIconImage: NSImageView = {
//let image = NSImageView(image: NSApp.applicationIconImage)
let image = NSImageView() let image = NSImageView()
image.image = NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) image.image = NSWorkspace.shared
.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false image.translatesAutoresizingMaskIntoConstraints = false
return image return image
@@ -45,7 +31,9 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) } textField.textColor = NSColor(name: nil) { getColors(.black, .white, for: $0) }
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -59,18 +47,23 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
textField.drawsBackground = false textField.drawsBackground = false
textField.alignment = .left textField.alignment = .left
textField.textColor = .gray textField.textColor = .gray
textField.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .bold) textField.font = NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .bold)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
private var changelogLabel: NSScrollView = { // TODO: Improve some more. Maybe make it scrollable. // TODO: Improve some more. Maybe make it scrollable.
private var changelogLabel: NSScrollView = {
let scrollableTextView = NSTextView.scrollableTextView() let scrollableTextView = NSTextView.scrollableTextView()
scrollableTextView.isHidden = true scrollableTextView.isHidden = true
(scrollableTextView.documentView as? NSTextView)?.isEditable = false (scrollableTextView.documentView as? NSTextView)?.isEditable = false
// (scrollableTextView.documentView as? NSTextView)?.drawsBackground = false
(scrollableTextView.documentView as? NSTextView)?.textColor = .gray (scrollableTextView.documentView as? NSTextView)?.textColor = .gray
(scrollableTextView.documentView as? NSTextView)?.font = NSFont.systemFont(ofSize: NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .regular) (scrollableTextView.documentView as? NSTextView)?.font =
NSFont.systemFont(ofSize: NSFontDescriptor
.preferredFontDescriptor(forTextStyle: .body).pointSize,
weight: .regular)
scrollableTextView.translatesAutoresizingMaskIntoConstraints = false scrollableTextView.translatesAutoresizingMaskIntoConstraints = false
return scrollableTextView return scrollableTextView
}() }()
@@ -139,26 +132,46 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 70), appIconImage.widthAnchor.constraint(equalToConstant: 70),
appIconImage.heightAnchor.constraint(equalTo: appIconImage.widthAnchor, multiplier: 1), appIconImage.heightAnchor
.constraint(equalTo: appIconImage.widthAnchor,
multiplier: 1),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing10),
appIconImage.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, constant: ViewConstants.spacing10), statusLabel.topAnchor
appIconImage.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: appIconImage.topAnchor,
constant: ViewConstants.spacing10),
statusLabel.leadingAnchor
.constraint(equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing10),
statusLabel.topAnchor.constraint(equalTo: appIconImage.topAnchor, constant: ViewConstants.spacing2), subStatusLabel.topAnchor
statusLabel.leadingAnchor.constraint(equalTo: appIconImage.trailingAnchor, constant: ViewConstants.spacing10), .constraint(equalTo: statusLabel.bottomAnchor,
constant: ViewConstants.spacing10),
subStatusLabel.leadingAnchor
.constraint(equalTo: statusLabel.leadingAnchor),
subStatusLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: ViewConstants.spacing10), changelogLabel.widthAnchor
subStatusLabel.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), .constraint(equalTo: stackView.widthAnchor),
changelogLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor),
changelogLabel.heightAnchor.constraint(equalToConstant: 150), changelogLabel.heightAnchor.constraint(equalToConstant: 150),
stackView.topAnchor.constraint(equalTo: subStatusLabel.bottomAnchor, constant: ViewConstants.spacing2), stackView.topAnchor
stackView.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor), .constraint(equalTo: subStatusLabel.bottomAnchor,
view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: ViewConstants.spacing20), constant: ViewConstants.spacing2),
view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: ViewConstants.spacing20), stackView.leadingAnchor
.constraint(equalTo: statusLabel.leadingAnchor),
view.trailingAnchor
.constraint(equalTo: stackView.trailingAnchor,
constant: ViewConstants.spacing20),
view.bottomAnchor
.constraint(equalTo: stackView.bottomAnchor,
constant: ViewConstants.spacing20),
buttonsStackView.heightAnchor.constraint(equalTo: updateButton.heightAnchor), buttonsStackView.heightAnchor
.constraint(equalTo: updateButton.heightAnchor),
]) ])
} }
@@ -192,36 +205,44 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
case .check: case .check:
break break
case .latest: case .latest:
self?.statusLabel.stringValue = UpdateStatus.latest.rawValue self?.statusLabel.stringValue = UpdateStatus.latest
.rawValue
break break
case .checking: case .checking:
self?.statusLabel.stringValue = UpdateStatus.checking.rawValue self?.statusLabel.stringValue = UpdateStatus.checking
.rawValue
self?.updateButton.keyEquivalent = "" self?.updateButton.keyEquivalent = ""
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
case .available: case .available:
self?.updateButton.title = "Download" self?.updateButton.title = "Download"
self?.updateButton.keyEquivalent = "" self?.updateButton.keyEquivalent = ""
self?.statusLabel.stringValue = UpdateStatus.available.rawValue self?.statusLabel.stringValue = UpdateStatus.available
.rawValue
self?.updateButton.action = #selector(self!.downloadUpdate) self?.updateButton.action = #selector(self!.downloadUpdate)
case .downloading: case .downloading:
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.downloading.rawValue self?.statusLabel.stringValue = UpdateStatus.downloading
.rawValue
case .extracting: case .extracting:
self?.updateButton.isEnabled = false self?.updateButton.isEnabled = false
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.extracting.rawValue self?.statusLabel.stringValue = UpdateStatus.extracting
.rawValue
case .install: case .install:
self?.updateButton.title = "Install" self?.updateButton.title = "Install"
self?.progressIndicator.isHidden = false self?.progressIndicator.isHidden = false
self?.statusLabel.stringValue = UpdateStatus.install.rawValue self?.statusLabel.stringValue = UpdateStatus.install
.rawValue
self?.updateButton.action = #selector(self!.installUpdate) self?.updateButton.action = #selector(self!.installUpdate)
case .checkFailed: case .checkFailed:
self?.statusLabel.stringValue = UpdateStatus.checkFailed.rawValue self?.statusLabel.stringValue = UpdateStatus.checkFailed
.rawValue
case .installFailed: case .installFailed:
self?.statusLabel.stringValue = UpdateStatus.installFailed.rawValue self?.statusLabel.stringValue = UpdateStatus.installFailed
.rawValue
self?.updateButton.action = #selector(self!.checkForUpdate) self?.updateButton.action = #selector(self!.checkForUpdate)
} }
} }
@@ -241,8 +262,13 @@ class UpdateViewController: NSViewController, UpdateManagerDelegate {
self?.subStatusLabel.isHidden = false self?.subStatusLabel.isHidden = false
self?.changelogLabel.isHidden = false self?.changelogLabel.isHidden = false
for (i, change) in model.changes.enumerated() { for (i, change) in model.changes.enumerated() {
(self?.changelogLabel.documentView as? NSTextView)?.string += "\u{2022} \(change)" (self?.changelogLabel.documentView as?
if i < model.changes.count-1 { (self?.changelogLabel.documentView as? NSTextView)?.string += "\n" } NSTextView)?.string +=
"\u{2022} \(change)"
if i < model.changes.count-1 {
(self?.changelogLabel.documentView as?
NSTextView)?.string += "\n"
}
} }
} }
} }

View File

@@ -20,7 +20,8 @@ FRAMEWORKS = -framework AppKit
$(EXEC): ./arm64/$(EXEC) ./x86_64/$(EXEC) $(EXEC): ./arm64/$(EXEC) ./x86_64/$(EXEC)
lipo -create -output $(EXEC) $^ && \ lipo -create -output $(EXEC) $^ && \
codesign -s ${DEVELOPER_ID} -f --timestamp -o runtime $(EXEC) codesign -s ${APPLE_DEVELOPER_ID_APPLICATION} -f --timestamp \
-o runtime $(EXEC)
all: $(EXEC) all: $(EXEC)