Skip to content

Commit

Permalink
Merge pull request #235 from wakatime/feature/scroll-monitored-apps
Browse files Browse the repository at this point in the history
Make monitored apps scrollable
  • Loading branch information
alanhamlett authored Mar 25, 2024
2 parents 56e58cf + 6c76bd4 commit 38e7fb7
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 67 deletions.
202 changes: 136 additions & 66 deletions WakaTime/Views/MonitoredAppsView.swift
Original file line number Diff line number Diff line change
@@ -1,97 +1,167 @@
import AppKit

class MonitoredAppsView: NSView {
init() {
super.init(frame: .zero)

let stackView = NSStackView(frame: .zero)
stackView.orientation = .vertical
stackView.distribution = .equalSpacing
stackView.alignment = .leading
class MonitoredAppsView: NSView, NSOutlineViewDataSource, NSOutlineViewDelegate {
struct AppData: Equatable {
let bundleId: String
let icon: NSImage
let name: String
let tag: Int
}

addSubview(stackView)
private var outlineView: NSOutlineView!
private lazy var apps: [AppData] = {
var apps = [AppData]()
let bundleIds = MonitoredApp.allBundleIds.filter { !MonitoredApp.unsupportedAppIds.contains($0) }
var index = 0
for bundleId in bundleIds {
if let icon = AppInfo.getIcon(bundleId: bundleId),
let name = AppInfo.getAppName(bundleId: bundleId) {
apps.append(AppData(bundleId: bundleId, icon: icon, name: name, tag: index))
index += 1
}

let setAppBundleId = bundleId.appending("-setapp")
if let icon = AppInfo.getIcon(bundleId: setAppBundleId),
let name = AppInfo.getAppName(bundleId: setAppBundleId) {
apps.append(AppData(bundleId: setAppBundleId, icon: icon, name: name, tag: index))
index += 1
}
}
return apps
}()

stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32).isActive = true
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32).isActive = true
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 32).isActive = true
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32).isActive = true
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)

buildView(stackView: stackView)
setupOutlineView()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func buildView(stackView: NSStackView) {
for (index, bundleId) in MonitoredApp.allBundleIds.enumerated() {
guard !MonitoredApp.unsupportedAppIds.contains(bundleId) else { continue }
private func setupOutlineView() {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
outlineView = NSOutlineView()
outlineView.dataSource = self
outlineView.delegate = self

scrollView.documentView = outlineView
addSubview(scrollView)

scrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
])

let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("AppColumn"))
outlineView.addTableColumn(column)
outlineView.headerView = nil
outlineView.outlineTableColumn = column
outlineView.indentationPerLevel = 0.0
}

buildViewForApp(index: index * 2, stackView: stackView, bundleId: bundleId)
buildViewForApp(
index: (index * 2) + 1,
stackView: stackView,
bundleId: bundleId.appending("-setapp"))
}
func reloadData() {
outlineView.reloadData()
}

// MARK: NSOutlineViewDataSource

func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
apps.count
}

func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
false
}

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
apps[index]
}

stackView.addArrangedSubview(NSView())
// MARK: NSOutlineViewDelegate

func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
false
}

func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
50
}

