diff --git a/resources/l10n/Localizable.strings b/resources/l10n/Localizable.strings index c0182d7d..5b4ada37 100644 --- a/resources/l10n/Localizable.strings +++ b/resources/l10n/Localizable.strings @@ -22,6 +22,12 @@ /* No comment provided by engineer. */ "Active screen" = "Active screen"; +/* No comment provided by engineer. */ +"Add a running app" = "Add a running app"; + +/* No comment provided by engineer. */ +"Add an app from disk" = "Add an app from disk"; + /* No comment provided by engineer. */ "Additional controls" = "Additional controls"; diff --git a/src/logic/events/WindowCaptureEvents.swift b/src/logic/events/WindowCaptureEvents.swift index f86e7e10..72fb0d00 100644 --- a/src/logic/events/WindowCaptureEvents.swift +++ b/src/logic/events/WindowCaptureEvents.swift @@ -38,7 +38,7 @@ class WindowCaptureScreenshots { if let cachedWindow = (cachedSCWindows.first { $0.windowID == notCachedWindow }) { oneTimeCapture(cachedWindow, source) } else { - Logger.error { "wid:\(notCachedWindow) was not found in SCShareableContent windows" } + Logger.debug { "wid:\(notCachedWindow) was not found in SCShareableContent windows" } } } } diff --git a/src/ui/generic-components/TableView.swift b/src/ui/generic-components/TableView.swift index 804775c4..03ef614e 100644 --- a/src/ui/generic-components/TableView.swift +++ b/src/ui/generic-components/TableView.swift @@ -1,55 +1,55 @@ import Cocoa class BlacklistView: NSScrollView { - override class var isCompatibleWithResponsiveScrolling: Bool { true } - convenience init(width: CGFloat = 500, height: CGFloat = 378) { self.init(frame: .zero) translatesAutoresizingMaskIntoConstraints = false - borderType = .bezelBorder + borderType = .noBorder hasHorizontalScroller = false hasVerticalScroller = true - verticalScrollElasticity = .none usesPredominantAxisScrolling = true documentView = TableView(nil) fit(width, height) + wantsLayer = true + layer!.cornerRadius = TableGroupView.cornerRadius + layer!.masksToBounds = true + contentView.wantsLayer = true + contentView.layer!.cornerRadius = TableGroupView.cornerRadius + contentView.layer!.masksToBounds = true + } + + override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { + axis == .vertical } override func scrollWheel(with event: NSEvent) { - guard shouldHandleVerticalScroll(event) else { - super.scrollWheel(with: event) - return - } - if canScrollInEventDirection(event) { - super.scrollWheel(with: event) - } else { - parentScrollView()?.scrollWheel(with: event) + let before = contentView.bounds.origin + super.scrollWheel(with: event) + DispatchQueue.main.async { [weak self] in + guard let self, self.shouldForwardToParent(event, before) else { return } + self.parentScrollView()?.scrollWheel(with: event) } } - private func shouldHandleVerticalScroll(_ event: NSEvent) -> Bool { + private func shouldForwardToParent(_ event: NSEvent, _ before: CGPoint) -> Bool { + guard isVerticalScroll(event) else { return false } + guard abs(contentView.bounds.origin.y - before.y) < 0.01 else { return false } + return isAtVerticalBoundary(event) + } + + private func isVerticalScroll(_ event: NSEvent) -> Bool { abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) && abs(event.scrollingDeltaY) > 0.1 } - private func canScrollInEventDirection(_ event: NSEvent) -> Bool { - let maxOffset = maxVerticalOffset() - guard maxOffset > 0 else { return false } - let y = contentView.bounds.origin.y + private func isAtVerticalBoundary(_ event: NSEvent) -> Bool { + guard let content = documentView else { return false } + let visible = contentView.documentVisibleRect let dy = normalizedVerticalDelta(event) - if dy > 0 { - return y > 0.5 - } - if dy < 0 { - return y < maxOffset - 0.5 - } + if dy > 0 { return visible.minY <= content.bounds.minY + 0.5 } + if dy < 0 { return visible.maxY >= content.bounds.maxY - 0.5 } return false } - private func maxVerticalOffset() -> CGFloat { - guard let content = documentView else { return 0 } - return max(0, content.fittingSize.height - contentView.bounds.height) - } - private func normalizedVerticalDelta(_ event: NSEvent) -> CGFloat { let delta = event.hasPreciseScrollingDeltas ? event.scrollingDeltaY : event.deltaY return event.isDirectionInvertedFromDevice ? -delta : delta @@ -58,9 +58,7 @@ class BlacklistView: NSScrollView { private func parentScrollView() -> NSScrollView? { var parent = superview while let view = parent { - if let scrollView = view as? NSScrollView { - return scrollView - } + if let scrollView = view as? NSScrollView { return scrollView } parent = view.superview } return nil @@ -70,6 +68,10 @@ class BlacklistView: NSScrollView { class TableView: NSTableView { var items = Preferences.blacklist + override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { + axis == .vertical + } + convenience init(_: Int?) { self.init() translatesAutoresizingMaskIntoConstraints = false @@ -158,14 +160,19 @@ class TableView: NSTableView { button.translatesAutoresizingMaskIntoConstraints = false button.allowsExpansionToolTips = true button.lineBreakMode = .byTruncatingTail + let cell = button.cell as! NSPopUpButtonCell + cell.bezelStyle = .regularSquare + cell.arrowPosition = .arrowAtBottom + cell.imagePosition = .imageOverlaps let cases: [MacroPreference] = isHidePref ? BlacklistHidePreference.allCases : BlacklistIgnorePreference.allCases button.addItems(withTitles: cases.map { $0.localizedString }) button.selectItem(at: isHidePref ? item.hide.index : item.ignore.index) button.onAction = { self.wasUpdated(colId, $0) } let parent = NSView() parent.addSubview(button) + button.leadingAnchor.constraint(equalTo: parent.leadingAnchor).isActive = true button.centerYAnchor.constraint(equalTo: parent.centerYAnchor).isActive = true - button.widthAnchor.constraint(equalTo: parent.widthAnchor).isActive = true + button.widthAnchor.constraint(lessThanOrEqualTo: parent.widthAnchor).isActive = true return parent } } diff --git a/src/ui/settings-window/tabs/BlacklistsTab.swift b/src/ui/settings-window/tabs/BlacklistsTab.swift index 1acc6922..2138d333 100644 --- a/src/ui/settings-window/tabs/BlacklistsTab.swift +++ b/src/ui/settings-window/tabs/BlacklistsTab.swift @@ -4,26 +4,104 @@ import Sparkle class BlacklistsTab { static func initTab() -> NSView { 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 - if ($0 as! NSSegmentedControl).selectedSegment == 0 { - let dialog = NSOpenPanel() - dialog.allowsMultipleSelection = false - dialog.allowedFileTypes = ["app"] - dialog.canChooseDirectories = false - dialog.beginSheetModal(for: App.app.settingsWindow) { - if $0 == .OK, let url = dialog.url, let bundleId = Bundle(url: url)?.bundleIdentifier { - tableView.insertRow(bundleId) - } - } - } else { - tableView.removeSelectedRows() - } - } + let tableView = blacklist.documentView as! TableView + let addButton = makeAddButton(tableView) + let removeButton = makeRemoveButton(tableView) + let buttonsStack = NSStackView(views: [addButton, removeButton]) + buttonsStack.orientation = .horizontal + buttonsStack.spacing = 2 let table = TableGroupView(width: SettingsWindow.contentWidth) - _ = table.addRow(leftViews: [blacklist], secondaryViews: [add]) + _ = table.addRow(leftViews: [blacklist], secondaryViews: [buttonsStack]) let view = TableGroupSetView(originalViews: [table], bottomPadding: 0) return view } + + private static func makeAddButton(_ tableView: TableView) -> NSButton { + let button = makeCircleButton(systemSymbolName: "plus") + button.onAction = { [weak tableView] _ in + guard let tableView else { return } + showAddMenu(tableView, sender: button) + } + return button + } + + private static func makeRemoveButton(_ tableView: TableView) -> NSButton { + let button = makeCircleButton(systemSymbolName: "minus") + button.onAction = { [weak tableView] _ in + tableView?.removeSelectedRows() + } + return button + } + + private static func makeCircleButton(systemSymbolName: String) -> NSButton { + let button = NSButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isBordered = true + button.bezelStyle = .circular + button.showsBorderOnlyWhileMouseInside = false + if #available(macOS 11.0, *) { + button.image = NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil) + } else { + let templateName = systemSymbolName == "plus" ? NSImage.addTemplateName : NSImage.removeTemplateName + button.image = NSImage(named: templateName) + } + button.imagePosition = .imageOnly + button.imageScaling = .scaleProportionallyDown + button.widthAnchor.constraint(equalToConstant: 22).isActive = true + button.heightAnchor.constraint(equalToConstant: 22).isActive = true + return button + } + + private static func showAddMenu(_ tableView: TableView, sender: NSButton) { + let menu = NSMenu() + let runningAppsItem = NSMenuItem(title: NSLocalizedString("Add a running app", comment: ""), action: nil, keyEquivalent: "") + runningAppsItem.submenu = buildRunningAppsSubmenu(tableView) + menu.addItem(runningAppsItem) + let diskItem = NSMenuItem(title: NSLocalizedString("Add an app from disk", comment: ""), action: nil, keyEquivalent: "") + diskItem.representedObject = tableView + diskItem.target = BlacklistsTab.self + diskItem.action = #selector(addFromDisk(_:)) + menu.addItem(diskItem) + menu.popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height + 2), in: sender) + } + + @objc private static func addFromDisk(_ sender: NSMenuItem) { + guard let tableView = sender.representedObject as? TableView else { return } + let dialog = NSOpenPanel() + dialog.allowsMultipleSelection = false + dialog.allowedFileTypes = ["app"] + dialog.canChooseDirectories = false + dialog.beginSheetModal(for: App.app.settingsWindow) { + if $0 == .OK, let url = dialog.url, let bundleId = Bundle(url: url)?.bundleIdentifier { + tableView.insertRow(bundleId) + } + } + } + + private static func buildRunningAppsSubmenu(_ tableView: TableView) -> NSMenu { + let submenu = NSMenu() + let existingIds = Set(tableView.items.map { $0.bundleIdentifier }) + let apps = NSWorkspace.shared.runningApplications + .filter { $0.activationPolicy == .regular && $0.bundleIdentifier != nil && !existingIds.contains($0.bundleIdentifier!) } + .sorted { ($0.localizedName ?? "") < ($1.localizedName ?? "") } + for app in apps { + guard let bundleId = app.bundleIdentifier else { continue } + let item = NSMenuItem(title: app.localizedName ?? bundleId, action: nil, keyEquivalent: "") + if let path = app.bundleURL?.path { + let icon = NSWorkspace.shared.icon(forFile: path) + icon.size = NSSize(width: 16, height: 16) + item.image = icon + } + item.representedObject = (tableView, bundleId) + item.target = BlacklistsTab.self + item.action = #selector(addRunningApp(_:)) + submenu.addItem(item) + } + return submenu + } + + @objc private static func addRunningApp(_ sender: NSMenuItem) { + guard let (tableView, bundleId) = sender.representedObject as? (TableView, String) else { return } + tableView.insertRow(bundleId) + } } diff --git a/src/ui/settings-window/tabs/controls/ControlsTab.swift b/src/ui/settings-window/tabs/controls/ControlsTab.swift index b937621a..67425e9a 100644 --- a/src/ui/settings-window/tabs/controls/ControlsTab.swift +++ b/src/ui/settings-window/tabs/controls/ControlsTab.swift @@ -69,43 +69,38 @@ private class ShortcutSidebarRow: ClickHoverStackView { } private class ControlsSidebarScrollView: NSScrollView { - override class var isCompatibleWithResponsiveScrolling: Bool { true } + override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { + axis == .vertical + } override func scrollWheel(with event: NSEvent) { - guard shouldHandleVerticalScroll(event) else { - super.scrollWheel(with: event) - return - } - if canScrollInEventDirection(event) { - super.scrollWheel(with: event) - } else { - parentScrollView()?.scrollWheel(with: event) + let before = contentView.bounds.origin + super.scrollWheel(with: event) + DispatchQueue.main.async { [weak self] in + guard let self, self.shouldForwardToParent(event, before) else { return } + self.parentScrollView()?.scrollWheel(with: event) } } - private func shouldHandleVerticalScroll(_ event: NSEvent) -> Bool { + private func shouldForwardToParent(_ event: NSEvent, _ before: CGPoint) -> Bool { + guard isVerticalScroll(event) else { return false } + guard abs(contentView.bounds.origin.y - before.y) < 0.01 else { return false } + return isAtVerticalBoundary(event) + } + + private func isVerticalScroll(_ event: NSEvent) -> Bool { abs(event.scrollingDeltaY) > abs(event.scrollingDeltaX) && abs(event.scrollingDeltaY) > 0.1 } - private func canScrollInEventDirection(_ event: NSEvent) -> Bool { - let maxOffset = maxVerticalOffset() - guard maxOffset > 0 else { return false } - let y = contentView.bounds.origin.y + private func isAtVerticalBoundary(_ event: NSEvent) -> Bool { + guard let content = documentView else { return false } + let visible = contentView.documentVisibleRect let dy = normalizedVerticalDelta(event) - if dy > 0 { - return y > 0.5 - } - if dy < 0 { - return y < maxOffset - 0.5 - } + if dy > 0 { return visible.minY <= content.bounds.minY + 0.5 } + if dy < 0 { return visible.maxY >= content.bounds.maxY - 0.5 } return false } - private func maxVerticalOffset() -> CGFloat { - guard let content = documentView?.subviews.first else { return 0 } - return max(0, content.fittingSize.height - contentView.bounds.height) - } - private func normalizedVerticalDelta(_ event: NSEvent) -> CGFloat { let delta = event.hasPreciseScrollingDeltas ? event.scrollingDeltaY : event.deltaY return event.isDirectionInvertedFromDevice ? -delta : delta @@ -114,15 +109,19 @@ private class ControlsSidebarScrollView: NSScrollView { private func parentScrollView() -> NSScrollView? { var parent = superview while let view = parent { - if let scrollView = view as? NSScrollView { - return scrollView - } + if let scrollView = view as? NSScrollView { return scrollView } parent = view.superview } return nil } } +private class ControlsSidebarDocumentView: FlippedView { + override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { + axis == .vertical + } +} + class ControlsTab { static var shortcuts = [String: ATShortcut]() static var shortcutControls = [String: (CustomRecorderControl, String)]() @@ -314,9 +313,8 @@ class ControlsTab { rowsScrollView.hasVerticalScroller = true rowsScrollView.hasHorizontalScroller = false rowsScrollView.scrollerStyle = .overlay - rowsScrollView.verticalScrollElasticity = .none rowsScrollView.usesPredominantAxisScrolling = true - let documentView = FlippedView(frame: .zero) + let documentView = ControlsSidebarDocumentView(frame: .zero) documentView.translatesAutoresizingMaskIntoConstraints = false rowsScrollView.documentView = documentView documentView.addSubview(rows)