| // Copyright 2016 The Tulsi Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import Cocoa |
| import TulsiGenerator |
| |
| |
| /// NSTableView that posts a notification when live resizing completes. |
| final class MessageTableView: NSTableView { |
| override func viewDidEndLiveResize() { |
| super.viewDidEndLiveResize() |
| |
| // Give the delegate a chance to handle the resize now that the live operation is completed. |
| NotificationCenter.default.post(name: NSTableView.columnDidResizeNotification, |
| object: self) |
| } |
| } |
| |
| |
| /// View controller for the message output area in the Tulsi wizard. |
| final class MessageViewController: NSViewController, NSTableViewDelegate, NSUserInterfaceValidations { |
| let minRowHeight = CGFloat(16.0) |
| |
| @IBOutlet var messageArrayController: NSArrayController! |
| @IBOutlet weak var messageAreaScrollView: NSScrollView! |
| |
| // Display heights of each row in the message table. |
| var rowHeights = [Int: CGFloat]() |
| |
| @objc dynamic var messageCount: Int = 0 { |
| didSet { |
| // Assume that a reduction in the message count means all cached heights are invalid. |
| if messageCount < oldValue { |
| rowHeights.removeAll(keepingCapacity: true) |
| } |
| scrollToNewRowIfAtBottom() |
| } |
| } |
| |
| override func loadView() { |
| ValueTransformer.setValueTransformer(MessageTypeToImageValueTransformer(), |
| forName: NSValueTransformerName(rawValue: "MessageTypeToImageValueTransformer")) |
| super.loadView() |
| bind(NSBindingName(rawValue: "messageCount"), to: messageArrayController, withKeyPath: "arrangedObjects.@count", options: nil) |
| } |
| |
| @IBAction func copy(_ sender: AnyObject?) { |
| guard let selectedItems = messageArrayController.selectedObjects as? [NSPasteboardWriting], !selectedItems.isEmpty else { |
| return |
| } |
| |
| let pasteboard = NSPasteboard.general |
| pasteboard.clearContents() |
| pasteboard.writeObjects(selectedItems) |
| } |
| |
| @IBAction func clearMessages(_ sender: AnyObject?) { |
| (self.representedObject as! TulsiProjectDocument).clearMessages() |
| } |
| |
| // MARK: - NSUserInterfaceValidations |
| |
| func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { |
| if item.action == #selector(copy(_:)) { |
| return !messageArrayController.selectedObjects.isEmpty |
| } |
| return false |
| } |
| |
| // MARK: - NSTableViewDelegate |
| |
| func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { |
| if let height = rowHeights[row] { |
| return height |
| } |
| let message = (messageArrayController.arrangedObjects as! [UIMessage])[row] |
| let column = tableView.tableColumns.first! |
| let cell = column.dataCell as! NSTextFieldCell |
| cell.stringValue = message.text |
| let bounds = CGRect(x: 0, y: 0, width: column.width, height: CGFloat.greatestFiniteMagnitude) |
| let requiredSize = cell.cellSize(forBounds: bounds) |
| let height = max(requiredSize.height, minRowHeight) |
| rowHeights[row] = height |
| return height |
| } |
| |
| func tableViewColumnDidResize(_ notification: Notification) { |
| guard let tableView = notification.object as? NSTableView else { return } |
| // Wait until resizing completes before doing a lot of work. |
| if tableView.inLiveResize { |
| return |
| } |
| // Disable animation. |
| NSAnimationContext.beginGrouping() |
| NSAnimationContext.current.duration = 0 |
| rowHeights.removeAll(keepingCapacity: true) |
| let numRows = (messageArrayController.arrangedObjects as AnyObject).count! |
| let allRowsIndex = IndexSet(integersIn: 0..<numRows) |
| |
| tableView.noteHeightOfRows(withIndexesChanged: allRowsIndex) |
| NSAnimationContext.endGrouping() |
| } |
| |
| // MARK: - Private methods |
| |
| private func scrollToNewRowIfAtBottom() { |
| guard messageCount > 0, |
| let tableView = messageAreaScrollView.documentView as? NSTableView else { |
| return |
| } |
| |
| let lastRowIndex = messageCount - 1 |
| |
| let scrollContentViewBounds = messageAreaScrollView.contentView.bounds |
| let contentViewHeight = scrollContentViewBounds.height |
| |
| let newRowHeight = self.tableView(tableView, heightOfRow: lastRowIndex) + tableView.intercellSpacing.height |
| let bottomScrollY = tableView.frame.maxY - (contentViewHeight + newRowHeight) |
| |
| if scrollContentViewBounds.origin.y >= bottomScrollY { |
| tableView.scrollRowToVisible(lastRowIndex) |
| } |
| } |
| } |
| |
| |
| /// Transformer that converts a UIMessage type into an image to be displayed in the message view. |
| final class MessageTypeToImageValueTransformer : ValueTransformer { |
| override class func transformedValueClass() -> AnyClass { |
| return NSString.self |
| } |
| |
| override class func allowsReverseTransformation() -> Bool { |
| return false |
| } |
| |
| override func transformedValue(_ value: Any?) -> Any? { |
| guard let intValue = value as? Int, |
| let messageType = TulsiGenerator.LogMessagePriority(rawValue: intValue) else { |
| return nil |
| } |
| |
| switch messageType { |
| case .info, .debug, .syslog: |
| return NSImage(named: NSImage.Name(rawValue: "message_info")) |
| case .warning: |
| return NSImage(named: NSImage.Name(rawValue: "message_warning")) |
| case .error: |
| return NSImage(named: NSImage.Name(rawValue: "message_error")) |
| } |
| } |
| } |