iPad trackpad and key shortcuts improvements

This commit is contained in:
Ilya Laktyushin 2022-07-30 03:51:46 +03:00
parent 6c4a730b42
commit 5b8961d02a
46 changed files with 1035 additions and 83 deletions

View File

@ -7942,3 +7942,13 @@ Sorry for the inconvenience.";
"StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack"; "StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack";
"StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs"; "StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs";
"KeyCommand.LockWithPasscode" = "Lock with Passcode";
"KeyCommand.Play" = "Play Video";
"KeyCommand.Pause" = "Pause Video";
"KeyCommand.SeekBackward" = "Seek Backward";
"KeyCommand.SeekForward" = "Seek Forward";
"KeyCommand.Share" = "Share";
"KeyCommand.SwitchToPIP" = "Switch to Picture-in-Picture";
"KeyCommand.EnterFullscreen" = "Enter Fullscreen";
"KeyCommand.ExitFullscreen" = "Exit Fullscreen";

View File

@ -25,6 +25,8 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled self.gestureRecognizer?.isEnabled = self.sendButtonLongPressEnabled
} }
} }
private var sendButtonPointerInteraction: PointerInteraction?
private var validLayout: CGSize? private var validLayout: CGSize?
@ -39,7 +41,7 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
self.backgroundNode.clipsToBounds = true self.backgroundNode.clipsToBounds = true
self.sendButton = HighlightTrackingButtonNode(pointerStyle: .lift) self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
self.textNode = ImmediateTextNode() self.textNode = ImmediateTextNode()
self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor) self.textNode.attributedText = NSAttributedString(string: self.strings.MediaPicker_Send, font: Font.semibold(17.0), textColor: theme.chat.inputPanel.actionControlForegroundColor)
@ -90,6 +92,8 @@ final class AttachmentTextInputActionButtonsNode: ASDisplayNode {
strongSelf.sendButtonLongPressed?(strongSelf.sendContainerNode, recognizer) strongSelf.sendButtonLongPressed?(strongSelf.sendContainerNode, recognizer)
} }
} }
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift)
} }
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {

View File

@ -2424,10 +2424,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.composePressed() strongSelf.composePressed()
} }
}), }),
KeyShortcut(title: strings.KeyCommand_LockWithPasscode, input: "L", modifiers: [.command], action: { [weak self] in
if let strongSelf = self {
strongSelf.context.sharedContext.appLockContext.lock()
}
}),
KeyShortcut(title: strings.KeyCommand_Find, input: "\t", modifiers: [], action: toggleSearch), KeyShortcut(title: strings.KeyCommand_Find, input: "\t", modifiers: [], action: toggleSearch),
KeyShortcut(input: UIKeyCommand.inputEscape, modifiers: [], action: toggleSearch) KeyShortcut(input: UIKeyCommand.inputEscape, modifiers: [], action: toggleSearch)
] ]
let openTab: (Int) -> Void = { [weak self] index in
if let strongSelf = self {
let filters = strongSelf.chatListDisplayNode.containerNode.availableFilters
if index > filters.count - 1 {
return
}
switch filters[index] {
case .all:
strongSelf.selectTab(id: .all)
case let .filter(filter):
strongSelf.selectTab(id: .filter(filter.id))
}
}
}
let openChat: (Int) -> Void = { [weak self] index in let openChat: (Int) -> Void = { [weak self] index in
if let strongSelf = self { if let strongSelf = self {
if index == 0 { if index == 0 {
@ -2438,13 +2458,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
} }
let chatShortcuts: [KeyShortcut] = (0 ... 9).map { index in let folderShortcuts: [KeyShortcut] = (0 ... 9).map { index in
return KeyShortcut(input: "\(index)", modifiers: [.command], action: { return KeyShortcut(input: "\(index)", modifiers: [.command], action: {
if index == 0 {
openChat(0)
} else {
openTab(index - 1)
}
})
}
let chatShortcuts: [KeyShortcut] = (0 ... 9).map { index in
return KeyShortcut(input: "\(index)", modifiers: [.command, .alternate], action: {
openChat(index) openChat(index)
}) })
} }
return inputShortcuts + chatShortcuts return inputShortcuts + folderShortcuts + chatShortcuts
} }
override public func toolbarActionSelected(action: ToolbarActionOption) { override public func toolbarActionSelected(action: ToolbarActionOption) {

View File

@ -88,6 +88,8 @@ private final class ItemNode: ASDisplayNode {
private var theme: PresentationTheme? private var theme: PresentationTheme?
private var pointerInteraction: PointerInteraction?
init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) { init(pressed: @escaping (Bool) -> Void, requestedDeletion: @escaping () -> Void, contextGesture: @escaping (ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void) {
self.pressed = pressed self.pressed = pressed
self.requestedDeletion = requestedDeletion self.requestedDeletion = requestedDeletion
@ -189,6 +191,12 @@ private final class ItemNode: ASDisplayNode {
} }
} }
override func didLoad() {
super.didLoad()
self.pointerInteraction = PointerInteraction(view: self.containerNode.view, customInteractionView: nil, style: .insetRectangle(-10.0, 4.0))
}
@objc private func buttonPressed() { @objc private func buttonPressed() {
self.pressed(self.isDisabled) self.pressed(self.isDisabled)
} }

View File

@ -2106,7 +2106,7 @@ public final class ChatListNode: ListView {
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { update in |> deliverOnMainQueue).start(next: { update in
let entries = update.view.entries let entries = update.view.entries
if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[10 - index - 1] { if entries.count > index, case let .MessageEntry(index, _, _, _, _, renderedPeer, _, _, _, _) = entries[9 - index - 1] {
let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter) let location: ChatListNodeLocation = .scroll(index: index, sourceIndex: .absoluteLowerBound, scrollPosition: .center(.top), animated: true, filter: filter)
self.setChatListLocation(location) self.setChatListLocation(location)
self.peerSelected?(EnginePeer(renderedPeer.peer!), false, false, nil) self.peerSelected?(EnginePeer(renderedPeer.peer!), false, false, nil)

View File

@ -405,11 +405,10 @@ public class ContactsController: ViewController {
let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context) let controller = strongSelf.context.sharedContext.makePeersNearbyController(context: strongSelf.context)
controller.navigationPresentation = .master controller.navigationPresentation = .master
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller, animated: true, completion: { [weak self] in var controllers = navigationController.viewControllers.filter { !($0 is PermissionController) }
if let strongSelf = self { controllers.append(controller)
strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) navigationController.setViewControllers(controllers, animated: true)
} strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true)
})
} }
} }

View File

@ -2205,6 +2205,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
return self.dismissNode.view return self.dismissNode.view
} }
fileprivate func performHighlightedAction() {
self.presentationNode?.highlightGestureFinished(performAction: true)
}
fileprivate func decreaseHighlightedIndex() {
self.presentationNode?.decreaseHighlightedIndex()
}
fileprivate func increaseHighlightedIndex() {
self.presentationNode?.increaseHighlightedIndex()
}
} }
public final class ContextControllerLocationViewInfo { public final class ContextControllerLocationViewInfo {
@ -2350,7 +2362,7 @@ public protocol ContextControllerItemsContent: AnyObject {
) -> ContextControllerItemsNode ) -> ContextControllerItemsNode
} }
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol { public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder {
public struct Items { public struct Items {
public enum Content { public enum Content {
case list([ContextMenuItem]) case list([ContextMenuItem])
@ -2644,4 +2656,44 @@ public final class ContextController: ViewController, StandalonePresentableContr
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
self.controllerNode.addRelativeContentOffset(offset, transition: transition) self.controllerNode.addRelativeContentOffset(offset, transition: transition)
} }
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismissWithoutContent()
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismissWithoutContent()
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
self?.controllerNode.performHighlightedAction()
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.decreaseHighlightedIndex()
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.increaseHighlightedIndex()
}
)
]
}
} }

