aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift')
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift274
1 files changed, 274 insertions, 0 deletions
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift
new file mode 100644
index 0000000..39ce663
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewController: NSViewController {
+
+ enum LogColumn: String {
+ case time = "Time"
+ case logMessage = "LogMessage"
+
+ func createColumn() -> NSTableColumn {
+ return NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue))
+ }
+
+ func isRepresenting(tableColumn: NSTableColumn?) -> Bool {
+ return tableColumn?.identifier.rawValue == rawValue
+ }
+ }
+
+ let scrollView: NSScrollView = {
+ let scrollView = NSScrollView()
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = false
+ scrollView.borderType = .bezelBorder
+ return scrollView
+ }()
+
+ let tableView: NSTableView = {
+ let tableView = NSTableView()
+ let timeColumn = LogColumn.time.createColumn()
+ timeColumn.title = tr("macLogColumnTitleTime")
+ timeColumn.width = 160
+ timeColumn.resizingMask = []
+ tableView.addTableColumn(timeColumn)
+ let messageColumn = LogColumn.logMessage.createColumn()
+ messageColumn.title = tr("macLogColumnTitleLogMessage")
+ messageColumn.minWidth = 360
+ messageColumn.resizingMask = .autoresizingMask
+ tableView.addTableColumn(messageColumn)
+ tableView.rowSizeStyle = .custom
+ tableView.rowHeight = 16
+ tableView.usesAlternatingRowBackgroundColors = true
+ tableView.usesAutomaticRowHeights = true
+ tableView.allowsColumnReordering = false
+ tableView.allowsColumnResizing = true
+ tableView.allowsMultipleSelection = true
+ return tableView
+ }()
+
+ let progressIndicator: NSProgressIndicator = {
+ let progressIndicator = NSProgressIndicator()
+ progressIndicator.controlSize = .small
+ progressIndicator.isIndeterminate = true
+ progressIndicator.style = .spinning
+ progressIndicator.isDisplayedWhenStopped = false
+ return progressIndicator
+ }()
+
+ let closeButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macLogButtonTitleClose")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ let saveButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macLogButtonTitleSave")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ let logViewHelper: LogViewHelper?
+ var logEntries = [LogViewHelper.LogEntry]()
+ var isFetchingLogEntries = false
+ var isInScrolledToEndMode = true
+
+ private var updateLogEntriesTimer: Timer?
+
+ init() {
+ logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ closeButton.target = self
+ closeButton.action = #selector(closeClicked)
+
+ saveButton.target = self
+ saveButton.action = #selector(saveClicked)
+ saveButton.isEnabled = false
+
+ let clipView = NSClipView()
+ clipView.documentView = tableView
+ scrollView.contentView = clipView
+
+ _ = NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: clipView, queue: OperationQueue.main) { [weak self] _ in
+ guard let self = self else { return }
+ let lastVisibleRowIndex = self.tableView.row(at: NSPoint(x: 0, y: self.scrollView.contentView.documentVisibleRect.maxY - 1))
+ self.isInScrolledToEndMode = lastVisibleRowIndex < 0 || lastVisibleRowIndex == self.logEntries.count - 1
+ }
+
+ _ = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: tableView, queue: OperationQueue.main) { [weak self] _ in
+ guard let self = self else { return }
+ if self.isInScrolledToEndMode {
+ DispatchQueue.main.async {
+ self.tableView.scroll(NSPoint(x: 0, y: self.tableView.frame.maxY - clipView.documentVisibleRect.height))
+ }
+ }
+ }
+
+ let margin: CGFloat = 20
+ let internalSpacing: CGFloat = 10
+
+ let buttonRowStackView = NSStackView()
+ buttonRowStackView.addView(closeButton, in: .leading)
+ buttonRowStackView.addView(saveButton, in: .trailing)
+ buttonRowStackView.orientation = .horizontal
+ buttonRowStackView.spacing = internalSpacing
+
+ let containerView = NSView()
+ [scrollView, progressIndicator, buttonRowStackView].forEach { view in
+ containerView.addSubview(view)
+ view.translatesAutoresizingMaskIntoConstraints = false
+ }
+ NSLayoutConstraint.activate([
+ scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: margin),
+ scrollView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+ containerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: margin),
+ buttonRowStackView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: internalSpacing),
+ buttonRowStackView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+ containerView.rightAnchor.constraint(equalTo: buttonRowStackView.rightAnchor, constant: margin),
+ containerView.bottomAnchor.constraint(equalTo: buttonRowStackView.bottomAnchor, constant: margin),
+ progressIndicator.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
+ progressIndicator.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 640),
+ containerView.widthAnchor.constraint(lessThanOrEqualToConstant: 1200),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
+ ])
+
+ containerView.frame = NSRect(x: 0, y: 0, width: 640, height: 480)
+
+ view = containerView
+
+ progressIndicator.startAnimation(self)
+ startUpdatingLogEntries()
+ }
+
+ func updateLogEntries() {
+ guard !isFetchingLogEntries else { return }
+ isFetchingLogEntries = true
+ logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in
+ guard let self = self else { return }
+ defer {
+ self.isFetchingLogEntries = false
+ }
+ if !self.progressIndicator.isHidden {
+ self.progressIndicator.stopAnimation(self)
+ self.saveButton.isEnabled = true
+ }
+ guard !fetchedLogEntries.isEmpty else { return }
+ let oldCount = self.logEntries.count
+ self.logEntries.append(contentsOf: fetchedLogEntries)
+ self.tableView.insertRows(at: IndexSet(integersIn: oldCount ..< oldCount + fetchedLogEntries.count), withAnimation: .slideDown)
+ }
+ }
+
+ func startUpdatingLogEntries() {
+ updateLogEntries()
+ updateLogEntriesTimer?.invalidate()
+ let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.updateLogEntries()
+ }
+ updateLogEntriesTimer = timer
+ RunLoop.main.add(timer, forMode: .common)
+ }
+
+ func stopUpdatingLogEntries() {
+ updateLogEntriesTimer?.invalidate()
+ updateLogEntriesTimer = nil
+ }
+
+ override func viewWillAppear() {
+ view.window?.setFrameAutosaveName(NSWindow.FrameAutosaveName("LogWindow"))
+ }
+
+ override func viewWillDisappear() {
+ super.viewWillDisappear()
+ stopUpdatingLogEntries()
+ }
+
+ @objc func saveClicked() {
+ let savePanel = NSSavePanel()
+ savePanel.prompt = tr("macSheetButtonExportLog")
+ savePanel.nameFieldLabel = tr("macNameFieldExportLog")
+
+ let dateFormatter = ISO8601DateFormatter()
+ dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
+ let timeStampString = dateFormatter.string(from: Date())
+ savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
+
+ savePanel.beginSheetModal(for: self.view.window!) { [weak self] response in
+ guard response == .OK else { return }
+ guard let destinationURL = savePanel.url else { return }
+
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
+ guard isWritten else {
+ DispatchQueue.main.async { [weak self] in
+ ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
+ }
+ return
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.presentingViewController?.dismiss(self)
+ }
+ }
+
+ }
+ }
+
+ @objc func closeClicked() {
+ presentingViewController?.dismiss(self)
+ }
+
+ @objc func copy(_ sender: Any?) {
+ let text = tableView.selectedRowIndexes.sorted().reduce("") { $0 + self.logEntries[$1].text() + "\n" }
+ let pasteboard = NSPasteboard.general
+ pasteboard.clearContents()
+ pasteboard.writeObjects([text as NSString])
+ }
+}
+
+extension LogViewController: NSTableViewDataSource {
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return logEntries.count
+ }
+}
+
+extension LogViewController: NSTableViewDelegate {
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
+ let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
+ cell.text = logEntries[row].timestamp
+ return cell
+ } else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
+ let cell: LogViewMessageCell = tableView.dequeueReusableCell()
+ cell.text = logEntries[row].message
+ return cell
+ } else {
+ fatalError()
+ }
+ }
+}
+
+extension LogViewController {
+ override func cancelOperation(_ sender: Any?) {
+ closeClicked()
+ }
+}