feat: can add running apps into blacklist (closes #5268)

This commit is contained in:
lwouis committed 2026-02-19 23:20:14 +01:00
1 parent 4b85fb8cf7
commit d5d699659c
5 files changed
+170 -81

No files matched your search

+6
View File
@@ -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";
+1 -1
View File
@@ -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" }
}
}
}
+39 -32
View File
@@ -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
}
}
+96 -18
View File
@@ -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)
}
}
@@ -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)