View File

@ -21,6 +21,9 @@ public protocol ContextControllerActionsStackItemNode: ASDisplayNode {
func highlightGestureMoved(location: CGPoint) func highlightGestureMoved(location: CGPoint)
func highlightGestureFinished(performAction: Bool) func highlightGestureFinished(performAction: Bool)
func decreaseHighlightedIndex()
func increaseHighlightedIndex()
} }
public protocol ContextControllerActionsStackItem: AnyObject { public protocol ContextControllerActionsStackItem: AnyObject {
@ -585,6 +588,34 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
} }
} }
} }
func decreaseHighlightedIndex() {
let previousHighlightedItemNode: Item? = self.highlightedItemNode
if let highlightedItemNode = self.highlightedItemNode, let index = self.itemNodes.firstIndex(where: { $0 === highlightedItemNode }) {
self.highlightedItemNode = self.itemNodes[max(0, index - 1)]
} else {
self.highlightedItemNode = self.itemNodes.first
}
if previousHighlightedItemNode !== self.highlightedItemNode {
previousHighlightedItemNode?.node.updateIsHighlighted(isHighlighted: false)
self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true)
}
}
func increaseHighlightedIndex() {
let previousHighlightedItemNode: Item? = self.highlightedItemNode
if let highlightedItemNode = self.highlightedItemNode, let index = self.itemNodes.firstIndex(where: { $0 === highlightedItemNode }) {
self.highlightedItemNode = self.itemNodes[min(self.itemNodes.count - 1, index + 1)]
} else {
self.highlightedItemNode = self.itemNodes.first
}
if previousHighlightedItemNode !== self.highlightedItemNode {
previousHighlightedItemNode?.node.updateIsHighlighted(isHighlighted: false)
self.highlightedItemNode?.node.updateIsHighlighted(isHighlighted: true)
}
}
} }
private let items: [ContextMenuItem] private let items: [ContextMenuItem]
@ -663,6 +694,12 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
func highlightGestureFinished(performAction: Bool) { func highlightGestureFinished(performAction: Bool) {
} }
func decreaseHighlightedIndex() {
}
func increaseHighlightedIndex() {
}
} }
private let content: ContextControllerItemsContent private let content: ContextControllerItemsContent
@ -925,6 +962,14 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
} }
self.node.highlightGestureFinished(performAction: performAction) self.node.highlightGestureFinished(performAction: performAction)
} }
func decreaseHighlightedIndex() {
self.node.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.node.increaseHighlightedIndex()
}
} }
private let getController: () -> ContextControllerProtocol? private let getController: () -> ContextControllerProtocol?
@ -1229,6 +1274,18 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
} }
} }
func decreaseHighlightedIndex() {
if let topItemContainer = self.itemContainers.last {
topItemContainer.decreaseHighlightedIndex()
}
}
func increaseHighlightedIndex() {
if let topItemContainer = self.itemContainers.last {
topItemContainer.increaseHighlightedIndex()
}
}
func updatePanSelection(isEnabled: Bool) { func updatePanSelection(isEnabled: Bool) {
if let selectionPanGesture = self.selectionPanGesture { if let selectionPanGesture = self.selectionPanGesture {
selectionPanGesture.isEnabled = isEnabled selectionPanGesture.isEnabled = isEnabled

View File

@ -315,6 +315,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
} }
} }
func decreaseHighlightedIndex() {
self.actionsStackNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.actionsStackNode.increaseHighlightedIndex()
}
func replaceItems(items: ContextController.Items, animated: Bool) { func replaceItems(items: ContextController.Items, animated: Bool) {
self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated) self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items), animated: animated)
} }

View File

@ -31,5 +31,8 @@ protocol ContextControllerPresentationNode: ASDisplayNode {
func highlightGestureMoved(location: CGPoint, hover: Bool) func highlightGestureMoved(location: CGPoint, hover: Bool)
func highlightGestureFinished(performAction: Bool) func highlightGestureFinished(performAction: Bool)
func decreaseHighlightedIndex()
func increaseHighlightedIndex()
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition)
} }

View File

@ -87,13 +87,7 @@ public class ActionSheetButtonNode: ActionSheetItemNode {
self.button.highligthedChanged = { [weak self] highlighted in self.button.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self { if let strongSelf = self {
if highlighted { strongSelf.setHighlighted(highlighted, animated: true)
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
} else {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
})
}
} }
} }
@ -104,16 +98,35 @@ public class ActionSheetButtonNode: ActionSheetItemNode {
} }
} }
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
self.highlightedUpdated(highlighted)
if highlighted {
self.backgroundNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
} else {
if animated {
UIView.animate(withDuration: 0.3, animations: {
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
})
} else {
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
}
}
}
override func performAction() {
self.buttonPressed()
}
public override func didLoad() { public override func didLoad() {
super.didLoad() super.didLoad()
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor strongSelf.setHighlighted(true, animated: false)
} }
}, willExit: { [weak self] in }, willExit: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor strongSelf.setHighlighted(false, animated: false)
} }
}) })
} }

View File

@ -6,7 +6,7 @@ public protocol ActionSheetGroupOverlayNode: ASDisplayNode {
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
} }
open class ActionSheetController: ViewController, PresentableController, StandalonePresentableController { open class ActionSheetController: ViewController, PresentableController, StandalonePresentableController, KeyShortcutResponder {
private var actionSheetNode: ActionSheetControllerNode { private var actionSheetNode: ActionSheetControllerNode {
return self.displayNode as! ActionSheetControllerNode return self.displayNode as! ActionSheetControllerNode
} }
@ -94,4 +94,44 @@ open class ActionSheetController: ViewController, PresentableController, Standal
self.actionSheetNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node) self.actionSheetNode.setItemGroupOverlayNode(groupIndex: groupIndex, node: node)
} }
} }
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.performHighlightedAction()
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.decreaseHighlightedIndex()
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
self?.actionSheetNode.increaseHighlightedIndex()
}
)
]
}
} }

View File