func buildViewForApp(index: Int, stackView: NSStackView, bundleId: String) {
guard
let image = AppInfo.getIcon(bundleId: bundleId),
let appName = AppInfo.getAppName(bundleId: bundleId)
else { return }
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let appData = item as? AppData else { return nil }

let cellView = outlineView.makeView(
withIdentifier: NSUserInterfaceItemIdentifier("AppCell"),
owner: self
) as? NSTableCellView ?? NSTableCellView()

let currentStackView = NSStackView(frame: .zero)
currentStackView.orientation = .horizontal
currentStackView.distribution = .gravityAreas
// Clear existing subviews to prevent duplication
cellView.subviews.forEach { $0.removeFromSuperview() }

currentStackView.spacing = 32
let imageView = NSImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = appData.icon
imageView.image?.size = NSSize(width: 20, height: 20)

let imageView = NSImageView(frame: NSRect(x: 0, y: 0, width: 15, height: 15))
imageView.image = image
imageView.layer?.cornerRadius = imageView.frame.height / 2
currentStackView.addArrangedSubview(imageView)
currentStackView.setCustomSpacing(8, after: imageView)
let nameLabel = NSTextField(labelWithString: appData.name)
nameLabel.translatesAutoresizingMaskIntoConstraints = false

let nameLabel = NSTextField(labelWithString: appName)
nameLabel.alignment = .left
let isMonitored = MonitoringManager.isAppMonitored(for: appData.bundleId)
let switchControl = NSSwitch()
switchControl.state = MonitoringManager.isAppMonitored(for: bundleId) ? .on : .off
switchControl.translatesAutoresizingMaskIntoConstraints = false
switchControl.state = isMonitored ? .on : .off
switchControl.target = self
switchControl.tag = index
switchControl.action = #selector(switchToggled(_:))
switchControl.tag = appData.tag

currentStackView.addArrangedSubview(nameLabel)
currentStackView.addArrangedSubview(switchControl)
cellView.addSubview(imageView)
cellView.addSubview(nameLabel)
cellView.addSubview(switchControl)

stackView.addArrangedSubview(currentStackView)
currentStackView.translatesAutoresizingMaskIntoConstraints = false
currentStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
currentStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true
currentStackView.huggingPriority(for: .horizontal)
nameLabel.widthAnchor.constraint(equalTo: currentStackView.widthAnchor, multiplier: 0.7, constant: 0).isActive = true
// Determine if the current item is the last in the list
let isLastItem = apps.last == appData

let divider = NSView(frame: NSRect(x: 0, y: 0, width: stackView.frame.width, height: 1))
divider.wantsLayer = true
divider.layer?.backgroundColor = NSColor.darkGray.cgColor
divider.translatesAutoresizingMaskIntoConstraints = false
divider.heightAnchor.constraint(equalToConstant: 1).isActive = true
if !isLastItem {
let divider = NSView()
divider.translatesAutoresizingMaskIntoConstraints = false
divider.wantsLayer = true
divider.layer?.backgroundColor = NSColor.separatorColor.cgColor

stackView.addArrangedSubview(divider)
}

@objc func switchToggled(_ sender: NSSwitch) {
let isSetApp = !sender.tag.isMultiple(of: 2)
let index = (isSetApp ? sender.tag - 1 : sender.tag) / 2
var bundleId = MonitoredApp.allBundleIds[index]
cellView.addSubview(divider)

if isSetApp {
bundleId = bundleId.appending("-setapp")
NSLayoutConstraint.activate([
divider.heightAnchor.constraint(equalToConstant: 1),
divider.leadingAnchor.constraint(equalTo: cellView.leadingAnchor),
divider.trailingAnchor.constraint(equalTo: cellView.trailingAnchor),
divider.bottomAnchor.constraint(equalTo: cellView.bottomAnchor)
])
}

MonitoringManager.set(monitoringState: sender.state == .on ? .on : .off, for: bundleId)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 5),
imageView.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
imageView.widthAnchor.constraint(equalToConstant: 20),
imageView.heightAnchor.constraint(equalToConstant: 20),

nameLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
nameLabel.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
nameLabel.trailingAnchor.constraint(equalTo: switchControl.leadingAnchor, constant: -5),

switchControl.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -10),
switchControl.centerYAnchor.constraint(equalTo: cellView.centerYAnchor),
])

return cellView
}

@objc func switchToggled(_ sender: NSSwitch) {
let appData = apps[sender.tag]
MonitoringManager.set(monitoringState: sender.state == .on ? .on : .off, for: appData.bundleId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class MonitoredAppsWindowController: NSWindowController {
self.init(window: nil)

let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 150),
contentRect: NSRect(x: 0, y: 0, width: 400, height: 450),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
Expand All @@ -17,4 +17,9 @@ class MonitoredAppsWindowController: NSWindowController {
window.contentView = monitoredAppsView
self.window = window
}

override func showWindow(_ sender: Any?) {
monitoredAppsView.reloadData()
super.showWindow(sender)
}
}

0 comments on commit 38e7fb7

Please sign in to comment.