feat: can add running apps into blacklist (closes #5268)
This commit is contained in:
1 parent
4b85fb8cf7
commit
d5d699659c
5 files changed
+170
-81
No files matched your search
@@ -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";
|
||||
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user