Abhishek
4 years ago
39 changed files with 481 additions and 98 deletions
@ -0,0 +1,53 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import XCTest |
||||||
|
|
||||||
|
class CopyToClipboardTests: XCTestCase { |
||||||
|
var app: XCUIApplication! |
||||||
|
|
||||||
|
override func setUp() { |
||||||
|
continueAfterFailure = false |
||||||
|
app = XCUIApplication() |
||||||
|
app.launch() |
||||||
|
|
||||||
|
if app.tables["FloatingTableView"].exists == false { |
||||||
|
app.tapMenubarIcon() |
||||||
|
app.buttons["FloatingPin"].click() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override func tearDownWithError() throws { |
||||||
|
app = nil |
||||||
|
} |
||||||
|
|
||||||
|
func testFullCopy() throws { |
||||||
|
let cellCount = app.tables["FloatingTableView"].cells.count |
||||||
|
var clipboardValue = String() |
||||||
|
for cellIndex in 0..<cellCount { |
||||||
|
let cell = app.tables["FloatingTableView"].cells.element(boundBy: cellIndex) |
||||||
|
let customLabel = cell.staticTexts["CustomNameLabelForCell"].value ?? "Nil Custom Label" |
||||||
|
let time = cell.staticTexts["ActualTime"].value ?? "Nil Value" |
||||||
|
clipboardValue.append(contentsOf: "\(customLabel) - \(time)\n") |
||||||
|
} |
||||||
|
|
||||||
|
app.buttons["Share"].click() |
||||||
|
app/*@START_MENU_TOKEN@*/.menuItems["Copy All Times"]/*[[".dialogs[\"Clocker Panel\"]",".buttons[\"Share\"]",".menus.menuItems[\"Copy All Times\"]",".menuItems[\"Copy All Times\"]"],[[[-1,3],[-1,2],[-1,1,2],[-1,0,1]],[[-1,3],[-1,2],[-1,1,2]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.click() |
||||||
|
|
||||||
|
let clipboard = NSPasteboard.general.string(forType: .string) |
||||||
|
XCTAssert(clipboardValue == clipboard) |
||||||
|
} |
||||||
|
|
||||||
|
func testIndividualTimezoneCopy() { |
||||||
|
let cell = app.tables["FloatingTableView"].cells.firstMatch |
||||||
|
let customLabel = cell.staticTexts["CustomNameLabelForCell"].value ?? "Nil Custom Label" |
||||||
|
let time = cell.staticTexts["ActualTime"].value ?? "Nil Value" |
||||||
|
let expectedValue = "\(customLabel) - \(time)" |
||||||
|
|
||||||
|
// Tap to copy! |
||||||
|
cell.tap() |
||||||
|
|
||||||
|
let clipboard = NSPasteboard.general.string(forType: .string) ?? "Empty Pasteboard" |
||||||
|
XCTAssert(expectedValue == clipboard, "Clipboard value (\(clipboard)) doesn't match expected result") |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,164 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Foundation |
||||||
|
import AppKit |
||||||
|
|
||||||
|
extension CGRect { |
||||||
|
static func center(of layer: CALayer) -> CGPoint { |
||||||
|
let parentSize = layer.frame.size |
||||||
|
return CGPoint(x: parentSize.width / 2, y: parentSize.height / 2) |
||||||
|
} |
||||||
|
static func center(of parent: NSView) -> CGPoint { |
||||||
|
let parentSize = parent.frame.size |
||||||
|
return CGPoint(x: parentSize.width / 2, y: parentSize.height / 6) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension String { |
||||||
|
func size(with fontSize: CGFloat) -> CGSize { |
||||||
|
let attr: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: fontSize)] |
||||||
|
let size = NSString(string: self).size(withAttributes: attr) |
||||||
|
return size |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fileprivate class HideAnimationDelegate: NSObject, CAAnimationDelegate { |
||||||
|
private weak var view: NSView? |
||||||
|
fileprivate init(view: NSView) { |
||||||
|
self.view = view |
||||||
|
} |
||||||
|
fileprivate static func delegate(forView NSView: NSView) -> CAAnimationDelegate { |
||||||
|
return HideAnimationDelegate(view: NSView) |
||||||
|
} |
||||||
|
fileprivate func animationDidStart(_ anim: CAAnimation) { |
||||||
|
view?.layer?.opacity = 0.0 |
||||||
|
} |
||||||
|
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { |
||||||
|
view?.removeFromSuperview() |
||||||
|
view = nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func hideAnimation(view: NSView, style: Style) { |
||||||
|
let anim = CABasicAnimation(keyPath: "opacity") |
||||||
|
let timing = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) |
||||||
|
anim.timingFunction = timing |
||||||
|
let currentLayerTime = view.layer?.convertTime(CACurrentMediaTime(), from: nil) |
||||||
|
anim.beginTime = currentLayerTime! + CFTimeInterval(style.fadeInOutDelay) |
||||||
|
anim.duration = CFTimeInterval(style.fadeInOutDuration) |
||||||
|
anim.fromValue = 1.0 |
||||||
|
anim.toValue = 0.0 |
||||||
|
anim.isRemovedOnCompletion = false |
||||||
|
anim.delegate = HideAnimationDelegate.delegate(forView: view) |
||||||
|
|
||||||
|
view.layer?.add(anim, forKey: "hide animation") |
||||||
|
} |
||||||
|
|
||||||
|
public protocol Style { |
||||||
|
var fontSize: CGFloat {get} |
||||||
|
var horizontalMargin: CGFloat {get} |
||||||
|
var verticalMargin: CGFloat {get} |
||||||
|
var cornerRadius: CGFloat {get} |
||||||
|
var font: NSFont {get} |
||||||
|
var backgroundColor: NSColor {get} |
||||||
|
var foregroundColor: NSColor {get} |
||||||
|
var fadeInOutDuration: CGFloat {get} |
||||||
|
var fadeInOutDelay: CGFloat {get} |
||||||
|
var labelOriginWithMargin: CGPoint {get} |
||||||
|
var activitySize: CGSize {get} |
||||||
|
} |
||||||
|
|
||||||
|
extension Style { |
||||||
|
public var labelOriginWithMargin: CGPoint { |
||||||
|
return CGPoint(x: horizontalMargin, y: verticalMargin) |
||||||
|
} |
||||||
|
public var fontSize: CGFloat {return 12} |
||||||
|
public var font: NSFont { |
||||||
|
if let avenirFont = NSFont(name: "Avenir-Light", size: fontSize) { |
||||||
|
return avenirFont |
||||||
|
} |
||||||
|
return NSFont.systemFont(ofSize: fontSize) |
||||||
|
} |
||||||
|
public var horizontalMargin: CGFloat {return 10} |
||||||
|
public var verticalMargin: CGFloat {return 5} |
||||||
|
public var cornerRadius: CGFloat {return 8} |
||||||
|
public var backgroundColor: NSColor {return .black} |
||||||
|
public var foregroundColor: NSColor {return .white} |
||||||
|
public var activitySize: CGSize {return CGSize(width: 100, height: 100)} |
||||||
|
public var fadeInOutDuration: CGFloat {return 1.0} |
||||||
|
public var fadeInOutDelay: CGFloat {return 1.0} |
||||||
|
} |
||||||
|
|
||||||
|
public struct DefaultStyle: Style { |
||||||
|
public static let shared = DefaultStyle() |
||||||
|
} |
||||||
|
|
||||||
|
private struct ToastKeys { |
||||||
|
static var ActiveToast = "TSToastActiveToastKey" |
||||||
|
} |
||||||
|
|
||||||
|
class ToastView: NSView { |
||||||
|
private let message: String |
||||||
|
private let labelSize: CGSize |
||||||
|
private let style: Style |
||||||
|
init(message: String) { |
||||||
|
self.message = message |
||||||
|
self.style = DefaultStyle() |
||||||
|
self.labelSize = message.size(with: style.fontSize) |
||||||
|
let size = CGSize( |
||||||
|
width: labelSize.width + style.horizontalMargin*2, |
||||||
|
height: labelSize.height + style.verticalMargin*2 |
||||||
|
) |
||||||
|
let rect = CGRect(origin: .zero, size: size) |
||||||
|
super.init(frame: rect) |
||||||
|
wantsLayer = true |
||||||
|
} |
||||||
|
required init?(coder aDecoder: NSCoder) { fatalError() } |
||||||
|
|
||||||
|
override func viewDidMoveToSuperview() { |
||||||
|
super.viewDidMoveToSuperview() |
||||||
|
if superview != nil { |
||||||
|
configure() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private func configure() { |
||||||
|
frame = superview?.bounds ?? NSRect.zero |
||||||
|
let rect = CGRect(origin: style.labelOriginWithMargin, size: labelSize) |
||||||
|
let sizeWithMargin = CGSize( |
||||||
|
width: rect.width + style.horizontalMargin*2, |
||||||
|
height: rect.height + style.verticalMargin*2 |
||||||
|
) |
||||||
|
let rectWithMargin = CGRect( |
||||||
|
origin: .zero, // position is manipulated later anyways |
||||||
|
size: sizeWithMargin |
||||||
|
) |
||||||
|
// outside Container |
||||||
|
let container = CALayer() |
||||||
|
container.frame = rectWithMargin |
||||||
|
container.position = CGRect.center(of: superview!) |
||||||
|
container.backgroundColor = style.backgroundColor.cgColor |
||||||
|
container.cornerRadius = style.cornerRadius |
||||||
|
layer?.addSublayer(container) |
||||||
|
// inside TextLayer |
||||||
|
let text = CATextLayer() |
||||||
|
text.frame = rect |
||||||
|
text.position = CGRect.center(of: container) |
||||||
|
text.string = message |
||||||
|
text.font = NSFont.systemFont(ofSize: style.fontSize) |
||||||
|
text.fontSize = style.fontSize |
||||||
|
text.alignmentMode = .center |
||||||
|
text.foregroundColor = style.foregroundColor.cgColor |
||||||
|
text.backgroundColor = style.backgroundColor.cgColor |
||||||
|
text.contentsScale = layer?.contentsScale ?? 0 // For Retina Display |
||||||
|
container.addSublayer(text) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension NSView { |
||||||
|
public func makeToast(_ message: String) { |
||||||
|
let toast = ToastView(message: message) |
||||||
|
self.addSubview(toast) |
||||||
|
hideAnimation(view: toast, style: DefaultStyle.shared) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue