Files
Mike Renoir d520880874 bugfixes
2023-11-30 17:56:07 +04:00

304 lines
11 KiB
Swift

//
// TooltipController.swift
// TGUIKit
//
// Created by Mikhail Filimonov on 04/04/2019.
// Copyright © 2019 Telegram. All rights reserved.
//
import Cocoa
import SwiftSignalKit
import AppKit
private final class TooltipView: View {
let textView = TextView()
private let textContainer = View()
let cornerView = ImageView()
var button: TextButton?
var didRemoveFromWindow:(()->Void)?
weak var view: NSView? {
didSet {
oldValue?.removeObserver(self, forKeyPath: "window")
if let view = view {
view.addObserver(self, forKeyPath: "window", options: .new, context: nil)
}
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if self.view?.window == nil {
self.didRemoveFromWindow?()
}
}
var cornerX: CGFloat? = nil {
didSet {
needsLayout = true
}
}
func move(corner cornerX: CGFloat, animated: Bool) {
self.cornerX = cornerX
let point: NSPoint
if frame.width > 44 {
point = NSMakePoint(max(min(cornerX - cornerView.frame.width / 2, frame.width - cornerView.frame.width - .cornerRadius), .cornerRadius), textContainer.frame.maxY)
} else {
let center = self.focus(cornerView.frame.size)
point = NSMakePoint(center.minX, textContainer.frame.maxY)
}
cornerView.change(pos: point, animated: animated)
}
required init(frame frameRect: NSRect) {
super.init(frame: frameRect)
textContainer.backgroundColor = .black
textContainer.addSubview(textView)
addSubview(textContainer)
addSubview(cornerView)
cornerView.animates = false
cornerView.image = generateImage(NSMakeSize(30, 10), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(NSColor.black.cgColor)
context.scaleBy(x: 0.333, y: 0.333)
let _ = try? drawSvgPath(context, path: "M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ")
context.fillPath()
})!
cornerView.sizeToFit()
textView.disableBackgroundDrawing = true
textView.isSelectable = false
textContainer.layer?.cornerRadius = .cornerRadius
}
func update(text: NSAttributedString, button: (String, ()->Void)?, maxWidth: CGFloat, interactions: TextViewInteractions, animated: Bool) {
if let buttonData = button {
let button: TextButton
if self.button == nil {
button = TextButton()
self.button = button
textContainer.addSubview(button)
} else {
button = self.button!
}
button.removeAllHandlers()
button.set(text: buttonData.0, for: .Normal)
button.set(font: .medium(.title), for: .Normal)
button.set(color: .accent, for: .Normal)
_ = button.sizeToFit()
button.set(handler: { _ in
buttonData.1()
}, for: .Click)
} else {
self.button?.removeFromSuperview()
self.button = nil
}
let layout = TextViewLayout(text, alignment: .center, alwaysStaticItems: true)
layout.measure(width: maxWidth - (self.button != nil ? self.button!.frame.width : 0))
textView.update(layout)
textContainer.change(size: NSMakeSize(max(40, layout.layoutSize.width + 18 + (self.button != nil ? self.button!.frame.width : 0)), max(layout.layoutSize.height + 8, button != nil ? 40 : 0)), animated: animated)
change(size: NSMakeSize(textContainer.frame.width, textContainer.frame.height + 14), animated: animated)
needsLayout = true
layout.interactions = interactions
}
override func layout() {
super.layout()
textContainer.centerX(y: 0)
if let button = button {
textView.centerY(x: 7)
button.centerY(x: textView.frame.maxX + 5)
} else {
textView.center()
}
if let cornerX = cornerX, frame.width > 44 {
cornerView.setFrameOrigin(max(min(cornerX - cornerView.frame.width / 2, frame.width - cornerView.frame.width - .cornerRadius), .cornerRadius), textContainer.frame.maxY)
} else {
cornerView.centerX(y: textContainer.frame.maxY)
}
}
deinit {
DispatchQueue.main.async {
removeShownAnimation = false
}
view?.removeObserver(self, forKeyPath: "window")
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TooltipController: ViewController {
}
private var removeShownAnimation:Bool = false
private let delayDisposable = MetaDisposable()
private var shouldRemoveTooltip: Bool = true
public func tooltip(for view: NSView, text: String, attributedText: NSAttributedString? = nil, interactions: TextViewInteractions = TextViewInteractions(), button: (String, ()->Void)? = nil, maxWidth: CGFloat = 350, autoCorner: Bool = true, offset: NSPoint = .zero, timeout: Double = 3.0, updateText: @escaping(@escaping(String)->Bool)->Void = { _ in }) -> Void {
guard let window = view.window as? Window else { return }
if view.visibleRect.height != view.frame.height {
// return
}
let tooltip: TooltipView
let isExists: Bool
if let exists = window.contentView?.subviews.first(where: { $0 is TooltipView }) as? TooltipView {
tooltip = exists
isExists = true
shouldRemoveTooltip = false
} else {
tooltip = TooltipView(frame: NSZeroRect)
isExists = false
shouldRemoveTooltip = (NSEvent.pressedMouseButtons & (1 << 0)) == 0
}
tooltip.view = view
window.contentView?.addSubview(tooltip)
let location = window.mouseLocationOutsideOfEventStream
let text = attributedText ?? NSAttributedString.initialize(string: text, color: .white, font: .medium(.text))
updateText() { [weak tooltip] text in
tooltip?.update(text: .initialize(string: text, color: .white, font: .medium(.text)), button: button, maxWidth: maxWidth, interactions: interactions, animated: false)
return tooltip != nil
}
tooltip.update(text: text, button: button, maxWidth: maxWidth, interactions: interactions, animated: isExists)
var removeTooltip:(Bool) -> Void = { _ in
}
let updatePosition:(Bool)->Void = { [weak tooltip, weak view, weak window] animated in
if let tooltip = tooltip, let view = view, let window = window {
var point = view.convert(NSZeroPoint, to: nil)
if window.contentView!.isFlipped {
point.y = window.contentView!.frame.height - point.y - view.frame.height / 2 - 10
}
point.y += offset.y
let pos = NSMakePoint(min(max(floorToScreenPixels(System.backingScale, point.x - (tooltip.frame.width - view.frame.width) / 2), 10), window.frame.width - tooltip.frame.width - 10), point.y)
// if view.visibleRect.height != view.frame.height {
// removeTooltip(true)
// } else {
tooltip.change(pos: pos, animated: isExists || animated)
// }
}
}
updatePosition(isExists)
if autoCorner {
let point = tooltip.convert(NSMakePoint(floorToScreenPixels(System.backingScale, view.frame.width / 2), 0), from: view)
tooltip.move(corner: point.x, animated: isExists)
} else {
let mousePoint = tooltip.convert(location, from: nil)
tooltip.move(corner: mousePoint.x, animated: isExists)
}
let scroll = view.enclosingScrollView as? TableView
scroll?.addScroll(listener: TableScrollListener.init(dispatchWhenVisibleRangeUpdated: false, { _ in
updatePosition(true)
}))
if !isExists && !removeShownAnimation {
CATransaction.begin()
tooltip.layer?.animateAlpha(from: 0.3, to: 1, duration: 0.2)
tooltip.layer?.animatePosition(from: NSMakePoint(tooltip.frame.minX, tooltip.frame.minY - 5), to: tooltip.frame.origin)
CATransaction.commit()
}
removeTooltip = { [weak tooltip] animated in
guard let tooltip = tooltip else { return }
if animated {
CATransaction.begin()
tooltip.layer?.animatePosition(from: tooltip.frame.origin, to: NSMakePoint(tooltip.frame.minX, tooltip.frame.minY - 5), removeOnCompletion: false)
//tooltip.change(pos: NSMakePoint(tooltip.frame.minX, tooltip.frame.minY - 5), animated: true)
tooltip.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak tooltip] _ in
if let tooltip = tooltip {
tooltip.removeFromSuperview()
window.removeAllHandlers(for: tooltip)
}
})
// tooltip.change(opacity: 0, true, removeOnCompletion: false, duration: 0.2, completion: { [weak tooltip] _ in
// if let tooltip = tooltip {
// tooltip.removeFromSuperview()
// window.removeAllHandlers(for: tooltip)
// }
// })
CATransaction.commit()
} else {
removeShownAnimation = true
tooltip.removeFromSuperview()
window.removeAllHandlers(for: tooltip)
}
}
tooltip.didRemoveFromWindow = {
removeTooltip(true)
}
delayDisposable.set((Signal<Never, NoError>.complete() |> delay(timeout, queue: .mainQueue())).start(completed: {
removeTooltip(true)
}))
window.set(mouseHandler: { _ -> KeyHandlerResult in
DispatchQueue.main.async {
if shouldRemoveTooltip {
removeTooltip(true)
}
shouldRemoveTooltip = !isExists
}
return .rejected
}, with: tooltip, for: .leftMouseUp, priority: .supreme)
window.set(mouseHandler: { _ -> KeyHandlerResult in
removeTooltip(false)
return .rejected
}, with: tooltip, for: .scrollWheel, priority: .supreme)
window.set(handler: { _ -> KeyHandlerResult in
removeTooltip(false)
return .rejected
}, with: tooltip, for: .All, priority: .supreme)
}
public func removeAllTooltips(_ window: Window) {
for subview in window.contentView!.subviews.reversed() {
if subview is TooltipView {
subview.removeFromSuperview()
}
}
}