3535 lines
126 KiB
Swift
3535 lines
126 KiB
Swift
//
|
|
// TableView.swift
|
|
// TGUIKit
|
|
//
|
|
// Created by keepcoder on 07/09/16.
|
|
// Copyright © 2016 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import SwiftSignalKit
|
|
import AVFoundation
|
|
|
|
public enum TableSeparator {
|
|
case bottom;
|
|
case top;
|
|
case right;
|
|
case left;
|
|
case none;
|
|
}
|
|
|
|
struct RevealAction {
|
|
|
|
|
|
}
|
|
|
|
private let duration: Double = 0.2
|
|
|
|
public class RowAnimateView: View {
|
|
public var stableId:AnyHashable?
|
|
}
|
|
|
|
public final class RevealTableItemController : ViewController {
|
|
public let item: TableRowItem
|
|
public init(item: TableRowItem) {
|
|
self.item = item
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
public protocol RevealTableView {
|
|
var additionalRevealDelta: CGFloat { get }
|
|
var containerX: CGFloat { get }
|
|
var rightRevealWidth: CGFloat { get }
|
|
var leftRevealWidth: CGFloat { get }
|
|
var endRevealState:SwipeDirection? { get set }
|
|
|
|
var width: CGFloat { get }
|
|
|
|
func initRevealState()
|
|
|
|
func moveReveal(delta: CGFloat)
|
|
func completeReveal(direction: SwipeDirection)
|
|
}
|
|
|
|
public enum TableBackgroundMode: Equatable {
|
|
case plain
|
|
case color(color: NSColor)
|
|
case gradient(colors: [NSColor], rotation: Int32?)
|
|
case background(image: NSImage, intensity: Int32?, colors: [NSColor]?, rotation: Int32?)
|
|
case tiled(image: NSImage)
|
|
public var hasWallpaper: Bool {
|
|
switch self {
|
|
case .plain:
|
|
return false
|
|
case .color:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
public class TableResortController {
|
|
fileprivate let startTimeout: Double
|
|
fileprivate var resortRow: Int?
|
|
internal fileprivate(set) var resortView: TableRowView? {
|
|
didSet {
|
|
if resortView == nil {
|
|
oldValue?.isResorting = false
|
|
} else {
|
|
resortView?.isResorting = true
|
|
}
|
|
}
|
|
}
|
|
fileprivate var inResorting: Bool = false
|
|
fileprivate var startLocation: NSPoint = NSZeroPoint
|
|
fileprivate var startRowLocation: NSPoint = NSZeroPoint
|
|
|
|
fileprivate var currentHoleIndex: Int?
|
|
fileprivate var prevHoleIndex: Int?
|
|
|
|
public var resortRange: NSRange
|
|
fileprivate let start:(Int)->Void
|
|
fileprivate let resort:(Int)->Void
|
|
fileprivate let complete:(Int, Int)->Void
|
|
fileprivate let updateItems:(TableRowView?, [TableRowItem])->Void
|
|
public init(resortRange: NSRange, startTimeout: Double = 0.3, start:@escaping(Int)->Void, resort:@escaping(Int)->Void, complete:@escaping(Int, Int)->Void, updateItems:@escaping(TableRowView?, [TableRowItem])->Void = { _, _ in }) {
|
|
self.resortRange = resortRange
|
|
self.startTimeout = startTimeout
|
|
self.start = start
|
|
self.resort = resort
|
|
self.complete = complete
|
|
self.updateItems = updateItems
|
|
}
|
|
|
|
func clear() {
|
|
resortView = nil
|
|
resortRow = nil
|
|
startLocation = NSZeroPoint
|
|
startRowLocation = NSZeroPoint
|
|
}
|
|
|
|
var isResorting: Bool {
|
|
return resortView != nil
|
|
}
|
|
|
|
func canResort(_ row: Int) -> Bool {
|
|
return resortRange.indexIn(row)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public class UpdateTransition<T> {
|
|
public let inserted:[(Int,T)]
|
|
public let updated:[(Int,T)]
|
|
public let deleted:[Int]
|
|
public let animateVisibleOnly: Bool
|
|
public init(deleted:[Int], inserted:[(Int,T)], updated:[(Int,T)], animateVisibleOnly: Bool = true) {
|
|
self.inserted = inserted
|
|
self.updated = updated
|
|
self.deleted = deleted
|
|
self.animateVisibleOnly = animateVisibleOnly
|
|
}
|
|
|
|
deinit {
|
|
var bp:Int = 0
|
|
bp += 1
|
|
}
|
|
|
|
public var isEmpty:Bool {
|
|
return inserted.isEmpty && updated.isEmpty && deleted.isEmpty
|
|
}
|
|
|
|
public var description: String {
|
|
return "inserted: \(inserted.count), updated:\(updated.count), deleted:\(deleted.count)"
|
|
}
|
|
}
|
|
public struct TableSearchVisibleData {
|
|
let cancelImage: CGImage?
|
|
let cancel:()->Void
|
|
let updateState: (SearchState?)->Void
|
|
public init(cancelImage: CGImage? = nil, cancel: @escaping()->Void, updateState: @escaping(SearchState?)->Void) {
|
|
self.cancelImage = cancelImage
|
|
self.cancel = cancel
|
|
self.updateState = updateState
|
|
}
|
|
}
|
|
|
|
public enum TableSearchViewState : Equatable {
|
|
case none((SearchState?)->Void)
|
|
case visible(TableSearchVisibleData)
|
|
|
|
public static func ==(lhs: TableSearchViewState, rhs: TableSearchViewState) -> Bool {
|
|
switch lhs {
|
|
case .none:
|
|
if case .none = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .visible:
|
|
if case .visible = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public class TableUpdateTransition : UpdateTransition<TableRowItem> {
|
|
public let state:TableScrollState
|
|
public let animated:Bool
|
|
public let grouping:Bool
|
|
public let searchState: TableSearchViewState?
|
|
public let isPartOfTransition: Bool
|
|
public let isOnMainQueue: Bool
|
|
fileprivate let uniqueId = arc4random64()
|
|
public let groupInOne: Bool
|
|
public init(deleted:[Int], inserted:[(Int,TableRowItem)], updated:[(Int,TableRowItem)], animated:Bool = false, state:TableScrollState = .none(nil), grouping:Bool = true, animateVisibleOnly: Bool = false, searchState: TableSearchViewState? = nil, isPartOfTransition: Bool = false, groupInOne: Bool = false) {
|
|
self.animated = animated
|
|
self.state = state
|
|
self.grouping = grouping
|
|
self.searchState = searchState
|
|
self.isPartOfTransition = isPartOfTransition
|
|
self.isOnMainQueue = Queue.mainQueue().isCurrent()
|
|
self.groupInOne = groupInOne
|
|
super.init(deleted: deleted, inserted: inserted, updated: updated, animateVisibleOnly: animateVisibleOnly)
|
|
}
|
|
public override var description: String {
|
|
return "inserted: \(inserted.count), updated:\(updated.count), deleted:\(deleted.count), state: \(state), animated: \(animated)"
|
|
}
|
|
|
|
public func withUpdatedState(_ state: TableScrollState) -> TableUpdateTransition {
|
|
return .init(deleted: self.deleted, inserted: self.inserted, updated: self.updated, animated: self.animated, state: state, grouping: self.grouping, animateVisibleOnly: self.animateVisibleOnly, searchState: self.searchState)
|
|
}
|
|
|
|
public static var Empty: TableUpdateTransition {
|
|
return .init(deleted: [], inserted: [], updated: [])
|
|
}
|
|
deinit {
|
|
var bp:Int = 0
|
|
bp += 1
|
|
}
|
|
}
|
|
|
|
public final class TableEntriesTransition<T> : TableUpdateTransition {
|
|
public let entries:T
|
|
public init(deleted:[Int], inserted:[(Int,TableRowItem)], updated:[(Int,TableRowItem)], entries:T, animated:Bool = false, state:TableScrollState = .none(nil), grouping:Bool = true) {
|
|
self.entries = entries
|
|
super.init(deleted: deleted, inserted: inserted, updated: updated, animated:animated, state: state, grouping:grouping)
|
|
}
|
|
}
|
|
|
|
public protocol TableViewDelegate : AnyObject {
|
|
|
|
func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void;
|
|
func selectionWillChange(row:Int, item:TableRowItem, byClick:Bool) -> Bool;
|
|
func isSelectable(row:Int, item:TableRowItem) -> Bool;
|
|
|
|
func findGroupStableId(for stableId: AnyHashable) -> AnyHashable?
|
|
|
|
func longSelect(row:Int, item:TableRowItem) -> Void
|
|
}
|
|
|
|
public extension TableViewDelegate {
|
|
func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? {
|
|
return nil
|
|
}
|
|
func longSelect(row:Int, item:TableRowItem) -> Void {
|
|
|
|
}
|
|
}
|
|
|
|
public enum TableSavingSide : Equatable {
|
|
case lower
|
|
case upper
|
|
case aroundIndex(AnyHashable)
|
|
}
|
|
|
|
public func ==(lhs: TableSavingSide, rhs: TableSavingSide) -> Bool {
|
|
switch lhs {
|
|
case .lower:
|
|
if case .lower = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .upper:
|
|
if case .upper = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .aroundIndex(id):
|
|
if case .aroundIndex(id) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct TableScrollFocus : Equatable {
|
|
public static func == (lhs: TableScrollFocus, rhs: TableScrollFocus) -> Bool {
|
|
return lhs.focus == rhs.focus
|
|
}
|
|
let focus:Bool
|
|
let string: String?
|
|
let action:((NSView)->Void)?
|
|
public init(focus: Bool, string: String? = nil, action: ((NSView)->Void)? = nil) {
|
|
self.focus = focus
|
|
self.action = action
|
|
self.string = string
|
|
}
|
|
|
|
|
|
}
|
|
|
|
public enum TableScrollState :Equatable {
|
|
case top(id: AnyHashable, innerId: AnyHashable?, animated: Bool, focus: TableScrollFocus, inset: CGFloat); // stableId, animated, focus, inset
|
|
case bottom(id: AnyHashable, innerId: AnyHashable?, animated: Bool, focus: TableScrollFocus, inset: CGFloat); // stableId, animated, focus, inset
|
|
case center(id: AnyHashable, innerId: AnyHashable?, animated: Bool, focus: TableScrollFocus, inset: CGFloat); // stableId, animated, focus, inset
|
|
case saveVisible(TableSavingSide)
|
|
case none(TableAnimationInterface?);
|
|
case down(Bool);
|
|
case up(Bool);
|
|
case upOffset(Bool, CGFloat);
|
|
|
|
public static var CenterEmpty: TableScrollState {
|
|
return .center(id: 0, innerId: nil, animated: true, focus: .init(focus: true), inset: 0)
|
|
}
|
|
public static func CenterEmptyAction(_ action: @escaping (NSView)->Void)-> TableScrollState {
|
|
return .center(id: 0, innerId: nil, animated: true, focus: .init(focus: true, action: action), inset: 0)
|
|
}
|
|
public static func CenterActionEmpty(_ f:@escaping(NSView)->Void) -> TableScrollState {
|
|
return .center(id: 0, innerId: nil, animated: true, focus: .init(focus: true, action: f), inset: 0)
|
|
}
|
|
}
|
|
|
|
public extension TableScrollState {
|
|
|
|
func swap(to stableId:AnyHashable, innerId: AnyHashable? = nil) -> TableScrollState {
|
|
switch self {
|
|
case let .top(_, _, animated, focus, inset):
|
|
return .top(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: inset)
|
|
case let .bottom(_, _, animated, focus, inset):
|
|
return .bottom(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: inset)
|
|
case let .center(_, _, animated, focus, inset):
|
|
return .center(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: inset)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
|
|
func focus(action: @escaping(NSView)->Void) -> TableScrollState {
|
|
switch self {
|
|
case let .top(id, innerId, animated, focus, inset):
|
|
return .top(id: id, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: focus.string, action: action), inset: inset)
|
|
case let .bottom(id, innerId, animated, focus, inset):
|
|
return .bottom(id: id, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: focus.string, action: action), inset: inset)
|
|
case let .center(id, innerId, animated, focus, inset):
|
|
return .center(id: id, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: focus.string, action: action), inset: inset)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
|
|
func withoutFocus() -> TableScrollState {
|
|
switch self {
|
|
case let .top(id, innerId, animated, focus, inset):
|
|
return .top(id: id, innerId: innerId, animated: animated, focus: .init(focus: false), inset: inset)
|
|
case let .bottom(id, innerId, animated, focus, inset):
|
|
return .bottom(id: id, innerId: innerId, animated: animated, focus: .init(focus: false), inset: inset)
|
|
case let .center(id, innerId, animated, focus, inset):
|
|
return .center(id: id, innerId: innerId, animated: animated, focus: .init(focus: false), inset: inset)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
|
|
var action: ((NSView)->Void)? {
|
|
switch self {
|
|
case let .top(_, _, _, focus, _):
|
|
return focus.action
|
|
case let .bottom(_, _, _, focus, _):
|
|
return focus.action
|
|
case let .center(_, _, _, focus, _):
|
|
return focus.action
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func text(string: String?) -> TableScrollState {
|
|
switch self {
|
|
case let .top(stableId, innerId, animated, focus, inset):
|
|
return .top(id: stableId, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: string, action: focus.action), inset: inset)
|
|
case let .bottom(stableId, innerId, animated, focus, inset):
|
|
return .bottom(id: stableId, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: string, action: focus.action), inset: inset)
|
|
case let .center(stableId, innerId, animated, focus, inset):
|
|
return .center(id: stableId, innerId: innerId, animated: animated, focus: .init(focus: focus.focus, string: string, action: focus.action), inset: inset)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
|
|
func offset(_ inset: CGFloat) -> TableScrollState {
|
|
switch self {
|
|
case let .top(stableId, innerId, animated, focus, v):
|
|
return .top(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: v + inset)
|
|
case let .bottom(stableId, innerId, animated, focus, v):
|
|
return .bottom(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: v + inset)
|
|
case let .center(stableId, innerId, animated, focus, v):
|
|
return .center(id: stableId, innerId: innerId, animated: animated, focus: focus, inset: v + inset)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
|
|
var isNone: Bool {
|
|
switch self {
|
|
case let .none(animation):
|
|
return animation != nil
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var animated: Bool {
|
|
switch self {
|
|
case let .top(_, _, animated, _, _):
|
|
return animated
|
|
case let .bottom(_, _, animated, _, _):
|
|
return animated
|
|
case let .center(_, _, animated, _, _):
|
|
return animated
|
|
case .down(let animated):
|
|
return animated
|
|
case .up(let animated):
|
|
return animated
|
|
case let .upOffset(animated, _):
|
|
return animated
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
protocol SelectDelegate : AnyObject {
|
|
func selectRow(index:Int) -> Void;
|
|
func longAction(index:Int) -> Void;
|
|
}
|
|
|
|
private final class TableSearchView : View {
|
|
let searchView = SearchView(frame: NSZeroRect)
|
|
private var cancelButton:ImageButton?
|
|
required init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
addSubview(searchView)
|
|
background = presentation.colors.background
|
|
border = [.Bottom]
|
|
searchView.frame = NSMakeRect(10, 10, frame.width - 20, frame.height - 20)
|
|
}
|
|
|
|
func applySearchResponder() {
|
|
// _ = window?.makeFirstResponder(searchView.input)
|
|
searchView.layout()
|
|
_ = window?.makeFirstResponder(searchView.input)
|
|
searchView.change(state: .Focus, false)
|
|
}
|
|
|
|
func updateDatas(_ datas: TableSearchVisibleData) {
|
|
if let cancelImage = datas.cancelImage {
|
|
if self.cancelButton == nil {
|
|
self.cancelButton = ImageButton()
|
|
self.addSubview(cancelButton!)
|
|
}
|
|
cancelButton!.removeAllHandlers()
|
|
cancelButton!.set(image: cancelImage, for: .Normal)
|
|
cancelButton!.set(handler: { _ in
|
|
datas.cancel()
|
|
}, for: .Click)
|
|
_ = cancelButton!.sizeToFit()
|
|
} else {
|
|
cancelButton?.removeFromSuperview()
|
|
cancelButton = nil
|
|
}
|
|
|
|
|
|
searchView.searchInteractions = SearchInteractions({ state, _ in
|
|
datas.updateState(state)
|
|
}, { state in
|
|
datas.updateState(state)
|
|
})
|
|
needsLayout = true
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
if let cancelButton = self.cancelButton {
|
|
cancelButton.centerY(x: frame.width - 10 - cancelButton.frame.width)
|
|
searchView.frame = NSMakeRect(10, 10, frame.width - cancelButton.frame.width - 30, frame.height - 20)
|
|
} else {
|
|
searchView.frame = NSMakeRect(10, 10, frame.width - 20, frame.height - 20)
|
|
}
|
|
}
|
|
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
class TGFlipableTableView : NSTableView, CALayerDelegate {
|
|
|
|
var bottomInset:CGFloat = 0
|
|
private let longDisposable = MetaDisposable()
|
|
public var flip:Bool = true
|
|
|
|
public weak var sdelegate:SelectDelegate?
|
|
weak var table:TableView?
|
|
var border:BorderType?
|
|
|
|
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
backgroundColor = .clear
|
|
self.autoresizesSubviews = false
|
|
usesAlternatingRowBackgroundColors = false
|
|
layerContentsRedrawPolicy = .never
|
|
usesAutomaticRowHeights = false
|
|
}
|
|
|
|
override func becomeFirstResponder() -> Bool {
|
|
return false
|
|
}
|
|
|
|
override func rect(ofRow row: Int) -> NSRect {
|
|
return super.rect(ofRow: row)
|
|
}
|
|
override func row(at point: NSPoint) -> Int {
|
|
return super.row(at: point)
|
|
}
|
|
override func rows(in rect: NSRect) -> NSRange {
|
|
return super.rows(in: rect)
|
|
}
|
|
|
|
override func isAccessibilityElement() -> Bool {
|
|
return false
|
|
}
|
|
override func accessibilityParent() -> Any? {
|
|
return nil
|
|
}
|
|
|
|
override public static var isCompatibleWithResponsiveScrolling: Bool {
|
|
return true
|
|
}
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
var _mouseDownCanMoveWindow: Bool = false
|
|
override var mouseDownCanMoveWindow: Bool {
|
|
return _mouseDownCanMoveWindow
|
|
}
|
|
|
|
override var isFlipped: Bool {
|
|
return flip
|
|
}
|
|
|
|
// override func draw(_ dirtyRect: NSRect) {
|
|
//
|
|
// }
|
|
override var isOpaque: Bool {
|
|
return false
|
|
}
|
|
|
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
return super.hitTest(point)
|
|
}
|
|
|
|
override func addSubview(_ view: NSView) {
|
|
super.addSubview(view)
|
|
}
|
|
|
|
override func setFrameSize(_ newSize: NSSize) {
|
|
super.setFrameSize(newSize)
|
|
}
|
|
|
|
|
|
func draw(_ layer: CALayer, in ctx: CGContext) {
|
|
|
|
|
|
|
|
if let border = border {
|
|
|
|
ctx.setFillColor(presentation.colors.border.cgColor)
|
|
|
|
if border.contains(.Top) {
|
|
ctx.fill(NSMakeRect(0, NSHeight(self.frame) - .borderSize, NSWidth(self.frame), .borderSize))
|
|
}
|
|
if border.contains(.Bottom) {
|
|
ctx.fill(NSMakeRect(0, 0, NSWidth(self.frame), .borderSize))
|
|
}
|
|
if border.contains(.Left) {
|
|
ctx.fill(NSMakeRect(0, 0, .borderSize, NSHeight(self.frame)))
|
|
}
|
|
if border.contains(.Right) {
|
|
ctx.fill(NSMakeRect(NSWidth(self.frame) - .borderSize, 0, .borderSize, NSHeight(self.frame)))
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private var beforeRange: NSRange = NSMakeRange(NSNotFound, 0)
|
|
private var offsetOfStartItem: NSPoint = .zero
|
|
private var mouseDown: Bool = false
|
|
override func mouseDragged(with event: NSEvent) {
|
|
super.mouseDragged(with: event)
|
|
|
|
if let resortController = table?.resortController, beforeRange.length > 0, mouseDown {
|
|
if resortController.resortRange.indexIn(beforeRange.location) {
|
|
let point = self.convert(event.locationInWindow, from: nil)
|
|
let afterRange = self.rows(in: NSMakeRect(point.x, point.y, 1, 1))
|
|
if afterRange != beforeRange {
|
|
self.table?.startResorting(beforeRange, point.offsetBy(dx: -offsetOfStartItem.x, dy: -offsetOfStartItem.y))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
if event.clickCount == 1 {
|
|
mouseDown = true
|
|
let point = self.convert(event.locationInWindow, from: nil)
|
|
let beforeRange = self.rows(in: NSMakeRect(point.x, point.y, 1, 1))
|
|
self.beforeRange = beforeRange
|
|
if beforeRange.length > 0 {
|
|
if let resortController = table?.resortController{
|
|
if resortController.resortRange.indexIn(beforeRange.location) {
|
|
self.offsetOfStartItem = point
|
|
} else if let table = table, !table.alwaysOpenRowsOnMouseUp {
|
|
sdelegate?.selectRow(index: beforeRange.location)
|
|
}
|
|
} else if let table = table, !table.alwaysOpenRowsOnMouseUp {
|
|
sdelegate?.selectRow(index: beforeRange.location)
|
|
}
|
|
|
|
let signal: Signal<Void, NoError> = .complete() |> delay(0.5, queue: .mainQueue())
|
|
longDisposable.set(signal.start(completed: { [weak self] in
|
|
guard let `self` = self, let window = self.window else {
|
|
return
|
|
}
|
|
let point = self.convert(window.mouseLocationOutsideOfEventStream, from: nil)
|
|
let afterRange = self.rows(in: NSMakeRect(point.x, point.y, 1, 1))
|
|
|
|
if afterRange == beforeRange {
|
|
self.sdelegate?.longAction(index: afterRange.location)
|
|
}
|
|
}))
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
longDisposable.set(nil)
|
|
let point = self.convert(event.locationInWindow, from: nil)
|
|
let range = self.rows(in: NSMakeRect(point.x, point.y, 1, 1));
|
|
if range.length > 0, let table = table, mouseDown {
|
|
mouseDown = false
|
|
if let controller = self.table?.resortController {
|
|
if !controller.resortRange.indexIn(range.location) {
|
|
if controller.resortRow == nil, beforeRange.location == range.location && table.alwaysOpenRowsOnMouseUp {
|
|
sdelegate?.selectRow(index: range.location)
|
|
}
|
|
return
|
|
}
|
|
if range.length > 0, beforeRange.location == range.location {
|
|
sdelegate?.selectRow(index: range.location)
|
|
}
|
|
} else if table.alwaysOpenRowsOnMouseUp, beforeRange.location == range.location {
|
|
sdelegate?.selectRow(index: range.location)
|
|
}
|
|
}
|
|
mouseDown = false
|
|
}
|
|
|
|
|
|
deinit {
|
|
longDisposable.dispose()
|
|
}
|
|
|
|
|
|
|
|
var liveWidth:CGFloat = 0
|
|
|
|
override func viewWillStartLiveResize() {
|
|
super.viewWillStartLiveResize()
|
|
liveWidth = frame.width
|
|
}
|
|
|
|
|
|
override func viewDidEndLiveResize() {
|
|
super.viewDidEndLiveResize()
|
|
if liveWidth != frame.width {
|
|
liveWidth = frame.width
|
|
table?.layoutItems()
|
|
}
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
public protocol InteractionContentViewProtocol : AnyObject {
|
|
func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView?
|
|
func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable)
|
|
func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView)
|
|
func videoTimebase(for stableId: AnyHashable) -> CMTimebase?
|
|
func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?)
|
|
|
|
}
|
|
|
|
public class TableScrollListener : NSObject {
|
|
fileprivate let uniqueId:UInt32 = arc4random()
|
|
public var handler:(ScrollPosition)->Void
|
|
fileprivate let dispatchWhenVisibleRangeUpdated: Bool
|
|
fileprivate var first: Bool = true
|
|
fileprivate var dispatchRange: NSRange = NSMakeRange(NSNotFound, 0)
|
|
public init(dispatchWhenVisibleRangeUpdated: Bool = true, _ handler:@escaping(ScrollPosition)->Void) {
|
|
self.dispatchWhenVisibleRangeUpdated = dispatchWhenVisibleRangeUpdated
|
|
self.handler = handler
|
|
}
|
|
|
|
}
|
|
|
|
public struct TableAutohide {
|
|
public private(set) weak var item: TableRowItem?
|
|
let hideUntilOverscroll: Bool
|
|
let hideHandler:(Bool)->Void
|
|
public init(item: TableRowItem?, hideUntilOverscroll: Bool = false, hideHandler:@escaping(Bool)->Void = { _ in }) {
|
|
self.item = item
|
|
self.hideUntilOverscroll = hideUntilOverscroll
|
|
self.hideHandler = hideHandler
|
|
}
|
|
}
|
|
|
|
open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,SelectDelegate,InteractionContentViewProtocol, AppearanceViewProtocol {
|
|
|
|
private var searchView: TableSearchView?
|
|
private var rightBorder: View? = nil
|
|
public var separator:TableSeparator = .none
|
|
|
|
|
|
public var getBackgroundColor:()->NSColor = { presentation.colors.background } {
|
|
didSet {
|
|
if super.layer?.backgroundColor != .clear {
|
|
super.layer?.backgroundColor = self.getBackgroundColor().cgColor
|
|
}
|
|
self.needsDisplay = true
|
|
|
|
}
|
|
}
|
|
|
|
public var supplyment: InteractionContentViewProtocol? = nil
|
|
|
|
var list:[TableRowItem] = [TableRowItem]();
|
|
var tableView:TGFlipableTableView
|
|
|
|
public var allItems:[TableRowItem] {
|
|
return list
|
|
}
|
|
|
|
public var view: NSTableView {
|
|
return tableView
|
|
}
|
|
|
|
|
|
weak public var delegate:TableViewDelegate?
|
|
private var trackingArea:NSTrackingArea?
|
|
private var listhash:[AnyHashable:TableRowItem] = [AnyHashable:TableRowItem]();
|
|
|
|
private let mergePromise:Promise<TableUpdateTransition> = Promise()
|
|
private let mergeDisposable:MetaDisposable = MetaDisposable()
|
|
|
|
public var resortController: TableResortController? {
|
|
didSet {
|
|
|
|
}
|
|
}
|
|
|
|
public var selectedhash:AnyHashable? = nil
|
|
public var highlitedHash:AnyHashable? = nil
|
|
|
|
public var isUpdating: Bool {
|
|
return self.updating
|
|
}
|
|
private var updating:Bool = false
|
|
|
|
private var previousScroll:ScrollPosition?
|
|
public var needUpdateVisibleAfterScroll:Bool = false
|
|
private var scrollHandler:(_ scrollPosition:ScrollPosition) ->Void = {_ in}
|
|
|
|
private var backgroundView: ImageView?
|
|
|
|
private var scrollListeners:[TableScrollListener] = []
|
|
|
|
public var alwaysOpenRowsOnMouseUp: Bool = true
|
|
|
|
public var autohide: TableAutohide?
|
|
|
|
public var emptyChecker: (([TableRowItem]) -> Bool)? = nil
|
|
|
|
public var updatedItems:(([TableRowItem])->Void)? {
|
|
didSet {
|
|
updatedItems?(self.list)
|
|
}
|
|
}
|
|
|
|
public var beforeSetupItem:((TableRowView, TableRowItem)->Void)?
|
|
public var afterSetupItem:((TableRowView, TableRowItem)->Void)?
|
|
|
|
private var nextScrollEventIsAnimated: Bool = false
|
|
|
|
public var emptyItem:TableRowItem? {
|
|
didSet {
|
|
emptyItem?.table = self
|
|
updateEmpties()
|
|
}
|
|
}
|
|
private var emptyView:TableRowView?
|
|
|
|
public func addScroll(listener:TableScrollListener) {
|
|
var found: Bool = false
|
|
for enumerate in scrollListeners {
|
|
if enumerate.uniqueId == listener.uniqueId {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
scrollListeners.append(listener)
|
|
}
|
|
}
|
|
|
|
|
|
public var bottomInset:CGFloat = 0 {
|
|
didSet {
|
|
tableView.bottomInset = bottomInset
|
|
}
|
|
}
|
|
|
|
open override func viewDidChangeBackingProperties() {
|
|
|
|
}
|
|
|
|
open func updateLocalizationAndTheme(theme: PresentationTheme) {
|
|
if super.layer?.backgroundColor != .clear {
|
|
super.layer?.backgroundColor = self.getBackgroundColor().cgColor
|
|
}
|
|
stickView?.updateColors()
|
|
rightBorder?.backgroundColor = theme.colors.border
|
|
//tableView.background = .clear
|
|
// super.layer?.backgroundColor = .clear
|
|
self.needsDisplay = true
|
|
// tableView.needsDisplay = true
|
|
// clipView.needsDisplay = true
|
|
}
|
|
|
|
|
|
public func removeScroll(listener:TableScrollListener) {
|
|
var index:Int = 0
|
|
var found:Bool = false
|
|
for enumerate in scrollListeners {
|
|
if enumerate.uniqueId == listener.uniqueId {
|
|
found = true
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
if found {
|
|
scrollListeners.remove(at: index)
|
|
}
|
|
|
|
}
|
|
|
|
public var count:Int {
|
|
get {
|
|
return self.list.count
|
|
}
|
|
}
|
|
|
|
// open override func setNeedsDisplay(_ invalidRect: NSRect) {
|
|
//
|
|
// }
|
|
|
|
open override var isFlipped: Bool {
|
|
return true
|
|
}
|
|
|
|
public override init(frame frameRect: NSRect) {
|
|
self.tableView = TGFlipableTableView(frame: frameRect.size.bounds)
|
|
self.tableView.wantsLayer = true
|
|
self.tableView.autoresizesSubviews = false
|
|
super.init(frame: frameRect)
|
|
self.autoresizingMask = []
|
|
updateAfterInitialize(isFlipped:true, drawBorder: false)
|
|
}
|
|
|
|
public init(frame frameRect: NSRect, isFlipped:Bool = true, bottomInset:CGFloat = 0, drawBorder: Bool = false) {
|
|
self.tableView = TGFlipableTableView(frame: frameRect.size.bounds)
|
|
self.tableView.wantsLayer = true
|
|
self.tableView.autoresizesSubviews = false
|
|
super.init(frame: frameRect)
|
|
self.autoresizingMask = []
|
|
updateAfterInitialize(isFlipped: isFlipped, drawBorder: drawBorder)
|
|
}
|
|
|
|
public convenience init() {
|
|
self.init(frame: NSZeroRect)
|
|
}
|
|
|
|
public var border:BorderType? {
|
|
didSet {
|
|
self.clipView.border = border
|
|
self.tableView.border = border
|
|
|
|
if border == [.Right] {
|
|
if rightBorder == nil {
|
|
rightBorder = View()
|
|
rightBorder?.backgroundColor = presentation.colors.border
|
|
addSubview(rightBorder!)
|
|
needsLayout = true
|
|
}
|
|
} else {
|
|
rightBorder?.removeFromSuperview()
|
|
rightBorder = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
open override var backgroundColor: NSColor {
|
|
didSet {
|
|
// documentView?.background = backgroundColor
|
|
// contentView.background = backgroundColor
|
|
// self.clipView.backgroundColor = backgroundColor
|
|
// self.clipView.needsDisplay = true
|
|
// documentView?.needsDisplay = true
|
|
}
|
|
}
|
|
|
|
public func setIsFlipped(_ flipped: Bool) {
|
|
self.tableView.flip = flipped
|
|
}
|
|
|
|
public func updateAfterInitialize(isFlipped:Bool = true, bottomInset:CGFloat = 0, drawBorder: Bool = false) {
|
|
|
|
// self.tableView.setFrameSize(NSMakeSize(frame.width, self.tableView.frame.height))
|
|
|
|
self.tableView.flip = isFlipped
|
|
if #available(macOS 11.0, *) {
|
|
self.tableView.style = .fullWidth
|
|
}
|
|
self.automaticallyAdjustsContentInsets = false
|
|
// tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
clipView.autoresizingMask = []
|
|
clipView.autoresizesSubviews = false
|
|
|
|
self.tableView.autoresizingMask = []
|
|
self.tableView.rowSizeStyle = .custom
|
|
|
|
|
|
self.clipView.scrollDidComplete = { [weak self] _ in
|
|
self?.enqueueAwaitingIfNeeded()
|
|
}
|
|
|
|
// self.scrollsDynamically = true
|
|
// self.verticalLineScroll = 0
|
|
//self.verticalScrollElasticity = .none
|
|
self.autoresizesSubviews = false
|
|
|
|
self.tableView.table = self
|
|
|
|
self.bottomInset = bottomInset
|
|
self.tableView.bottomInset = bottomInset
|
|
|
|
if drawBorder {
|
|
self.clipView.border = BorderType([.Right])
|
|
self.tableView.border = BorderType([.Right])
|
|
}
|
|
|
|
self.hasVerticalScroller = true;
|
|
|
|
self.documentView = self.tableView;
|
|
// self.tableView.autoresizingMask = [.height]
|
|
// self.autoresizingMask = [.width, .height]
|
|
|
|
|
|
|
|
self.tableView.delegate = self;
|
|
self.tableView.dataSource = self;
|
|
self.tableView.sdelegate = self
|
|
self.tableView.allowsColumnReordering = false
|
|
self.tableView.headerView = nil;
|
|
self.tableView.intercellSpacing = NSMakeSize(0, 0)
|
|
self.tableView.columnAutoresizingStyle = .noColumnAutoresizing
|
|
// let tableColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "column"))
|
|
// tableColumn.width = frame.width
|
|
|
|
// self.tableView.addTableColumn(tableColumn)
|
|
|
|
|
|
mergeDisposable.set(mergePromise.get().start(next: { [weak self] (transition) in
|
|
self?.merge(with: transition)
|
|
}))
|
|
|
|
}
|
|
|
|
private func findBackgroundControllerView(view: NSView) -> BackgroundView? {
|
|
if let superview = view.superview {
|
|
for subview in superview.subviews {
|
|
if let subview = subview as? BackgroundView {
|
|
return subview
|
|
} else {
|
|
if let superview = subview.superview {
|
|
if let result = findBackgroundControllerView(view: superview) {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var findBackgroundControllerView: BackgroundView? {
|
|
return self.findBackgroundControllerView(view: self)
|
|
}
|
|
|
|
|
|
|
|
|
|
open override func layout() {
|
|
super.layout()
|
|
|
|
self.beginTableUpdates()
|
|
let item = TableRowItem(.zero, stableId: arc4random64())
|
|
let _ = self.addItem(item: item)
|
|
self.remove(at: self.count - 1)
|
|
self.endTableUpdates()
|
|
|
|
if let emptyView = emptyView, let superview = superview {
|
|
emptyView.frame = findBackgroundControllerView?.bounds ?? bounds
|
|
emptyView.centerX(y: superview.frame.height - emptyView.frame.height)
|
|
}
|
|
if let searchView = searchView {
|
|
searchView.setFrameSize(NSMakeSize(frame.width, searchView.frame.height))
|
|
}
|
|
|
|
if needsLayouItemsOnNextTransition {
|
|
layoutItems()
|
|
}
|
|
if let rightBorder = rightBorder {
|
|
rightBorder.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height)
|
|
}
|
|
|
|
if let stickView = stickView {
|
|
stickView.frame = NSMakeRect(stickView.frame.minX, stickView.frame.minY, frame.width, stickView.frame.height)
|
|
}
|
|
}
|
|
|
|
|
|
func layoutIfNeeded(with range:NSRange, oldWidth:CGFloat) {
|
|
|
|
|
|
let visibleItems = self.visibleItems()
|
|
beginTableUpdates()
|
|
for i in range.min ..< range.max {
|
|
let item = self.item(at: i)
|
|
let before = item.heightValue
|
|
let updated = item.makeSize(frame.width, oldWidth: oldWidth)
|
|
let after = item.heightValue
|
|
if (before != after && updated) || item.instantlyResize || inLiveResize {
|
|
reloadData(row: i, animated: false)
|
|
noteHeightOfRow(i, false)
|
|
}
|
|
}
|
|
endTableUpdates()
|
|
if !tableView.inLiveResize && oldWidth != 0 {
|
|
saveScrollState(visibleItems)
|
|
}
|
|
|
|
for listener in scrollListeners {
|
|
listener.handler(self.scrollPosition().current)
|
|
}
|
|
}
|
|
|
|
private var liveScrollStartPosition: NSPoint?
|
|
|
|
public func resetLiveScroll() {
|
|
liveScrollStartPosition = nil
|
|
}
|
|
|
|
|
|
public var _scrollWillStartLiveScrolling:(()->Void)?
|
|
public var _scrollDidLiveScrolling:(()->Void)?
|
|
public var _scrollDidEndLiveScrolling:(()->Void)?
|
|
|
|
public private(set) var liveScrolling: Bool = false
|
|
|
|
open func scrollWillStartLiveScrolling() {
|
|
self.clipView.cancelScrolling()
|
|
liveScrollStartPosition = documentOffset
|
|
_scrollWillStartLiveScrolling?()
|
|
liveScrolling = true
|
|
}
|
|
private var liveScrollStack:[CGFloat] = []
|
|
open func scrollDidLiveScrolling() {
|
|
|
|
liveScrollStack.append(documentOffset.y)
|
|
if documentOffset.y < -10, let liveScrollStartPosition = liveScrollStartPosition, let autohide = self.autohide, let item = autohide.item {
|
|
|
|
if liveScrollStartPosition.y <= 0 && liveScrollStack.max() ?? 0 <= item.height / 2 {
|
|
if item.isAutohidden {
|
|
item.unhideItem(animated: true)
|
|
autohide.hideHandler(false)
|
|
liveScrollStack.removeAll()
|
|
}
|
|
}
|
|
}
|
|
_scrollDidLiveScrolling?()
|
|
}
|
|
|
|
|
|
public var updateScrollPoint:((NSPoint)->NSPoint)? = nil
|
|
private var beginPendingTime:CFAbsoluteTime?
|
|
private var dispatchRange: NSRange = NSMakeRange(NSNotFound, 0)
|
|
|
|
public var scrollDidUpdate: ((ScrollPosition)->Void)? = nil
|
|
|
|
|
|
private func updateScroll() {
|
|
|
|
|
|
self.scrollDidChangedBounds()
|
|
|
|
if self.updating == true {
|
|
return
|
|
}
|
|
var isNextCallLocked: Bool {
|
|
if let beginPendingTime = beginPendingTime {
|
|
if CFAbsoluteTimeGetCurrent() - beginPendingTime < 0.05 {
|
|
return true
|
|
}
|
|
}
|
|
beginPendingTime = CFAbsoluteTimeGetCurrent()
|
|
return false
|
|
}
|
|
|
|
let reqCount = self.count / 6
|
|
|
|
self.updateStickAfterScroll(self.nextScrollEventIsAnimated)
|
|
self.nextScrollEventIsAnimated = false
|
|
let scroll = self.scrollPosition(self.visibleRows())
|
|
|
|
if (!self.updating && !self.clipView.isAnimateScrolling) {
|
|
|
|
let range = scroll.current.visibleRows
|
|
|
|
if range.location == NSNotFound {
|
|
return;
|
|
}
|
|
|
|
if(scroll.current.rect != self.previousScroll?.rect) {
|
|
|
|
switch(scroll.current.direction) {
|
|
case .top:
|
|
if(range.location <= reqCount) {
|
|
if !isNextCallLocked {
|
|
self.scrollHandler(scroll.current)
|
|
}
|
|
self.previousScroll = scroll.current
|
|
}
|
|
case .bottom:
|
|
if(self.count - (range.location + range.length) <= reqCount) {
|
|
if !isNextCallLocked {
|
|
self.scrollHandler(scroll.current)
|
|
}
|
|
self.previousScroll = scroll.current
|
|
}
|
|
case .none:
|
|
if !isNextCallLocked {
|
|
self.scrollHandler(scroll.current)
|
|
}
|
|
self.previousScroll = scroll.current
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
for listener in self.scrollListeners {
|
|
if !listener.dispatchWhenVisibleRangeUpdated || listener.first || !NSEqualRanges(scroll.current.visibleRows, listener.dispatchRange) {
|
|
listener.handler(scroll.current)
|
|
listener.first = false
|
|
listener.dispatchRange = scroll.current.visibleRows
|
|
}
|
|
}
|
|
|
|
if self.needUpdateVisibleAfterScroll {
|
|
let range = self.visibleRows()
|
|
for i in range.location ..< range.location + range.length {
|
|
if let view = self.viewNecessary(at: i) {
|
|
view.updateMouse(animated: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
open func scrollDidChangedBounds() {
|
|
|
|
if let autohide = autohide, let item = autohide.item, autohide.hideUntilOverscroll, let _ = liveScrollStartPosition {
|
|
let rect = self.rectOf(item: item)
|
|
|
|
if !item.isAutohidden, documentOffset.y >= rect.maxY {
|
|
item.hideItem(animated: false, reload: false)
|
|
|
|
liveScrollStartPosition = nil
|
|
liveScrollStack.removeAll()
|
|
self.merge(with: TableUpdateTransition(deleted: [item.index], inserted: [(item.index, item)], updated: [], animated: false, state: .saveVisible(.upper)))
|
|
|
|
autohide.hideHandler(true)
|
|
}
|
|
}
|
|
|
|
self.scrollDidLiveScrolling()
|
|
|
|
|
|
var point = self.clipView.bounds.origin
|
|
if let updateScrollPoint = updateScrollPoint {
|
|
point = updateScrollPoint(point)
|
|
}
|
|
let position = ScrollPosition(NSMakeRect(point.x, point.y,contentView.documentRect.width, contentView.documentRect.height), .none, NSMakeRange(0, 0))
|
|
|
|
self.scrollDidUpdate?(position)
|
|
|
|
}
|
|
|
|
open func scrollDidEndLiveScrolling() {
|
|
liveScrolling = false
|
|
if let autohide = self.autohide {
|
|
if let autohideItem = autohide.item {
|
|
let rect = self.rectOf(item: autohideItem)
|
|
if (documentOffset.y > (rect.minY + (rect.height / 2))) && documentOffset.y < rect.maxY {
|
|
// scroll(to: .top(id: autohideItem.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: autohideItem.height), completion: { [weak self] _ in
|
|
// self?.liveScrollStartPosition = nil
|
|
// })
|
|
} else if documentOffset.y > 0 && documentOffset.y < rect.maxY {
|
|
// scroll(to: .top(id: autohideItem.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), completion: { [weak self] _ in
|
|
// self?.liveScrollStartPosition = nil
|
|
// })
|
|
} else {
|
|
liveScrollStartPosition = nil
|
|
}
|
|
} else {
|
|
liveScrollStartPosition = nil
|
|
}
|
|
} else {
|
|
liveScrollStartPosition = nil
|
|
}
|
|
liveScrollStack.removeAll()
|
|
_scrollDidEndLiveScrolling?()
|
|
}
|
|
|
|
open override func viewDidMoveToSuperview() {
|
|
if superview != nil {
|
|
let clipView = self.contentView
|
|
|
|
NotificationCenter.default.addObserver(forName: NSScrollView.didEndLiveScrollNotification, object: self, queue: nil, using: { [weak self] _ in
|
|
self?.scrollDidEndLiveScrolling()
|
|
})
|
|
|
|
NotificationCenter.default.addObserver(forName: NSScrollView.willStartLiveScrollNotification, object: self, queue: nil, using: { [weak self] _ in
|
|
self?.scrollWillStartLiveScrolling()
|
|
})
|
|
|
|
NotificationCenter.default.addObserver(forName: NSScrollView.didLiveScrollNotification, object: self, queue: nil, using: { [weak self] _ in
|
|
self?.scrollDidLiveScrolling()
|
|
})
|
|
|
|
|
|
NotificationCenter.default.addObserver(forName: NSScrollView.boundsDidChangeNotification, object: clipView, queue: nil, using: { [weak self] _ in
|
|
CATransaction.begin()
|
|
if self?.superview != nil {
|
|
self?.updateScroll()
|
|
}
|
|
CATransaction.commit()
|
|
})
|
|
|
|
} else {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
}
|
|
|
|
|
|
private var stickClass:AnyClass?
|
|
private var stickView:TableStickView?
|
|
|
|
public var p_stickView: NSView? {
|
|
return stickView
|
|
}
|
|
|
|
private var stickItem:TableStickItem? {
|
|
didSet {
|
|
if stickItem != oldValue {
|
|
if let stickHandler = stickHandler {
|
|
stickHandler(stickItem)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private var stickHandler:((TableStickItem?)->Void)?
|
|
private var firstTime: Bool = false
|
|
public func set(stickClass:AnyClass?, visible: Bool = true, handler:@escaping(TableStickItem?)->Void) {
|
|
self.stickClass = stickClass
|
|
self.stickHandler = handler
|
|
self.firstTime = true
|
|
if let stickClass = stickClass as? TableStickItem.Type {
|
|
if stickView == nil {
|
|
let stickItem:TableStickItem = stickClass.init(frame.size)
|
|
stickItem.table = self
|
|
self.stickItem = stickItem
|
|
if visible {
|
|
let vz = stickItem.viewClass() as! TableStickView.Type
|
|
stickView = vz.init(frame:NSMakeRect(0, 0, NSWidth(self.frame), stickItem.heightValue))
|
|
stickView!.header = true
|
|
stickView!.set(item: stickItem, animated: false)
|
|
stickView!.updateLayout(size: stickView!.frame.size, transition: .immediate)
|
|
// tableView.addSubview(stickView!)
|
|
}
|
|
}
|
|
|
|
updateStickAfterScroll(false)
|
|
|
|
} else {
|
|
stickView?.removeFromSuperview()
|
|
stickView = nil
|
|
stickItem = nil
|
|
}
|
|
|
|
}
|
|
|
|
public func optionalItem(at:Int) -> TableRowItem? {
|
|
return at < count && at >= 0 ? self.item(at: at) : nil
|
|
}
|
|
|
|
private var needsLayouItemsOnNextTransition:Bool = false
|
|
|
|
public func layouItemsOnNextTransition() {
|
|
needsLayouItemsOnNextTransition = true
|
|
}
|
|
public func layoutItems() {
|
|
|
|
let visibleItems = self.visibleItems()
|
|
|
|
self.beginTableUpdates()
|
|
self.enumerateItems { item in
|
|
_ = item.makeSize(frame.width, oldWidth: item.width)
|
|
reloadData(row: item.index, animated: false)
|
|
return true
|
|
}
|
|
self.endTableUpdates()
|
|
|
|
|
|
self.saveScrollState(visibleItems)
|
|
|
|
self.needsLayouItemsOnNextTransition = false
|
|
}
|
|
|
|
public override var _mouseDownCanMoveWindow: Bool {
|
|
didSet {
|
|
clipView._mouseDownCanMoveWindow = _mouseDownCanMoveWindow
|
|
tableView._mouseDownCanMoveWindow = _mouseDownCanMoveWindow
|
|
}
|
|
}
|
|
|
|
open override var mouseDownCanMoveWindow: Bool {
|
|
return _mouseDownCanMoveWindow
|
|
}
|
|
|
|
private func saveScrollState(_ visibleItems: [(TableRowItem,CGFloat,CGFloat)]) -> Void {
|
|
//
|
|
if !visibleItems.isEmpty {
|
|
var nrect:NSRect = NSZeroRect
|
|
|
|
let strideTo:StrideTo<Int> = stride(from: visibleItems.count - 1, to: -1, by: -1)
|
|
|
|
for i in strideTo {
|
|
let visible = visibleItems[i]
|
|
if let item = self.item(stableId: visible.0.stableId) {
|
|
|
|
nrect = rectOf(item: item)
|
|
|
|
if let view = viewNecessary(at: i) {
|
|
if view.isInsertionAnimated {
|
|
break
|
|
}
|
|
}
|
|
|
|
let y:CGFloat
|
|
if !tableView.isFlipped {
|
|
y = nrect.minY - (frame.height - visible.1) + nrect.height
|
|
} else {
|
|
y = nrect.minY - visible.1
|
|
}
|
|
|
|
self.clipView.updateBounds(to: NSMakePoint(0, y))
|
|
// reflectScrolledClipView(clipView)
|
|
// flashScrollers()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getScrollY(_ visibleItems: [(TableRowItem,CGFloat,CGFloat)]) -> CGFloat? {
|
|
if !visibleItems.isEmpty {
|
|
var nrect:NSRect = NSZeroRect
|
|
|
|
let strideTo:StrideTo<Int> = stride(from: visibleItems.count - 1, to: -1, by: -1)
|
|
|
|
for i in strideTo {
|
|
let visible = visibleItems[i]
|
|
if let item = self.item(stableId: visible.0.stableId) {
|
|
|
|
nrect = rectOf(item: item)
|
|
|
|
if let view = viewNecessary(at: i) {
|
|
if view.isInsertionAnimated {
|
|
break
|
|
}
|
|
}
|
|
|
|
let y:CGFloat
|
|
if !tableView.isFlipped {
|
|
y = nrect.minY - (frame.height - visible.1) + nrect.height
|
|
} else {
|
|
y = nrect.minY - visible.1
|
|
}
|
|
|
|
return y
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
|
|
private let stickTimeoutDisposable = MetaDisposable()
|
|
private var previousStickMinY: CGFloat? = nil
|
|
|
|
private var stickTopInset: CGFloat = 0
|
|
public func updateStickInset(_ inset: CGFloat, animated: Bool) {
|
|
if stickTopInset != inset {
|
|
stickTopInset = inset
|
|
updateStickAfterScroll(animated)
|
|
}
|
|
}
|
|
|
|
|
|
public func updateStickAfterScroll(_ animated: Bool) -> Void {
|
|
let visibleRect = self.tableView.visibleRect
|
|
let range = self.tableView.rows(in: NSMakeRect(visibleRect.minX, visibleRect.minY, visibleRect.width, visibleRect.height - stickTopInset))
|
|
|
|
if let stickClass = stickClass, !updating {
|
|
// if documentSize.height > frame.height {
|
|
|
|
let flipped = tableView.isFlipped
|
|
|
|
|
|
var index:Int = flipped ? range.location : range.location + range.length - 1
|
|
|
|
|
|
|
|
let scrollInset = self.documentOffset.y - stickTopInset + (flipped ? 0 : frame.height)
|
|
var item:TableRowItem? = optionalItem(at: index)
|
|
|
|
if !flipped {
|
|
while let s = item, !s.isKind(of: stickClass) {
|
|
index += 1
|
|
item = self.optionalItem(at: index)
|
|
}
|
|
} else {
|
|
while let s = item, !s.isKind(of: stickClass) {
|
|
index -= 1
|
|
item = self.optionalItem(at: index)
|
|
}
|
|
}
|
|
|
|
|
|
if item == nil && !flipped {
|
|
index = range.location + range.length
|
|
while item == nil && index < count {
|
|
if let s = self.optionalItem(at: index), s.isKind(of: stickClass) {
|
|
item = s
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
|
|
if let someItem = item as? TableStickItem {
|
|
var currentStick:TableStickItem?
|
|
|
|
if !flipped {
|
|
for index in stride(from: someItem.index - 1, to: -1, by: -1) {
|
|
let item = self.optionalItem(at: index)
|
|
if let item = item, item.isKind(of: stickClass) {
|
|
currentStick = item as? TableStickItem
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
for index in someItem.index + 1 ..< count {
|
|
let item = self.optionalItem(at: index)
|
|
if let item = item, item.isKind(of: stickClass) {
|
|
currentStick = item as? TableStickItem
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if someItem.singletonItem {
|
|
currentStick = someItem
|
|
}
|
|
|
|
if stickView?.item != item {
|
|
stickView?.set(item: someItem, animated: animated)
|
|
stickView?.updateIsVisible(!firstTime, animated: animated)
|
|
}
|
|
|
|
if let item = stickItem {
|
|
if let view = (viewNecessary(at: item.index) as? TableStickView) {
|
|
view.updateIsVisible((!firstTime || !view.header), animated: animated)
|
|
}
|
|
}
|
|
|
|
stickItem = currentStick ?? someItem
|
|
|
|
if let stickView = stickView {
|
|
let scrollerIndex = subviews.firstIndex(where: { $0 is NSScroller })
|
|
let stickViewIndex = subviews.firstIndex(where: { $0 is TableStickView })
|
|
if let scrollerIndex = scrollerIndex, stickViewIndex == nil || stickViewIndex! > scrollerIndex {
|
|
addSubview(stickView, positioned: .below, relativeTo: subviews[scrollerIndex])
|
|
}
|
|
}
|
|
|
|
stickView?.setFrameSize(frame.width, someItem.heightValue)
|
|
let itemRect:NSRect = someItem.view?.visibleRect ?? NSZeroRect
|
|
|
|
if let item = stickItem, item.isKind(of: stickClass), let stickView = stickView {
|
|
let rect:NSRect = self.rectOf(item: item)
|
|
let dif:CGFloat
|
|
if currentStick != nil {
|
|
dif = min(scrollInset - rect.maxY, item.heightValue)
|
|
} else {
|
|
dif = item.heightValue
|
|
}
|
|
var yTopOffset:CGFloat
|
|
if !flipped {
|
|
yTopOffset = min((scrollInset - rect.maxY) - rect.height, 0)
|
|
} else {
|
|
yTopOffset = min(-(rect.height + (scrollInset - rect.minY)), 0)
|
|
}
|
|
if yTopOffset <= -rect.height {
|
|
yTopOffset = 0
|
|
}
|
|
if scrollInset <= 0, item.singletonItem {
|
|
yTopOffset = abs(scrollInset)
|
|
}
|
|
|
|
let updatedPoint = NSMakePoint(0, yTopOffset + stickTopInset)
|
|
if stickView.frame.origin != updatedPoint {
|
|
stickView._change(pos: updatedPoint, animated: animated)
|
|
}
|
|
stickView.header = abs(dif) <= item.heightValue
|
|
|
|
if !firstTime {
|
|
let rows:[Int] = [tableView.row(at: NSMakePoint(0, min(scrollInset - stickView.frame.height, documentSize.height - stickView.frame.height))), tableView.row(at: NSMakePoint(0, scrollInset))]
|
|
var applied: Bool = false
|
|
for row in rows {
|
|
let row = min(max(0, row), list.count - 1)
|
|
if let dateItem = self.item(at: row) as? TableStickItem, let view = dateItem.view as? TableStickView {
|
|
if documentOffset.y > (documentSize.height - frame.height) && !tableView.isFlipped {
|
|
yTopOffset = -1
|
|
}
|
|
view.updateIsVisible(yTopOffset < 0 , animated: false)
|
|
applied = true
|
|
}
|
|
}
|
|
if !applied {
|
|
self.enumerateVisibleViews(with: { view in
|
|
(view as? TableStickView)?.updateIsVisible(true, animated: false)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if previousStickMinY == nil {
|
|
previousStickMinY = documentOffset.y
|
|
}
|
|
|
|
|
|
|
|
if previousStickMinY != documentOffset.y {
|
|
stickView.isHidden = false
|
|
previousStickMinY = documentOffset.y
|
|
if !animated || stickView.layer?.opacity != 0 {
|
|
stickView.updateIsVisible(true, animated: true)
|
|
firstTime = false
|
|
}
|
|
}
|
|
if tableView.isFlipped {
|
|
stickView.isHidden = (documentOffset.y <= 0 && !item.singletonItem)// && !stickView.isAlwaysUp
|
|
} else {
|
|
stickView.isHidden = documentSize.height <= frame.height || documentOffset.y > (documentSize.height - frame.height)
|
|
}
|
|
|
|
stickTimeoutDisposable.set((Signal<Void, NoError>.single(Void()) |> delay(2.0, queue: Queue.mainQueue())).start(next: { [weak stickView] in
|
|
|
|
if itemRect.height == 0, let stickView = stickView {
|
|
stickView.updateIsVisible(false, animated: true)
|
|
}
|
|
}))
|
|
|
|
}
|
|
|
|
} else {
|
|
if index == -1 {
|
|
if animated, let stickView = self.stickView {
|
|
stickView._change(pos: NSMakePoint(0, -stickView.frame.height), animated: animated, removeOnCompletion: false, completion: { [weak stickView] _ in
|
|
stickView?.removeFromSuperview()
|
|
})
|
|
} else {
|
|
stickView?.removeFromSuperview()
|
|
}
|
|
} else {
|
|
stickView?.setFrameOrigin(0, 0)
|
|
stickView?.header = true
|
|
stickView?.isHidden = true
|
|
}
|
|
|
|
self.enumerateViews(with: { view in
|
|
(view as? TableStickView)?.updateIsVisible(true, animated: false)
|
|
return true
|
|
})
|
|
}
|
|
|
|
// }
|
|
}
|
|
}
|
|
|
|
|
|
public func resetScrollNotifies() ->Void {
|
|
self.previousScroll = nil
|
|
resetScroll()
|
|
}
|
|
|
|
public func scrollUp(offset: CGFloat = 30.0) {
|
|
self.clipView.scroll(to: NSMakePoint(0, min(clipView.bounds.minY + offset, clipView.bounds.maxY)), animated: true)
|
|
self.reflectScrolledClipView(clipView)
|
|
}
|
|
|
|
|
|
public func scrollDown(offset: CGFloat = 30.0) {
|
|
self.clipView.scroll(to: NSMakePoint(0, max(clipView.bounds.minY - offset, 0)), animated: true)
|
|
self.reflectScrolledClipView(clipView)
|
|
}
|
|
|
|
public func notifyScrollHandlers() -> Void {
|
|
let scroll = scrollPosition(visibleRows()).current
|
|
for listener in scrollListeners {
|
|
listener.handler(scroll)
|
|
}
|
|
}
|
|
|
|
public var topVisibleRow:Int? {
|
|
let visible = visibleItems()
|
|
if !isFlipped {
|
|
return visible.first?.0.index
|
|
} else {
|
|
return visible.last?.0.index
|
|
}
|
|
}
|
|
|
|
public var bottomVisibleRow:Int? {
|
|
let visible = visibleItems()
|
|
if isFlipped {
|
|
return visible.first?.0.index
|
|
} else {
|
|
return visible.last?.0.index
|
|
}
|
|
}
|
|
|
|
open override func setFrameOrigin(_ newOrigin: NSPoint) {
|
|
super.setFrameOrigin(newOrigin);
|
|
self.updateTrackingAreas();
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func selectedItem() -> TableRowItem? {
|
|
|
|
let hash = selectedhash
|
|
if let hash = hash {
|
|
return self.item(stableId:hash)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func isSelected(_ item:TableRowItem) ->Bool {
|
|
return selectedhash == item.stableId
|
|
}
|
|
|
|
public func highlightedItem() -> TableRowItem? {
|
|
|
|
let hash = highlitedHash
|
|
if let hash = hash {
|
|
return self.item(stableId: hash)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func isHighlighted(_ item:TableRowItem) ->Bool {
|
|
return highlitedHash == item.stableId
|
|
}
|
|
|
|
public func item(stableId:AnyHashable) -> TableRowItem? {
|
|
return self.listhash[stableId];
|
|
}
|
|
|
|
public func index(of:TableRowItem) -> Int? {
|
|
if let it = self.listhash[of.stableId] {
|
|
return self.list.firstIndex(of: it)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func index(hash:AnyHashable) -> Int? {
|
|
|
|
if let it = self.listhash[hash] {
|
|
return it.index
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
fileprivate func startResorting(_ range: NSRange, _ offset: CGPoint) {
|
|
guard let window = _window else {return}
|
|
|
|
let point = tableView.convert(window.mouseLocationOutsideOfEventStream, from: nil)
|
|
if range.length > 0, let controller = resortController, controller.canResort(range.location), let view = viewNecessary(at: range.location) {
|
|
controller.resortRow = range.location
|
|
controller.currentHoleIndex = range.location
|
|
controller.resortView = view
|
|
controller.startLocation = NSMakePoint(round(point.y), round(point.y))
|
|
controller.startRowLocation = view.frame.origin.offsetBy(dx: 0, dy: round(offset.y))
|
|
controller.start(range.location)
|
|
|
|
view.frame = convert(view.frame, from: view.superview)
|
|
|
|
|
|
|
|
addSubview(view)
|
|
view.isHidden = false
|
|
window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in
|
|
guard let controller = self?.resortController, controller.isResorting else {return .rejected}
|
|
self?.stopResorting()
|
|
return .invoked
|
|
}, with: self, for: .leftMouseUp, priority: .modal)
|
|
|
|
var first: Bool = true
|
|
|
|
window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in
|
|
if let controller = self?.resortController, let view = controller.resortView, let `self` = self {
|
|
|
|
self.contentView.autoscroll(with: event)
|
|
|
|
var point = self.tableView.convert(event.locationInWindow, from: nil)
|
|
point.x = 0
|
|
let difference = (controller.startLocation.y - point.y)
|
|
|
|
if view.superview != self {
|
|
view.frame = self.convert(view.frame, from: view.superview)
|
|
let item = self.item(at: range.location)
|
|
view.set(item: item, animated: false)
|
|
view.updateLayout(size: view.frame.size, transition: .immediate)
|
|
controller.resortView = view
|
|
self.addSubview(view)
|
|
}
|
|
view.isHidden = false
|
|
|
|
var newPoint = NSMakePoint(view.frame.minX, max(controller.startRowLocation.y - difference, 0))
|
|
newPoint.y -= self.documentOffset.y //self.convert(newPoint, from: self.tableView)
|
|
newPoint = NSMakePoint(round(newPoint.x), round(newPoint.y))
|
|
if first {
|
|
view.layer?.animatePosition(from: NSMakePoint(0, view.frame.minY - newPoint.y), to: .zero, duration: duration, timingFunction: .spring, removeOnCompletion: true, additive: true)
|
|
}
|
|
view.setFrameOrigin(newPoint)
|
|
first = false
|
|
self.updateMovableItem(point)
|
|
return .invoked
|
|
} else {
|
|
return .rejected
|
|
}
|
|
}, with: self, for: .leftMouseDragged, priority: .modal)
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
fileprivate func stopResorting() {
|
|
if let controller = resortController, let current = controller.currentHoleIndex, let start = controller.resortRow {
|
|
|
|
|
|
NSAnimationContext.runAnimationGroup({ ctx in
|
|
if controller.resortRange.location != NSNotFound {
|
|
var y: CGFloat = 0
|
|
for i in 0 ..< controller.resortRange.location {
|
|
y += self.list[i].heightValue
|
|
}
|
|
for i in controller.resortRange.location ..< controller.resortRange.max {
|
|
if let resortView = controller.resortView {
|
|
if i == current {
|
|
if current > start {
|
|
y -= (resortView.frame.height - item(at: i).heightValue)
|
|
}
|
|
y -= self.documentOffset.y
|
|
let point = NSMakePoint(resortView.frame.minX, y)
|
|
//convert(, from: tableView)
|
|
resortView.animator().setFrameOrigin(point)
|
|
y = 0
|
|
break
|
|
}
|
|
y += item(at: i).heightValue
|
|
}
|
|
}
|
|
}
|
|
}, completionHandler: {
|
|
|
|
CATransaction.begin()
|
|
|
|
let view = controller.resortView
|
|
controller.clear()
|
|
if let view = view {
|
|
view.frame = self.tableView.convert(view.frame, from: view.superview)
|
|
self.tableView.addSubview(view)
|
|
}
|
|
|
|
self.beginTableUpdates()
|
|
|
|
self.moveItem(from: start, to: current, redraw: false, animation: .none)
|
|
self.tableView.removeRows(at: IndexSet(integer: start), withAnimation: .none)
|
|
self.tableView.insertRows(at: IndexSet(integer: current), withAnimation: .none)
|
|
self.endTableUpdates()
|
|
|
|
CATransaction.commit()
|
|
|
|
if controller.resortRange.location != NSNotFound {
|
|
delay(0.2, closure: {
|
|
controller.complete(start, current)
|
|
})
|
|
}
|
|
|
|
})
|
|
|
|
|
|
_window?.remove(object: self, for: .leftMouseUp)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
private var maxResortHeight: CGFloat {
|
|
guard let controller = resortController else {return 0}
|
|
var height: CGFloat = 0
|
|
for i in 0 ..< controller.resortRange.max {
|
|
height += item(at: i).heightValue
|
|
}
|
|
return height
|
|
}
|
|
|
|
|
|
private func moveHole(at fromIndex: Int, to toIndex: Int, animated: Bool) {
|
|
var y: CGFloat = 0
|
|
|
|
guard let controller = resortController, let resortRow = controller.resortRow, let resortView = controller.resortView else {return}
|
|
if controller.resortRange.location == NSNotFound {
|
|
self.stopResorting()
|
|
return
|
|
}
|
|
for i in 0 ..< controller.resortRange.location {
|
|
y += self.list[i].heightValue
|
|
}
|
|
|
|
if toIndex > resortRow {
|
|
|
|
y = maxResortHeight
|
|
|
|
for i in stride(from: controller.resortRange.max - 1, to: -1, by: -1) {
|
|
let view = viewNecessary(at: i, makeIfNecessary: false)
|
|
if i == toIndex {
|
|
y -= resortView.frame.height// view.frame.height
|
|
}
|
|
if view != controller.resortView {
|
|
y -= self.item(at: i).heightValue
|
|
view?.animator().setFrameOrigin(0, y)
|
|
}
|
|
}
|
|
} else {
|
|
for i in controller.resortRange.location ..< controller.resortRange.max {
|
|
let view = viewNecessary(at: i, makeIfNecessary: false)
|
|
if i == toIndex {
|
|
y += resortView.frame.height
|
|
}
|
|
if view != controller.resortView {
|
|
view?.animator().setFrameOrigin(0, y)
|
|
y += self.item(at: i).heightValue
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private func updateMovableItem(_ point: NSPoint) {
|
|
|
|
|
|
guard let controller = resortController else {return}
|
|
|
|
let row = tableView.row(at: point)
|
|
if row != -1 {
|
|
let row = min(max(tableView.row(at: point), controller.resortRange.location), controller.resortRange.max - 1)
|
|
controller.prevHoleIndex = controller.currentHoleIndex
|
|
controller.currentHoleIndex = row
|
|
if controller.prevHoleIndex != controller.currentHoleIndex {
|
|
moveHole(at: controller.prevHoleIndex!, to: controller.currentHoleIndex!, animated: true)
|
|
controller.updateItems(controller.resortView, self.list.filter { controller.canResort($0.index) })
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private var _window:Window? {
|
|
return window as? Window
|
|
}
|
|
|
|
public func insert(item:TableRowItem, at:Int = 0, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Bool {
|
|
// assert(self.item(stableId:item.stableId) == nil, "inserting existing row inTable: \(self.item(stableId:item.stableId)!.className), new: \(item.className), stableId: \(item.stableId)")
|
|
self.listhash[item.stableId] = item;
|
|
let at = min(at, list.count)
|
|
self.list.insert(item, at: at);
|
|
item.table = self;
|
|
item._index = at
|
|
let animation = animation != .none ? item.animatable ? animation : .none : .none
|
|
|
|
NSAnimationContext.current.duration = animation != .none ? duration : 0.0
|
|
NSAnimationContext.current.timingFunction = animation == .none ? nil : CAMediaTimingFunction(name: .easeOut)
|
|
if(redraw) {
|
|
self.tableView.insertRows(at: IndexSet(integer: at), withAnimation: animation)
|
|
// self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: at))
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
public func addItem(item:TableRowItem, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Bool {
|
|
return self.insert(item: item, at: self.count, redraw: redraw, animation:animation)
|
|
}
|
|
|
|
public func insert(items:[TableRowItem], at:Int = 0, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void {
|
|
|
|
|
|
var current:Int = 0;
|
|
for item in items {
|
|
|
|
if(self.insert(item: item, at: at + current, redraw: false)) {
|
|
current += 1;
|
|
}
|
|
|
|
}
|
|
|
|
if(current != 0 && redraw) {
|
|
self.tableView.insertRows(at: IndexSet(integersIn: at ..< current + at), withAnimation: animation)
|
|
self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integersIn: at ..< current + at))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public var firstItem:TableRowItem? {
|
|
return self.list.first
|
|
}
|
|
|
|
public var lastItem:TableRowItem? {
|
|
return self.list.last
|
|
}
|
|
|
|
public func noteHeightOfRow(_ row:Int, _ animated:Bool = true) {
|
|
if !animated {
|
|
NSAnimationContext.current.duration = 0
|
|
NSAnimationContext.current.timingFunction = nil
|
|
}
|
|
tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row))
|
|
let item = self.item(at: row)
|
|
let height = item.heightValue
|
|
|
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: duration, curve: .easeOut) : .immediate
|
|
if let view = item.view {
|
|
view.set(item: item, animated: animated)
|
|
view.updateLayout(size: NSMakeSize(frame.width, height), transition: transition)
|
|
transition.updateFrame(view: view, frame: CGRect(origin: view.frame.origin, size: NSMakeSize(frame.width, height)))
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public func reloadData(row:Int, animated:Bool = false, options: NSTableView.AnimationOptions = .effectFade, presentAsNew: Bool = false) -> Void {
|
|
if let view = self.viewNecessary(at: row) {
|
|
let item = self.item(at: row)
|
|
|
|
if view.isKind(of: item.viewClass()) && !presentAsNew {
|
|
|
|
let height:CGFloat = item.heightValue
|
|
let width:CGFloat = self is HorizontalTableView ? item.width : frame.width
|
|
|
|
let rect = CGRect(origin: view.frame.origin, size: CGSize(width: width, height: height))
|
|
|
|
let animated = animated && view.canAnimateUpdate(item)
|
|
|
|
|
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: duration, curve: .easeOut) : .immediate
|
|
|
|
|
|
view.set(item: item, animated: animated)
|
|
view.updateLayout(size: rect.size, transition: transition)
|
|
transition.updateFrame(view: view, frame: rect)
|
|
NSAnimationContext.current.duration = animated ? duration : 0.0
|
|
NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row))
|
|
|
|
return
|
|
}
|
|
}
|
|
if let item = self.optionalItem(at: row) {
|
|
let animated = visibleRows().contains(row) && item.view != nil && animated
|
|
NSAnimationContext.current.duration = animated ? duration : 0.0
|
|
NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
self.tableView.beginUpdates()
|
|
self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none])
|
|
self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none])
|
|
self.tableView.endUpdates()
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate func reloadHeightItems() {
|
|
beginTableUpdates()
|
|
self.enumerateItems { item -> Bool in
|
|
if item.reloadOnTableHeightChanged {
|
|
self.reloadData(row: item.index)
|
|
}
|
|
return true
|
|
}
|
|
endTableUpdates()
|
|
}
|
|
|
|
public func fitToSize() {
|
|
}
|
|
|
|
|
|
public func moveItem(from:Int, to:Int, changeItem:TableRowItem? = nil, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void {
|
|
|
|
|
|
var item:TableRowItem = self.item(at:from);
|
|
let animation: NSTableView.AnimationOptions = animation != .none ? item.animatable ? animation : .none : .none
|
|
NSAnimationContext.current.duration = animation != .none ? duration : 0.0
|
|
NSAnimationContext.current.timingFunction = animation != .none ? CAMediaTimingFunction(name: .easeOut) : nil
|
|
|
|
if let change = changeItem {
|
|
assert(change.stableId == item.stableId)
|
|
change.table = self
|
|
self.listhash.removeValue(forKey: item.stableId)
|
|
self.listhash[change.stableId] = change
|
|
item = change
|
|
}
|
|
|
|
self.list.remove(at: from);
|
|
|
|
self.list.insert(item, at: to);
|
|
|
|
item._index = to
|
|
|
|
if(redraw) {
|
|
self.reloadData(row: to)
|
|
}
|
|
|
|
}
|
|
|
|
public func beginUpdates() -> Void {
|
|
updating = true
|
|
resetScroll(visibleRows())
|
|
self.previousScroll = nil
|
|
}
|
|
|
|
public func endUpdates() -> Void {
|
|
updating = false
|
|
resetScroll(visibleRows())
|
|
self.previousScroll = nil
|
|
}
|
|
|
|
public func rectOf(item:TableRowItem) -> NSRect {
|
|
let rect = self.tableView.rect(ofRow: item.index)
|
|
|
|
return CGRect(origin: CGPoint(x: round(rect.minX), y: round(rect.minY)), size: rect.size)
|
|
}
|
|
|
|
public func rectOf(index:Int) -> NSRect {
|
|
if index >= 0 && index < list.count {
|
|
return self.tableView.rect(ofRow: index)
|
|
}
|
|
return .zero
|
|
}
|
|
|
|
public func remove(at:Int, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void {
|
|
if at < count {
|
|
let item = self.list.remove(at: at);
|
|
self.listhash.removeValue(forKey: item.stableId)
|
|
|
|
viewNecessary(at: at)?.onRemove(animation)
|
|
|
|
item._index = nil
|
|
|
|
let animation: NSTableView.AnimationOptions = animation != .none ? item.animatable ? animation : .none : .none
|
|
NSAnimationContext.current.duration = animation == .none ? 0.0 : duration
|
|
NSAnimationContext.current.timingFunction = animation == .none ? nil : CAMediaTimingFunction(name: .easeOut)
|
|
|
|
if(redraw) {
|
|
self.tableView.removeRows(at: IndexSet(integer:at), withAnimation: animation)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func remove(range:Range<Int>, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void {
|
|
|
|
for i in range.lowerBound ..< range.upperBound {
|
|
remove(at: i, redraw: false)
|
|
}
|
|
|
|
if(redraw) {
|
|
self.tableView.removeRows(at: IndexSet(integersIn:range), withAnimation: animation != .none ? .effectFade : .none)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public func removeAll(redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void {
|
|
let count:Int = self.count;
|
|
self.list.removeAll()
|
|
self.listhash.removeAll()
|
|
|
|
if(redraw) {
|
|
self.tableView.removeRows(at: IndexSet(integersIn: 0 ..< count), withAnimation: animation != .none ? .effectFade : .none)
|
|
}
|
|
self.tableView.removeAllSubviews()
|
|
}
|
|
|
|
public func selectNext(_ scroll:Bool = true, _ animated:Bool = false, turnDirection: Bool = true) -> Void {
|
|
var previousInset: CGFloat = 0
|
|
if let hash = selectedhash {
|
|
let selectedItem = self.item(stableId: hash)
|
|
if let selectedItem = selectedItem {
|
|
previousInset = -selectedItem.heightValue
|
|
var selectedIndex = self.index(of: selectedItem)!
|
|
selectedIndex += 1
|
|
|
|
if selectedIndex == count {
|
|
if turnDirection {
|
|
selectedIndex = 0
|
|
} else {
|
|
selectedIndex = count - 1
|
|
}
|
|
}
|
|
if let delegate = delegate {
|
|
let sIndex = selectedIndex
|
|
for i in sIndex ..< list.count {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false) {
|
|
selectedIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
_ = select(item: item(at: selectedIndex))
|
|
}
|
|
|
|
|
|
} else {
|
|
if let delegate = delegate {
|
|
for item in list {
|
|
if delegate.selectionWillChange(row: item.index, item: item, byClick: false) {
|
|
_ = self.select(item: item)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
if let hash = selectedhash, scroll {
|
|
self.scroll(to: .top(id: hash, innerId: nil, animated: animated, focus: .init(focus: false), inset: 0), inset: NSEdgeInsets(), toVisible: true)
|
|
}
|
|
}
|
|
|
|
public func selectPrev(_ scroll:Bool = true, _ animated:Bool = false, turnDirection: Bool = true) -> Void {
|
|
var previousInset: CGFloat = 0
|
|
if let hash = selectedhash {
|
|
let selectedItem = self.item(stableId: hash)
|
|
if let selectedItem = selectedItem {
|
|
previousInset = selectedItem.heightValue
|
|
|
|
var selectedIndex = self.index(of: selectedItem)!
|
|
selectedIndex -= 1
|
|
|
|
if selectedIndex == -1 {
|
|
if turnDirection {
|
|
selectedIndex = count - 1
|
|
} else {
|
|
selectedIndex = 0
|
|
}
|
|
}
|
|
|
|
if let delegate = delegate {
|
|
let sIndex = selectedIndex
|
|
for i in stride(from: sIndex, to: -1, by: -1) {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false) {
|
|
selectedIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
_ = select(item: item(at: selectedIndex))
|
|
}
|
|
|
|
|
|
} else {
|
|
if let delegate = delegate {
|
|
for i in stride(from: list.count - 1, to: -1, by: -1) {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false) {
|
|
_ = self.select(item: item(at: i))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if let hash = selectedhash, scroll {
|
|
self.scroll(to: .bottom(id: hash, innerId: nil, animated: animated, focus: .init(focus: false), inset: 0), inset: NSEdgeInsets(), toVisible: true)
|
|
}
|
|
}
|
|
|
|
public func highlightNext(_ scroll:Bool = true, _ animated:Bool = false, turnDirection: Bool = true) -> Void {
|
|
var previousInset: CGFloat = 0
|
|
if let hash = highlitedHash {
|
|
let highlighteditem = self.item(stableId: hash)
|
|
|
|
if let highlighteditem = highlighteditem {
|
|
previousInset = -highlighteditem.heightValue
|
|
var selectedIndex = self.index(of: highlighteditem)!
|
|
selectedIndex += 1
|
|
|
|
if selectedIndex == count {
|
|
if turnDirection {
|
|
selectedIndex = 0
|
|
} else {
|
|
selectedIndex = count - 1
|
|
}
|
|
}
|
|
if let delegate = delegate {
|
|
let sIndex = selectedIndex
|
|
for i in sIndex ..< list.count {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false), selectedItem()?.index != i {
|
|
selectedIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = highlight(item: item(at: selectedIndex))
|
|
}
|
|
|
|
|
|
} else {
|
|
if let delegate = delegate {
|
|
for item in list {
|
|
if delegate.selectionWillChange(row: item.index, item: item, byClick: false), selectedItem()?.index != item.index {
|
|
_ = self.highlight(item: item)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
if let hash = highlitedHash, scroll {
|
|
self.scroll(to: .top(id: hash, innerId: nil, animated: animated, focus: .init(focus: false), inset: previousInset), inset: NSEdgeInsets(), toVisible: true)
|
|
}
|
|
}
|
|
|
|
public func highlightPrev(_ scroll:Bool = true, _ animated:Bool = false, turnDirection: Bool = true) -> Void {
|
|
var previousInset: CGFloat = 0
|
|
|
|
if let hash = highlitedHash {
|
|
let highlightedItem = self.item(stableId: hash)
|
|
if let highlightedItem = highlightedItem {
|
|
previousInset = highlightedItem.heightValue
|
|
var selectedIndex = self.index(of: highlightedItem)!
|
|
selectedIndex -= 1
|
|
|
|
if selectedIndex == -1 {
|
|
if turnDirection {
|
|
selectedIndex = count - 1
|
|
} else {
|
|
selectedIndex = 0
|
|
}
|
|
}
|
|
|
|
if let delegate = delegate {
|
|
let sIndex = selectedIndex
|
|
for i in stride(from: sIndex, to: -1, by: -1) {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false), selectedItem()?.index != i {
|
|
selectedIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = highlight(item: item(at: selectedIndex))
|
|
}
|
|
|
|
|
|
} else {
|
|
if let delegate = delegate {
|
|
for i in stride(from: list.count - 1, to: -1, by: -1) {
|
|
if delegate.selectionWillChange(row: i, item: item(at: i), byClick: false), selectedItem()?.index != i {
|
|
_ = self.highlight(item: item(at: i))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if let hash = highlitedHash, scroll {
|
|
self.scroll(to: .bottom(id: hash, innerId: nil, animated: animated, focus: .init(focus: false), inset: previousInset), inset: NSEdgeInsets(), toVisible: true)
|
|
}
|
|
}
|
|
|
|
|
|
public var isEmpty:Bool {
|
|
|
|
if let emptyChecker = emptyChecker {
|
|
return emptyChecker(self.list)
|
|
}
|
|
|
|
return self.list.isEmpty || (!tableView.isFlipped && list.count == 1)
|
|
}
|
|
|
|
public func reloadData(width: CGFloat? = nil) -> Void {
|
|
self.beginTableUpdates()
|
|
self.enumerateItems { item -> Bool in
|
|
_ = item.makeSize(width ?? frame.width)
|
|
self.reloadData(row: item.index)
|
|
return true
|
|
}
|
|
self.endTableUpdates()
|
|
}
|
|
|
|
public func reloadHeight() {
|
|
self.beginTableUpdates()
|
|
for index in 0 ..< list.count {
|
|
NSAnimationContext.current.duration = 0
|
|
NSAnimationContext.current.timingFunction = nil
|
|
tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: index))
|
|
}
|
|
self.endTableUpdates()
|
|
// self.tableView.tile()
|
|
}
|
|
|
|
public func item(at:Int) -> TableRowItem {
|
|
return self.list[at]
|
|
}
|
|
|
|
public func visibleRows(_ insetHeight:CGFloat = 0) -> NSRange {
|
|
//self.tableView.visibleRect
|
|
var rect = NSMakeRect(0, documentOffset.y, self.tableView.visibleRect.width, self.tableView.visibleRect.height).insetBy(dx: 0, dy: -insetHeight)
|
|
if insetHeight == 0, contentInsets.top > 0 {
|
|
rect.size.height -= contentInsets.top
|
|
}
|
|
let range = self.tableView.rows(in: rect)
|
|
return range
|
|
}
|
|
|
|
public var listHeight:CGFloat {
|
|
var height:CGFloat = 0
|
|
for item in list {
|
|
height += item.heightValue
|
|
}
|
|
return height
|
|
}
|
|
|
|
public func row(at point:NSPoint) -> Int {
|
|
return tableView.row(at: NSMakePoint(point.x, point.y - bottomInset))
|
|
}
|
|
|
|
public func viewNecessary(at row:Int, makeIfNecessary: Bool = false) -> TableRowView? {
|
|
if row < 0 || row >= count {
|
|
if row == -1000 {
|
|
return emptyView
|
|
}
|
|
return nil
|
|
}
|
|
if let resortView = self.resortController?.resortView {
|
|
if resortView.item?.stableId == self.item(at: row).stableId {
|
|
return resortView
|
|
}
|
|
}
|
|
return self.tableView.rowView(atRow: row, makeIfNecessary: makeIfNecessary) as? TableRowView
|
|
}
|
|
|
|
|
|
public func select(item:TableRowItem, notify:Bool = true, byClick:Bool = false) -> Bool {
|
|
|
|
if let delegate = delegate, delegate.isSelectable(row: item.index, item: item) {
|
|
if(self.item(stableId:item.stableId) != nil) {
|
|
if !notify || delegate.selectionWillChange(row: item.index, item: item, byClick: byClick) {
|
|
let new = item.stableId != selectedhash
|
|
if new {
|
|
self.cancelSelection();
|
|
}
|
|
self.selectedhash = item.stableId
|
|
self.cancelHighlight()
|
|
item.prepare(true)
|
|
self.reloadData(row:item.index, animated: true)
|
|
if notify {
|
|
delegate.selectionDidChange(row: item.index, item: item, byClick:byClick, isNew:new)
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
public func highlight(item:TableRowItem, notify:Bool = true, byClick:Bool = false) -> Bool {
|
|
|
|
if let delegate = delegate, delegate.isSelectable(row: item.index, item: item) {
|
|
if(self.item(stableId:item.stableId) != nil) {
|
|
if !notify || delegate.selectionWillChange(row: item.index, item: item, byClick: byClick) {
|
|
let new = item.stableId != selectedhash
|
|
if new {
|
|
self.cancelHighlight();
|
|
}
|
|
highlitedHash = item.stableId
|
|
item.prepare(true)
|
|
self.reloadData(row:item.index, animated: true)
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
|
|
}
|
|
|
|
public func changeSelection(stableId:AnyHashable?) {
|
|
if let stableId = stableId {
|
|
if let item = self.item(stableId: stableId) {
|
|
_ = self.select(item:item, notify:false)
|
|
} else {
|
|
cancelSelection()
|
|
self.selectedhash = stableId
|
|
}
|
|
} else {
|
|
cancelSelection()
|
|
}
|
|
}
|
|
|
|
public func cancelSelection() -> Void {
|
|
if let hash = selectedhash {
|
|
if let item = self.item(stableId: hash) {
|
|
item.prepare(false)
|
|
selectedhash = nil
|
|
self.reloadData(row:item.index, animated: true)
|
|
} else {
|
|
selectedhash = nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public func cancelHighlight() -> Void {
|
|
if let hash = highlitedHash {
|
|
if let item = self.item(stableId: hash) {
|
|
item.prepare(false)
|
|
highlitedHash = nil
|
|
self.reloadData(row: item.index, animated: true)
|
|
} else {
|
|
highlitedHash = nil
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
func rowView(item:TableRowItem) -> TableRowView {
|
|
let identifier:String = item.identifier
|
|
|
|
if let resortView = self.resortController?.resortView {
|
|
if resortView.item?.stableId == item.stableId {
|
|
return resortView
|
|
}
|
|
}
|
|
|
|
var view: NSView? = item.isUniqueView ? nil : self.tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier), owner: self.tableView)
|
|
|
|
if(view == nil) {
|
|
if let item = item as? TableStickItem, item.singletonItem {
|
|
view = TableRowView(frame: NSMakeRect(0, 0, frame.width, item.heightValue))
|
|
} else {
|
|
view = makeView(for: item)
|
|
}
|
|
view?.identifier = NSUserInterfaceItemIdentifier(rawValue: identifier)
|
|
}
|
|
if view!.frame.height != item.heightValue {
|
|
view?.setFrameSize(NSMakeSize(frame.width, item.heightValue))
|
|
}
|
|
return view as! TableRowView;
|
|
}
|
|
|
|
private func makeView(at index: Int) -> TableRowView {
|
|
let item = self.item(at: index)
|
|
let vz = item.viewClass() as! TableRowView.Type
|
|
let view = vz.init(frame:NSMakeRect(0, 0, frame.width, item.heightValue))
|
|
return view
|
|
}
|
|
private func makeView(for item: TableRowItem) -> TableRowView {
|
|
let vz = item.viewClass() as! TableRowView.Type
|
|
let view = vz.init(frame:NSMakeRect(0, 0, frame.width, item.heightValue))
|
|
return view
|
|
}
|
|
|
|
public func numberOfRows(in tableView: NSTableView) -> Int {
|
|
return self.count;
|
|
}
|
|
|
|
public func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
|
|
return max(self.item(at: row).heightValue, 1)
|
|
}
|
|
|
|
public func tableView(_ tableView: NSTableView, isGroupRow row: Int) -> Bool {
|
|
return false;
|
|
}
|
|
|
|
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
|
|
return nil
|
|
}
|
|
|
|
|
|
|
|
public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
|
let item: TableRowItem = self.item(at: row);
|
|
|
|
let view: TableRowView = self.rowView(item: item);
|
|
|
|
self.beforeSetupItem?(view, item)
|
|
|
|
view.set(item: item, animated: false)
|
|
view.updateLayout(size: view.frame.size, transition: .immediate)
|
|
self.afterSetupItem?(view, item)
|
|
|
|
|
|
return view
|
|
}
|
|
|
|
|
|
func visibleItems() -> [(TableRowItem,CGFloat,CGFloat)] { // item, top offset, bottom offset
|
|
|
|
var list:[(TableRowItem,CGFloat,CGFloat)] = []
|
|
|
|
let visible = visibleRows()
|
|
|
|
for i in visible.min ..< visible.max {
|
|
let item = self.item(at: i)
|
|
let rect = rectOf(index: i)
|
|
if !tableView.isFlipped {
|
|
let top = frame.height - (rect.minY - documentOffset.y) - rect.height
|
|
let bottom = (rect.minY - documentOffset.y)
|
|
list.append((item,top,bottom))
|
|
} else {
|
|
let top = rect.minY - documentOffset.y
|
|
let bottom = frame.height - (rect.minY - documentOffset.y) - rect.height
|
|
list.append((item,top,bottom))
|
|
//fatalError("not supported")
|
|
}
|
|
|
|
// list.append(item,)
|
|
}
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
func itemRects() -> [(TableRowItem, NSRect, Int)] {
|
|
var ilist:[(TableRowItem,NSRect,Int)] = [(TableRowItem,NSRect,Int)]()
|
|
|
|
for i in 0 ..< self.list.count {
|
|
ilist.append((item(at: i),self.rectOf(index: i), i))
|
|
|
|
}
|
|
|
|
return ilist;
|
|
|
|
}
|
|
|
|
public func beginTableUpdates() {
|
|
self.tableView.beginUpdates()
|
|
}
|
|
|
|
public func endTableUpdates() {
|
|
self.tableView.endUpdates()
|
|
}
|
|
|
|
public func stopMerge() {
|
|
mergeDisposable.set(nil)
|
|
mergePromise.set(.single(TableUpdateTransition(deleted: [], inserted: [], updated: [])))
|
|
}
|
|
|
|
public func startMerge() {
|
|
mergeDisposable.set((mergePromise.get() |> deliverOnMainQueue).start(next: { [weak self] transition in
|
|
self?.merge(with: transition)
|
|
}))
|
|
}
|
|
|
|
public func merge(with transition:Signal<TableUpdateTransition, NoError>) {
|
|
mergePromise.set(transition |> deliverOnMainQueue)
|
|
}
|
|
|
|
public var isBoundsAnimated: Bool {
|
|
return contentView.layer?.animation(forKey: "bounds") != nil
|
|
}
|
|
|
|
private var first:Bool = true
|
|
|
|
|
|
|
|
public func merge(with transition:TableUpdateTransition, appearAnimated: Bool = false) -> Void {
|
|
self.merge(with: transition, forceApply: false, appearAnimated: appearAnimated)
|
|
}
|
|
|
|
// private var awaitingTransitions: [TableUpdateTransition] = []
|
|
|
|
private var processedIds: Set<Int64> = Set()
|
|
private var firstSearchAppear = true
|
|
private var currentSearchState: SearchState?
|
|
|
|
private func enqueueAwaitingIfNeeded() {
|
|
// while !awaitingTransitions.isEmpty && !self.clipView.isAnimateScrolling {
|
|
// self.merge(with: awaitingTransitions.remove(at: 0), forceApply: true, appearAnimated: false)
|
|
// }
|
|
}
|
|
|
|
private func merge(with transition:TableUpdateTransition, forceApply: Bool, appearAnimated: Bool) -> Void {
|
|
|
|
|
|
if processedIds.contains(transition.uniqueId) {
|
|
return
|
|
}
|
|
self.processedIds.insert(transition.uniqueId)
|
|
|
|
assertOnMainThread()
|
|
assert(!updating)
|
|
|
|
if case .saveVisible = transition.state {
|
|
clipView.cancelScrolling()
|
|
}
|
|
|
|
resetScrollNotifies()
|
|
|
|
|
|
let previousList = self.list
|
|
|
|
let oldEmpty = self.isEmpty
|
|
|
|
self.beginUpdates()
|
|
|
|
let documentOffset = self.documentOffset
|
|
|
|
|
|
let visibleItems = self.visibleItems()
|
|
let visibleRange = self.visibleRows()
|
|
|
|
for (_, item) in list.enumerated() {
|
|
item._index = nil
|
|
}
|
|
|
|
|
|
|
|
var inserted:[(TableRowItem, NSTableView.AnimationOptions)] = []
|
|
var removed:[(Int, TableRowItem)] = []
|
|
|
|
if transition.groupInOne {
|
|
self.tableView.beginUpdates()
|
|
}
|
|
|
|
|
|
if transition.grouping && !transition.isEmpty, !transition.state.isNone, !transition.groupInOne {
|
|
self.tableView.beginUpdates()
|
|
}
|
|
|
|
for rdx in transition.deleted.reversed() {
|
|
let effect:NSTableView.AnimationOptions
|
|
if case let .none(interface) = transition.state, interface != nil {
|
|
effect = (visibleRange.indexIn(rdx) || !transition.animateVisibleOnly) ? .effectFade : .none
|
|
} else {
|
|
effect = transition.animated && (visibleRange.indexIn(rdx) || !transition.animateVisibleOnly) ? .effectFade : .none
|
|
}
|
|
if rdx < visibleRange.location {
|
|
removed.append((rdx, item(at: rdx)))
|
|
}
|
|
self.remove(at: rdx, redraw: true, animation:effect)
|
|
}
|
|
|
|
for (i, item) in list.enumerated() {
|
|
item._index = i
|
|
}
|
|
|
|
if transition.grouping && !transition.isEmpty, !transition.state.isNone, !transition.groupInOne {
|
|
self.tableView.endUpdates()
|
|
}
|
|
|
|
|
|
if transition.grouping && !transition.isEmpty, !transition.state.isNone, !transition.groupInOne {
|
|
self.tableView.beginUpdates()
|
|
}
|
|
|
|
for (idx, item) in transition.inserted {
|
|
let effect:NSTableView.AnimationOptions
|
|
if case let .none(interface) = transition.state, interface != nil {
|
|
effect = (visibleRange.indexIn(idx) || !transition.animateVisibleOnly) ? .effectFade : .none
|
|
} else {
|
|
effect = transition.animated && (visibleRange.indexIn(idx) || !transition.animateVisibleOnly) ? .effectFade : .none
|
|
}
|
|
_ = self.insert(item: item, at:idx, redraw: true, animation: effect)
|
|
|
|
if item.animatable {
|
|
inserted.append((item, effect))
|
|
}
|
|
}
|
|
|
|
for (i, item) in list.enumerated() {
|
|
item._index = i
|
|
}
|
|
|
|
if transition.grouping && !transition.isEmpty, !transition.state.isNone, !transition.groupInOne {
|
|
self.tableView.endUpdates()
|
|
}
|
|
|
|
|
|
for inserted in inserted {
|
|
var accept: Bool = true
|
|
let index = inserted.0.index
|
|
|
|
let item: TableRowItem?
|
|
|
|
if let current = removed.first(where: { $0.0 == index })?.1 {
|
|
item = current
|
|
} else if let current = transition.updated.first(where: { $0.0 == index })?.1 {
|
|
item = current
|
|
} else {
|
|
item = nil
|
|
}
|
|
|
|
if let item = item {
|
|
if object_getClassName(item) == object_getClassName(inserted.0) {
|
|
accept = false
|
|
}
|
|
}
|
|
if case let .none(interface) = transition.state, interface != nil {
|
|
accept = false
|
|
}
|
|
inserted.0.view?.onInsert(inserted.1, appearAnimated: appearAnimated && accept)
|
|
}
|
|
|
|
let state: TableScrollState
|
|
|
|
if case .none = transition.state, !transition.deleted.isEmpty || !transition.inserted.isEmpty {
|
|
state = transition.state
|
|
} else {
|
|
state = transition.state
|
|
}
|
|
|
|
|
|
func saveVisible(_ side: TableSavingSide) {
|
|
|
|
self.clipView.cancelScrolling()
|
|
var nrect:NSRect = NSZeroRect
|
|
|
|
let strideTo:StrideTo<Int>
|
|
|
|
var aroundIndex: AnyHashable?
|
|
|
|
if !tableView.isFlipped {
|
|
switch side {
|
|
case .lower:
|
|
strideTo = stride(from: 0, to: visibleItems.count, by: 1)
|
|
case .upper:
|
|
strideTo = stride(from: visibleItems.count - 1, to: -1, by: -1)
|
|
case .aroundIndex(let index):
|
|
aroundIndex = index
|
|
strideTo = stride(from: 0, to: visibleItems.count, by: 1)
|
|
}
|
|
} else {
|
|
switch side {
|
|
case .upper:
|
|
strideTo = stride(from: visibleItems.count - 1, to: -1, by: -1)
|
|
case .lower:
|
|
strideTo = stride(from: 0, to: visibleItems.count, by: 1)
|
|
case .aroundIndex(let index):
|
|
aroundIndex = index
|
|
strideTo = stride(from: 0, to: visibleItems.count, by: 1)
|
|
}
|
|
}
|
|
|
|
for i in strideTo {
|
|
let visible = visibleItems[i]
|
|
|
|
if let aroundIndex = aroundIndex {
|
|
if aroundIndex != visible.0.stableId {
|
|
continue
|
|
}
|
|
}
|
|
|
|
var item = self.item(stableId: visible.0.stableId)
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: visible.0.stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
if let item = item {
|
|
|
|
if !item.canBeAnchor {
|
|
continue
|
|
}
|
|
|
|
|
|
|
|
nrect = rectOf(item: item)
|
|
|
|
if let view = viewNecessary(at: i) {
|
|
if view.isInsertionAnimated {
|
|
break
|
|
}
|
|
}
|
|
|
|
let y:CGFloat
|
|
|
|
if !tableView.isFlipped {
|
|
y = nrect.minY - (frame.height - visible.1) + nrect.height
|
|
} else {
|
|
y = nrect.minY - visible.1
|
|
}
|
|
self.clipView.updateBounds(to: NSMakePoint(0, y), cancel: true)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
switch state {
|
|
case let .none(animation):
|
|
if !oldEmpty {
|
|
let result = animation?.animate(table:self, documentOffset: documentOffset, added: inserted.map{ $0.0 }, removed: removed.map { $0.1 }, previousRange: visibleRange)
|
|
if let result = result, result.isEmpty {
|
|
if !transition.isEmpty, contentView.bounds.minY > 0 {
|
|
saveVisible(.lower)
|
|
}
|
|
}
|
|
}
|
|
case .bottom, .top, .center:
|
|
self.scroll(to: transition.state, previousDocumentOffset: documentOffset)
|
|
case .up, .down, .upOffset:
|
|
self.scroll(to: transition.state, previousDocumentOffset: documentOffset)
|
|
case let .saveVisible(side):
|
|
saveVisible(side)
|
|
}
|
|
|
|
var nonAnimatedItems: [(Int, TableRowItem)] = []
|
|
var animatedItems: [(Int, TableRowItem)] = []
|
|
|
|
// var animated: Bool = transition.animated
|
|
// if case let .none(interface) = transition.state, interface != nil {
|
|
// animated = true
|
|
// }
|
|
for (index,item) in transition.updated {
|
|
var animated: Bool
|
|
animated = (visibleRange.indexIn(index) || !transition.animateVisibleOnly)
|
|
if case .saveVisible = state {
|
|
animated = transition.animated
|
|
} else if case .none(nil) = state {
|
|
animated = transition.animated
|
|
}
|
|
if animated {
|
|
animatedItems.append((index, item))
|
|
} else {
|
|
nonAnimatedItems.append((index, item))
|
|
}
|
|
}
|
|
|
|
|
|
|
|
let visible = self.visibleItems()
|
|
|
|
if !transition.groupInOne {
|
|
self.beginTableUpdates()
|
|
}
|
|
for (index, item) in nonAnimatedItems {
|
|
replace(item: item, at: index, animated: false)
|
|
}
|
|
if !transition.groupInOne {
|
|
self.endTableUpdates()
|
|
}
|
|
if !tableView.isFlipped, !nonAnimatedItems.isEmpty {
|
|
switch transition.state {
|
|
case .none, .top:
|
|
saveScrollState(visible)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
// self.beginTableUpdates()
|
|
for (index, item) in animatedItems {
|
|
replace(item: item, at: index, animated: true)
|
|
}
|
|
|
|
if transition.groupInOne {
|
|
self.tableView.endUpdates()
|
|
}
|
|
|
|
self.endUpdates()
|
|
|
|
|
|
|
|
self.updatedItems?(self.list)
|
|
|
|
// self.reflectScrolledClipView(self.clipView)
|
|
// self.tableView.tile()
|
|
|
|
if oldEmpty != isEmpty || first {
|
|
updateEmpties(animated: !first)
|
|
}
|
|
|
|
if let searchState = transition.searchState {
|
|
|
|
// if let scrollView = self.enclosingScrollView {
|
|
// scrollView.tile()
|
|
// scrollView.reflectScrolledClipView(scrollView.contentView)
|
|
// }
|
|
//
|
|
if self.searchView == nil {
|
|
self.searchView = TableSearchView(frame: NSMakeRect(0, -50, frame.width, 50))
|
|
addSubview(self.searchView!)
|
|
}
|
|
guard let searchView = self.searchView else {
|
|
return
|
|
}
|
|
switch searchState {
|
|
case let .none(updateState):
|
|
searchView.change(pos: NSMakePoint(0, -searchView.frame.height), animated: true)
|
|
searchView.searchView.cancel(true)
|
|
|
|
searchView.searchView.searchInteractions = SearchInteractions({ state, _ in
|
|
updateState(state)
|
|
}, { state in
|
|
updateState(state)
|
|
})
|
|
updateState(nil)
|
|
currentSearchState = nil
|
|
firstSearchAppear = true
|
|
case let .visible(data):
|
|
searchView.change(pos: NSZeroPoint, animated: true)
|
|
if firstSearchAppear {
|
|
searchView.applySearchResponder()
|
|
}
|
|
firstSearchAppear = false
|
|
searchView.updateDatas(data)
|
|
|
|
searchView.searchView.searchInteractions = SearchInteractions({ state, _ in
|
|
data.updateState(state)
|
|
}, { state in
|
|
data.updateState(state)
|
|
})
|
|
|
|
let searchState: SearchState = .init(state: searchView.searchView.state, request: searchView.searchView.query)
|
|
|
|
if searchState != self.currentSearchState {
|
|
self.currentSearchState = searchState
|
|
data.updateState(searchState)
|
|
}
|
|
}
|
|
} else {
|
|
self.searchView?.removeFromSuperview()
|
|
self.searchView = nil
|
|
}
|
|
|
|
first = false
|
|
performScrollEvent(transition.animated)
|
|
updateStickAfterScroll(transition.animated)
|
|
}
|
|
|
|
public func updateEmpties(animated: Bool = false) {
|
|
if let emptyItem = emptyItem {
|
|
if isEmpty {
|
|
if let empt = emptyView, !empt.isKind(of: emptyItem.viewClass()) || empt.item != emptyItem {
|
|
emptyView?.removeFromSuperview()
|
|
emptyView = nil
|
|
}
|
|
if emptyView == nil {
|
|
let vz = emptyItem.viewClass() as! TableRowView.Type
|
|
emptyView = vz.init(frame:bounds)
|
|
emptyView?.identifier = identifier
|
|
if animated, let emptyView = emptyView {
|
|
emptyView.layer?.animateAlpha(from: 0, to: 1, duration: duration)
|
|
}
|
|
}
|
|
|
|
if emptyView?.superview == nil {
|
|
addSubview(emptyView!)
|
|
}
|
|
|
|
if let emptyView = emptyView, let superview = superview {
|
|
emptyView.frame = findBackgroundControllerView?.bounds ?? bounds
|
|
emptyView.centerX(y: superview.frame.height - emptyView.frame.height)
|
|
|
|
if animated {
|
|
emptyView.layer?.animatePosition(from: emptyView.frame.origin.offsetBy(dx: 0, dy: 25), to: emptyView.frame.origin)
|
|
}
|
|
}
|
|
|
|
|
|
emptyView?.set(item: emptyItem)
|
|
emptyView?.needsLayout = true
|
|
|
|
tableView._change(opacity: 0, animated: animated)
|
|
} else {
|
|
if let emptyView = emptyView {
|
|
self.emptyView = nil
|
|
if animated {
|
|
emptyView.layer?.animateAlpha(from: 1, to: 0, duration: duration, removeOnCompletion: false, completion: { [weak emptyView] completed in
|
|
emptyView?.removeFromSuperview()
|
|
})
|
|
if animated {
|
|
emptyView.layer?.animatePosition(from: emptyView.frame.origin, to: emptyView.frame.origin.offsetBy(dx: 0, dy: 25), removeOnCompletion: false)
|
|
}
|
|
} else {
|
|
emptyView.removeFromSuperview()
|
|
}
|
|
}
|
|
tableView._change(opacity: 1, animated: animated)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public func replace(item:TableRowItem, at index:Int, animated:Bool) {
|
|
if index < count {
|
|
let prev = list[index]
|
|
listhash.removeValue(forKey: list[index].stableId)
|
|
list[index] = item
|
|
listhash[item.stableId] = item
|
|
item.table = self
|
|
item._index = index
|
|
reloadData(row: index, animated: animated, presentAsNew: false)
|
|
}
|
|
}
|
|
|
|
public func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView? {
|
|
|
|
if let supplyment = supplyment {
|
|
return supplyment.contentInteractionView(for: stableId, animateIn: animateIn)
|
|
}
|
|
var item = self.item(stableId: stableId)
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
|
|
if let item = item {
|
|
let view = viewNecessary(at:item.index)
|
|
if let view = view, !NSIsEmptyRect(view.visibleRect) {
|
|
return view.interactionContentView(for: stableId, animateIn: animateIn)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable) {
|
|
|
|
if let supplyment = supplyment {
|
|
return supplyment.interactionControllerDidFinishAnimation(interactive: interactive, for: stableId)
|
|
}
|
|
|
|
var item = self.item(stableId: stableId)
|
|
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
|
|
if let item = item {
|
|
let view = viewNecessary(at:item.index)
|
|
if let view = view, !NSIsEmptyRect(view.visibleRect) {
|
|
view.interactionControllerDidFinishAnimation(interactive: interactive, innerId: stableId)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView) {
|
|
|
|
if let supplyment = supplyment {
|
|
return supplyment.addAccesoryOnCopiedView(for: stableId, view: view)
|
|
}
|
|
|
|
var item = self.item(stableId: stableId)
|
|
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
|
|
if let item = item {
|
|
let rowView = viewNecessary(at:item.index)
|
|
if let rowView = rowView, !NSIsEmptyRect(view.visibleRect) {
|
|
rowView.addAccesoryOnCopiedView(innerId: stableId, view: view)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func videoTimebase(for stableId: AnyHashable) -> CMTimebase? {
|
|
var item = self.item(stableId: stableId)
|
|
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
|
|
if let item = item {
|
|
let view = viewNecessary(at:item.index)
|
|
if let view = view, !NSIsEmptyRect(view.visibleRect) {
|
|
return view.videoTimebase(for: stableId)
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
public func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) {
|
|
|
|
if let supplyment = supplyment {
|
|
return supplyment.applyTimebase(for: stableId, timebase: timebase)
|
|
}
|
|
|
|
var item = self.item(stableId: stableId)
|
|
|
|
if item == nil {
|
|
if let groupStableId = delegate?.findGroupStableId(for: stableId) {
|
|
item = self.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
|
|
if let item = item {
|
|
let view = viewNecessary(at:item.index)
|
|
if let view = view, !NSIsEmptyRect(view.visibleRect) {
|
|
view.applyTimebase(for: stableId, timebase: timebase)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func selectRow(index: Int) {
|
|
if self.count > index {
|
|
_ = self.select(item: self.item(at: index), byClick:true)
|
|
}
|
|
}
|
|
|
|
func longAction(index: Int) {
|
|
if self.count > index {
|
|
_ = self.delegate?.longSelect(row: index, item: self.list[index])
|
|
}
|
|
}
|
|
|
|
public override func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion:((Bool)->Void)? = nil) {
|
|
|
|
|
|
if animated {
|
|
|
|
if !tableView.isFlipped {
|
|
|
|
if self.frame.size != size {
|
|
let bounds = self.layer?.presentation()?.bounds ?? self.bounds
|
|
let y = (size.height - bounds.height)
|
|
|
|
self.layer?.animateBoundsOriginYAdditive(from: -y, to: 0, duration: duration)
|
|
|
|
|
|
if let layer = contentView.layer {
|
|
let animation = layer.makeAnimation(from: NSNumber(value: -y), to: NSNumber(value: 0), keyPath: "bounds.size.height", timingFunction: timingFunction, duration: duration, additive: true)
|
|
layer.add(animation, forKey: "height")
|
|
}
|
|
}
|
|
|
|
self.updateStickAfterScroll(animated)
|
|
return
|
|
}
|
|
}
|
|
|
|
super.change(size: size, animated: animated, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion)
|
|
self.updateStickAfterScroll(animated)
|
|
}
|
|
|
|
public func scroll(to state:TableScrollState, inset:NSEdgeInsets = NSEdgeInsets(), timingFunction: CAMediaTimingFunctionName = .spring, toVisible:Bool = false, ignoreLayerAnimation: Bool = false, previousDocumentOffset: CGPoint? = nil, completion: @escaping(Bool)->Void = { _ in }) {
|
|
|
|
var rowRect:NSRect = bounds
|
|
let documentOffset = previousDocumentOffset
|
|
|
|
let findItem:(AnyHashable)->TableRowItem? = { [weak self] stableId in
|
|
var item: TableRowItem? = self?.item(stableId: stableId)
|
|
if item == nil {
|
|
if let groupStableId = self?.delegate?.findGroupStableId(for: stableId) {
|
|
item = self?.item(stableId: groupStableId)
|
|
}
|
|
}
|
|
return item
|
|
}
|
|
|
|
var item:TableRowItem?
|
|
var animate:Bool = false
|
|
var focus: TableScrollFocus = .init(focus: false)
|
|
var relativeInset: CGFloat = 0
|
|
var innerId: AnyHashable? = nil
|
|
var addition: CGFloat = 0
|
|
switch state {
|
|
case let .center(stableId, _innerId, _animate, _focus, _inset):
|
|
item = findItem(stableId)
|
|
animate = _animate
|
|
relativeInset = _inset
|
|
focus = _focus
|
|
innerId = _innerId
|
|
case let .bottom(stableId, _innerId, _animate, _focus, _inset):
|
|
item = findItem(stableId)
|
|
animate = _animate
|
|
relativeInset = _inset
|
|
focus = _focus
|
|
innerId = _innerId
|
|
case let .top(stableId, _innerId, _animate, _focus, _inset):
|
|
item = findItem(stableId)
|
|
animate = _animate
|
|
relativeInset = _inset
|
|
focus = _focus
|
|
innerId = _innerId
|
|
addition = -contentInsets.top
|
|
case let .down(_animate):
|
|
animate = _animate
|
|
if !tableView.isFlipped {
|
|
rowRect.origin = NSZeroPoint
|
|
} else {
|
|
rowRect.origin = NSMakePoint(0, max(0,documentSize.height - frame.height))
|
|
}
|
|
case let .up(_animate):
|
|
animate = _animate
|
|
if !tableView.isFlipped {
|
|
rowRect.origin = NSMakePoint(0, max(documentSize.height,frame.height))
|
|
} else {
|
|
rowRect.origin = NSZeroPoint
|
|
}
|
|
case let .upOffset(_animate, offset):
|
|
animate = _animate
|
|
if !tableView.isFlipped {
|
|
rowRect.origin = NSMakePoint(0, max(documentSize.height,frame.height))
|
|
} else {
|
|
rowRect.origin = NSMakePoint(0, -contentInsets.top)
|
|
}
|
|
relativeInset = offset
|
|
default:
|
|
fatalError("for scroll to item, you can use only .top, center, .bottom enumeration")
|
|
}
|
|
|
|
let bottomInset = self.bottomInset != 0 ? (self.bottomInset) : 0
|
|
let height:CGFloat = self is HorizontalTableView ? frame.width : frame.height
|
|
|
|
let documentHeight = documentSize.height
|
|
|
|
if documentHeight < height {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if let item = item {
|
|
rowRect = self.rectOf(item: item)
|
|
var state = state
|
|
if case let .center(id, innerId, animated, focus, inset) = state, rowRect.height > height {
|
|
state = .top(id: id, innerId: innerId, animated: animated, focus: focus, inset: inset)
|
|
}
|
|
switch state {
|
|
case .bottom:
|
|
if tableView.isFlipped {
|
|
rowRect.origin.y -= (height - rowRect.height) - bottomInset
|
|
}
|
|
case .top:
|
|
// break
|
|
if !tableView.isFlipped {
|
|
rowRect.origin.y -= (height - rowRect.height) - bottomInset - contentInsets.top
|
|
}
|
|
case .center:
|
|
if !tableView.isFlipped {
|
|
rowRect.origin.y -= floorToScreenPixels(backingScaleFactor, (height - rowRect.height) / 2.0) - bottomInset
|
|
} else {
|
|
|
|
if rowRect.maxY > height/2.0 {
|
|
rowRect.origin.y -= floorToScreenPixels(backingScaleFactor, (height - rowRect.height) / 2.0) - bottomInset
|
|
} else {
|
|
rowRect.origin.y = 0
|
|
}
|
|
|
|
|
|
// fatalError("not implemented")
|
|
}
|
|
|
|
default:
|
|
fatalError("not implemented")
|
|
}
|
|
|
|
if toVisible {
|
|
let view = self.viewNecessary(at: item.index)
|
|
if let view = view, view.visibleRect.height == item.heightValue {
|
|
if focus.focus {
|
|
view.focusAnimation(innerId, text: focus.string)
|
|
focus.action?(view.interactableView)
|
|
}
|
|
completion(true)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
rowRect.origin.y = round(min(max(rowRect.minY + relativeInset, (!tableView.isFlipped ? 0 : -contentInsets.top)), documentSize.height - height) + inset.top)
|
|
|
|
if self.tableView.isFlipped {
|
|
rowRect.origin.y = min(rowRect.origin.y, documentSize.height - clipView.bounds.height)
|
|
}
|
|
if clipView.bounds.minY != rowRect.minY {
|
|
|
|
var applied = false
|
|
let scrollListener = TableScrollListener({ [weak self, weak item] position in
|
|
if let item = item, !applied {
|
|
DispatchQueue.main.async {
|
|
if let view = self?.viewNecessary(at: item.index), view.visibleRect.height > 10 {
|
|
applied = true
|
|
if focus.focus {
|
|
view.focusAnimation(innerId, text: focus.string)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
addScroll(listener: scrollListener)
|
|
|
|
let bounds = NSMakeRect(0, rowRect.minY + addition, clipView.bounds.width, clipView.bounds.height)
|
|
|
|
if animate {
|
|
clipView.scroll(to: bounds.origin, animated: animate, completion: { [weak self, weak item] completed in
|
|
if let `self` = self {
|
|
scrollListener.handler(self.scrollPosition().current)
|
|
self.removeScroll(listener: scrollListener)
|
|
completion(completed)
|
|
self.scrollDidChangedBounds()
|
|
if let view = item?.view?.interactableView {
|
|
focus.action?(view)
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
self.clipView.updateBounds(to: bounds.origin)
|
|
removeScroll(listener: scrollListener)
|
|
scrollListener.handler(self.scrollPosition().current)
|
|
}
|
|
} else {
|
|
if let item = item {
|
|
if focus.focus, let view = viewNecessary(at: item.index) {
|
|
view.focusAnimation(innerId, text: focus.string)
|
|
focus.action?(view.interactableView)
|
|
}
|
|
completion(true)
|
|
} else {
|
|
if let documentOffset = documentOffset, documentOffset != clipView.documentOffset {
|
|
clipView.scroll(to: documentOffset, animated: false)
|
|
clipView.scroll(to: rowRect.origin, animated: animate)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
open override func accessibilityParent() -> Any? {
|
|
return nil
|
|
}
|
|
|
|
open override func setFrameSize(_ newSize: NSSize) {
|
|
let visible = visibleItems()
|
|
let oldWidth = frame.width
|
|
let oldHeight = frame.height
|
|
super.setFrameSize(newSize)
|
|
|
|
if newSize.width > 0 || newSize.height > 0 {
|
|
if oldWidth != frame.width, newSize.width > 0 && newSize.height > 0 {
|
|
self.layoutIfNeeded(with: self.visibleRows(), oldWidth: oldWidth)
|
|
} else if oldHeight != frame.height {
|
|
self.reloadHeightItems()
|
|
}
|
|
}
|
|
|
|
//updateStickAfterScroll(false)
|
|
if oldWidth != newSize.width, !inLiveResize && newSize.width > 0 && newSize.height > 0 {
|
|
saveScrollState(visible)
|
|
}
|
|
}
|
|
|
|
public func setScrollHandler(_ handler: @escaping (_ scrollPosition:ScrollPosition) ->Void) -> Void {
|
|
|
|
scrollHandler = handler
|
|
|
|
}
|
|
|
|
|
|
public func enumerateItems(with callback:(TableRowItem)->Bool) {
|
|
for item in list {
|
|
if !callback(item) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
public func enumerateItems(reversed: Bool = false, with callback:(TableRowItem)->Bool) {
|
|
if reversed {
|
|
for item in list.reversed() {
|
|
if !callback(item) {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
for item in list {
|
|
if !callback(item) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func enumerateVisibleItems(reversed: Bool = false, inset: CGFloat = 0, with callback:(TableRowItem)->Bool) {
|
|
let visible = visibleRows(inset)
|
|
if reversed {
|
|
for i in stride(from: visible.location + visible.length - 1, to: visible.location - 1, by: -1) {
|
|
if !callback(list[i]) {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
for i in visible.location ..< visible.location + visible.length {
|
|
if !callback(list[i]) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public func enumerateViews(with callback:(TableRowView)->Bool) {
|
|
for index in 0 ..< list.count {
|
|
if let view = viewNecessary(at: index) {
|
|
if !callback(view) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func enumerateVisibleViews(with callback:(TableRowView)->Void, force: Bool = false) {
|
|
let visibleRows = self.visibleRows()
|
|
for index in visibleRows.location ..< visibleRows.location + visibleRows.length {
|
|
if let view = viewNecessary(at: index, makeIfNecessary: force) {
|
|
callback(view)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func performScrollEvent(_ animated: Bool = false) -> Void {
|
|
self.nextScrollEventIsAnimated = animated
|
|
self.resetScroll(visibleRows())
|
|
//NotificationCenter.default.post(name: NSView.boundsDidChangeNotification, object: self.contentView)
|
|
}
|
|
|
|
deinit {
|
|
mergeDisposable.dispose()
|
|
stickTimeoutDisposable.dispose()
|
|
}
|
|
|
|
|
|
public func sizeToFitIfNeeded() {
|
|
clipView.documentView?.setFrameSize(NSMakeSize(tableView.frame.width, min(tableView.frame.height, listHeight)))
|
|
clipView.needsLayout = true
|
|
}
|
|
|
|
}
|