diff --git a/README.md b/README.md index dc45f46..d8c5468 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Use your chosen method to toggle auto-scroll on/off, or click any other mouse bu ### Controlling Scrolling Once auto-scroll is activated: +- A blue anchor indicator appears at the activation point and the menu bar icon turns green while auto-scroll is active - Move cursor **up** to scroll **up** - Move cursor **down** to scroll **down** - Move further from the center point for faster scrolling @@ -78,6 +79,7 @@ Access additional options from the menu bar icon: - **Scroll Speed** - Sensitivity slider (0.2x - 3.0x) - **Activation Method** - Choose your preferred mouse/key combination - **Invert Scrolling Direction** - Reverse up/down behavior +- **Show Activation Indicator** - Toggle the on-screen anchor shown while auto-scroll is active - **Launch at Login** - Automatic startup option - **About Scrollapp** - App information and usage tips diff --git a/Scrollapp/ScrollappApp.swift b/Scrollapp/ScrollappApp.swift index e8dcd1a..eca0f7c 100644 --- a/Scrollapp/ScrollappApp.swift +++ b/Scrollapp/ScrollappApp.swift @@ -32,6 +32,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { var scrollMonitor: Any? var clickMonitor: Any? var scrollCursor: NSCursor? + var originIndicatorPanel: NSPanel? + var showActivationIndicator = true // Show on-screen anchor while auto-scrolling var isTrackpadMode = false var lastScrollTime: Date? var scrollDetectionWindow = [CGFloat]() @@ -86,6 +88,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // Load user preferences isDirectionInverted = UserDefaults.standard.bool(forKey: "invertScrollDirection") + showActivationIndicator = UserDefaults.standard.object(forKey: "showActivationIndicator") as? Bool ?? true launchAtLogin = UserDefaults.standard.bool(forKey: "launchAtLogin") scrollSensitivity = UserDefaults.standard.double(forKey: "scrollSensitivity") if scrollSensitivity == 0 { scrollSensitivity = 1.0 } // Default if not set @@ -168,6 +171,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { invertItem.state = isDirectionInverted ? .on : .off menu.addItem(invertItem) + // Add activation indicator toggle + let indicatorItem = NSMenuItem(title: "Show Activation Indicator", action: #selector(toggleActivationIndicator), keyEquivalent: "") + indicatorItem.state = showActivationIndicator ? .on : .off + menu.addItem(indicatorItem) + // Add launch at login toggle let launchItem = NSMenuItem(title: "Launch at Login", action: #selector(toggleLaunchAtLogin), keyEquivalent: "") launchItem.state = launchAtLogin ? .on : .off @@ -297,6 +305,64 @@ class AppDelegate: NSObject, NSApplicationDelegate { self?.scrollCursor?.set() // keep forcing cursor } RunLoop.current.add(scrollTimer!, forMode: .common) + + showOriginIndicator(at: point) + updateAutoScrollActiveState(active: true) + } + + // A menu bar app cannot change the pointer over other apps' windows + // (NSCursor.set() has no effect there), so a click-through overlay panel at + // the activation point is used to show that auto-scroll is active, similar + // to the Windows auto-scroll anchor. + func showOriginIndicator(at point: NSPoint) { + hideOriginIndicator() + guard showActivationIndicator else { return } + + let size: CGFloat = 36 + let frame = NSRect(x: point.x - size / 2, y: point.y - size / 2, width: size, height: size) + let panel = NSPanel(contentRect: frame, + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false) + panel.isOpaque = false + panel.backgroundColor = .clear + panel.hasShadow = false + panel.level = .screenSaver + panel.ignoresMouseEvents = true + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + panel.isReleasedWhenClosed = false + + let imageView = NSImageView(frame: NSRect(x: 0, y: 0, width: size, height: size)) + if let symbol = NSImage(systemSymbolName: "arrow.up.and.down.circle.fill", + accessibilityDescription: "Auto-scroll origin") { + imageView.image = symbol.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: size - 8, weight: .regular)) + } + imageView.contentTintColor = NSColor.systemBlue.withAlphaComponent(0.85) + imageView.imageScaling = .scaleProportionallyUpOrDown + panel.contentView = imageView + panel.orderFrontRegardless() + + originIndicatorPanel = panel + } + + func hideOriginIndicator() { + originIndicatorPanel?.close() + originIndicatorPanel = nil + } + + func updateAutoScrollActiveState(active: Bool) { + if let button = statusItem.button { + let symbolName = active ? "arrow.up.and.down.circle.fill" : "arrow.up.and.down.circle" + if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: "Scrollapp") { + image.isTemplate = true + button.image = image + button.contentTintColor = active ? .systemGreen : nil + } + } + + if let toggleItem = statusItem.menu?.items.first(where: { $0.action == #selector(toggleTrackpadMode) }) { + toggleItem.title = active ? "Stop Auto-Scroll" : "Start/Stop Auto-Scroll" + } } func performScroll() { @@ -374,6 +440,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { } RunLoop.current.add(scrollTimer!, forMode: .common) + if let point = originalPoint { + showOriginIndicator(at: point) + } + updateAutoScrollActiveState(active: true) + // Show feedback to user showTrackpadModeNotification() } @@ -448,6 +519,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { scrollTimer?.invalidate() scrollTimer = nil NSCursor.unhide() + hideOriginIndicator() + if isAutoScrolling { + updateAutoScrollActiveState(active: false) + } isAutoScrolling = false isTrackpadMode = false originalPoint = nil @@ -501,6 +576,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @objc func toggleActivationIndicator() { + showActivationIndicator = !showActivationIndicator + + // Save preference + UserDefaults.standard.set(showActivationIndicator, forKey: "showActivationIndicator") + + // Update menu item state + if let menu = statusItem.menu, + let indicatorItem = menu.items.first(where: { $0.action == #selector(toggleActivationIndicator) }) { + indicatorItem.state = showActivationIndicator ? .on : .off + } + + // Apply immediately if auto-scroll is currently active + if isAutoScrolling { + if showActivationIndicator, let point = originalPoint { + showOriginIndicator(at: point) + } else { + hideOriginIndicator() + } + } + } + @objc func toggleDirectionInversion() { // Toggle the inversion state isDirectionInverted = !isDirectionInverted