// // ProgressModal.swift // TGUIKit // // Created by keepcoder on 09/11/2016. // Copyright © 2016 Telegram. All rights reserved. // import Cocoa import SwiftSignalKit import AppKit import ColorPalette private final class ProgressModalView : NSVisualEffectView { private let progressView = ProgressIndicator(frame: NSMakeRect(0, 0, 32, 32)) override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true self.layer?.cornerRadius = 10.0 self.autoresizingMask = [] self.autoresizesSubviews = false self.addSubview(self.progressView) self.material = presentation.colors.isDark ? .dark : .light self.blendingMode = .withinWindow self.state = .active } override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(NSMakeSize(80, 80)) } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { super.layout() self.progressView.center() self.center() } } class ProgressModalController: ModalViewController { private var progressView:ProgressIndicator? override var background: NSColor { return .clear } override var contentBelowBackground: Bool { return true } override func becomeFirstResponder() -> Bool? { return nil } override func close(animationType: ModalAnimationCloseBehaviour = .common) { super.close(animationType: animationType) disposable.dispose() } override var containerBackground: NSColor { return .clear } override func viewClass() -> AnyClass { return ProgressModalView.self } override func viewDidResized(_ size: NSSize) { super.viewDidResized(size) } override func viewDidLoad() { super.viewDidLoad() readyOnce() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } deinit { disposable.dispose() } fileprivate let disposable: Disposable init(_ disposable: Disposable) { self.disposable = disposable super.init(frame:NSMakeRect(0,0,80,80)) self.bar = .init(height: 0) } } private final class SuccessModalView : NSVisualEffectView { private let imageView:ImageView = ImageView() private let textView: TextView = TextView() required override init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(textView) addSubview(imageView) self.wantsLayer = true self.layer?.cornerRadius = 10.0 self.autoresizingMask = [] self.autoresizesSubviews = false self.material = presentation.colors.isDark ? .dark : .light self.blendingMode = .withinWindow } override func layout() { super.layout() if !textView.isHidden { imageView.centerY(x: 20) textView.centerY(x: imageView.frame.maxX + 20) } else { imageView.center() } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateIcon(icon: CGImage, text: TextViewLayout?) { imageView.image = icon imageView.sizeToFit() textView.isSelectable = false textView.isHidden = text == nil textView.update(text) needsLayout = true } } class SuccessModalController : ModalViewController { override var background: NSColor { return .clear } override var contentBelowBackground: Bool { return true } override var containerBackground: NSColor { return .clear } override func viewClass() -> AnyClass { return SuccessModalView.self } private var genericView: SuccessModalView { return self.view as! SuccessModalView } override var redirectMouseAfterClosing: Bool { return true } override func viewDidResized(_ size: NSSize) { super.viewDidResized(size) } override func viewDidLoad() { super.viewDidLoad() genericView.updateIcon(icon: icon, text: text) readyOnce() } // override var handleEvents: Bool { // return false // } // override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } private let icon: CGImage private let text: TextViewLayout? private let _backgroundColor: NSColor init(_ icon: CGImage, text: TextViewLayout? = nil, background: NSColor) { self.icon = icon self.text = text self._backgroundColor = background super.init(frame:NSMakeRect(0, 0, text != nil ? 100 + text!.layoutSize.width : 80, text != nil ? max(icon.backingSize.height + 20, text!.layoutSize.height + 20) : 80)) self.bar = .init(height: 0) } } public func showModalProgress(signal:Signal, for window:Window, disposeAfterComplete: Bool = true) -> Signal { return Signal { subscriber in var signal = signal |> deliverOnMainQueue let beforeDisposable:DisposableSet = DisposableSet() let modal = ProgressModalController(beforeDisposable) let beforeModal:Signal = .single(Void()) |> delay(0.25, queue: Queue.mainQueue()) beforeDisposable.add(beforeModal.startStandalone(completed: { showModal(with: modal, for: window, animationType: .scaleCenter) })) signal = signal |> afterDisposed { modal.close() } beforeDisposable.add(signal.start(next: { next in subscriber.putNext(next) }, error: { error in subscriber.putError(error) if disposeAfterComplete { beforeDisposable.dispose() } modal.close() }, completed: { subscriber.putCompletion() if disposeAfterComplete { beforeDisposable.dispose() } modal.close() })) return beforeDisposable } } public func showModalSuccess(for window: Window, icon: CGImage, text: TextViewLayout? = nil, background: NSColor = presentation.colors.background, delay _delay: Double) -> Signal { let modal = SuccessModalController(icon, text: text, background: background) showModal(with: modal, for: window, animationType: .scaleCenter) return Signal({ [weak modal] _ -> Disposable in return ActionDisposable { modal?.close() } }) |> timeout(_delay, queue: Queue.mainQueue(), alternate: Signal({ [weak modal] _ -> Disposable in modal?.close() return EmptyDisposable })) } private final class TextAndLabelModalView : View { private let textView: TextView = TextView() private var titleView: TextView? private let visualEffectView = NSVisualEffectView(frame: NSZeroRect) private let overlay = Control() private var button: TextButton? required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.visualEffectView.material = .ultraDark self.visualEffectView.blendingMode = .withinWindow self.visualEffectView.state = .active self.visualEffectView.wantsLayer = true addSubview(self.visualEffectView) //self.backgroundColor = NSColor.black.withAlphaComponent(0.9) self.textView.disableBackgroundDrawing = true self.textView.isSelectable = false self.textView.userInteractionEnabled = true addSubview(self.textView) layer?.cornerRadius = 10 addSubview(overlay) self.textView.set(handler: { [weak self] _ in self?.button?.send(event: .Down) }, for: .Down) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) } func update(text: String, title: String?, button: String?, callback: ((String)->Void)?, maxSize: NSSize) -> NSSize { if let title = title { self.titleView = TextView() addSubview(self.titleView!) self.titleView?.disableBackgroundDrawing = true self.titleView?.isSelectable = false self.titleView?.userInteractionEnabled = false let titleLayout = TextViewLayout(.initialize(string: title, color: .white, font: .medium(.title)), maximumNumberOfLines: 1) titleLayout.measure(width: min(380, maxSize.width - 120)) self.titleView?.update(titleLayout) } let attr = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: .white), bold: MarkdownAttributeSet(font: .bold(.text), textColor: .white), link: MarkdownAttributeSet(font: .normal(.title), textColor: nightAccentPalette.link), linkAttribute: { contents in return (NSAttributedString.Key.link.rawValue, contents) })).mutableCopy() as! NSMutableAttributedString attr.detectBoldColorInString(with: .medium(.title)) let textLayout = TextViewLayout(attr) textLayout.interactions = .init(processURL: { contents in if let string = contents as? String { callback?(string) } }) overlay.set(handler: { _ in callback?("") }, for: .Down) textLayout.measure(width: min(400, maxSize.width - 80)) self.textView.update(textLayout) if let button = button { let current: TextButton if let view = self.button { current = view } else { current = TextButton() current.scaleOnClick = true self.button = current addSubview(current) current.set(handler: { _ in callback?("action") }, for: .Down) } current.set(font: .medium(.header), for: .Normal) current.set(color: nightAccentPalette.link, for: .Normal) current.set(text: button, for: .Normal) current.sizeToFit(NSMakeSize(10, 0)) addSubview(current) } else if let view = self.button { performSubviewRemoval(view, animated: false) self.button = nil } var size: NSSize = .zero if let titleView = self.titleView { size.width = max(textView.frame.width, titleView.frame.width) + 20 size.height = textView.frame.height + titleView.frame.height + 5 + 20 } else { size.width = textView.frame.width + 20 size.height = textView.frame.height + 20 } if let button = self.button { size.width += button.frame.width } return size } override func layout() { super.layout() overlay.frame = bounds visualEffectView.frame = bounds if let titleView = titleView { titleView.setFrameOrigin(NSMakePoint(10, 10)) textView.setFrameOrigin(NSMakePoint(10, titleView.frame.maxY + 5)) } else { textView.centerY(x: 10) } if let button = button { button.centerY(x: frame.width - button.frame.width - 10) } } } class TextAndLabelModalController: ModalViewController { override var background: NSColor { return .clear } override var redirectMouseAfterClosing: Bool { return true } override func becomeFirstResponder() -> Bool? { return nil } override var contentBelowBackground: Bool { return true } override var containerBackground: NSColor { return .clear } override func viewClass() -> AnyClass { return TextAndLabelModalView.self } override func viewDidResized(_ size: NSSize) { super.viewDidResized(size) } private var genericView: TextAndLabelModalView { return self.view as! TextAndLabelModalView } override func viewDidLoad() { super.viewDidLoad() let size = self.genericView.update(text: text, title: title, button: button, callback: self.callback, maxSize: windowSize) self.modal?.resize(with: size, animated: false) readyOnce() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override var responderPriority: HandlerPriority { return .modal } override var redirectUserInterfaceCalls: Bool { return true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override var canBecomeResponder: Bool { return false } override func firstResponder() -> NSResponder? { return nil } deinit { } private let text: String private let title: String? private let button: String? private let windowSize: NSSize private let callback:((String)->Void)? init(text: String, title: String?, button: String?, callback:((String)->Void)?, windowSize: NSSize) { self.text = text self.windowSize = windowSize self.title = title self.callback = callback self.button = button super.init(frame:NSMakeRect(0, 0, 80, 80)) self.bar = .init(height: 0) } } public func showModalText(for window: Window, text: String, title: String? = nil, button: String? = nil, callback:((String)->Void)? = nil) { let modal = TextAndLabelModalController(text: text, title: title, button: button, callback: callback, windowSize: window.frame.size) showModal(with: modal, for: window, animationType: .scaleCenter) let words = (text + " " + (title ?? "")).trimmingCharacters(in: CharacterSet.alphanumerics.inverted) let msPerChar: TimeInterval = 60 / 180 / 6 let showTime = max(min(msPerChar * TimeInterval(words.length), 10), 5) let signal = Signal({ _ -> Disposable in return ActionDisposable { } }) |> timeout(showTime, queue: Queue.mainQueue(), alternate: Signal({ [weak modal] _ -> Disposable in modal?.close() return EmptyDisposable })) _ = signal.start() }