@ -88,6 +88,18 @@ final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
func performHighlightedAction() {
self.itemGroupsContainerNode.performHighlightedAction()
}
func decreaseHighlightedIndex() {
self.itemGroupsContainerNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.itemGroupsContainerNode.increaseHighlightedIndex()
}
func updateTheme() { func updateTheme() {
self.leftDimView.backgroundColor = self.theme.dimColor self.leftDimView.backgroundColor = self.theme.dimColor
self.rightDimView.backgroundColor = self.theme.dimColor self.rightDimView.backgroundColor = self.theme.dimColor

View File

@ -13,7 +13,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate {
private let backgroundEffectView: UIVisualEffectView private let backgroundEffectView: UIVisualEffectView
private let scrollNode: ASScrollNode private let scrollNode: ASScrollNode
private var itemNodes: [ActionSheetItemNode] = [] var itemNodes: [ActionSheetItemNode] = []
private var leadingVisibleNodeCount: CGFloat = 100.0 private var leadingVisibleNodeCount: CGFloat = 100.0
private var validLayout: CGSize? private var validLayout: CGSize?

View File

@ -13,6 +13,8 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
} }
} }
private var highlightedItemIndex: Int? = nil
private var groups: [ActionSheetItemGroup] = [] private var groups: [ActionSheetItemGroup] = []
var groupNodes: [ActionSheetItemGroupNode] = [] var groupNodes: [ActionSheetItemGroupNode] = []
@ -26,6 +28,66 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
super.init() super.init()
} }
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
self.highlightedItemIndex = index
if update {
var groupIndex = 0
var i = 0
for _ in self.groups {
for itemNode in self.groupNodes[groupIndex].itemNodes {
if i == index {
itemNode.setHighlighted(true, animated: false)
} else {
itemNode.setHighlighted(false, animated: false)
}
i += 1
}
groupIndex += 1
}
}
}
func decreaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
}
func increaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
var groupIndex = 0
var maxAvailabledIndex = 0
for _ in self.groups {
for _ in self.groupNodes[groupIndex].itemNodes {
maxAvailabledIndex += 1
}
groupIndex += 1
}
self.setHighlightedItemIndex(min(maxAvailabledIndex - 1, currentHighlightedIndex + 1), update: true)
}
func performHighlightedAction() {
guard let highlightedItemIndex = self.highlightedItemIndex else {
return
}
var i = 0
var groupIndex = 0
for _ in self.groups {
for itemNode in self.groupNodes[groupIndex].itemNodes {
if i == highlightedItemIndex {
itemNode.performAction()
return
}
i += 1
}
groupIndex += 1
}
}
func setGroups(_ groups: [ActionSheetItemGroup]) { func setGroups(_ groups: [ActionSheetItemGroup]) {
self.groups = groups self.groups = groups
@ -34,6 +96,7 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
} }
self.groupNodes.removeAll() self.groupNodes.removeAll()
var i = 0
for group in groups { for group in groups {
let groupNode = ActionSheetItemGroupNode(theme: self.theme) let groupNode = ActionSheetItemGroupNode(theme: self.theme)
let itemNodes = group.items.map({ $0.node(theme: self.theme) }) let itemNodes = group.items.map({ $0.node(theme: self.theme) })
@ -42,6 +105,13 @@ final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
node.requestLayout = { [weak self] in node.requestLayout = { [weak self] in
self?.requestLayout?() self?.requestLayout?()
} }
let index = i
node.highlightedUpdated = { [weak self] highlighted in
if highlighted {
self?.highlightedItemIndex = index
}
}
i += 1
} }
groupNode.updateItemNodes(itemNodes, leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0) groupNode.updateItemNodes(itemNodes, leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
self.groupNodes.append(groupNode) self.groupNodes.append(groupNode)

View File

@ -13,6 +13,8 @@ open class ActionSheetItemNode: ASDisplayNode {
private var validSize: CGSize? private var validSize: CGSize?
var highlightedUpdated: (Bool) -> Void = { _ in }
public init(theme: ActionSheetControllerTheme) { public init(theme: ActionSheetControllerTheme) {
self.theme = theme self.theme = theme
@ -34,6 +36,14 @@ open class ActionSheetItemNode: ASDisplayNode {
return size return size
} }
func setHighlighted(_ highlighted: Bool, animated: Bool) {
}
func performAction() {
}
public func updateInternalLayout(_ calculatedSize: CGSize, constrainedSize: CGSize) { public func updateInternalLayout(_ calculatedSize: CGSize, constrainedSize: CGSize) {
self.validSize = constrainedSize self.validSize = constrainedSize

View File

@ -18,4 +18,16 @@ open class AlertContentNode: ASDisplayNode {
open func updateTheme(_ theme: AlertControllerTheme) { open func updateTheme(_ theme: AlertControllerTheme) {
} }
open func performHighlightedAction() {
}
open func decreaseHighlightedIndex() {
}
open func increaseHighlightedIndex() {
}
} }

View File

@ -71,7 +71,7 @@ public final class AlertControllerTheme: Equatable {
} }
} }
open class AlertController: ViewController, StandalonePresentableController { open class AlertController: ViewController, StandalonePresentableController, KeyShortcutResponder {
private var controllerNode: AlertControllerNode { private var controllerNode: AlertControllerNode {
return self.displayNode as! AlertControllerNode return self.displayNode as! AlertControllerNode
} }
@ -155,4 +155,44 @@ open class AlertController: ViewController, StandalonePresentableController {
self?.dismiss() self?.dismiss()
} }
} }
public var keyShortcuts: [KeyShortcut] {
return [
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismissAnimated()
}
),
KeyShortcut(
input: "\r",
modifiers: [],
action: { [weak self] in
self?.controllerNode.performHighlightedAction()
}
),
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.decreaseHighlightedIndex()
}
),
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [],
action: { [weak self] in
self?.controllerNode.increaseHighlightedIndex()
}
)
]
}
} }

View File

