feat: new settings experience (closes #5287)

This commit is contained in:
lwouis committed 2026-02-16 22:52:58 +01:00
1 parent 93e2b35dc7
commit 3890beb9d3
28 files changed
+1841 -384

No files matched your search

+7 -7
View File
@@ -290,7 +290,7 @@
D04BAD451966B43720120D2E /* Menubar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD905546AA93E5117B0A /* Menubar.swift */; };
D04BADA3F2DAB820D6FC6E75 /* AppCenterApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = D04BA48BC82D3060C3A1AB11 /* AppCenterApplication.m */; };
D04BADCDA9F9A6C3D6499877 /* SystemPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA7C6F2519091717F4B4E /* SystemPermissions.swift */; };
D04BADE4DDF7597749FB13BD /* PreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA099FA155FB4D517D66B /* PreferencesWindow.swift */; };
D04BADE4DDF7597749FB13BD /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA099FA155FB4D517D66B /* SettingsWindow.swift */; };
D04BAE8B16A06A10E2FA94DE /* AccessibilityEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8FDCF137D892114F5F3 /* AccessibilityEvents.swift */; };
D04BAEE31B6FFCDC779E6C17 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D04BAC2FF99F629CD4ED20FC /* MainMenu.xib */; };
D04BAF12DF5D15B9D7D316A4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA61693F710CD7BD054D7 /* InfoPlist.strings */; };
@@ -578,7 +578,7 @@
D04BA075E359F36C27CD5BAE /* package-lock.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "package-lock.json"; sourceTree = "<group>"; };
D04BA084F638725465234830 /* favicon-16x16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "favicon-16x16.png"; sourceTree = "<group>"; };
D04BA088B8A550D9367A4AAB /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = InfoPlist.strings; sourceTree = "<group>"; };
D04BA099FA155FB4D517D66B /* PreferencesWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindow.swift; sourceTree = "<group>"; };
D04BA099FA155FB4D517D66B /* SettingsWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = "<group>"; };
D04BA0AAAE82C72855DBBA26 /* update_appcast.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = update_appcast.sh; 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>"; };
D04BA0E071D2EDFDB9A20523 /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
@@ -1468,7 +1468,7 @@
children = (
D04BAAB92261FC04854FDDE9 /* App.swift */,
D04BA62E30174C336E4080FA /* main-window */,
D04BABCD12CA72B06290C248 /* preferences-window */,
D04BABCD12CA72B06290C248 /* settings-window */,
D04BAD905546AA93E5117B0A /* Menubar.swift */,
D04BA26C75F76C277653C932 /* FeedbackWindow.swift */,
D04BA2CBF267F267225CA671 /* generic-components */,
@@ -1886,14 +1886,14 @@
path = ru.lproj;
sourceTree = "<group>";
};
D04BABCD12CA72B06290C248 /* preferences-window */ = {
D04BABCD12CA72B06290C248 /* settings-window */ = {
isa = PBXGroup;
children = (
D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */,
D04BA3FA52AA40B827A72F10 /* tabs */,
D04BA099FA155FB4D517D66B /* PreferencesWindow.swift */,
D04BA099FA155FB4D517D66B /* SettingsWindow.swift */,
);
path = "preferences-window";
path = "settings-window";
sourceTree = "<group>";
};
D04BAC2B70BC798C57B0B492 /* l10n */ = {
@@ -2543,7 +2543,7 @@
D04BA56789A2AF52557A9BFF /* AppCenterCrashes.swift in Sources */,
D04BADA3F2DAB820D6FC6E75 /* AppCenterApplication.m in Sources */,
D04BAA16F21AC41A23DDA63D /* ControlsTab.swift in Sources */,
D04BADE4DDF7597749FB13BD /* PreferencesWindow.swift in Sources */,
D04BADE4DDF7597749FB13BD /* SettingsWindow.swift in Sources */,
BF0C8CC5057406014FD612CC /* ATShortcut.swift in Sources */,
BF0C8C7E96DB48120462DA00 /* TrafficLightButton.swift in Sources */,
BF0C898511686611E4D7D81E /* Button.swift in Sources */,
+19 -19
View File
@@ -1,15 +1,9 @@
/* No comment provided by engineer. */
"About" = "About";
/* Menubar option. %@ is AltTab */
"About %@" = "About %@";
/* No comment provided by engineer. */
"Accessibility" = "Accessibility";
/* No comment provided by engineer. */
"Acknowledgments" = "Acknowledgments";
/* No comment provided by engineer. */
"Active app" = "Active app";
@@ -26,7 +20,7 @@
"Advanced" = "Advanced";
/* No comment provided by engineer. */
"After release" = "After release";
"After keys are released" = "After keys are released";
/* No comment provided by engineer. */
"Align windows" = "Align windows";
@@ -109,6 +103,9 @@
/* No comment provided by engineer. */
"Are you sure you don’t want a response?" = "Are you sure you don’t want a response?";
/* No comment provided by engineer. */
"Arrow keys" = "Arrow keys";
/* No comment provided by engineer. */
"Ask whether to send crash reports" = "Ask whether to send crash reports";
@@ -121,9 +118,6 @@
/* No comment provided by engineer. */
"Cancel" = "Cancel";
/* No comment provided by engineer. */
"Cancel and hide" = "Cancel and hide";
/* No comment provided by engineer. */
"Center" = "Center";
@@ -169,9 +163,6 @@
/* No comment provided by engineer. */
"Disabled" = "Disabled";
/* No comment provided by engineer. */
"Do nothing" = "Do nothing";
/* No comment provided by engineer. */
"Done" = "Done";
@@ -241,6 +232,12 @@
/* No comment provided by engineer. */
"Ignore shortcuts when active" = "Ignore shortcuts when active";
/* No comment provided by engineer. */
"Keep open" = "Keep open";
/* No comment provided by engineer. */
"Keep open and search" = "Keep open and search";
/* No comment provided by engineer. */
"Language" = "Language";
@@ -262,6 +259,9 @@
/* No comment provided by engineer. */
"Light" = "Light";
/* No comment provided by engineer. */
"Lock search" = "Lock search";
/* No comment provided by engineer. */
"Medium" = "Medium";
@@ -325,12 +325,6 @@
/* No comment provided by engineer. */
"Preview selected window" = "Preview selected window";
/* No comment provided by engineer. */
"Preview the selected window." = "Preview the selected window.";
/* No comment provided by engineer. */
"Quit" = "Quit";
/* Menubar option. %@ is AltTab */
"Quit %@" = "Quit %@";
@@ -367,6 +361,9 @@
/* No comment provided by engineer. */
"Screen showing AltTab" = "Screen showing AltTab";
/* No comment provided by engineer. */
"Search" = "Search";
/* No comment provided by engineer. */
"Select next window" = "Select next window";
@@ -562,6 +559,9 @@
/* No comment provided by engineer. */
"View existing discussions" = "View existing discussions";
/* No comment provided by engineer. */
"Vim keys" = "Vim keys";
/* No comment provided by engineer. */
"Vim keys already assigned to other actions:\n%@" = "Vim keys already assigned to other actions:\n%@";
+2 -2
View File
@@ -59,7 +59,7 @@ class ATShortcut {
if triggerPhase == .down && (!App.app.appIsBeingUsed || index == nil || index == App.app.shortcutIndex) {
return true
}
if triggerPhase == .up && App.app.appIsBeingUsed && (index == nil || index == App.app.shortcutIndex) && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
if triggerPhase == .up && App.app.appIsBeingUsed && (index == nil || index == App.app.shortcutIndex) && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle == .focusOnRelease {
return true
}
}
@@ -85,7 +85,7 @@ class ATShortcut {
// Another issue is events being dropped by macOS, which we never receive
// Knowing this, we handle these edge-cases by double checking if holdShortcut is UP, when any shortcut state is UP
// If it is, then we trigger the holdShortcut action
if App.app.appIsBeingUsed && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
if App.app.appIsBeingUsed && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle == .focusOnRelease {
if let currentHoldShortcut = ControlsTab.shortcuts[Preferences.indexToName("holdShortcut", App.app.shortcutIndex)],
id == currentHoldShortcut.id {
let currentModifiers = cocoaToCarbonFlags(ModifierFlags.current)
+2 -2
View File
@@ -225,8 +225,8 @@ enum ShortcutStylePreference: CaseIterable, MacroPreference {
var localizedString: LocalizedString {
switch self {
case .focusOnRelease: return NSLocalizedString("Focus selected window", comment: "")
case .doNothingOnRelease: return NSLocalizedString("Do nothing", comment: "")
case .searchOnRelease: return NSLocalizedString("Search", comment: "")
case .doNothingOnRelease: return NSLocalizedString("Keep open", comment: "")
case .searchOnRelease: return NSLocalizedString("Keep open and search", comment: "")
}
}
}
+7 -1
View File
@@ -90,6 +90,7 @@ class Preferences {
"previewFocusedWindow": "false",
"screenRecordingPermissionSkipped": "false",
"trackpadHapticFeedbackEnabled": "true",
"settingsWindowShownOnFirstLaunch": "false",
]
// system preferences
@@ -129,6 +130,7 @@ class Preferences {
static var blacklist: [BlacklistEntry] { CachedUserDefaults.json("blacklist", [BlacklistEntry].self) }
static var previewSelectedWindow: Bool { CachedUserDefaults.bool("previewFocusedWindow") }
static var screenRecordingPermissionSkipped: Bool { CachedUserDefaults.bool("screenRecordingPermissionSkipped") }
static var settingsWindowShownOnFirstLaunch: Bool { CachedUserDefaults.bool("settingsWindowShownOnFirstLaunch") }
// macro values
static var appearanceStyle: AppearanceStylePreference { CachedUserDefaults.macroPref("appearanceStyle", AppearanceStylePreference.allCases) }
@@ -151,7 +153,7 @@ class Preferences {
static var showFullscreenWindows: [ShowHowPreference] { ["showFullscreenWindows", "showFullscreenWindows2", "showFullscreenWindows3", "showFullscreenWindows4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
static var showWindowlessApps: [ShowHowPreference] { ["showWindowlessApps", "showWindowlessApps2", "showWindowlessApps3", "showWindowlessApps4"].map { CachedUserDefaults.macroPref($0, ShowHowPreference.allCases) } }
static var windowOrder: [WindowOrderPreference] { ["windowOrder", "windowOrder2", "windowOrder3", "windowOrder4"].map { CachedUserDefaults.macroPref($0, WindowOrderPreference.allCases) } }
static var shortcutStyle: [ShortcutStylePreference] { ["shortcutStyle", "shortcutStyle2", "shortcutStyle3", "shortcutStyle4"].map { CachedUserDefaults.macroPref($0, ShortcutStylePreference.allCases) } }
static var shortcutStyle: ShortcutStylePreference { CachedUserDefaults.macroPref("shortcutStyle", ShortcutStylePreference.allCases) }
static var menubarIcon: MenubarIconPreference { CachedUserDefaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }
static var menubarIconShown: Bool { CachedUserDefaults.bool("menubarIconShown") }
static var language: LanguagePreference { CachedUserDefaults.macroPref("language", LanguagePreference.allCases) }
@@ -172,6 +174,10 @@ class Preferences {
UserDefaults.standard.register(defaults: defaultValues)
}
static func markSettingsWindowShownOnFirstLaunch() {
set("settingsWindowShownOnFirstLaunch", "true", false)
}
static func set<T>(_ key: String, _ value: T, _ notify: Bool = true) where T: Encodable {
UserDefaults.standard.set(key == "blacklist" ? jsonEncode(value) : value, forKey: key)
CachedUserDefaults.cache.removeValue(forKey: key)
+1 -1
View File
@@ -77,7 +77,7 @@ class TrackpadEvents {
private static func handleEventIfAppIsBeingUsed(_ fingersDown: Int, _ activeTouches: Set<NSTouch>, _ requiredFingers: Int) {
if fingersDown <= TriggerSwipeDetector.maxFingersDownDuringTrigger - requiredFingers {
if App.app.shortcutIndex == Preferences.gestureIndex && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle[App.app.shortcutIndex] == .focusOnRelease {
if App.app.shortcutIndex == Preferences.gestureIndex && !App.app.forceDoNothingOnRelease && Preferences.shortcutStyle == .focusOnRelease {
DispatchQueue.main.async {
App.app.focusTarget()
}
+39 -16
View File
@@ -16,11 +16,13 @@ class App: AppCenterApplication {
static let repository = "https://github.com/lwouis/alt-tab-macos"
static let website = "https://alt-tab-macos.netlify.app"
static let appIcon = CGImage.named("app.icns")
static var supportProjectAction: Selector { #selector(App.app.supportProject) }
static var app: App!
var isTerminating = false
var thumbnailsPanel: TilesPanel!
var previewPanel: PreviewPanel!
var preferencesWindow: PreferencesWindow!
var settingsWindow: SettingsWindow!
var aboutWindow: AboutWindow!
var permissionsWindow: PermissionsWindow?
var appIsBeingUsed = false
var shortcutIndex = 0
@@ -91,7 +93,8 @@ class App: AppCenterApplication {
}
private func allSecondaryWindowsCanBecomeKey(_ canBecomeKey_: Bool) {
preferencesWindow?.canBecomeKey_ = canBecomeKey_
settingsWindow?.canBecomeKey_ = canBecomeKey_
aboutWindow?.canBecomeKey_ = canBecomeKey_
feedbackWindow?.canBecomeKey_ = canBecomeKey_
permissionsWindow?.canBecomeKey_ = canBecomeKey_
}
@@ -158,8 +161,17 @@ class App: AppCenterApplication {
showSecondaryWindow(getOrCreateFeedbackWindow())
}
@objc func showPreferencesWindow() {
showSecondaryWindow(getOrCreatePreferencesWindow())
@objc func showSettingsWindow() {
showSecondaryWindow(getOrCreateSettingsWindow())
if settingsWindow?.isVisible != true {
settingsWindow = SettingsWindow()
showSecondaryWindow(settingsWindow)
settingsWindow?.orderFrontRegardless()
}
}
@objc func showAboutWindow() {
showSecondaryWindow(getOrCreateAboutWindow())
}
func showSecondaryWindow(_ window: NSWindow?) {
@@ -173,11 +185,18 @@ class App: AppCenterApplication {
}
}
private func getOrCreatePreferencesWindow() -> PreferencesWindow {
if preferencesWindow == nil {
preferencesWindow = PreferencesWindow()
private func getOrCreateSettingsWindow() -> SettingsWindow {
if settingsWindow == nil {
settingsWindow = SettingsWindow()
}
return preferencesWindow
return settingsWindow
}
private func getOrCreateAboutWindow() -> AboutWindow {
if aboutWindow == nil {
aboutWindow = AboutWindow()
}
return aboutWindow
}
private func getOrCreateFeedbackWindow() -> FeedbackWindow {
@@ -194,6 +213,14 @@ class App: AppCenterApplication {
return permissionsWindow!
}
@discardableResult
private func showSettingsWindowOnFirstLaunchIfNeeded() -> Bool {
guard !Preferences.settingsWindowShownOnFirstLaunch else { return false }
showSettingsWindow()
Preferences.markSettingsWindowShownOnFirstLaunch()
return true
}
func showPermissionsWindow() {
getOrCreatePermissionsWindow().show()
}
@@ -206,11 +233,6 @@ class App: AppCenterApplication {
showUi(0)
}
@objc func showAboutTab() {
getOrCreatePreferencesWindow().selectTab("about")
showPreferencesWindow()
}
func cycleSelection(_ direction: Direction, allowWrap: Bool = true) {
if direction == .up || direction == .down {
thumbnailsPanel.tilesView.navigateUpOrDown(direction, allowWrap: allowWrap)
@@ -296,7 +318,7 @@ class App: AppCenterApplication {
}
isFirstSummon = false
self.shortcutIndex = shortcutIndex
let shouldStartInSearchMode = Preferences.shortcutStyle[self.shortcutIndex] == .searchOnRelease
let shouldStartInSearchMode = Preferences.shortcutStyle == .searchOnRelease
thumbnailsPanel.tilesView.startSearchSession(shouldStartInSearchMode)
if shouldStartInSearchMode {
forceDoNothingOnRelease = true
@@ -393,13 +415,14 @@ extension App: NSApplicationDelegate {
CliEvents.observe()
Logger.info { "Finished launching AltTab" }
BenchmarkRunner.startIfNeeded()
showSettingsWindowOnFirstLaunchIfNeeded()
#if DEBUG
// self.showPreferencesWindow()
// self.showSettingsWindow()
#endif
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
showPreferencesWindow()
showSettingsWindow()
return true
}
+3 -3
View File
@@ -24,13 +24,13 @@ class Menubar {
permissionCalloutMenuItems = [permissionCalloutMenuItem, calloutSeparator]
addMenuItem(NSLocalizedString("Show", comment: "Menubar option"), #selector(App.app.showUiFromShortcut0), "", "eye")
menu.addItem(NSMenuItem.separator())
addMenuItem(NSLocalizedString("Settings…", comment: "Menubar option"), #selector(App.app.showPreferencesWindow), ",", "gear")
addMenuItem(NSLocalizedString("Settings…", comment: "Menubar option"), #selector(App.app.showSettingsWindow), ",", "gear")
addMenuItem(NSLocalizedString("Check for updates…", comment: "Menubar option"), #selector(App.app.checkForUpdatesNow), "", "checkmark.arrow.trianglehead.clockwise")
addMenuItem(NSLocalizedString("Check permissions…", comment: "Menubar option"), #selector(App.app.checkPermissions), "", "hand.raised")
menu.addItem(NSMenuItem.separator())
addMenuItem(String(format: NSLocalizedString("About %@", comment: "Menubar option. %@ is AltTab"), App.name), #selector(App.app.showAboutTab), "", "info.circle")
addMenuItem(String(format: NSLocalizedString("About %@", comment: "Menubar option. %@ is AltTab"), App.name), #selector(App.app.showAboutWindow), "", "info.circle")
addMenuItem(NSLocalizedString("Send feedback…", comment: "Menubar option"), #selector(App.app.showFeedbackPanel), "", "text.bubble")
addMenuItem(NSLocalizedString("Support this project", comment: "Menubar option"), #selector(App.app.supportProject), "", "heart.fill", .red)
addMenuItem(NSLocalizedString("Support this project", comment: "Menubar option"), App.supportProjectAction, "", "heart.fill", .red)
menu.addItem(NSMenuItem.separator())
addMenuItem(String(format: NSLocalizedString("Quit %@", comment: "Menubar option. %@ is AltTab"), App.name), #selector(NSApplication.terminate(_:)), "q", nil) // "xmark.rectangle" is not necessary; macos automatically recognizes Quit
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
@@ -26,7 +26,7 @@ class ImageTextButtonView: NSStackView {
static let spacing = CGFloat(5)
static let cornerRadius = CGFloat(7)
static let borderWidth = CGFloat(3)
static let padding = CGFloat(2)
static let padding = CGFloat(0)
var onClick: ActionClosure?
var button: NSButton!
@@ -81,22 +81,22 @@ class ImageTextButtonView: NSStackView {
imageView.layer?.cornerRadius = cornerRadius - 3
imageContainer.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: image.width),
imageView.heightAnchor.constraint(equalToConstant: image.height),
imageView.topAnchor.constraint(equalTo: imageContainer.topAnchor, constant: padding),
imageView.bottomAnchor.constraint(equalTo: imageContainer.bottomAnchor, constant: -padding),
imageView.leadingAnchor.constraint(equalTo: imageContainer.leadingAnchor, constant: padding),
imageView.trailingAnchor.constraint(equalTo: imageContainer.trailingAnchor, constant: -padding),
])
button.addSubview(imageContainer)
let imageAspectRatio = image.height / image.width
NSLayoutConstraint.activate([
imageContainer.centerXAnchor.constraint(equalTo: button.centerXAnchor),
imageContainer.centerYAnchor.constraint(equalTo: button.centerYAnchor),
imageContainer.widthAnchor.constraint(equalTo: button.widthAnchor),
imageContainer.heightAnchor.constraint(equalTo: button.heightAnchor),
imageContainer.topAnchor.constraint(equalTo: button.topAnchor),
imageContainer.bottomAnchor.constraint(equalTo: button.bottomAnchor),
imageContainer.leadingAnchor.constraint(equalTo: button.leadingAnchor),
imageContainer.trailingAnchor.constraint(equalTo: button.trailingAnchor),
button.topAnchor.constraint(equalTo: topAnchor, constant: ImageTextButtonView.borderWidth),
button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: ImageTextButtonView.borderWidth),
button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -ImageTextButtonView.borderWidth),
button.heightAnchor.constraint(equalTo: button.widthAnchor, multiplier: imageAspectRatio),
])
button.identifier = NSUserInterfaceItemIdentifier(rawName)
button.onAction = { control in
+70 -24
View File
@@ -7,7 +7,7 @@ class TableGroupSetView: NSStackView {
static let spacing = CGFloat(20)
static let tableGroupSpacing = CGFloat(5)
static let othersSpacing = CGFloat(10)
static let titleTableGroupSpacing = CGFloat(30)
static let titleTableGroupSpacing = CGFloat(20)
static let padding = CGFloat(20)
static let leftRightPadding = 2 * TableGroupSetView.padding
@@ -18,11 +18,16 @@ class TableGroupSetView: NSStackView {
toolsViews: [NSView]? = nil,
spacing: CGFloat = TableGroupSetView.spacing,
tableGroupSpacing: CGFloat = TableGroupSetView.tableGroupSpacing,
titleTableGroupSpacing: CGFloat = TableGroupSetView.titleTableGroupSpacing,
othersSpacing: CGFloat = TableGroupSetView.othersSpacing,
padding: CGFloat = TableGroupSetView.padding,
topPadding: CGFloat? = nil,
bottomPadding: CGFloat? = nil,
othersAlignment: NSLayoutConstraint.Attribute = .trailing,
toolsAlignment: NSLayoutConstraint.Attribute = .centerX) {
self.init(frame: .zero)
let resolvedTopPadding = topPadding ?? padding
let resolvedBottomPadding = bottomPadding ?? padding
self.spacing = spacing
orientation = .vertical
self.originalViews = originalViews
@@ -33,39 +38,71 @@ class TableGroupSetView: NSStackView {
if view is TableGroupView {
if !lastViewWasTableGroup {
// Only reset other views if we are switching from non-TableGroupView to TableGroupView
addContinuousOthersToSetViews(&continuousOthers, views: &verticalViews, othersSpacing: othersSpacing, padding: padding, alignment: othersAlignment)
addContinuousOthersToSetViews(&continuousOthers,
views: &verticalViews,
othersSpacing: othersSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding,
alignment: othersAlignment)
}
continuousTableGroups.append(view)
lastViewWasTableGroup = true
} else if view is IllustratedImageThemeView {
lastViewWasTableGroup = false
addContinuousTableGroupsToSetViews(&continuousTableGroups, views: &verticalViews, tableGroupSpacing: tableGroupSpacing, padding: padding)
addContinuousOthersToSetViews(&continuousOthers, views: &verticalViews, othersSpacing: othersSpacing, padding: padding, alignment: othersAlignment)
addToolsViewToSetViews([view], views: &verticalViews, padding: padding, alignment: toolsAlignment)
addContinuousTableGroupsToSetViews(&continuousTableGroups,
views: &verticalViews,
tableGroupSpacing: tableGroupSpacing,
titleTableGroupSpacing: titleTableGroupSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding)
addContinuousOthersToSetViews(&continuousOthers,
views: &verticalViews,
othersSpacing: othersSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding,
alignment: othersAlignment)
addToolsViewToSetViews([view], views: &verticalViews, horizontalPadding: padding, topPadding: resolvedTopPadding, alignment: toolsAlignment)
} else {
if lastViewWasTableGroup {
// Only reset table group views if we are switching from TableGroupView to non-TableGroupView
addContinuousTableGroupsToSetViews(&continuousTableGroups, views: &verticalViews, tableGroupSpacing: tableGroupSpacing, padding: padding)
addContinuousTableGroupsToSetViews(&continuousTableGroups,
views: &verticalViews,
tableGroupSpacing: tableGroupSpacing,
titleTableGroupSpacing: titleTableGroupSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding)
}
continuousOthers.append(view)
lastViewWasTableGroup = false
}
}
// Ensure any remaining views are added
addContinuousTableGroupsToSetViews(&continuousTableGroups, views: &verticalViews, tableGroupSpacing: tableGroupSpacing, padding: padding)
addContinuousOthersToSetViews(&continuousOthers, views: &verticalViews, othersSpacing: othersSpacing, padding: padding, alignment: othersAlignment)
addContinuousTableGroupsToSetViews(&continuousTableGroups,
views: &verticalViews,
tableGroupSpacing: tableGroupSpacing,
titleTableGroupSpacing: titleTableGroupSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding)
addContinuousOthersToSetViews(&continuousOthers,
views: &verticalViews,
othersSpacing: othersSpacing,
horizontalPadding: padding,
topPadding: resolvedTopPadding,
alignment: othersAlignment)
if let toolsViews {
addToolsViewToSetViews(toolsViews, views: &verticalViews, padding: padding, alignment: toolsAlignment)
addToolsViewToSetViews(toolsViews, views: &verticalViews, horizontalPadding: padding, topPadding: resolvedTopPadding, alignment: toolsAlignment)
}
if let lastStackView = verticalViews.last {
lastStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding).isActive = true
lastStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -resolvedBottomPadding).isActive = true
}
}
func addContinuousTableGroupsToSetViews(_ continuousTableGroups: inout [NSView],
views: inout [NSView],
tableGroupSpacing: CGFloat,
padding: CGFloat) {
titleTableGroupSpacing: CGFloat,
horizontalPadding: CGFloat,
topPadding: CGFloat) {
if !continuousTableGroups.isEmpty {
let stackView = NSStackView()
stackView.orientation = .vertical
@@ -76,10 +113,10 @@ class TableGroupSetView: NSStackView {
var spacerHeight = CGFloat(0)
if index == 0 && !views.isEmpty {
// at first, height: spacing + padding
spacerHeight = TableGroupSetView.titleTableGroupSpacing - spacing
spacerHeight = titleTableGroupSpacing - spacing
} else if index > 0 {
// inner, height: tableGroupSpacing + padding
spacerHeight = TableGroupSetView.titleTableGroupSpacing
spacerHeight = titleTableGroupSpacing
}
let spacerView = NSView()
spacerView.translatesAutoresizingMaskIntoConstraints = false
@@ -92,12 +129,17 @@ class TableGroupSetView: NSStackView {
}
continuousTableGroups.removeAll()
addArrangedSubview(stackView)
setStackViewConstraints(stackView, isFirst: views.isEmpty, padding: padding)
setStackViewConstraints(stackView, isFirst: views.isEmpty, horizontalPadding: horizontalPadding, topPadding: topPadding)
views.append(stackView)
}
}
func addContinuousOthersToSetViews(_ continuousOthers: inout [NSView], views: inout [NSView], othersSpacing: CGFloat, padding: CGFloat, alignment: NSLayoutConstraint.Attribute) {
func addContinuousOthersToSetViews(_ continuousOthers: inout [NSView],
views: inout [NSView],
othersSpacing: CGFloat,
horizontalPadding: CGFloat,
topPadding: CGFloat,
alignment: NSLayoutConstraint.Attribute) {
if !continuousOthers.isEmpty {
let stackView = NSStackView()
stackView.orientation = .horizontal
@@ -106,12 +148,16 @@ class TableGroupSetView: NSStackView {
stackView.setViews(continuousOthers, in: .leading)
continuousOthers.removeAll()
addArrangedSubview(stackView)
setStackViewConstraints(stackView, isFirst: views.isEmpty, padding: padding)
setStackViewConstraints(stackView, isFirst: views.isEmpty, horizontalPadding: horizontalPadding, topPadding: topPadding)
views.append(stackView)
}
}
func addToolsViewToSetViews(_ originalViews: [NSView], views: inout [NSView], padding: CGFloat, alignment: NSLayoutConstraint.Attribute) {
func addToolsViewToSetViews(_ originalViews: [NSView],
views: inout [NSView],
horizontalPadding: CGFloat,
topPadding: CGFloat,
alignment: NSLayoutConstraint.Attribute) {
if !originalViews.isEmpty {
let stackView = NSStackView()
stackView.orientation = .horizontal
@@ -119,18 +165,18 @@ class TableGroupSetView: NSStackView {
stackView.alignment = alignment
stackView.setViews(originalViews, in: .leading)
addArrangedSubview(stackView)
setStackViewConstraints(stackView, isFirst: views.isEmpty, padding: padding)
setStackViewConstraints(stackView, isFirst: views.isEmpty, horizontalPadding: horizontalPadding, topPadding: topPadding)
views.append(stackView)
}
}
func setStackViewConstraints(_ stackView: NSStackView, isFirst: Bool, padding: CGFloat) {
func setStackViewConstraints(_ stackView: NSStackView, isFirst: Bool, horizontalPadding: CGFloat, topPadding: CGFloat) {
stackView.translatesAutoresizingMaskIntoConstraints = false
if isFirst {
stackView.topAnchor.constraint(equalTo: topAnchor, constant: padding).isActive = true
stackView.topAnchor.constraint(equalTo: topAnchor, constant: topPadding).isActive = true
}
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding).isActive = true
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding).isActive = true
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalPadding).isActive = true
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding).isActive = true
}
}
@@ -230,7 +276,7 @@ class TableGroupView: ClickHoverStackView {
titleStackView.spacing = TableGroupView.rowIntraSpacing
if let title {
titleLabel.stringValue = title
titleLabel.font = NSFont.boldSystemFont(ofSize: 13)
titleLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium)
titleLabel.alignment = .natural
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.maximumNumberOfLines = 0
@@ -396,7 +442,7 @@ class TableGroupView: ClickHoverStackView {
static func makeText(_ leftText: String?, bold: Bool = false) -> NSTextField {
let leftLabel = NSTextField(labelWithString: leftText ?? "")
if bold {
leftLabel.font = NSFont.boldSystemFont(ofSize: 13)
leftLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium)
}
leftLabel.alignment = .left
leftLabel.lineBreakMode = .byWordWrapping
+33 -2
View File
@@ -1,14 +1,45 @@
import Cocoa
class BlacklistView: NSScrollView {
convenience init() {
override class var isCompatibleWithResponsiveScrolling: Bool { true }
convenience init(width: CGFloat = 500, height: CGFloat = 378) {
self.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
borderType = .bezelBorder
hasHorizontalScroller = false
hasVerticalScroller = true
verticalScrollElasticity = .none
usesPredominantAxisScrolling = true
documentView = TableView(nil)
fit(500, 378)
fit(width, height)
}
override func scrollWheel(with event: NSEvent) {
guard shouldHandleVerticalScroll(event) else {
super.scrollWheel(with: event)
return
}
let y = contentView.bounds.origin.y
super.scrollWheel(with: event)
if abs(contentView.bounds.origin.y - y) < 0.5 {
parentScrollView()?.scrollWheel(with: event)
}
}
private func shouldHandleVerticalScroll(_ event: NSEvent) -> Bool {
abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) && abs(event.scrollingDeltaY) > 0.1
}
private func parentScrollView() -> NSScrollView? {
var parent = superview
while let view = parent {
if let scrollView = view as? NSScrollView {
return scrollView
}
parent = view.superview
}
return nil
}
}
+1 -1
View File
@@ -125,7 +125,7 @@ class TilesView: NSObject {
}
private func configureSearchField() {
searchField.placeholderString = NSLocalizedString("Search windows", comment: "")
searchField.placeholderString = NSLocalizedString("Search", comment: "")
searchField.sendsSearchStringImmediately = true
searchField.sendsWholeSearchString = true
searchField.bezelStyle = .roundedBezel
@@ -1,126 +0,0 @@
import Cocoa
class PreferencesWindow: NSWindow {
static let width = CGFloat(520)
var toolbarItems = [NSToolbarItem.Identifier: (Int, NSToolbarItem, NSView)]()
var canBecomeKey_ = true
override var canBecomeKey: Bool { canBecomeKey_ }
var largestTabWidth: CGFloat!
convenience init() {
self.init(contentRect: .zero, styleMask: [.titled, .miniaturizable, .closable], backing: .buffered, defer: false)
setupWindow()
setupView()
}
private func setupWindow() {
isReleasedWhenClosed = false
hidesOnDeactivate = false
addQuitButton()
}
private func addQuitButton() {
let quitButton = NSButton(title: NSLocalizedString("Quit", comment: ""), target: nil, action: #selector(NSApplication.terminate(_:)))
let titleBarView = standardWindowButton(.closeButton)!.superview!
titleBarView.addSubview(quitButton)
quitButton.translatesAutoresizingMaskIntoConstraints = false
quitButton.topAnchor.constraint(equalTo: titleBarView.topAnchor, constant: 5).isActive = true
if windowTitlebarLayoutDirection == .rightToLeft {
quitButton.leftAnchor.constraint(equalTo: titleBarView.leftAnchor, constant: 8).isActive = true
} else {
quitButton.rightAnchor.constraint(equalTo: titleBarView.rightAnchor, constant: -8).isActive = true
}
}
private func setupView() {
toolbar = NSToolbar(identifier: "1")
toolbar!.delegate = self
// toolbar breaks with the new default style on macOS 11; we force the classic style (see #914)
if #available(macOS 11.0, *) { toolbarStyle = .preference }
toolbar!.displayMode = .iconAndLabel
toolbar!.showsBaselineSeparator = true
[
(0, NSLocalizedString("General", comment: ""), "general", "switch.2", GeneralTab.initTab()),
(1, NSLocalizedString("Controls", comment: ""), "controls", "command", ControlsTab.initTab()),
(2, NSLocalizedString("Appearance", comment: ""), "appearance", "paintpalette", AppearanceTab.initTab()),
(3, NSLocalizedString("Policies", comment: ""), "policies", "antenna.radiowaves.left.and.right", PoliciesTab.initTab()),
(4, NSLocalizedString("Blacklists", comment: ""), "blacklists", "hand.raised", BlacklistsTab.initTab()),
(5, NSLocalizedString("About", comment: ""), "about", "info.circle", AboutTab.initTab()),
(6, NSLocalizedString("Acknowledgments", comment: ""), "acknowledgments", "hand.thumbsup", AcknowledgmentsTab.initTab()),
].forEach { makeToolbarItem($0.0, $0.1, $0.2, $0.3, $0.4) }
largestTabWidth = Array(toolbarItems.values).reduce(CGFloat(0)) { max($0, $1.2.subviews[0].fittingSize.width) }
Array(toolbarItems.values).forEach {
$0.2.fit(largestTabWidth, $0.2.subviews[0].fittingSize.height)
}
refreshControlsFromPreferences()
selectTab("general")
}
private func refreshControlsFromPreferences() {
GeneralTab.refreshControlsFromPreferences()
PoliciesTab.refreshControlsFromPreferences()
}
func selectTab(_ id: String) {
toolbar!.selectedItemIdentifier = NSToolbarItem.Identifier(rawValue: id)
tabItemClicked(toolbarItems[toolbar!.selectedItemIdentifier!]!.1)
}
func makeToolbarItem(_ index: Int, _ label: String, _ id: String, _ image: String, _ view: NSView) {
let identifier = NSToolbarItem.Identifier(rawValue: id)
let item = NSToolbarItem(itemIdentifier: identifier)
item.label = label
if #available(macOS 11.0, *) {
// each systemSymbolName has a different minimum macOS requirements; we need to make sure it exists
if let image = NSImage(systemSymbolName: image, accessibilityDescription: nil) {
item.image = image
}
}
// it seems that new instances of NSToolbarItems have their image set to a 1x1 image
if item.image == nil || item.image!.size == NSSize(width: 1, height: 1) {
item.image = NSImage.initCopy(id)
item.image!.isTemplate = true
item.maxSize = NSSize(width: 22, height: 22)
}
item.target = self
item.action = #selector(tabItemClicked)
let wrapView = NSView(frame: .zero)
wrapView.translatesAutoresizingMaskIntoConstraints = false
wrapView.subviews = [view]
view.centerXAnchor.constraint(equalTo: wrapView.centerXAnchor).isActive = true
toolbarItems[identifier] = (index, item, wrapView)
toolbar!.insertItem(withItemIdentifier: identifier, at: index)
}
@objc func tabItemClicked(_ item: NSToolbarItem) {
let item = toolbarItems[item.itemIdentifier]!
contentView = item.2
title = item.1.label
// Reset focused ring
makeFirstResponder(contentView?.subviews[0])
recalculateKeyViewLoop()
}
override func close() {
hideAppIfLastWindowIsClosed()
super.close()
}
}
extension PreferencesWindow: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
return toolbarItems[itemIdentifier]!.1
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return Array(toolbarItems.keys)
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return Array(toolbarItems.keys)
}
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return Array(toolbarItems.keys)
}
}
@@ -1,50 +0,0 @@
import Cocoa
class AboutTab {
static func initTab() -> NSView {
let appIcon = LightImageView()
appIcon.translatesAutoresizingMaskIntoConstraints = false
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 256, height: 256))
appIcon.fit(256, 256)
let appText = StackView([
BoldLabel(App.name),
NSTextField(wrappingLabelWithString: NSLocalizedString("Version", comment: "") + " " + App.version),
NSTextField(wrappingLabelWithString: App.licence),
HyperlinkLabel(NSLocalizedString("Source code repository", comment: ""), App.repository),
HyperlinkLabel(NSLocalizedString("Latest releases", comment: ""), App.repository + "/releases"),
], .vertical)
appText.spacing = GridView.interPadding / 2
let rowToSeparate = 3
appText.views[rowToSeparate].topAnchor.constraint(equalTo: appText.views[rowToSeparate - 1].bottomAnchor, constant: GridView.interPadding).isActive = true
let appInfo = NSStackView(views: [appIcon, appText])
appIcon.translatesAutoresizingMaskIntoConstraints = false
appInfo.spacing = GridView.interPadding
appInfo.alignment = .centerY
let sendFeedback = makeButtonWithIcon(NSLocalizedString("Send feedback…", comment: ""), #selector(App.app.showFeedbackPanel), "text.bubble")
let supportProject = makeButtonWithIcon(NSLocalizedString("Support this project", comment: ""), #selector(App.app.supportProject), "heart.fill", .red)
let grid = GridView([
[appInfo],
[sendFeedback],
[supportProject],
])
let sendFeedbackCell = grid.cell(atColumnIndex: 0, rowIndex: 1)
sendFeedbackCell.xPlacement = .center
sendFeedbackCell.row!.topPadding = GridView.interPadding
let supportProjectCell = grid.cell(atColumnIndex: 0, rowIndex: 2)
supportProjectCell.xPlacement = .center
grid.fit()
return grid
}
private static func makeButtonWithIcon(_ title: String, _ selector: Selector, _ symbolName: String?, _ color: NSColor? = nil) -> NSButton {
let button = NSButton(title: title, target: nil, action: selector)
if #available(macOS 26.0, *), let symbolName {
button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
button.imagePosition = .imageLeading
if let color {
button.image = button.image?.withSymbolConfiguration(.init(paletteColors: [color]))
}
}
return button
}
}
@@ -1,46 +0,0 @@
import Cocoa
class AcknowledgmentsTab {
// going taller than this will crop the view on small screens or low scaling modes on retina displays
static let maxTabHeight = CGFloat(450)
static func initTab() -> NSView {
let textViews: [NSTextView] = ["Contributors", "Acknowledgments"].map {
let markdownFileUrl = Bundle.main.url(forResource: $0, withExtension: "md")!
var content = try! String(contentsOf: markdownFileUrl, encoding: .utf8)
if content.last == "\n" {
content.removeLast()
}
let attributedString = Markdown.toAttributedString(content)
let textView = NSTextView()
textView.textContainer!.widthTracksTextView = true
textView.translatesAutoresizingMaskIntoConstraints = false
textView.drawsBackground = false
textView.isSelectable = true
textView.isEditable = false
textView.enabledTextCheckingTypes = 0
textView.frame.size.width = 240
textView.textStorage!.setAttributedString(attributedString)
textView.layoutManager!.ensureLayout(for: textView.textContainer!)
textView.frame = textView.layoutManager!.usedRect(for: textView.textContainer!)
textView.fit(textView.frame.width, textView.frame.height)
return textView
}
let subGrid = GridView([textViews])
subGrid.fit()
let scrollView = NSScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.drawsBackground = false
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.documentView = FlippedView(frame: .zero)
scrollView.documentView!.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView!.subviews = [subGrid]
let totalWidth = subGrid.fittingSize.width
scrollView.frame.size = NSSize(width: totalWidth, height: maxTabHeight)
scrollView.contentView.frame.size = scrollView.frame.size
scrollView.documentView!.frame.size = subGrid.fittingSize
scrollView.fit(totalWidth, maxTabHeight)
return scrollView
}
}
@@ -34,6 +34,7 @@ class MouseHoverView: NSView {
class ClickHoverImageView: MouseHoverView {
var infoCircle: NSView!
var onClick: EventClosure?
var searchableStrings = [String]()
init(infoCircle: NSView) {
super.init(frame: .zero)
@@ -92,9 +93,16 @@ class LabelAndControl: NSObject {
}
let view = NSStackView(views: buttonViews)
view.orientation = .horizontal
view.distribution = .fillEqually
view.spacing = buttonSpacing
view.alignment = .centerY
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentHuggingPriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
buttonViews.forEach {
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
$0.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
}
return view
}
@@ -130,12 +138,18 @@ class LabelAndControl: NSObject {
}
static func makeInfoButton(size: CGFloat = 16,
searchableTooltipTexts: [String] = [],
onClick: EventClosure? = nil,
onMouseEntered: EventClosure? = nil,
onMouseExited: EventClosure? = nil) -> ClickHoverImageView {
let imageView = TileFontIconView(symbol: .circledInfo, size: size, color: .labelColor)
imageView.translatesAutoresizingMaskIntoConstraints = false
let view = ClickHoverImageView(infoCircle: imageView)
view.searchableStrings = Array(Set(searchableTooltipTexts.map {
$0.trimmingCharacters(in: .whitespacesAndNewlines)
}.filter {
!$0.isEmpty
}))
view.onClick = onClick
view.onMouseEntered = onMouseEntered
view.onMouseExited = onMouseExited
File diff suppressed because it is too large. Load diff
+124
View File
@@ -0,0 +1,124 @@
import Cocoa
class AboutTab {
static func initTab() -> NSView {
makeContentView()
}
static func makeContentView(_ fitToContent: Bool = true, _ showFeedbackButton: Bool = true, _ centerHero: Bool = false) -> NSView {
let appIcon = LightImageView()
appIcon.translatesAutoresizingMaskIntoConstraints = false
appIcon.updateContents(.cgImage(App.appIcon), NSSize(width: 128, height: 128))
appIcon.fit(128, 128)
let appText = StackView([
BoldLabel(App.name),
NSTextField(wrappingLabelWithString: NSLocalizedString("Version", comment: "") + " " + App.version),
NSTextField(wrappingLabelWithString: App.licence),
HyperlinkLabel(NSLocalizedString("Source code repository", comment: ""), App.repository),
HyperlinkLabel(NSLocalizedString("Latest releases", comment: ""), App.repository + "/releases"),
], .vertical)
appText.spacing = GridView.interPadding / 2
let rowToSeparate = 3
appText.views[rowToSeparate].topAnchor.constraint(equalTo: appText.views[rowToSeparate - 1].bottomAnchor, constant: GridView.interPadding).isActive = true
let appInfo = NSStackView(views: [appIcon, appText])
appIcon.translatesAutoresizingMaskIntoConstraints = false
appInfo.spacing = GridView.interPadding
appInfo.alignment = .centerY
let supportProject = makeSupportProjectButton()
let rows = [[appInfo], [supportProject]]
let grid = GridView(rows, 0)
if centerHero {
grid.cell(atColumnIndex: 0, rowIndex: 0).xPlacement = .center
}
let supportProjectCell = grid.cell(atColumnIndex: 0, rowIndex: showFeedbackButton ? 2 : 1)
supportProjectCell.xPlacement = .center
if fitToContent {
grid.fit()
}
return grid
}
static func makeSupportProjectButton() -> NSButton {
let button = makeButtonWithIcon(NSLocalizedString("Support this project", comment: ""), App.supportProjectAction, "heart.fill", .red)
styleSupportProjectButton(button)
return button
}
private static func styleSupportProjectButton(_ button: NSButton) {
button.bezelStyle = .rounded
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: SettingsWindow.sidebarActionButtonHeight).isActive = true
}
private static func makeButtonWithIcon(_ title: String, _ selector: Selector, _ symbolName: String?, _ color: NSColor? = nil) -> NSButton {
let button = NSButton(title: title, target: nil, action: selector)
if #available(macOS 26.0, *), let symbolName {
button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
button.imagePosition = .imageLeading
if let color {
button.image = button.image?.withSymbolConfiguration(.init(paletteColors: [color]))
}
}
return button
}
}
class AboutWindow: NSPanel {
private static let contentPadding = CGFloat(24)
var canBecomeKey_ = true
override var canBecomeKey: Bool { canBecomeKey_ }
convenience init() {
self.init(contentRect: NSRect(x: 0, y: 0, width: 600, height: 450), styleMask: [.utilityWindow, .titled, .closable], backing: .buffered, defer: false)
setupWindow()
setupView()
}
private func setupWindow() {
isReleasedWhenClosed = false
hidesOnDeactivate = false
title = String(format: NSLocalizedString("About %@", comment: ""), App.name)
}
private func setupView() {
let scrollView = NSScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.drawsBackground = false
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.scrollerStyle = .overlay
let documentView = FlippedView(frame: .zero)
documentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView = documentView
let stack = NSStackView()
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 30
stack.translatesAutoresizingMaskIntoConstraints = false
let aboutView = AboutTab.makeContentView(false, false, true)
let acknowledgmentsColumnWidth = frame.width - 2 * Self.contentPadding
let acknowledgmentsView = AcknowledgmentsTab.makeContentView(columnWidth: acknowledgmentsColumnWidth, shouldFit: false, verticallyStacked: true)
acknowledgmentsView.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(aboutView)
stack.addArrangedSubview(acknowledgmentsView)
documentView.addSubview(stack)
contentView = scrollView
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: documentView.topAnchor, constant: Self.contentPadding),
stack.leadingAnchor.constraint(equalTo: documentView.leadingAnchor, constant: Self.contentPadding),
stack.trailingAnchor.constraint(equalTo: documentView.trailingAnchor, constant: -Self.contentPadding),
stack.bottomAnchor.constraint(equalTo: documentView.bottomAnchor, constant: -Self.contentPadding),
documentView.widthAnchor.constraint(equalTo: scrollView.contentView.widthAnchor),
aboutView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
aboutView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
acknowledgmentsView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
acknowledgmentsView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
])
}
override func close() {
hideAppIfLastWindowIsClosed()
super.close()
}
}
@@ -0,0 +1,69 @@
import Cocoa
class AcknowledgmentsTab {
// going taller than this will crop the view on small screens or low scaling modes on retina displays
static let maxTabHeight = CGFloat(450)
static func initTab() -> NSView {
makeScrollableView(makeContentView(), maxTabHeight)
}
static func makeContentView(columnWidth: CGFloat = 240, shouldFit: Bool = true, verticallyStacked: Bool = false) -> NSView {
let sectionNames = ["Contributors", "Acknowledgments"]
let textViews: [NSTextView] = sectionNames.map { makeTextView($0, columnWidth) }
let content: NSView
if verticallyStacked {
let stack = NSStackView(views: textViews)
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = GridView.interPadding
stack.translatesAutoresizingMaskIntoConstraints = false
content = stack
} else {
content = GridView([textViews])
}
if shouldFit {
content.fit()
}
return content
}
private static func makeTextView(_ sectionName: String, _ columnWidth: CGFloat) -> NSTextView {
let markdownFileUrl = Bundle.main.url(forResource: sectionName, withExtension: "md")!
var content = try! String(contentsOf: markdownFileUrl, encoding: .utf8)
if content.last == "\n" {
content.removeLast()
}
let attributedString = Markdown.toAttributedString(content)
let textView = NSTextView()
textView.textContainer!.widthTracksTextView = true
textView.translatesAutoresizingMaskIntoConstraints = false
textView.drawsBackground = false
textView.isSelectable = true
textView.isEditable = false
textView.enabledTextCheckingTypes = 0
textView.frame.size.width = columnWidth
textView.textStorage!.setAttributedString(attributedString)
textView.layoutManager!.ensureLayout(for: textView.textContainer!)
textView.frame = textView.layoutManager!.usedRect(for: textView.textContainer!)
textView.fit(textView.frame.width, textView.frame.height)
return textView
}
private static func makeScrollableView(_ content: NSView, _ maxHeight: CGFloat) -> NSView {
let scrollView = NSScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.drawsBackground = false
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.documentView = FlippedView(frame: .zero)
scrollView.documentView!.translatesAutoresizingMaskIntoConstraints = false
scrollView.documentView!.subviews = [content]
let totalWidth = content.fittingSize.width
scrollView.frame.size = NSSize(width: totalWidth, height: maxHeight)
scrollView.contentView.frame.size = scrollView.frame.size
scrollView.documentView!.frame.size = content.fittingSize
scrollView.fit(totalWidth, maxHeight)
return scrollView
}
}
@@ -3,7 +3,7 @@ import Sparkle
class BlacklistsTab {
static func initTab() -> NSView {
let blacklist = BlacklistView()
let blacklist = BlacklistView(width: SettingsWindow.contentWidth - 2 * TableGroupView.padding)
let add = NSSegmentedControl(images: [NSImage(named: NSImage.addTemplateName)!, NSImage(named: NSImage.removeTemplateName)!], trackingMode: .momentary, target: nil, action: nil)
add.onAction = {
let tableView = blacklist.documentView as! TableView
@@ -12,7 +12,7 @@ class BlacklistsTab {
dialog.allowsMultipleSelection = false
dialog.allowedFileTypes = ["app"]
dialog.canChooseDirectories = false
dialog.beginSheetModal(for: App.app.preferencesWindow) {
dialog.beginSheetModal(for: App.app.settingsWindow) {
if $0 == .OK, let url = dialog.url, let bundleId = Bundle(url: url)?.bundleIdentifier {
tableView.insertRow(bundleId)
}
@@ -21,11 +21,9 @@ class BlacklistsTab {
tableView.removeSelectedRows()
}
}
let table = TableGroupView(width: PreferencesWindow.width)
let table = TableGroupView(width: SettingsWindow.contentWidth)
_ = table.addRow(leftViews: [blacklist], secondaryViews: [add])
let view = TableGroupSetView(originalViews: [table])
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: view.fittingSize.width).isActive = true
let view = TableGroupSetView(originalViews: [table], bottomPadding: 0)
return view
}
}
@@ -16,8 +16,6 @@ class GeneralTab {
])
let language = TableGroupView.Row(leftTitle: NSLocalizedString("Language", comment: ""),
rightViews: [LabelAndControl.makeDropdown("language", LanguagePreference.allCases, extraAction: setLanguageCallback)])
let resetPreferences = NSButton(title: NSLocalizedString("Reset settings and restart…", comment: ""), target: self, action: #selector(GeneralTab.resetPreferences))
if #available(macOS 11.0, *) { resetPreferences.hasDestructiveAction = true }
for i in 0..<MenubarIconPreference.allCases.count {
let image = NSImage.initCopy("menubar-\(i)")
image.isTemplate = i < 2
@@ -28,14 +26,12 @@ class GeneralTab {
cell.arrowPosition = .arrowAtBottom
cell.imagePosition = .imageOverlaps
enableDraggingOffMenubarIcon(menuIconShownToggle)
let table = TableGroupView(width: PreferencesWindow.width)
let table = TableGroupView(width: SettingsWindow.contentWidth)
table.addRow(startAtLogin)
table.addRow(menubarIcon)
table.addNewTable()
table.addRow(language)
let view = TableGroupSetView(originalViews: [table], toolsViews: [resetPreferences], toolsAlignment: .trailing)
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: view.fittingSize.width).isActive = true
let view = TableGroupSetView(originalViews: [table], bottomPadding: 0)
return view
}
@@ -15,14 +15,11 @@ class PoliciesTab {
static func initTab() -> NSView {
PoliciesTab.updatesPolicyDropdown = LabelAndControl.makeDropdown("updatePolicy", UpdatePolicyPreference.allCases)
PoliciesTab.crashPolicyDropdown = LabelAndControl.makeDropdown("crashPolicy", CrashPolicyPreference.allCases)
let table = TableGroupView(width: PreferencesWindow.width)
let table = TableGroupView(width: SettingsWindow.contentWidth)
table.addRow(leftText: NSLocalizedString("Updates policy", comment: ""), rightViews: [PoliciesTab.updatesPolicyDropdown])
table.addRow(leftText: NSLocalizedString("Crash reports policy", comment: ""), rightViews: [PoliciesTab.crashPolicyDropdown])
table.fit()
let checkForUpdates = NSButton(title: NSLocalizedString("Check for updates now…", comment: ""), target: nil, action: #selector(PoliciesTab.checkForUpdatesNow))
let view = TableGroupSetView(originalViews: [table, checkForUpdates])
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: view.fittingSize.width).isActive = true
let view = TableGroupSetView(originalViews: [table, checkForUpdates], bottomPadding: 0)
return view
}
@@ -14,7 +14,6 @@ class AnimationsSheet: SheetWindow {
rightViews: LabelAndControl.makeSwitch("fadeOutAnimation"))
table.addRow(leftText: NSLocalizedString("Fade in animation of Preview", comment: ""),
rightViews: LabelAndControl.makeSwitch("previewFadeInAnimation"))
table.fit()
return table
}
}
@@ -135,7 +135,6 @@ class ShowHideIllustratedView {
table.onMouseExited = { event, view in
IllustratedImageThemeView.resetImage(self.illustratedImageView, event, view)
}
table.fit()
let view = TableGroupSetView(originalViews: [table], padding: 0)
return view
}
@@ -158,7 +157,7 @@ class ShowHideIllustratedView {
hideStatusIcons.supportedStyles = [.thumbnails, .titles]
hideStatusIcons.leftViews = [TableGroupView.makeText(NSLocalizedString("Hide status icons", comment: ""))]
hideStatusIcons.subTitle = NSLocalizedString("AltTab will show if the window is currently minimized or fullscreen with a status icon.", comment: "")
hideStatusIcons.rightViews.append(LabelAndControl.makeInfoButton(onMouseEntered: { event, view in
hideStatusIcons.rightViews.append(LabelAndControl.makeInfoButton(searchableTooltipTexts: [hideStatusIcons.subTitle!], onMouseEntered: { event, view in
Popover.shared.show(event: event, positioningView: view, message: hideStatusIcons.subTitle!)
}, onMouseExited: { event, view in
Popover.shared.hide()
@@ -195,7 +194,7 @@ class ShowHideIllustratedView {
showTabsAsWindows.supportedStyles = [.thumbnails, .appIcons, .titles]
showTabsAsWindows.leftViews = [TableGroupView.makeText(NSLocalizedString("Show standard tabs as windows", comment: ""))]
showTabsAsWindows.subTitle = NSLocalizedString("Some apps like Finder or Preview use standard tabs which act like independent windows. Some other apps like web browsers use custom tabs which act in unique ways and are not actual windows. AltTab can't list those separately.", comment: "")
showTabsAsWindows.rightViews.append(LabelAndControl.makeInfoButton(onMouseEntered: { event, view in
showTabsAsWindows.rightViews.append(LabelAndControl.makeInfoButton(searchableTooltipTexts: [featureUnavailable, showTabsAsWindows.subTitle!], onMouseEntered: { event, view in
if ShowHideIllustratedView.isDisabledOnApplications(showTabsAsWindows) {
Popover.shared.show(event: event, positioningView: view, message: featureUnavailable)
} else {
@@ -210,32 +209,10 @@ class ShowHideIllustratedView {
}
}))
showHideRows.append(showTabsAsWindows)
var previewSelectedWindow = ShowHideRowInfo()
previewSelectedWindow.rowId = "previewFocusedWindow"
previewSelectedWindow.uncheckedImage = "hide_preview_focused_window"
previewSelectedWindow.checkedImage = "show_preview_focused_window"
previewSelectedWindow.supportedStyles = [.thumbnails, .appIcons, .titles]
previewSelectedWindow.leftViews = [TableGroupView.makeText(NSLocalizedString("Preview selected window", comment: ""))]
previewSelectedWindow.subTitle = NSLocalizedString("Preview the selected window.", comment: "")
previewSelectedWindow.rightViews.append(LabelAndControl.makeInfoButton(onMouseEntered: { event, view in
if ShowHideIllustratedView.isDisabledOnApplications(previewSelectedWindow) {
Popover.shared.show(event: event, positioningView: view, message: featureUnavailable)
} else {
Popover.shared.show(event: event, positioningView: view, message: previewSelectedWindow.subTitle!)
}
}, onMouseExited: { event, view in
Popover.shared.hide()
}))
previewSelectedWindow.rightViews.append(LabelAndControl.makeSwitch(previewSelectedWindow.rowId, extraAction: { sender in
if !ShowHideIllustratedView.isDisabledOnApplications(previewSelectedWindow) {
self.onCheckboxClicked(sender: sender, rowId: previewSelectedWindow.rowId)
}
}))
showHideRows.append(previewSelectedWindow)
}
static func isDisabledOnApplications(_ row: ShowHideRowInfo) -> Bool {
let contains = ["showTabsAsWindows", "previewFocusedWindow"].contains(where: { $0 == row.rowId })
let contains = ["showTabsAsWindows"].contains(where: { $0 == row.rowId })
return contains && Preferences.onlyShowApplications()
}
@@ -312,6 +289,10 @@ class ShowHideIllustratedView {
class Popover: NSPopover {
static let shared = Popover()
private var hidingInitiated = true
private var searchQuery = ""
private var searchMatchRanges: ((String, String) -> [Range<Int>])?
private var currentMessage = ""
private weak var currentMessageLabel: NSTextField?
override init() {
super.init()
@@ -328,6 +309,12 @@ class Popover: NSPopover {
performClose(nil)
}
func updateSearchContext(_ query: String, _ searchMatchRanges: @escaping (String, String) -> [Range<Int>]) {
searchQuery = query
self.searchMatchRanges = searchMatchRanges
applySearchHighlightToCurrentMessage()
}
func show(event: NSEvent, positioningView: NSView, message: String, extraView: NSView? = nil) {
if !hidingInitiated { return }
hidingInitiated = false
@@ -348,6 +335,9 @@ class Popover: NSPopover {
actualView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
actualView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
])
currentMessage = message
currentMessageLabel = label
applySearchHighlightToCurrentMessage()
contentViewController?.view = view
// Convert the mouse location to the positioning view's coordinate system
let locationInWindow = event.locationInWindow
@@ -355,11 +345,45 @@ class Popover: NSPopover {
let rect = CGRect(origin: locationInPositioningView, size: .zero)
show(relativeTo: rect, of: positioningView, preferredEdge: .minX)
}
private func applySearchHighlightToCurrentMessage() {
guard let label = currentMessageLabel else { return }
let attributes: [NSAttributedString.Key: Any] = [
.font: label.font ?? NSFont.systemFont(ofSize: 12),
.foregroundColor: label.textColor ?? NSColor.labelColor
]
let attributed = NSMutableAttributedString(string: currentMessage, attributes: attributes)
guard let searchMatchRanges, !currentMessage.isEmpty else {
label.attributedStringValue = attributed
return
}
let ranges = searchMatchRanges(searchQuery, currentMessage)
guard !ranges.isEmpty else {
label.attributedStringValue = attributed
return
}
ranges.compactMap {
characterRangeToNSRange($0, in: currentMessage)
}.forEach {
attributed.addAttribute(.backgroundColor, value: NSColor.systemYellow.withAlphaComponent(0.5), range: $0)
attributed.addAttribute(.foregroundColor, value: NSColor(calibratedWhite: 0.12, alpha: 1), range: $0)
}
label.attributedStringValue = attributed
}
private func characterRangeToNSRange(_ range: Range<Int>, in text: String) -> NSRange? {
if range.lowerBound < 0 || range.upperBound > text.count || range.isEmpty { return nil }
let start = text.index(text.startIndex, offsetBy: range.lowerBound)
let end = text.index(text.startIndex, offsetBy: range.upperBound)
return NSRange(start..<end, in: text)
}
}
extension Popover: NSPopoverDelegate {
func popoverWillClose(_ notification: Notification) {
hidingInitiated = true
currentMessage = ""
currentMessageLabel = nil
}
}
@@ -368,6 +392,7 @@ class AppearanceTab: NSObject {
static var animationsButton: NSButton!
static var customizeStyleSheet: CustomizeStyleSheet!
static var animationsSheet: AnimationsSheet!
static var previewSelectedWindowRowInfo: TableGroupView.RowInfo!
static func initTab() -> NSView {
customizeStyleButton = NSButton(title: getCustomizeStyleButtonTitle(), target: self, action: #selector(showCustomizeStyleSheet))
@@ -380,33 +405,63 @@ class AppearanceTab: NSObject {
private static func makeView() -> NSStackView {
let appearanceView = makeAppearanceView()
let multipleScreensView = makeMultipleScreensView()
let view = TableGroupSetView(originalViews: [appearanceView, multipleScreensView, animationsButton])
let view = TableGroupSetView(originalViews: [appearanceView, multipleScreensView, animationsButton], titleTableGroupSpacing: 15, bottomPadding: 0)
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: view.fittingSize.width).isActive = true
return view
}
private static func makeAppearanceView() -> NSView {
let table = TableGroupView(title: NSLocalizedString("Appearance", comment: ""),
subTitle: NSLocalizedString("Switch between 3 different styles. You can customize them.", comment: ""),
width: PreferencesWindow.width)
let table = TableGroupView(subTitle: NSLocalizedString("Switch between 3 different styles. You can customize them.", comment: ""),
width: SettingsWindow.contentWidth)
table.addRow(secondaryViews: [LabelAndControl.makeImageRadioButtons("appearanceStyle", AppearanceStylePreference.allCases, extraAction: { _ in
toggleCustomizeStyleButton()
updatePreviewSelectedWindowState()
}, buttonSpacing: 10)], secondaryViewsAlignment: .centerX)
table.addRow(leftText: NSLocalizedString("Size", comment: ""),
rightViews: [LabelAndControl.makeSegmentedControl("appearanceSize", AppearanceSizePreference.allCases, segmentWidth: 100)])
table.addRow(leftText: NSLocalizedString("Theme", comment: ""),
rightViews: [LabelAndControl.makeSegmentedControl("appearanceTheme", AppearanceThemePreference.allCases, segmentWidth: 100)])
addAfterKeysReleasedRow(table)
addPreviewSelectedWindowRow(table)
table.addRow(rightViews: customizeStyleButton)
table.fit()
return table
}
private static func addAfterKeysReleasedRow(_ table: TableGroupView) {
table.addRow(leftText: NSLocalizedString("After keys are released", comment: ""),
rightViews: [LabelAndControl.makeDropdown("shortcutStyle", ShortcutStylePreference.allCases)])
}
private static func addPreviewSelectedWindowRow(_ table: TableGroupView) {
previewSelectedWindowRowInfo = table.addRow(leftText: NSLocalizedString("Preview selected window", comment: ""),
rightViews: [LabelAndControl.makeSwitch("previewFocusedWindow")])
updatePreviewSelectedWindowState()
}
static func updatePreviewSelectedWindowState() {
guard let rowInfo = previewSelectedWindowRowInfo else { return }
let isEnabled = !isPreviewSelectedWindowDisabled()
rowInfo.leftViews?.forEach { view in
if let textField = view as? NSTextField {
textField.textColor = isEnabled ? .textColor : .gray
}
}
rowInfo.rightViews?.forEach { view in
if let switchControl = view as? Switch {
switchControl.isEnabled = isEnabled
}
}
}
private static func isPreviewSelectedWindowDisabled() -> Bool {
return Preferences.onlyShowApplications()
}
private static func makeMultipleScreensView() -> NSView {
let table = TableGroupView(title: NSLocalizedString("Multiple screens", comment: ""), width: PreferencesWindow.width)
let table = TableGroupView(title: NSLocalizedString("Multiple screens", comment: ""), width: SettingsWindow.contentWidth)
_ = table.addRow(leftText: NSLocalizedString("Show on", comment: ""),
rightViews: LabelAndControl.makeDropdown("showOnScreen", ShowOnScreenPreference.allCases))
table.fit()
return table
}
@@ -430,10 +485,10 @@ class AppearanceTab: NSObject {
}
@objc static func showCustomizeStyleSheet() {
App.app.preferencesWindow.beginSheet(customizeStyleSheet)
App.app.settingsWindow.beginSheetWithSearchHighlight(customizeStyleSheet)
}
@objc static func showAnimationsSheet() {
App.app.preferencesWindow.beginSheet(animationsSheet)
App.app.settingsWindow.beginSheetWithSearchHighlight(animationsSheet)
}
}
@@ -53,15 +53,17 @@ class CustomizeStyleSheet: SheetWindow {
}))
titleTruncation = TableGroupView.Row(leftTitle: NSLocalizedString("Title truncation", comment: ""),
rightViews: LabelAndControl.makeRadioButtons("titleTruncation", TitleTruncationPreference.allCases))
let showAppWindowsInfo = LabelAndControl.makeInfoButton(onMouseEntered: { (event, view) in
let showAppWindowsTooltip = NSLocalizedString("Show an item in the switcher for each window, or for each application. Windows will be focused, whereas applications will be activated.", comment: "")
let showAppWindowsInfo = LabelAndControl.makeInfoButton(searchableTooltipTexts: [showAppWindowsTooltip], onMouseEntered: { (event, view) in
Popover.shared.show(event: event, positioningView: view,
message: NSLocalizedString("Show an item in the switcher for each window, or for each application. Windows will be focused, whereas applications will be activated.", comment: ""))
message: showAppWindowsTooltip)
}, onMouseExited: { (event, view) in
Popover.shared.hide()
})
showAppsOrWindows = TableGroupView.Row(leftTitle: NSLocalizedString("Show in switcher", comment: ""),
rightViews: LabelAndControl.makeRadioButtons("showAppsOrWindows", ShowAppsOrWindowsPreference.allCases, extraAction: { _ in
self.showHideIllustratedView.setStateOnApplications()
AppearanceTab.updatePreviewSelectedWindowState()
self.toggleAppNamesWindowTitles()
self.showAppsOrWindowsIllustratedImage()
}) + [showAppWindowsInfo])
@@ -87,7 +89,6 @@ class CustomizeStyleSheet: SheetWindow {
table.onMouseExited = { event, view in
IllustratedImageThemeView.resetImage(self.illustratedImageView, event, view)
}
table.fit()
let view = TableGroupSetView(originalViews: [table], padding: 0)
return view
}
@@ -101,7 +102,6 @@ class CustomizeStyleSheet: SheetWindow {
table.onMouseExited = { event, view in
IllustratedImageThemeView.resetImage(self.illustratedImageView, event, view)
}
table.fit()
let view = TableGroupSetView(originalViews: [table], padding: 0)
toggleAppNamesWindowTitles()
return view
@@ -111,7 +111,6 @@ class CustomizeStyleSheet: SheetWindow {
let table = makeAppWindowTableGroupView()
table.addNewTable()
table.addRow(titleTruncation)
table.fit()
let view = TableGroupSetView(originalViews: [table], padding: 0)
toggleAppNamesWindowTitles()
return view
@@ -17,12 +17,12 @@ class AdditionalControlsSheet: SheetWindow {
ControlsTab.arrowKeysEnabledCallback(ControlsTab.arrowKeysCheckbox)
ControlsTab.vimKeysEnabledCallback(ControlsTab.vimKeysCheckbox)
let table1 = TableGroupView(title: NSLocalizedString("Additional controls", comment: ""),
width: PreferencesWindow.width)
width: SheetWindow.width)
_ = table1.addRow(enableArrows)
_ = table1.addRow(enableVimKeys)
_ = table1.addRow(enableMouse)
let table2 = TableGroupView(title: NSLocalizedString("Miscellaneous", comment: ""),
width: PreferencesWindow.width)
width: SheetWindow.width)
_ = table2.addRow(enableCursorFollowFocus)
_ = table2.addRow(enableTrackpadHapticFeedback)
let view = TableGroupSetView(originalViews: [table1, table2], padding: 0)
@@ -76,10 +76,7 @@ class ControlsTab {
// trigger shortcutChanged for these shortcuts to trigger .restrictModifiers
[holdShortcut, holdShortcut2, holdShortcut3].forEach { ControlsTab.shortcutChangedCallback($0[1] as! NSControl) }
[nextWindowShortcut, nextWindowShortcut2, nextWindowShortcut3].forEach { ControlsTab.shortcutChangedCallback($0[0] as! NSControl) }
let tabs = StackView(tableGroupViews, .vertical)
tabs.translatesAutoresizingMaskIntoConstraints = false
tabs.fit()
let table = TableGroupView(hasHeader: true, width: PreferencesWindow.width)
let table = TableGroupView(hasHeader: true, width: SettingsWindow.contentWidth)
let tab = NSSegmentedControl(labels: [
NSLocalizedString("Shortcut 1", comment: ""),
NSLocalizedString("Shortcut 2", comment: ""),
@@ -88,17 +85,15 @@ class ControlsTab {
], trackingMode: .selectOne, target: self, action: #selector(switchTab(_:)))
tab.selectedSegment = 0
tab.segmentStyle = .automatic
tab.widthAnchor.constraint(equalToConstant: PreferencesWindow.width).isActive = true
tab.widthAnchor.constraint(equalToConstant: SettingsWindow.contentWidth).isActive = true
table.addHeader(views: [tab])
let additionalControlsButton = NSButton(title: NSLocalizedString("Additional controls…", comment: ""), target: self, action: #selector(ControlsTab.showAdditionalControlsSettings))
let shortcutsButton = NSButton(title: NSLocalizedString("Shortcuts when active…", comment: ""), target: self, action: #selector(ControlsTab.showShortcutsSettings))
let tools = StackView([additionalControlsButton, shortcutsButton], .horizontal)
let view = TableGroupSetView(originalViews: [table, tab1View, tab2View, tab3View, tab6View], toolsViews: [tools], toolsAlignment: .trailing)
view.translatesAutoresizingMaskIntoConstraints = false
let view = TableGroupSetView(originalViews: [table, tab1View, tab2View, tab3View, tab6View], toolsViews: [tools], bottomPadding: 0, toolsAlignment: .trailing)
shortcutsWhenActiveSheet = ShortcutsWhenActiveSheet()
additionalControlsSheet = AdditionalControlsSheet()
ControlsTab.switchIndexTab(0)
view.fit()
return view
}
@@ -113,7 +108,7 @@ class ControlsTab {
private static func gestureTab(_ index: Int) -> TableGroupView {
let label = NSLocalizedString("You may need to disable some conflicting system gestures", comment: "")
let button = NSButton(title: NSLocalizedString("Open Trackpad Settings…", comment: ""), target: self, action: #selector(openSystemGestures(_:)))
let infoBtn = LabelAndControl.makeInfoButton(onMouseEntered: { event, view in
let infoBtn = LabelAndControl.makeInfoButton(searchableTooltipTexts: [label], onMouseEntered: { event, view in
Popover.shared.show(event: event, positioningView: view, message: label, extraView: button)
})
let gesture = LabelAndControl.makeDropdown("nextWindowGesture", GesturePreference.allCases)
@@ -138,10 +133,8 @@ class ControlsTab {
let showFullscreenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showFullscreenWindows", index), ShowHowPreference.allCases.filter { $0 != .showAtTheEnd }) // this filter is ok for serialization because the filtered value is last in the enum
let showWindowlessApps = LabelAndControl.makeDropdown(Preferences.indexToName("showWindowlessApps", index), ShowHowPreference.allCases)
let windowOrder = LabelAndControl.makeDropdown(Preferences.indexToName("windowOrder", index), WindowOrderPreference.allCases)
let shortcutStyle = LabelAndControl.makeDropdown(Preferences.indexToName("shortcutStyle", index), ShortcutStylePreference.allCases)
let table = TableGroupView(width: PreferencesWindow.width)
let table = TableGroupView(width: SettingsWindow.contentWidth)
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Trigger shortcut", comment: ""), rightViews: trigger))
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("After release", comment: ""), rightViews: [shortcutStyle]))
table.addNewTable()
table.addRow(leftViews: [TableGroupView.makeText(NSLocalizedString("Show windows from applications", comment: ""))], rightViews: [appsToShow])
table.addRow(leftViews: [TableGroupView.makeText(NSLocalizedString("Show windows from Spaces", comment: ""))], rightViews: [spacesToShow])
@@ -151,7 +144,6 @@ class ControlsTab {
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Show fullscreen windows", comment: ""), rightViews: [showFullscreenWindows]))
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Show apps with no open window", comment: ""), rightViews: [showWindowlessApps]))
table.addRow(TableGroupView.Row(leftTitle: NSLocalizedString("Order windows by", comment: ""), rightViews: [windowOrder]))
table.fit()
return table
}
@@ -171,11 +163,11 @@ class ControlsTab {
}
@objc static func showShortcutsSettings() {
App.app.preferencesWindow.beginSheet(shortcutsWhenActiveSheet)
App.app.settingsWindow.beginSheetWithSearchHighlight(shortcutsWhenActiveSheet)
}
@objc static func showAdditionalControlsSettings() {
App.app.preferencesWindow.beginSheet(additionalControlsSheet)
App.app.settingsWindow.beginSheetWithSearchHighlight(additionalControlsSheet)
}
private static func addShortcut(_ triggerPhase: ShortcutTriggerPhase, _ scope: ShortcutScope, _ shortcut: Shortcut, _ controlId: String, _ index: Int?) {
@@ -278,9 +270,9 @@ class ControlsTab {
}
}
if !conflicts.isEmpty {
// if the app is still launching (App.app.preferencesWindow == nil) and we have a conflict
// if the app is still launching (App.app.settingsWindow == nil) and we have a conflict
// then we don't show the user a dialog, and simply disable vim keys
if App.app.preferencesWindow == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }) {
if App.app.settingsWindow == nil || !shouldClearConflictingShortcuts(conflicts.map { $0.value }) {
return false
}
conflicts.forEach {