diff --git a/alt-tab-macos.xcodeproj/project.pbxproj b/alt-tab-macos.xcodeproj/project.pbxproj index 9627f76e8..83e4236d6 100644 --- a/alt-tab-macos.xcodeproj/project.pbxproj +++ b/alt-tab-macos.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ D04BAD4DE538FDF7E7532EE2 /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD32E130E4A061DC8332 /* Labels.swift */; }; D04BAE2E8E9B9898A4DF9B3B /* FontIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAED53465957807CBF8B2 /* FontIcon.swift */; }; D04BAE369A14C3126A1606FE /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8F1AA48A323EE5638DC /* HelperExtensions.swift */; }; + D04BAE53AE67492CF654B4AE /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE70A24C83D571C77B1A /* TabViewController.swift */; }; D04BAEF78503D7A2CEFB9E9E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA44C837F3A67403B9DB /* main.swift */; }; F029861A378EC1417106FEC3 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298E42A818112B290FF6C7 /* TextField.swift */; }; F0298AB28A3CE5DBEC385730 /* HyperlinkLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */; }; @@ -92,6 +93,7 @@ D04BADCB1C0F50340A6CAFC2 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; D04BAE1243C9B4BE3ED1B524 /* 7 windows - 2 lines - extra wide window.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "7 windows - 2 lines - extra wide window.jpg"; sourceTree = ""; }; D04BAE5BBE182DD5DDFE2E3E /* ThumbnailsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsPanel.swift; sourceTree = ""; }; + D04BAE70A24C83D571C77B1A /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; }; D04BAE80772D25834E440975 /* Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; D04BAE93A5854C501639C640 /* update_homebrew_cask.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = update_homebrew_cask.sh; sourceTree = ""; }; D04BAEA3EDC4F80FA23DBEC4 /* CGWindowID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindowID.swift; sourceTree = ""; }; @@ -257,6 +259,7 @@ F0298E42A818112B290FF6C7 /* TextField.swift */, F0298708E2B13DBD4738AE76 /* HyperlinkLabel.swift */, D04BAED53465957807CBF8B2 /* FontIcon.swift */, + D04BAE70A24C83D571C77B1A /* TabViewController.swift */, ); path = ui; sourceTree = ""; @@ -388,6 +391,7 @@ D04BA1BA0B3F2E0A47883569 /* Application.swift in Sources */, D04BA2378832FD7E5DE3BC23 /* Applications.swift in Sources */, D04BAAD43731608067734ED3 /* DispatchQueues.swift in Sources */, + D04BAE53AE67492CF654B4AE /* TabViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/alt-tab-macos/ui/Labels.swift b/alt-tab-macos/ui/Labels.swift index ff91ce334..85d1be2d5 100644 --- a/alt-tab-macos/ui/Labels.swift +++ b/alt-tab-macos/ui/Labels.swift @@ -7,6 +7,7 @@ class BaseLabel: NSTextView { init(_ text: String) { super.init(frame: .zero) + _init() string = text } @@ -20,7 +21,6 @@ class BaseLabel: NSTextView { backgroundColor = .clear isSelectable = false isEditable = false - font = Preferences.font enabledTextCheckingTypes = 0 } } diff --git a/alt-tab-macos/ui/PreferencesWindow.swift b/alt-tab-macos/ui/PreferencesWindow.swift index f781fffee..d931c47da 100644 --- a/alt-tab-macos/ui/PreferencesWindow.swift +++ b/alt-tab-macos/ui/PreferencesWindow.swift @@ -2,22 +2,35 @@ import Cocoa import Foundation class PreferencesWindow: NSWindow, NSWindowDelegate { - let width = CGFloat(496) - let height = CGFloat(256) // auto expands to content height (but does not auto shrink) - let padding = CGFloat(40) - var labelWidth: CGFloat { - return (width - padding) * CGFloat(0.45) - } + let tabViewController = TabViewController() + let padding = CGFloat(20) + let interPadding = CGFloat(10) var windowCloseRequested = false override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) { - let initialRect = NSRect(x: 0, y: 0, width: width, height: height) - super.init(contentRect: initialRect, styleMask: style, backing: backingStoreType, defer: flag) + super.init(contentRect: .zero, styleMask: style, backing: backingStoreType, defer: flag) title = App.name + " Preferences" hidesOnDeactivate = false isReleasedWhenClosed = false styleMask.insert([.miniaturizable, .closable]) - contentView = makeContentView() + tabViewController.tabStyle = .toolbar + contentViewController = tabViewController + makeTabViews() + } + + private func makeTabViews() { + for tabTuple in [ + ("Shortcuts", makeShortcutsView(), NSImage.preferencesGeneralName), + ("Appearance", makeAppearanceView(), NSImage.colorPanelName), + ("About", makeAboutView(), NSImage.infoName) + ] { + let viewController = NSViewController() + viewController.view = tabTuple.1 + let tabViewItem = NSTabViewItem(viewController: viewController) + tabViewItem.label = tabTuple.0 + tabViewItem.image = NSImage(named: tabTuple.2)! + tabViewController.addTabViewItem(tabViewItem) + } } func show() { @@ -45,24 +58,7 @@ class PreferencesWindow: NSWindow, NSWindowDelegate { } } - 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 = padding * 0.3 - wrappingView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding * 0.5).isActive = true - wrappingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: padding * -0.5).isActive = true - wrappingView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: padding * 0.5).isActive = true - wrappingView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: padding * -0.5).isActive = true - - return contentView - } - - private func makePreferencesViews() -> [NSView] { + private func makeShortcutsView() -> NSGridView { // TODO: make the validators be a part of each Preference let tabKeyCodeValidator: ((String) -> Bool) = { guard let int = Int($0) else { @@ -77,10 +73,14 @@ class PreferencesWindow: NSWindow, NSWindowDelegate { return whitelistedKeycodes.contains(int) } - return [ + return makeGridLayout([ makeLabelWithDropdown("Alt key", "metaKey", Preferences.metaKeyMacro.labels), makeLabelWithInput("Tab key", "tabKeyCode", 33, "KeyCodes Reference", "https://eastmanreference.com/complete-list-of-applescript-key-codes", tabKeyCodeValidator), - makeHorizontalSeparator(), + ]) + } + + private func makeAppearanceView() -> NSGridView { + return makeGridLayout([ makeLabelWithDropdown("Theme", "theme", Preferences.themeMacro.labels), makeLabelWithSlider("Max size on screen", "maxScreenUsage", 10, 100, 10, true, "%"), makeLabelWithSlider("Min windows per row", "minCellsPerRow", 1, 20, 20, true), @@ -88,82 +88,93 @@ class PreferencesWindow: NSWindow, NSWindowDelegate { makeLabelWithSlider("Min rows of windows", "minRows", 1, 20, 20, true), makeLabelWithSlider("Window app icon size", "iconSize", 0, 64, 11, false, "px"), makeLabelWithSlider("Window title font size", "fontHeight", 0, 64, 11, false, "px"), - makeLabelWithCheckbox("Hide space number labels", "hideSpaceNumberLabels"), - makeHorizontalSeparator(), + makeLabelWithDropdown("Show on", "showOnScreen", Preferences.showOnScreenMacro.labels), makeLabelWithSlider("Apparition delay", "windowDisplayDelay", 0, 2000, 11, false, "ms"), - makeLabelWithDropdown("Show on", "showOnScreen", Preferences.showOnScreenMacro.labels) - ] + makeLabelWithCheckbox("Hide space number labels", "hideSpaceNumberLabels"), + ]) } - private func makeHorizontalSeparator() -> NSView { - let view = NSBox() - view.boxType = .separator + private func makeAboutView() -> NSGridView { + return makeGridLayout([ + [NSTextField(wrappingLabelWithString: "\(App.name) #VERSION#"), ], + [HyperlinkLabel(labelWithUrl: "Source code repository", nsUrl: NSURL(string: "https://github.com/lwouis/alt-tab-macos")!)], + ]) + } - return view + private func makeGridLayout(_ controls: [[NSView]]) -> NSGridView { + let gridView = NSGridView(views: controls) + gridView.yPlacement = .fill + gridView.columnSpacing = interPadding + gridView.rowSpacing = interPadding + gridView.column(at: 0).xPlacement = .trailing + gridView.column(at: 0).leadingPadding = padding + gridView.column(at: gridView.numberOfColumns - 1).trailingPadding = padding + gridView.row(at: 0).topPadding = padding + gridView.row(at: gridView.numberOfRows - 1).bottomPadding = padding + gridView.widthAnchor.constraint(equalToConstant: gridView.fittingSize.width).isActive = true + gridView.rowAlignment = .lastBaseline + for i in 0.. Bool)? = nil) -> NSStackView { + private func makeLabelWithInput(_ labelText: String, _ rawName: String, _ width: CGFloat, _ suffixText: String? = nil, _ suffixUrl: String? = nil, _ validator: ((String) -> Bool)? = nil) -> [NSView] { let input = TextField(Preferences.rawValues[rawName]!) input.validationHandler = validator input.delegate = input input.visualizeValidationState() - if width != nil { - input.widthAnchor.constraint(equalToConstant: width!).isActive = true - } - - return makeLabelWithProvidedControl(labelText, rawName, input, suffixText, nil, suffixUrl) + input.widthAnchor.constraint(equalToConstant: width).isActive = true + input.heightAnchor.constraint(equalToConstant: input.fittingSize.height).isActive = true + let views = makeLabelWithProvidedControl(labelText, rawName, input) + return [views[0], NSStackView(views: [views[1], makeSuffix(rawName, suffixText!, suffixUrl)])] } - private func makeLabelWithCheckbox(_ labelText: String, _ rawName: String) -> NSStackView { + private func makeLabelWithCheckbox(_ labelText: String, _ rawName: String) -> [NSView] { let checkbox = NSButton.init(checkboxWithTitle: "", target: nil, action: nil) setControlValue(checkbox, Preferences.rawValues[rawName]!) return makeLabelWithProvidedControl(labelText, rawName, checkbox) } - private func makeLabelWithDropdown(_ labelText: String, _ rawName: String, _ values: [String], _ suffixText: String? = nil) -> NSStackView { + private func makeLabelWithDropdown(_ labelText: String, _ rawName: String, _ values: [String], _ suffixText: String? = nil) -> [NSView] { let popUp = NSPopUpButton() popUp.addItems(withTitles: values) popUp.selectItem(withTitle: Preferences.rawValues[rawName]!) - return makeLabelWithProvidedControl(labelText, rawName, popUp, suffixText) } - private func makeLabelWithSlider(_ labelText: String, _ rawName: String, _ minValue: Double, _ maxValue: Double, _ numberOfTickMarks: Int, _ allowsTickMarkValuesOnly: Bool, _ unitText: String = "") -> NSStackView { + private func makeLabelWithSlider(_ labelText: String, _ rawName: String, _ minValue: Double, _ maxValue: Double, _ numberOfTickMarks: Int, _ allowsTickMarkValuesOnly: Bool, _ unitText: String = "") -> [NSView] { let value = Preferences.rawValues[rawName]! - let suffixText = value + unitText + let suffixText = value + " " + unitText let slider = NSSlider() slider.minValue = minValue slider.maxValue = maxValue slider.stringValue = value - slider.numberOfTickMarks = numberOfTickMarks - slider.allowsTickMarkValuesOnly = allowsTickMarkValuesOnly - slider.tickMarkPosition = .below +// slider.numberOfTickMarks = numberOfTickMarks +// slider.allowsTickMarkValuesOnly = allowsTickMarkValuesOnly +// slider.tickMarkPosition = .below slider.isContinuous = true - - return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText, 60) + return makeLabelWithProvidedControl(labelText, rawName, slider, suffixText) } - private func makeLabelWithProvidedControl(_ labelText: String?, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixWidth: CGFloat? = nil, _ suffixUrl: String? = nil) -> NSStackView { - let label = NSTextField(wrappingLabelWithString: (labelText != nil ? labelText! + ": " : "")) - label.alignment = .right - label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true - label.identifier = NSUserInterfaceItemIdentifier(rawName + ControlIdentifierDiscriminator.LABEL.rawValue) - label.isSelectable = false - + private func makeLabelWithProvidedControl(_ labelText: String?, _ rawName: String, _ control: NSControl, _ suffixText: String? = nil, _ suffixUrl: String? = nil) -> [NSView] { + let label = makeLabel(labelText, rawName) control.identifier = NSUserInterfaceItemIdentifier(rawName) control.target = self control.action = #selector(controlWasChanged) - let containerView = NSStackView(views: [label, control]) - - if suffixText != nil { - let suffix = makeSuffix(rawName, suffixText!, suffixWidth, suffixUrl) - containerView.addView(suffix, in: .leading) - } + return [label, control, suffixText != nil ? makeSuffix(rawName, suffixText!, suffixUrl) : NSView()] + } - return containerView + private func makeLabel(_ labelText: String?, _ rawName: String) -> NSTextField { + let label = NSTextField(wrappingLabelWithString: labelText != nil ? labelText! + ": " : "") + label.widthAnchor.constraint(equalToConstant: label.fittingSize.width).isActive = true + label.heightAnchor.constraint(equalToConstant: label.fittingSize.height).isActive = true + label.alignment = .right + label.identifier = NSUserInterfaceItemIdentifier(rawName + ControlIdentifierDiscriminator.LABEL.rawValue) + return label } - private func makeSuffix(_ controlName: String, _ text: String, _ width: CGFloat? = nil, _ url: String? = nil) -> NSTextField { + private func makeSuffix(_ controlName: String, _ text: String, _ url: String? = nil) -> NSTextField { let suffix: NSTextField if url == nil { suffix = NSTextField(labelWithString: text) @@ -172,10 +183,8 @@ class PreferencesWindow: NSWindow, NSWindowDelegate { } suffix.textColor = .gray suffix.identifier = NSUserInterfaceItemIdentifier(controlName + ControlIdentifierDiscriminator.SUFFIX.rawValue) - if width != nil { - suffix.widthAnchor.constraint(equalToConstant: width!).isActive = true - } - + suffix.widthAnchor.constraint(equalToConstant: suffix.fittingSize.width).isActive = true + suffix.heightAnchor.constraint(equalToConstant: suffix.fittingSize.height).isActive = true return suffix } diff --git a/alt-tab-macos/ui/StatusItem.swift b/alt-tab-macos/ui/StatusItem.swift index 6071d47f4..b50e09d2c 100644 --- a/alt-tab-macos/ui/StatusItem.swift +++ b/alt-tab-macos/ui/StatusItem.swift @@ -15,7 +15,7 @@ class StatusItem { action: #selector(app.showPreferencesPanel), keyEquivalent: ",") item.menu!.addItem( - withTitle: "Quit \(App.name) #VERSION#", + withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") return item diff --git a/alt-tab-macos/ui/TabViewController.swift b/alt-tab-macos/ui/TabViewController.swift new file mode 100644 index 000000000..c146bb4fb --- /dev/null +++ b/alt-tab-macos/ui/TabViewController.swift @@ -0,0 +1,19 @@ +import Cocoa +import Foundation + +class TabViewController: NSTabViewController { + override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) { + super.tabView(tabView, didSelect: tabViewItem) + guard let tabViewItem = tabViewItem, let window = view.window else { return } + window.title = tabViewItem.label + resizeWindowToFit(tabViewItem, window) + } + + private func resizeWindowToFit(_ tabViewItem: NSTabViewItem, _ window: NSWindow) { + let contentFrame = window.frameRect(forContentRect: NSRect(origin: .zero, size: tabViewItem.view!.frame.size)) + let toolbarHeight = window.frame.size.height - contentFrame.size.height + let newOrigin = NSPoint(x: window.frame.origin.x, y: window.frame.origin.y + toolbarHeight) + let newFrame = NSRect(origin: newOrigin, size: contentFrame.size) + window.setFrame(newFrame, display: false, animate: true) + } +} \ No newline at end of file diff --git a/alt-tab-macos/ui/TextField.swift b/alt-tab-macos/ui/TextField.swift index 9e34f60fb..687315cb0 100644 --- a/alt-tab-macos/ui/TextField.swift +++ b/alt-tab-macos/ui/TextField.swift @@ -6,6 +6,8 @@ class TextField: NSTextField, NSTextFieldDelegate { public convenience init(_ value: String) { self.init(string: value) + usesSingleLineMode = true + font = .labelFont(ofSize: NSFont.systemFontSize) wantsLayer = true layer?.borderWidth = 1 } diff --git a/ci/set_version_in_app.sh b/ci/set_version_in_app.sh index b4d78dc22..bbe70446d 100755 --- a/ci/set_version_in_app.sh +++ b/ci/set_version_in_app.sh @@ -4,6 +4,6 @@ set -exu version="$(cat VERSION.txt)" # set the version for the app menubar menu text -sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/ui/StatusItem.swift +sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/ui/PreferencesWindow.swift # set the version in the app meta-data for the AppStore and app "Get Info" panel sed -i '' -e "s/#VERSION#/$version/" alt-tab-macos/Info.plist