Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
97 changes: 97 additions & 0 deletions Scrollapp/ScrollappApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down