395 lines
18 KiB
Swift
395 lines
18 KiB
Swift
//
|
|
// SectionViewController.swift
|
|
// TGUIKit
|
|
//
|
|
// Created by keepcoder on 03/08/2017.
|
|
// Copyright © 2017 Telegram. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import SwiftSignalKit
|
|
import AppKit
|
|
|
|
private final class SectionControllerArguments {
|
|
let select:(Int)->Void
|
|
init(select:@escaping(Int)->Void) {
|
|
self.select = select
|
|
}
|
|
}
|
|
|
|
public class SectionControllerView : View {
|
|
fileprivate let header: View = View()
|
|
fileprivate let selector:View = View()
|
|
fileprivate let container: View = View()
|
|
private weak var current: ViewController?
|
|
|
|
public var selectorIndex:Int = 0 {
|
|
didSet {
|
|
if selectorIndex != oldValue {
|
|
var index:Int = 0
|
|
for hContainer in header.subviews {
|
|
for t in hContainer.subviews {
|
|
if let t = t as? TextView {
|
|
t.update(TextViewLayout(.initialize(string: t.textLayout?.attributedString.string, color: selectorIndex == index ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle))
|
|
}
|
|
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
public required init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
addSubview(header)
|
|
addSubview(container)
|
|
addSubview(selector)
|
|
updateLocalizationAndTheme(theme: presentation)
|
|
needsLayout = true
|
|
}
|
|
|
|
fileprivate func layout(sections: [SectionControllerItem], selected: Int, hasHeaderView: Bool, arguments: SectionControllerArguments) {
|
|
header.removeAllSubviews()
|
|
header.isHidden = !hasHeaderView
|
|
self.selectorIndex = selected
|
|
for i in 0 ..< sections.count {
|
|
let section = sections[i]
|
|
let headerContainer = Control(frame: NSMakeRect(0, 0, 0, 50))
|
|
let title: TextView = TextView()
|
|
title.isSelectable = false
|
|
title.userInteractionEnabled = false
|
|
title.backgroundColor = presentation.colors.background
|
|
title.update(TextViewLayout(.initialize(string: section.title(), color: i == selected ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle))
|
|
headerContainer.addSubview(title)
|
|
header.addSubview(headerContainer)
|
|
headerContainer.border = [.Bottom]
|
|
headerContainer.set(handler: { _ in
|
|
arguments.select(i)
|
|
}, for: .Click)
|
|
}
|
|
needsLayout = true
|
|
}
|
|
|
|
|
|
fileprivate func select(controller: ViewController, index: Int, animated: Bool, notifyApper: Bool = true) {
|
|
let previousIndex = self.selectorIndex
|
|
let previous = self.current
|
|
self.current = controller
|
|
selectorIndex = index
|
|
|
|
controller.view.frame = container.bounds
|
|
if notifyApper {
|
|
previous?.viewWillDisappear(animated)
|
|
}
|
|
|
|
let duration: Double = 0.2
|
|
|
|
container.addSubview(controller.view)
|
|
|
|
if animated {
|
|
CATransaction.begin()
|
|
let container = header.subviews[index]
|
|
selector.change(pos: NSMakePoint(container.frame.minX, selector.frame.minY), animated: animated, duration: duration, timingFunction: .spring)
|
|
|
|
|
|
let pto: NSPoint
|
|
let nfrom: NSPoint
|
|
|
|
|
|
if previousIndex < index {
|
|
pto = NSMakePoint(-container.frame.width, 0)
|
|
nfrom = NSMakePoint(container.frame.width, 0)
|
|
} else {
|
|
pto = NSMakePoint(container.frame.width, 0)
|
|
nfrom = NSMakePoint(-container.frame.width, 0)
|
|
}
|
|
|
|
previous?.view._change(pos: pto, animated: animated, duration: duration, timingFunction: .spring, completion: { [weak previous, weak controller] complete in
|
|
if complete {
|
|
previous?.view.removeFromSuperview()
|
|
previous?.viewDidDisappear(animated)
|
|
controller?.viewDidAppear(animated)
|
|
}
|
|
})
|
|
controller.view.layer?.animatePosition(from: nfrom, to: NSZeroPoint, duration: duration, timingFunction: .spring, removeOnCompletion: true)
|
|
CATransaction.commit()
|
|
} else {
|
|
container.removeAllSubviews()
|
|
previous?.viewDidDisappear(animated)
|
|
container.addSubview(controller.view)
|
|
controller.viewDidAppear(animated)
|
|
}
|
|
needsLayout = true
|
|
}
|
|
|
|
public override func updateLocalizationAndTheme(theme: PresentationTheme) {
|
|
super.updateLocalizationAndTheme(theme: theme)
|
|
self.backgroundColor = presentation.colors.background
|
|
selector.backgroundColor = presentation.colors.accent
|
|
container.backgroundColor = presentation.colors.background
|
|
var index:Int = 0
|
|
for hContainer in header.subviews {
|
|
hContainer.background = presentation.colors.background
|
|
for t in hContainer.subviews {
|
|
if let t = t as? TextView {
|
|
let layout = TextViewLayout(.initialize(string: t.textLayout?.attributedString.string, color: selectorIndex == index ? presentation.colors.accent : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle)
|
|
layout.measure(width: hContainer.frame.width - 20)
|
|
t.update(layout)
|
|
t.center()
|
|
}
|
|
|
|
t.background = presentation.colors.background
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
public override func layout() {
|
|
super.layout()
|
|
header.setFrameSize(NSMakeSize(frame.width, header.isHidden ? 0 : 50.0))
|
|
|
|
let width = floorToScreenPixels(backingScaleFactor, frame.width / CGFloat(max(header.subviews.count, 3)))
|
|
|
|
selector.frame = NSMakeRect(CGFloat(selectorIndex) * width, header.frame.height - .borderSize, width, .borderSize)
|
|
container.frame = NSMakeRect(0, header.frame.maxY, frame.width, frame.height - header.frame.height)
|
|
container.subviews.first?.frame = container.bounds
|
|
|
|
var x:CGFloat = 0
|
|
for i in 0 ..< header.subviews.count {
|
|
let hContainer = header.subviews[i]
|
|
let width = i == header.subviews.count - 1 ? frame.width - x : width
|
|
hContainer.frame = NSMakeRect(x, 0, width, hContainer.frame.height)
|
|
if let textView = hContainer.subviews.first as? TextView {
|
|
textView.textLayout?.measure(width: width - 10)
|
|
textView.update(textView.textLayout)
|
|
textView.center()
|
|
}
|
|
x += width
|
|
}
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
public class SectionControllerItem {
|
|
let title: ()->String
|
|
public let controller: ViewController
|
|
public init(title: @escaping()->String, controller: ViewController) {
|
|
self.title = title
|
|
self.controller = controller
|
|
}
|
|
}
|
|
|
|
|
|
open class SectionViewController: GenericViewController<SectionControllerView> {
|
|
|
|
private var sections:[SectionControllerItem] = []
|
|
public var selectedSection:SectionControllerItem
|
|
public private(set) var selectedIndex: Int = -1
|
|
private let disposable = MetaDisposable()
|
|
|
|
public var selectionUpdateHandler:((Int)->Void)?
|
|
|
|
public func addSection(_ section: SectionControllerItem) {
|
|
sections.append(section)
|
|
}
|
|
|
|
public override func updateLocalizationAndTheme(theme: PresentationTheme) {
|
|
let arguments = SectionControllerArguments { [weak self] index in
|
|
self?.select(index, true)
|
|
}
|
|
genericView.layout(sections: sections, selected: selectedIndex, hasHeaderView: self.hasHeaderView, arguments: arguments)
|
|
genericView.updateLocalizationAndTheme(theme: theme)
|
|
}
|
|
|
|
deinit {
|
|
disposable.dispose()
|
|
}
|
|
|
|
public func select(_ index:Int, _ animated: Bool, notifyApper: Bool = true) {
|
|
if selectedIndex != index || !animated {
|
|
selectedSection = sections[index]
|
|
let controller = sections[index].controller
|
|
controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height + 50)
|
|
controller.loadViewIfNeeded()
|
|
selectedIndex = index
|
|
selectionUpdateHandler?(index)
|
|
if notifyApper {
|
|
controller.viewWillAppear(animated)
|
|
}
|
|
disposable.set((sections[index].controller.ready.get() |> filter {$0} |> take(1)).start(next: { [weak self, weak controller] ready in
|
|
if let strongSelf = self, let controller = controller {
|
|
strongSelf.genericView.select(controller: controller, index: index, animated: animated, notifyApper: notifyApper)
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
override public func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
selectedSection.controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50)
|
|
selectedSection.controller.viewWillAppear(animated)
|
|
}
|
|
override public func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
selectedSection.controller.viewWillDisappear(animated)
|
|
window?.remove(object: self, for: .Tab)
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
selectedSection.controller.viewDidAppear(animated)
|
|
|
|
window?.set(handler: { [weak self] _ -> KeyHandlerResult in
|
|
guard let `self` = self else {return .rejected}
|
|
|
|
if !self.sections.isEmpty {
|
|
var index:Int = self.selectedIndex
|
|
if index == self.sections.count - 1 {
|
|
index = 0
|
|
} else {
|
|
index += 1
|
|
}
|
|
self.select(index, true)
|
|
}
|
|
|
|
return .invoked
|
|
}, with: self, for: .Tab, priority: .high)
|
|
|
|
|
|
window?.add(swipe: { [weak self] direction, _ -> SwipeHandlerResult in
|
|
guard let `self` = self, !self.sections.isEmpty else {return .nothing}
|
|
|
|
if !self.selectedSection.controller.supportSwipes {
|
|
return .nothing
|
|
}
|
|
|
|
switch direction {
|
|
case let .left(state):
|
|
|
|
switch state {
|
|
case .start:
|
|
if self.selectedIndex > 0 {
|
|
let new = self.sections[self.selectedIndex - 1].controller
|
|
new._frameRect = self.genericView.container.bounds
|
|
new.view.frame = self.genericView.container.bounds
|
|
new.viewWillAppear(false)
|
|
self.genericView.container.addSubview(new.view, positioned: .below, relativeTo: self.selectedSection.controller.view)
|
|
|
|
return .success(new)
|
|
}
|
|
case let .swiping(delta, controller):
|
|
|
|
let delta = min(max(0, delta), controller.frame.width)
|
|
|
|
// self.genericView.selector.change(pos: NSMakePoint(container.frame.minX, genericView.selector.frame.minY), animated: animated, timingFunction: CAMediaTimingFunctionName.spring)
|
|
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
|
|
|
|
self.genericView.selector.setFrameOrigin(NSMakePoint(selectorFrame.minX - selectorFrame.width * (delta / controller.frame.width), self.genericView.selector.frame.minY))
|
|
self.selectedSection.controller.frame = NSMakeRect(delta, self.selectedSection.controller.frame.minY, self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.height)
|
|
controller.frame = NSMakeRect(delta - controller.frame.width, controller.frame.minY, controller.frame.width, controller.frame.height)
|
|
return .deltaUpdated(available: delta)
|
|
case let .success(_, controller):
|
|
controller.view._change(pos: NSMakePoint(0, controller.frame.minY), animated: true)
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex - 1].frame
|
|
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: animated)
|
|
self.selectedSection.controller.view._change(pos: NSMakePoint(self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak self] completed in
|
|
if completed, let index = self?.sections.lastIndex(where: {$0.controller == controller}) {
|
|
self?.select(index, false, notifyApper: false)
|
|
}
|
|
})
|
|
case let .failed(_, controller):
|
|
controller.view._change(pos: NSMakePoint(-controller.frame.width, controller.frame.minY), animated: true)
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
|
|
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: animated)
|
|
self.selectedSection.controller.view._change(pos: NSMakePoint(0, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak controller] completed in
|
|
if completed {
|
|
controller?.removeFromSuperview()
|
|
}
|
|
})
|
|
}
|
|
|
|
break
|
|
case let .right(state):
|
|
switch state {
|
|
case .start:
|
|
if self.selectedIndex < self.sections.count - 1 {
|
|
let new = self.sections[self.selectedIndex + 1].controller
|
|
new._frameRect = self.genericView.container.bounds
|
|
new.view.frame = self.genericView.container.bounds
|
|
new.viewWillAppear(false)
|
|
self.genericView.container.addSubview(new.view, positioned: .below, relativeTo: self.selectedSection.controller.view)
|
|
return .success(new)
|
|
}
|
|
case let .swiping(delta, controller):
|
|
|
|
let delta = min(max(0, delta), controller.frame.width)
|
|
|
|
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
|
|
self.genericView.selector.setFrameOrigin(NSMakePoint(selectorFrame.minX + selectorFrame.width * ( delta / controller.frame.width), self.genericView.selector.frame.minY))
|
|
|
|
self.selectedSection.controller.frame = NSMakeRect(-delta, self.selectedSection.controller.frame.minY, self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.height)
|
|
controller.frame = NSMakeRect(controller.frame.width - delta, controller.frame.minY, controller.frame.width, controller.frame.height)
|
|
return .deltaUpdated(available: delta)
|
|
case let .success(_, controller):
|
|
controller.view._change(pos: NSMakePoint(0, controller.frame.minY), animated: true)
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex + 1].frame
|
|
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: true)
|
|
self.selectedSection.controller.view._change(pos: NSMakePoint(-self.selectedSection.controller.frame.width, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak self] completed in
|
|
if completed, let index = self?.sections.lastIndex(where: {$0.controller == controller}) {
|
|
self?.select(index, false, notifyApper: false)
|
|
}
|
|
})
|
|
case let .failed(_, controller):
|
|
controller.view._change(pos: NSMakePoint(controller.frame.width, controller.frame.minY), animated: true)
|
|
let selectorFrame = self.genericView.header.subviews[self.selectedIndex].frame
|
|
self.genericView.selector.change(pos: NSMakePoint(selectorFrame.minX, self.genericView.selector.frame.minY), animated: true)
|
|
self.selectedSection.controller.view._change(pos: NSMakePoint(0, self.selectedSection.controller.frame.minY), animated: true, completion: { [weak controller] completed in
|
|
if completed {
|
|
controller?.removeFromSuperview()
|
|
}
|
|
})
|
|
}
|
|
case .none:
|
|
break
|
|
}
|
|
|
|
return .nothing
|
|
|
|
}, with: self.genericView, identifier: SwipeIdentifier("section-swipe"))
|
|
|
|
}
|
|
override public func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
selectedSection.controller.viewDidDisappear(animated)
|
|
}
|
|
|
|
open override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
let arguments = SectionControllerArguments { [weak self] index in
|
|
self?.select(index, true)
|
|
}
|
|
genericView.layout(sections: sections, selected: selectedIndex, hasHeaderView: self.hasHeaderView, arguments: arguments)
|
|
select(selectedIndex, false)
|
|
|
|
self.ready.set(sections[selectedIndex].controller.ready.get())
|
|
}
|
|
|
|
private let hasHeaderView: Bool
|
|
|
|
public init(sections: [SectionControllerItem], selected: Int = 0, hasHeaderView: Bool = true, hasBar: Bool = false) {
|
|
assert(!sections.isEmpty)
|
|
self.sections = sections
|
|
self.selectedSection = sections[selected]
|
|
self.selectedIndex = selected
|
|
self.hasHeaderView = hasHeaderView
|
|
super.init()
|
|
bar = .init(height: hasBar ? 50 : 0)
|
|
}
|
|
|
|
}
|