diff --git a/alt-tab-macos/ui/Application.swift b/alt-tab-macos/ui/Application.swift index 0c8315293..3ce2d2118 100644 --- a/alt-tab-macos/ui/Application.swift +++ b/alt-tab-macos/ui/Application.swift @@ -124,4 +124,13 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate { func currentlySelectedWindow() -> OpenWindow? { return openWindows.count > selectedOpenWindow ? openWindows[selectedOpenWindow] : nil } + + func relaunch(afterDelay seconds: TimeInterval = 0.5) -> Never { + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""] + task.launch() + self.terminate(nil) + exit(0) + } } diff --git a/alt-tab-macos/ui/PreferencesPanel.swift b/alt-tab-macos/ui/PreferencesPanel.swift index 0e27da284..fd188bb4e 100644 --- a/alt-tab-macos/ui/PreferencesPanel.swift +++ b/alt-tab-macos/ui/PreferencesPanel.swift @@ -1,82 +1,143 @@ import Cocoa -class PreferencesPanel: NSPanel, NSTextViewDelegate { - var maxScreenUsage: NSTextView? - var windowPadding: NSTextView? - var cellPadding: NSTextView? - var cellBorderWidth: NSTextView? - var maxThumbnailsPerRow: NSTextView? - var iconSize: NSTextView? - var fontHeight: NSTextView? - var interItemPadding: NSTextView? - var tabKeyCode: NSTextView? - var windowDisplayDelay: NSTextView? +class PreferencesPanel: NSPanel { + // ui: base layout + let panelWidth = CGFloat(400) + let panelHeight = CGFloat(400) // gets auto adjusted to content height + let panelPadding = CGFloat(40) + + // ui: preferences elements + var maxScreenUsage: NSTextField? + var maxThumbnailsPerRow: NSTextField? + var iconSize: NSTextField? + var fontHeight: NSTextField? + var tabKeyCode: NSTextField? + var windowDisplayDelay: NSTextField? var metaKey: NSPopUpButton? var theme: NSPopUpButton? var showOnScreen: NSPopUpButton? - var inputsMap = [NSTextView: String]() + + var invisibleTextField: NSTextField? // default firstResponder and used for triggering of focus loose + var inputsMap = [NSTextField: String]() override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) title = Application.name + " Preferences" + titlebarAppearsTransparent = true hidesOnDeactivate = false - contentView = makeGridView(makeLabelsAndInputs(), makeWarningLabel()) + contentView = makeContentView() + + // setup hidden element + invisibleTextField = NSTextField() + invisibleTextField?.isHidden = true + contentView?.subviews.append(invisibleTextField!) + self.initialFirstResponder = invisibleTextField + } + + private func makeContentView() -> NSView { + let wrappingView = NSStackView(views: makePreferencesViews()) + let contentView = NSView() + contentView.addSubview(wrappingView) + + // visual setup + wrappingView.orientation = .vertical + wrappingView.alignment = .left + wrappingView.spacing = panelPadding * 0.3 + wrappingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: panelPadding * 0.5).isActive = true + wrappingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: panelPadding * 0.5 * -1).isActive = true + wrappingView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: panelPadding * 0.5).isActive = true + wrappingView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: panelPadding * 0.5 * -1).isActive = true + + return contentView } - private func makeLabelsAndInputs() -> [[NSView]] { + private func makePreferencesViews() -> [NSView] { return [ - makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels), makeLabelWithDropdown(\PreferencesPanel.metaKey, "Meta key to activate the app", "metaKey", Preferences.metaKeyMacro.labels), - makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key (HIToolbox.Events)", "tabKeyCode"), - makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size (screen %)", "maxScreenUsage"), + makeLabelWithInput(\PreferencesPanel.tabKeyCode, "Tab key", "tabKeyCode", "KeyCode"), + makeHorizontalSeparator(), + makeLabelWithDropdown(\PreferencesPanel.theme, "Main window theme", "theme", Preferences.themeMacro.labels), + makeLabelWithInput(\PreferencesPanel.maxScreenUsage, "Max window size", "maxScreenUsage", "% of screen"), makeLabelWithInput(\PreferencesPanel.maxThumbnailsPerRow, "Max thumbnails per row", "maxThumbnailsPerRow"), - makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size (px)", "iconSize"), - makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size (px)", "fontHeight"), - makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay (ms)", "windowDisplayDelay"), - makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels) + makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size", "iconSize", "px"), + makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size", "fontHeight", "px"), + makeHorizontalSeparator(), + makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay", "windowDisplayDelay", "ms"), + makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels), + makeHorizontalSeparator(), + makeRestartHint() ] } - private func makeGridView(_ rows: [[NSView]], _ warningLabel: BaseLabel) -> NSGridView { - let gridView = NSGridView(views: rows) - gridView.setContentHuggingPriority(.defaultLow, for: .horizontal) - gridView.setContentHuggingPriority(.defaultLow, for: .vertical) - gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true - gridView.addRow(with: [warningLabel, NSGridCell.emptyContentView]) - gridView.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: gridView.numberOfRows - 1, length: 1)) - return gridView + private func makeHorizontalSeparator() -> NSView { + let view = NSBox() + view.boxType = .separator + + return view } - private func makeWarningLabel() -> BaseLabel { - let warningLabel = BaseLabel("Some settings require restarting the app to apply") - warningLabel.textColor = .systemRed - warningLabel.alignment = .center - return warningLabel + @objc private func restartButtonAction() { + self.makeFirstResponder(invisibleTextField) + + if let delegate = Application.shared.delegate as? Application { + delegate.relaunch() + } } - private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath, _ labelText: String, _ rawName: String) -> [NSTextView] { - let label = BaseLabel(labelText) + private func makeRestartHint() -> NSStackView { + let field = NSTextField(wrappingLabelWithString: "Some settings require restarting the app to apply: ") + field.textColor = .systemRed + field.alignment = .right + field.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true + + let button = NSButton() + button.title = "↻ Restart" + button.bezelStyle = .rounded + button.target = self + button.action = #selector(restartButtonAction) + + let container = NSStackView(views: [field, button]) + container.alignment = .bottom + return container + } + + private func makeLabelWithInput(_ keyPath: ReferenceWritableKeyPath, _ labelText: String, _ rawName: String, _ suffixText: String? = nil) -> NSStackView { + let label = NSTextField(wrappingLabelWithString: labelText + ": ") label.alignment = .right - let input = NSTextView() - input.delegate = self - input.font = Preferences.font - input.string = Preferences.rawValues[rawName]! - input.widthAnchor.constraint(equalToConstant: 40).isActive = true + label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true + + let input = NSTextField(string: Preferences.rawValues[rawName]!) + input.target = self + input.action = #selector(textDidEndEditing) + input.widthAnchor.constraint(equalToConstant: 32).isActive = true + let containerView = NSStackView(views: [label, input]) + + if suffixText != nil { + let suffix = NSTextField(labelWithString: suffixText!) + suffix.textColor = .gray + containerView.addView(suffix, in: .leading) + } + self[keyPath: keyPath] = input inputsMap[input] = rawName - return [label, input] + + return containerView } - private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath, _ labelText: String, _ rawName: String, _ values: [String]) -> [NSView] { - let label = BaseLabel(labelText) + private func makeLabelWithDropdown(_ keyPath: ReferenceWritableKeyPath, _ labelText: String, _ rawName: String, _ values: [String]) -> NSStackView { + let label = NSTextField(wrappingLabelWithString: labelText + ": ") label.alignment = .right + label.widthAnchor.constraint(equalToConstant: (panelWidth - panelPadding) * 0.5).isActive = true + 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] + + return NSStackView(views: [label, input]) } @objc func dropdownDidChange(sender: AnyObject) throws { @@ -95,15 +156,15 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate { } } - func textDidEndEditing(_ notification: Notification) { - if let textView = notification.object as? NSTextView { - let key = inputsMap[textView]! + @objc func textDidEndEditing(sender: AnyObject) { + if let textField = sender as? NSTextField { + let key = inputsMap[textField]! do { - try Preferences.updateAndValidateFromString(key, textView.string) + try Preferences.updateAndValidateFromString(key, textField.stringValue) try Preferences.saveRawToDisk() } catch { debugPrint(key, error) - textView.string = Preferences.rawValues[key]! + textField.stringValue = Preferences.rawValues[key]! } } }