@ -83,6 +83,18 @@ final class AlertControllerNode: ASDisplayNode {
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:)))) self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
} }
func performHighlightedAction() {
self.contentNode.performHighlightedAction()
}
func decreaseHighlightedIndex() {
self.contentNode.decreaseHighlightedIndex()
}
func increaseHighlightedIndex() {
self.contentNode.increaseHighlightedIndex()
}
func updateTheme(_ theme: AlertControllerTheme) { func updateTheme(_ theme: AlertControllerTheme) {
if let effectView = self.effectNode.view as? UIVisualEffectView { if let effectView = self.effectNode.view as? UIVisualEffectView {
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark) effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)

View File

@ -32,7 +32,7 @@ open class HighlightTrackingButtonNode: ASButtonNode {
public var highligthedChanged: (Bool) -> Void = { _ in } public var highligthedChanged: (Bool) -> Void = { _ in }
private let pointerStyle: PointerStyle? private let pointerStyle: PointerStyle?
private var pointerInteraction: PointerInteraction? public var pointerInteraction: PointerInteraction?
public init(pointerStyle: PointerStyle? = nil) { public init(pointerStyle: PointerStyle? = nil) {
self.pointerStyle = pointerStyle self.pointerStyle = pointerStyle

View File

@ -31,15 +31,11 @@ extension UIKeyModifierFlags: Hashable {
extension KeyShortcut { extension KeyShortcut {
var uiKeyCommand: UIKeyCommand { var uiKeyCommand: UIKeyCommand {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *), !self.title.isEmpty { let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title)
let command = UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title) if #available(iOS 15.0, *), ["\t", UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow, UIKeyCommand.inputLeftArrow, UIKeyCommand.inputRightArrow].contains(command.input) && self.modifiers.isEmpty {
if #available(iOS 15.0, *), ["\t", UIKeyCommand.inputUpArrow].contains(command.input) { command.wantsPriorityOverSystemBehavior = true
command.wantsPriorityOverSystemBehavior = true
}
return command
} else {
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)))
} }
return command
} }
func isEqual(to command: UIKeyCommand) -> Bool { func isEqual(to command: UIKeyCommand) -> Bool {

View File

@ -117,6 +117,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
var statusBarStyle: StatusBarStyle = .Ignore var statusBarStyle: StatusBarStyle = .Ignore
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)? var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
private var panRecognizer: InteractiveTransitionGestureRecognizer?
public init(controllerRemoved: @escaping (ViewController) -> Void) { public init(controllerRemoved: @escaping (ViewController) -> Void) {
self.controllerRemoved = controllerRemoved self.controllerRemoved = controllerRemoved
@ -132,9 +134,13 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
} }
return .right return .right
}) })
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
panRecognizer.delegate = self panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer) self.view.addGestureRecognizer(panRecognizer)
/*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in /*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
@ -155,6 +161,24 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
return false return false
} }
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.controllers.count == 1 {
return false
}
return true
} else {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false return false
} }

View File

@ -97,6 +97,9 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
} }
return .right return .right
}) })
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
self.panRecognizer = panRecognizer self.panRecognizer = panRecognizer
if let layout = self.validLayout { if let layout = self.validLayout {
switch layout.metrics.widthClass { switch layout.metrics.widthClass {
@ -115,6 +118,24 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
} }
} }
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.isDismissed {
return false
}
return true
} else {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false return false
} }

View File

@ -99,6 +99,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
self.addSubnode(node) self.addSubnode(node)
self.invalidateCalculatedLayout() self.invalidateCalculatedLayout()
self.setNeedsLayout() self.setNeedsLayout()
self.updatePointerInteraction()
} }
} }
} }
@ -210,7 +211,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
if self.node != nil { if self.node != nil {
pointerStyle = .lift pointerStyle = .lift
} else { } else {
pointerStyle = .default pointerStyle = .insetRectangle(-8.0, 2.0)
} }
self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle) self.pointerInteraction = PointerInteraction(node: self, style: pointerStyle)
} }

View File

@ -3,8 +3,9 @@ import AsyncDisplayKit
public enum PointerStyle { public enum PointerStyle {
case `default` case `default`
case insetRectangle(CGFloat, CGFloat)
case rectangle(CGSize) case rectangle(CGSize)
case circle case circle(CGFloat?)
case caret case caret
case lift case lift
case hover case hover
@ -12,7 +13,8 @@ public enum PointerStyle {
@available(iOSApplicationExtension 13.4, iOS 13.4, *) @available(iOSApplicationExtension 13.4, iOS 13.4, *)
private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelegate { private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelegate {
weak var pointerInteraction: UIPointerInteraction? private weak var pointerInteraction: UIPointerInteraction?
private weak var customInteractionView: UIView?
private let style: PointerStyle private let style: PointerStyle
@ -33,7 +35,9 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
} }
} }
func setup(view: UIView) { func setup(view: UIView, customInteractionView: UIView?) {
self.customInteractionView = customInteractionView
let pointerInteraction = UIPointerInteraction(delegate: self) let pointerInteraction = UIPointerInteraction(delegate: self)
view.addInteraction(pointerInteraction) view.addInteraction(pointerInteraction)
self.pointerInteraction = pointerInteraction self.pointerInteraction = pointerInteraction
@ -41,7 +45,10 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
var pointerStyle: UIPointerStyle? = nil var pointerStyle: UIPointerStyle? = nil
if let interactionView = interaction.view {
let interactionView = self.customInteractionView ?? interaction.view
if let interactionView = interactionView {
let targetedPreview = UITargetedPreview(view: interactionView) let targetedPreview = UITargetedPreview(view: interactionView)
switch self.style { switch self.style {
case .default: case .default:
@ -50,11 +57,15 @@ private final class PointerInteractionImpl: NSObject, UIPointerInteractionDelega
let minHeight: CGFloat = 40.0 let minHeight: CGFloat = 40.0
let size: CGSize = CGSize(width: targetedPreview.size.width + horizontalPadding * 2.0, height: max(minHeight, targetedPreview.size.height + verticalPadding * 2.0)) let size: CGSize = CGSize(width: targetedPreview.size.width + horizontalPadding * 2.0, height: max(minHeight, targetedPreview.size.height + verticalPadding * 2.0))
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius)) pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
case let .insetRectangle(x, y):
let insetSize = CGSize(width: targetedPreview.size.width - x * 2.0, height: targetedPreview.size.height - y * 2.0)
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - insetSize.width / 2.0, y: targetedPreview.view.center.y - insetSize.height / 2.0), size: insetSize), radius: UIPointerShape.defaultCornerRadius))
case let .rectangle(size): case let .rectangle(size):
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius)) pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .roundedRect(CGRect(origin: CGPoint(x: targetedPreview.view.center.x - size.width / 2.0, y: targetedPreview.view.center.y - size.height / 2.0), size: size), radius: UIPointerShape.defaultCornerRadius))
case .circle: case let .circle(diameter):
let maxSide = max(targetedPreview.size.width, targetedPreview.size.height) let maxSide = max(targetedPreview.size.width, targetedPreview.size.height)
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .path(UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: maxSide, height: maxSide))))) let finalDiameter = diameter ?? maxSide
pointerStyle = UIPointerStyle(effect: .highlight(targetedPreview), shape: .path(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: floorToScreenPixels(targetedPreview.view.center.x - finalDiameter / 2.0), y: floorToScreenPixels(targetedPreview.view.center.y - finalDiameter / 2.0)), size: CGSize(width: finalDiameter, height: finalDiameter)))))
case .caret: case .caret:
pointerStyle = UIPointerStyle(shape: .verticalBeam(length: 24.0), constrainedAxes: .vertical) pointerStyle = UIPointerStyle(shape: .verticalBeam(length: 24.0), constrainedAxes: .vertical)
case .lift: case .lift:
@ -106,13 +117,13 @@ public final class PointerInteraction {
self.init(view: node.view, style: style, willEnter: willEnter, willExit: willExit) self.init(view: node.view, style: style, willEnter: willEnter, willExit: willExit)
} }
public init(view: UIView, style: PointerStyle = .default, willEnter: @escaping () -> Void = {}, willExit: @escaping () -> Void = {}) { public init(view: UIView, customInteractionView: UIView? = nil, style: PointerStyle = .default, willEnter: @escaping () -> Void = {}, willExit: @escaping () -> Void = {}) {
self.style = style self.style = style
self.willEnter = willEnter self.willEnter = willEnter
self.willExit = willExit self.willExit = willExit
if #available(iOSApplicationExtension 13.4, iOS 13.4, *) { if #available(iOSApplicationExtension 13.4, iOS 13.4, *) {
self.withImpl { impl in self.withImpl { impl in
impl.setup(view: view) impl.setup(view: view, customInteractionView: customInteractionView)
} }
} }
} }

View File

@ -29,8 +29,8 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private var pointerInteraction: PointerInteraction? var highlightedUpdated: (Bool) -> Void = { _ in }
public init(theme: AlertControllerTheme, action: TextAlertAction) { public init(theme: AlertControllerTheme, action: TextAlertAction) {
self.theme = theme self.theme = theme
self.action = action self.action = action
@ -45,16 +45,7 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
self.highligthedChanged = { [weak self] value in self.highligthedChanged = { [weak self] value in
if let strongSelf = self { if let strongSelf = self {
if value { strongSelf.setHighlighted(value, animated: true)
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.backgroundNode.alpha = 1.0
} else if !strongSelf.backgroundNode.alpha.isZero {
strongSelf.backgroundNode.alpha = 0.0
strongSelf.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
} }
} }
@ -68,15 +59,38 @@ public final class TextAlertContentActionNode: HighlightableButtonNode {
self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in self.pointerInteraction = PointerInteraction(node: self, style: .hover, willEnter: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.backgroundNode.alpha = 0.25 strongSelf.setHighlighted(true, animated: false)
} }
}, willExit: { [weak self] in }, willExit: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.backgroundNode.alpha = 1.0 strongSelf.setHighlighted(false, animated: false)
} }
}) })
} }
func performAction() {
if self.actionEnabled {
self.action.action()
}
}
func setHighlighted(_ highlighted: Bool, animated: Bool) {
self.highlightedUpdated(highlighted)
if highlighted {
if self.backgroundNode.supernode == nil {
self.insertSubnode(self.backgroundNode, at: 0)
}
self.backgroundNode.alpha = 1.0
} else {
if animated {
UIView.animate(withDuration: 0.3, animations: {
self.backgroundNode.alpha = 0.0
})
} else {
self.backgroundNode.alpha = 0.0
}
}
}
public var actionEnabled: Bool = true { public var actionEnabled: Bool = true {
didSet { didSet {
self.isUserInteractionEnabled = self.actionEnabled self.isUserInteractionEnabled = self.actionEnabled
@ -142,6 +156,8 @@ public final class TextAlertContentNode: AlertContentNode {
return self._dismissOnOutsideTap return self._dismissOnOutsideTap
} }
private var highlightedItemIndex: Int? = nil
public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? { public var textAttributeAction: (NSAttributedString.Key, (Any) -> Void)? {
didSet { didSet {
if let (attribute, textAttributeAction) = self.textAttributeAction { if let (attribute, textAttributeAction) = self.textAttributeAction {
@ -224,8 +240,17 @@ public final class TextAlertContentNode: AlertContentNode {
self.addSubnode(self.actionNodesSeparator) self.addSubnode(self.actionNodesSeparator)
var i = 0
for actionNode in self.actionNodes { for actionNode in self.actionNodes {
self.addSubnode(actionNode) self.addSubnode(actionNode)
let index = i
actionNode.highlightedUpdated = { [weak self] highlighted in
if highlighted {
self?.highlightedItemIndex = index
}
}
i += 1
} }
for separatorNode in self.actionVerticalSeparators { for separatorNode in self.actionVerticalSeparators {
@ -233,6 +258,49 @@ public final class TextAlertContentNode: AlertContentNode {
} }
} }
func setHighlightedItemIndex(_ index: Int?, update: Bool = false) {
self.highlightedItemIndex = index
if update {
var i = 0
for actionNode in self.actionNodes {
if i == index {
actionNode.setHighlighted(true, animated: false)
} else {
actionNode.setHighlighted(false, animated: false)
}
i += 1
}
}
}
override public func decreaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? 0
self.setHighlightedItemIndex(max(0, currentHighlightedIndex - 1), update: true)
}
override public func increaseHighlightedIndex() {
let currentHighlightedIndex = self.highlightedItemIndex ?? -1
self.setHighlightedItemIndex(min(self.actionNodes.count - 1, currentHighlightedIndex + 1), update: true)
}
override public func performHighlightedAction() {
guard let highlightedItemIndex = self.highlightedItemIndex else {
return
}
var i = 0
for itemNode in self.actionNodes {
if i == highlightedItemIndex {
itemNode.performAction()
return
}
i += 1
}
}
override public func updateTheme(_ theme: AlertControllerTheme) { override public func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme self.theme = theme

View File

@ -52,13 +52,13 @@ public final class ToolbarNode: ASDisplayNode {
self.leftTitle = ImmediateTextNode() self.leftTitle = ImmediateTextNode()
self.leftTitle.displaysAsynchronously = false self.leftTitle.displaysAsynchronously = false
self.leftButton = HighlightTrackingButtonNode() self.leftButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
self.rightTitle = ImmediateTextNode() self.rightTitle = ImmediateTextNode()
self.rightTitle.displaysAsynchronously = false self.rightTitle.displaysAsynchronously = false
self.rightButton = HighlightTrackingButtonNode() self.rightButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
self.middleTitle = ImmediateTextNode() self.middleTitle = ImmediateTextNode()
self.middleTitle.displaysAsynchronously = false self.middleTitle.displaysAsynchronously = false
self.middleButton = HighlightTrackingButtonNode() self.middleButton = HighlightTrackingButtonNode(pointerStyle: .insetRectangle(2.0, 2.0))
super.init() super.init()

View File

@ -329,7 +329,7 @@ public struct GalleryConfiguration {
} }
} }
public class GalleryController: ViewController, StandalonePresentableController { public class GalleryController: ViewController, StandalonePresentableController, KeyShortcutResponder {
public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) public static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), enableBackgroundBlur: false, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear) public static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007aff), disabledButtonColor: UIColor(rgb: 0xd0d0d0), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), enableBackgroundBlur: false, separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
@ -1384,4 +1384,67 @@ public class GalleryController: ViewController, StandalonePresentableController
} }
} }
} }
public var keyShortcuts: [KeyShortcut] {
var keyShortcuts: [KeyShortcut] = []
keyShortcuts.append(
KeyShortcut(
title: "",
input: UIKeyCommand.inputUpArrow,
modifiers: [.command],
action: { [weak self] in
self?.dismiss(forceAway: false)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: "",
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismiss(forceAway: false)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: self.galleryNode.areControlsHidden ? self.presentationData.strings.KeyCommand_ExitFullscreen : self.presentationData.strings.KeyCommand_EnterFullscreen,
input: "F",
modifiers: [.control, .command],
action: { [weak self] in
if let strongSelf = self {
strongSelf.galleryNode.setControlsHidden(!strongSelf.galleryNode.areControlsHidden, animated: true)
}
}
)
)
if self.galleryNode.pager.items.count > 1 {
if self.galleryNode.pager.canGoToPreviousItem() {
keyShortcuts.append(
KeyShortcut(
input: UIKeyCommand.inputLeftArrow,
modifiers: [],
action: { [weak self] in
self?.galleryNode.pager.goToPreviousItem()
}
)
)
}
if self.galleryNode.pager.canGoToNextItem() {
keyShortcuts.append(
KeyShortcut(
input: UIKeyCommand.inputRightArrow,
modifiers: [],
action: { [weak self] in
self?.galleryNode.pager.goToNextItem()
}
)
)
}
}
let itemNodeShortcuts = self.galleryNode.pager.centralItemNode()?.keyShortcuts ?? []
keyShortcuts.append(contentsOf: itemNodeShortcuts)
return keyShortcuts
}
} }

View File

@ -103,4 +103,8 @@ open class GalleryItemNode: ASDisplayNode {
open func contentSize() -> CGSize? { open func contentSize() -> CGSize? {
return nil return nil
} }
open var keyShortcuts: [KeyShortcut] {
return []
}
} }

View File

@ -457,7 +457,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
} }
} }
private func canGoToPreviousItem() -> Bool { func canGoToPreviousItem() -> Bool {
if self.disableTapNavigation { if self.disableTapNavigation {
return false return false
} }
@ -468,7 +468,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
} }
} }
private func canGoToNextItem() -> Bool { func canGoToNextItem() -> Bool {
if self.disableTapNavigation { if self.disableTapNavigation {
return false return false
} }
@ -479,13 +479,13 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
} }
} }
private func goToPreviousItem() { func goToPreviousItem() {
if let index = self.centralItemIndex, index > 0 { if let index = self.centralItemIndex, index > 0 {
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false)) self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false))
} }
} }
private func goToNextItem() { func goToNextItem() {
if let index = self.centralItemIndex, index < self.items.count - 1 { if let index = self.centralItemIndex, index < self.items.count - 1 {
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1, synchronous: false)) self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1, synchronous: false))
} }

View File

@ -192,6 +192,7 @@ class ChatImageGalleryItem: GalleryItem {
final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext private let context: AccountContext
private var message: Message? private var message: Message?
private let presentationData: PresentationData
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var recognizedContentNode: RecognizedContentContainer? private var recognizedContentNode: RecognizedContentContainer?
@ -220,6 +221,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context self.context = context
self.presentationData = presentationData
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = .subsequentUpdates self.imageNode.contentAnimations = .subsequentUpdates
@ -798,6 +800,60 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
self.recognitionOverlayContentNode.isHidden = true self.recognitionOverlayContentNode.isHidden = true
} }
private func canDelete() -> Bool {
guard let message = self.message else {
return false
}
var canDelete = false
if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
} else if let _ = peer as? TelegramGroup {
canDelete = true
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
}
} else {
canDelete = false
}
return canDelete
}
override var keyShortcuts: [KeyShortcut] {
let strings = self.presentationData.strings
var keyShortcuts: [KeyShortcut] = []
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_Share,
input: "S",
modifiers: [.command],
action: { [weak self] in
self?.footerContentNode.actionButtonPressed()
}
)
)
if self.canDelete() {
keyShortcuts.append(
KeyShortcut(
input: "\u{8}",
modifiers: [],
action: { [weak self] in
self?.footerContentNode.deleteButtonPressed()
}
)
)
}
return keyShortcuts
}
} }
/*private func tileRectForImage(_ mappedImage: CGImage, rect: CGRect) -> CGRect { /*private func tileRectForImage(_ mappedImage: CGImage, rect: CGRect) -> CGRect {

View File

@ -775,6 +775,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
private var scrubbingFrames = false private var scrubbingFrames = false
private var scrubbingFrameDisposable: Disposable? private var scrubbingFrameDisposable: Disposable?
private var isPlaying = false
private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true) private let isPlayingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true) private let isInteractingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true) private let controlsVisiblePromise = ValuePromise<Bool>(true, ignoreRepeated: true)
@ -1066,6 +1067,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
self.footerContentNode.scrubberView = scrubberView self.footerContentNode.scrubberView = scrubberView
self.isPlaying = false
self.isPlayingPromise.set(false) self.isPlayingPromise.set(false)
if item.hideControls { if item.hideControls {
@ -1346,6 +1348,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} else if strongSelf.actionAtEnd == .stop { } else if strongSelf.actionAtEnd == .stop {
strongSelf.isPlayingPromise.set(false) strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
if strongSelf.isCentral == true { if strongSelf.isCentral == true {
strongSelf.updateControlsVisibility(true) strongSelf.updateControlsVisibility(true)
} }
@ -1358,8 +1361,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if !disablePlayerControls && strongSelf.isCentral == true && isPlaying { if !disablePlayerControls && strongSelf.isCentral == true && isPlaying {
strongSelf.isPlayingPromise.set(true) strongSelf.isPlayingPromise.set(true)
strongSelf.isPlaying = true
} else if !isPlaying { } else if !isPlaying {
strongSelf.isPlayingPromise.set(false) strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
} }
var fetching = false var fetching = false
@ -1471,6 +1476,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true { if strongSelf.actionAtEnd == .stop && strongSelf.isCentral == true {
strongSelf.isPlayingPromise.set(false) strongSelf.isPlayingPromise.set(false)
strongSelf.isPlaying = false
strongSelf.updateControlsVisibility(true) strongSelf.updateControlsVisibility(true)
} }
} }
@ -1591,6 +1597,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} else { } else {
self.isPlayingPromise.set(false) self.isPlayingPromise.set(false)
self.isPlaying = false
self.dismissOnOrientationChange = false self.dismissOnOrientationChange = false
if videoNode.ownsContentNode { if videoNode.ownsContentNode {
@ -2710,6 +2717,78 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.playbackRatePromise.set(self.playbackRate ?? 1.0) self.playbackRatePromise.set(self.playbackRate ?? 1.0)
} }
override var keyShortcuts: [KeyShortcut] {
let strings = self.presentationData.strings
var keyShortcuts: [KeyShortcut] = []
keyShortcuts.append(
KeyShortcut(
title: self.isPlaying ? strings.KeyCommand_Pause : strings.KeyCommand_Play,
input: " ",
modifiers: [],
action: { [weak self] in
self?.footerContentNode.playbackControl?()
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SeekBackward,
input: UIKeyCommand.inputLeftArrow,
modifiers: [.shift],
action: { [weak self] in
self?.footerContentNode.seekBackward?(5)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SeekForward,
input: UIKeyCommand.inputRightArrow,
modifiers: [.shift],
action: { [weak self] in
self?.footerContentNode.seekForward?(5)
}
)
)
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_Share,
input: "S",
modifiers: [.command],
action: { [weak self] in
self?.footerContentNode.actionButtonPressed()
}
)
)
if self.hasPictureInPicture {
keyShortcuts.append(
KeyShortcut(
title: strings.KeyCommand_SwitchToPIP,
input: "P",
modifiers: [.command],
action: { [weak self] in
self?.pictureInPictureButtonPressed()
}
)
)
}
if self.canDelete() {
keyShortcuts.append(
KeyShortcut(
input: "\u{8}",
modifiers: [],
action: { [weak self] in
self?.footerContentNode.deleteButtonPressed()
}
)
)
}
return keyShortcuts
}
} }
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {

View File

@ -197,11 +197,18 @@ class PremiumStarComponent: Component {
self.previousInteractionTimestamp = CACurrentMediaTime() self.previousInteractionTimestamp = CACurrentMediaTime()
let keys = [
"rotate",
"tapRotate"
]
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1) for key in keys {
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1) node.removeAnimation(forKey: key, blendOutDuration: 0.1)
}
} else { } else {
node.removeAllAnimations() for key in keys {
node.removeAnimation(forKey: key)
}
} }
switch gesture.state { switch gesture.state {
@ -337,6 +344,7 @@ class PremiumStarComponent: Component {
} }
private func onReady() { private func onReady() {
self.setupScaleAnimation()
self.setupGradientAnimation() self.setupGradientAnimation()
self.setupShineAnimation() self.setupShineAnimation()
@ -355,6 +363,22 @@ class PremiumStarComponent: Component {
self.timer?.start() self.timer?.start()
} }
private func setupScaleAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0
animation.fromValue = NSValue(scnVector3: SCNVector3(x: 0.1, y: 0.1, z: 0.1))
animation.toValue = NSValue(scnVector3: SCNVector3(x: 0.115, y: 0.115, z: 0.115))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "scale")
}
private func setupGradientAnimation() { private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return return

View File

@ -125,6 +125,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
if let layout = self.validLayout { if let layout = self.validLayout {
let _ = self.updateLayout(layout, transition: .immediate) let _ = self.updateLayout(layout, transition: .immediate)
} }
self.updatePointerInteraction()
} }
} }
@ -140,6 +142,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
if let layout = self.validLayout { if let layout = self.validLayout {
let _ = self.updateLayout(layout, transition: .immediate) let _ = self.updateLayout(layout, transition: .immediate)
} }
self.updatePointerInteraction()
} }
} }
@ -151,6 +155,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
if let layout = self.validLayout { if let layout = self.validLayout {
let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut)) let _ = self.updateLayout(layout, transition: .animated(duration: 0.2, curve: .easeInOut))
} }
self.updatePointerInteraction()
} }
public var selectedIndexChanged: (Int) -> Void = { _ in } public var selectedIndexChanged: (Int) -> Void = { _ in }
@ -212,6 +218,18 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
self.gestureRecognizer = gestureRecognizer self.gestureRecognizer = gestureRecognizer
} }
private func updatePointerInteraction() {
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
if i == self.selectedIndex {
itemNode.pointerInteraction = PointerInteraction(view: itemNode.view, customInteractionView: self.selectionNode.view, style: .lift)
} else {
itemNode.pointerInteraction = PointerInteraction(view: itemNode.view, style: .insetRectangle(2.0, 2.0))
}
}
}
private func setupButtons() { private func setupButtons() {
for i in 0 ..< self.itemNodes.count { for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i] let itemNode = self.itemNodes[i]
@ -233,6 +251,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
} }
} }
} }
self.updatePointerInteraction()
} }
private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) { private func updateButtonsHighlights(highlightedIndex: Int?, gestureSelectedIndex: Int?) {
@ -379,6 +399,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
if let layout = strongSelf.validLayout { if let layout = strongSelf.validLayout {
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide)) let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))
} }
strongSelf.updatePointerInteraction()
} }
}) })
} }
@ -415,6 +437,8 @@ public final class SegmentedControlNode: ASDisplayNode, UIGestureRecognizerDeleg
if commit { if commit {
strongSelf._selectedIndex = gestureSelectedIndex strongSelf._selectedIndex = gestureSelectedIndex
strongSelf.selectedIndexChanged(gestureSelectedIndex) strongSelf.selectedIndexChanged(gestureSelectedIndex)
strongSelf.updatePointerInteraction()
} else { } else {
if let layout = strongSelf.validLayout { if let layout = strongSelf.validLayout {
let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide)) let _ = strongSelf.updateLayout(layout, transition: .animated(duration: 0.2, curve: .slide))

View File

@ -1325,8 +1325,9 @@ public final class SparseItemGrid: ASDisplayNode {
} }
} }
public init(theme: PresentationTheme) { public init(theme: PresentationTheme, initialZoomLevel: ZoomLevel? = nil) {
self.theme = theme self.theme = theme
self.initialZoomLevel = initialZoomLevel
self.scrollingArea = SparseItemGridScrollingArea() self.scrollingArea = SparseItemGridScrollingArea()

View File

@ -226,6 +226,11 @@ final class AuthorizedApplicationContext {
} }
} }
strongSelf.mainWindow.forEachViewController(f) strongSelf.mainWindow.forEachViewController(f)
if let globalOverlayController = strongSelf.rootController.globalOverlayControllers.last {
if !f(globalOverlayController) {
return
}
}
}) })
} }
} }

View File

@ -15841,6 +15841,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
}), }),
KeyShortcut(input: "U", modifiers: [.command], action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline), inputMode)
}
}
}),
KeyShortcut(input: "X", modifiers: [.command, .shift], action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough), inputMode)
}
}
}),
KeyShortcut(input: "P", modifiers: [.command, .shift], action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler), inputMode)
}
}
}),
KeyShortcut(input: "K", modifiers: [.command], action: { [weak self] in KeyShortcut(input: "K", modifiers: [.command], action: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.interfaceInteraction?.openLinkEditing() strongSelf.interfaceInteraction?.openLinkEditing()
@ -15912,11 +15933,107 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
inputShortcuts = [] inputShortcuts = []
} }
inputShortcuts.append(
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismiss(animated: true, completion: nil)
}
)
)
if canReplyInChat(self.presentationInterfaceState) {
inputShortcuts.append(
KeyShortcut(
input: UIKeyCommand.inputUpArrow,
modifiers: [.alternate, .command],
action: { [weak self] in
guard let strongSelf = self else {
return
}
if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(before: replyMessageId) {
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
var updatedState = state.updatedInterfaceState({ state in
return state.withUpdatedReplyMessageId(message.id)
})
if updatedState.inputMode == .none {
updatedState = updatedState.updatedInputMode({ _ in .text })
}
return updatedState
})
strongSelf.navigateToMessage(messageLocation: .id(message.id, nil), animated: true)
}
} else {
strongSelf.scrollToEndOfHistory()
Queue.mainQueue().after(0.1, {
let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView()
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
var updatedState = state.updatedInterfaceState({ state in
return state.withUpdatedReplyMessageId(lastMessage?.id)
})
if updatedState.inputMode == .none {
updatedState = updatedState.updatedInputMode({ _ in .text })
}
return updatedState
})
if let lastMessage = lastMessage {
strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, nil), animated: true)
}
})
}
}
)
)
inputShortcuts.append(
KeyShortcut(
input: UIKeyCommand.inputDownArrow,
modifiers: [.alternate, .command],
action: { [weak self] in
guard let strongSelf = self else {
return
}
if let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId {
let lastMessage = strongSelf.chatDisplayNode.historyNode.latestMessageInCurrentHistoryView()
var updatedReplyMessageId: MessageId?
if replyMessageId == lastMessage?.id {
updatedReplyMessageId = nil
} else if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(after: replyMessageId) {
updatedReplyMessageId = message.id
}
strongSelf.updateChatPresentationInterfaceState(interactive: true, { state in
var updatedState = state.updatedInterfaceState({ state in
return state.withUpdatedReplyMessageId(updatedReplyMessageId)
})
if updatedState.inputMode == .none {
updatedState = updatedState.updatedInputMode({ _ in .text })
} else if updatedReplyMessageId == nil {
updatedState = updatedState.updatedInputMode({ _ in .none })
}
return updatedState
})
if let updatedReplyMessageId = updatedReplyMessageId {
strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageId, nil), animated: true)
}
}
}
)
)
}
var canEdit = false var canEdit = false
if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil { if self.presentationInterfaceState.interfaceState.effectiveInputState.inputText.length == 0 && self.presentationInterfaceState.interfaceState.editMessage == nil {
canEdit = true canEdit = true
} }
if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() { if canEdit, let message = self.chatDisplayNode.historyNode.firstMessageForEditInCurrentHistoryView() {
inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in inputShortcuts.append(KeyShortcut(input: UIKeyCommand.inputUpArrow, action: { [weak self] in
if let strongSelf = self { if let strongSelf = self {

View File

@ -88,6 +88,23 @@ enum ChatHistoryEntry: Identifiable, Comparable {
return MessageIndex.absoluteLowerBound() return MessageIndex.absoluteLowerBound()
} }
} }
var firstIndex: MessageIndex {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.index
case let .MessageGroupEntry(_, messages, _):
return messages[0].0.index
case let .UnreadEntry(index, _):
return index
case let .ReplyCountEntry(index, _, _, _):
return index
case .ChatInfoEntry:
return MessageIndex.absoluteLowerBound()
case .SearchEntry:
return MessageIndex.absoluteLowerBound()
}
}
static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { static func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool {
switch lhs { switch lhs {

View File

@ -2195,6 +2195,34 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
return nil return nil
} }
public func messageInCurrentHistoryView(after messageId: MessageId) -> Message? {
if let historyView = self.historyView {
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index < historyView.filteredEntries.count - 1 {
let nextEntry = historyView.filteredEntries[index + 1]
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
return message
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
return firstMessage.0
}
}
}
return nil
}
public func messageInCurrentHistoryView(before messageId: MessageId) -> Message? {
if let historyView = self.historyView {
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index > 0 {
let nextEntry = historyView.filteredEntries[index - 1]
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
return message
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
return firstMessage.0
}
}
}
return nil
}
public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? {
if let historyView = self.historyView { if let historyView = self.historyView {
for entry in historyView.filteredEntries { for entry in historyView.filteredEntries {

View File

@ -56,21 +56,21 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.theme = theme self.theme = theme
self.peerMedia = peerMedia self.peerMedia = peerMedia
self.deleteButton = HighlightableButtonNode(pointerStyle: .default) self.deleteButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.deleteButton.isEnabled = false self.deleteButton.isEnabled = false
self.deleteButton.isAccessibilityElement = true self.deleteButton.isAccessibilityElement = true
self.deleteButton.accessibilityLabel = strings.VoiceOver_MessageContextDelete self.deleteButton.accessibilityLabel = strings.VoiceOver_MessageContextDelete
self.reportButton = HighlightableButtonNode(pointerStyle: .default) self.reportButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.reportButton.isEnabled = false self.reportButton.isEnabled = false
self.reportButton.isAccessibilityElement = true self.reportButton.isAccessibilityElement = true
self.reportButton.accessibilityLabel = strings.VoiceOver_MessageContextReport self.reportButton.accessibilityLabel = strings.VoiceOver_MessageContextReport
self.forwardButton = HighlightableButtonNode(pointerStyle: .default) self.forwardButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.forwardButton.isAccessibilityElement = true self.forwardButton.isAccessibilityElement = true
self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward self.forwardButton.accessibilityLabel = strings.VoiceOver_MessageContextForward
self.shareButton = HighlightableButtonNode(pointerStyle: .default) self.shareButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.shareButton.isAccessibilityElement = true self.shareButton.isAccessibilityElement = true
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare

View File

@ -50,12 +50,12 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode {
} }
init(theme: PresentationTheme) { init(theme: PresentationTheme) {
self.upButton = HighlightableButtonNode(pointerStyle: .default) self.upButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
self.upButton.isEnabled = false self.upButton.isEnabled = false
self.downButton = HighlightableButtonNode(pointerStyle: .default) self.downButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
self.downButton.isEnabled = false self.downButton.isEnabled = false
self.calendarButton = HighlightableButtonNode() self.calendarButton = HighlightableButtonNode()
self.membersButton = HighlightableButtonNode(pointerStyle: .default) self.membersButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
self.measureResultsLabel = TextNode() self.measureResultsLabel = TextNode()
self.measureResultsLabel.displaysAsynchronously = false self.measureResultsLabel.displaysAsynchronously = false
self.resultsButton = HighlightableButtonNode(pointerStyle: .default) self.resultsButton = HighlightableButtonNode(pointerStyle: .default)

View File

@ -31,7 +31,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
} }
} }
var micButtonPointerInteraction: PointerInteraction? private var micButtonPointerInteraction: PointerInteraction?
private var sendButtonPointerInteraction: PointerInteraction?
private var validLayout: CGSize? private var validLayout: CGSize?
@ -50,9 +51,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor self.backgroundNode.backgroundColor = theme.chat.inputPanel.actionControlFillColor
self.backgroundNode.clipsToBounds = true self.backgroundNode.clipsToBounds = true
self.backdropNode = ChatMessageBubbleBackdrop() self.backdropNode = ChatMessageBubbleBackdrop()
self.sendButton = HighlightTrackingButtonNode(pointerStyle: .lift) self.sendButton = HighlightTrackingButtonNode(pointerStyle: nil)
self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .default) self.expandMediaInputButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
super.init() super.init()
@ -107,7 +108,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
} }
} }
self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle) self.micButtonPointerInteraction = PointerInteraction(view: self.micButton, style: .circle(36.0))
self.sendButtonPointerInteraction = PointerInteraction(view: self.sendButton.view, customInteractionView: self.backgroundNode.view, style: .lift)
} }
func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) { func updateTheme(theme: PresentationTheme, wallpaper: TelegramWallpaper) {

View File

@ -51,7 +51,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings) self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
super.init() super.init(pointerStyle: .circle(30.0))
self.isAccessibilityElement = true self.isAccessibilityElement = true
self.accessibilityTraits = [.button] self.accessibilityTraits = [.button]
@ -754,7 +754,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.sendAsAvatarContainerNode.animateScale = false self.sendAsAvatarContainerNode.animateScale = false
self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) self.sendAsAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle) self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle(36.0))
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
self.attachmentButton.accessibilityTraits = [.button] self.attachmentButton.accessibilityTraits = [.button]
self.attachmentButton.isAccessibilityElement = true self.attachmentButton.isAccessibilityElement = true

View File

@ -61,7 +61,7 @@ final class IconButtonNode: HighlightTrackingButtonNode {
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
super.init(pointerStyle: .circle) super.init(pointerStyle: .circle(nil))
self.addSubnode(self.iconNode) self.addSubnode(self.iconNode)

View File

@ -1054,7 +1054,7 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
super.init(pointerStyle: .default) super.init(pointerStyle: .insetRectangle(-8.0, 2.0))
self.isAccessibilityElement = true self.isAccessibilityElement = true
self.accessibilityTraits = .button self.accessibilityTraits = .button

View File

@ -7780,7 +7780,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
} }
public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortcutResponder {
private let context: AccountContext private let context: AccountContext
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
private let peerId: PeerId private let peerId: PeerId
@ -8278,6 +8278,37 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) let controller = ContextController(account: primary.0.account, presentationData: self.presentationData, source: .extracted(SettingsTabBarContextExtractedContentSource(controller: self, sourceNode: sourceNode)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller)
} }
public var keyShortcuts: [KeyShortcut] {
if self.isSettings {
return [
KeyShortcut(
input: "0",
modifiers: [.command],
action: { [weak self] in
self?.controllerNode.openSettings(section: .savedMessages)
}
)
]
} else {
return [
KeyShortcut(
input: "W",
modifiers: [.command],
action: { [weak self] in
self?.dismiss(animated: true, completion: nil)
}
),
KeyShortcut(
input: UIKeyCommand.inputEscape,
modifiers: [],
action: { [weak self] in
self?.dismiss(animated: true, completion: nil)
}
)
]
}
}
} }
private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource { private final class SettingsTabBarContextExtractedContentSource: ContextExtractedContentSource {