diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 000e8bc..407ca30 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; }; BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; }; BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; }; + BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */; }; BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593C921F3F8B7003412A6 /* GameSave.swift */; }; BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593CB21F3F8C2003412A6 /* _GameSave.swift */; }; BFEE507D23E7612300416151 /* liblibDeSmuME.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF76BDE823E649150073702C /* liblibDeSmuME.a */; }; @@ -286,6 +287,7 @@ BFDF71DA22F94CDF0074D92E /* DSDeltaCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = DSDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = ""; }; BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = ""; }; + BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SymbolFallback.swift"; sourceTree = ""; }; BFE593C921F3F8B7003412A6 /* GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSave.swift; sourceTree = ""; }; BFE593CB21F3F8C2003412A6 /* _GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _GameSave.swift; sourceTree = ""; }; BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -351,6 +353,7 @@ BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */, BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */, BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */, + BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */, ); path = Extensions; sourceTree = ""; @@ -1076,6 +1079,7 @@ BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */, BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */, + BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */, BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */, BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */, BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */, diff --git a/Delta/Components/Action.swift b/Delta/Components/Action.swift index 8385988..e0b1477 100644 --- a/Delta/Components/Action.swift +++ b/Delta/Components/Action.swift @@ -17,8 +17,7 @@ extension Action case destructive case selected - var alertActionStyle: UIAlertAction.Style - { + var alertActionStyle: UIAlertAction.Style { switch self { case .default, .selected: return .default @@ -27,8 +26,7 @@ extension Action } } - var previewActionStyle: UIPreviewAction.Style? - { + var previewActionStyle: UIPreviewAction.Style? { switch self { case .default: return .default @@ -40,11 +38,40 @@ extension Action } } +@available(iOS 13, *) +extension Action.Style +{ + var menuAttributes: UIMenuElement.Attributes { + switch self + { + case .default, .cancel, .selected: return [] + case .destructive: return .destructive + } + } + + var menuState: UIMenuElement.State { + switch self + { + case .default, .cancel, .destructive: return .off + case .selected: return .on + } + } +} + struct Action { - let title: String - let style: Style - let action: ((Action) -> Void)? + var title: String + var style: Style + var image: UIImage? = nil + var action: ((Action) -> Void)? + + init(title: String, style: Style = .default, image: UIImage? = nil, action: ((Action) -> Void)? = nil) + { + self.title = title + self.style = style + self.image = image + self.action = action + } } extension UIAlertAction @@ -82,6 +109,19 @@ extension UIAlertController } } +@available(iOS 13.0, *) +extension UIAction +{ + convenience init?(_ action: Action) + { + guard action.style != .cancel else { return nil } + + self.init(title: action.title, image: action.image, attributes: action.style.menuAttributes, state: action.style.menuState) { _ in + action.action?(action) + } + } +} + extension RangeReplaceableCollection where Iterator.Element == Action { var alertActions: [UIAlertAction] { @@ -93,4 +133,10 @@ extension RangeReplaceableCollection where Iterator.Element == Action let actions = self.compactMap { UIPreviewAction($0) } return actions } + + @available(iOS 13.0, *) + var menuActions: [UIAction] { + let actions = self.compactMap { UIAction($0) } + return actions + } } diff --git a/Delta/Emulation/PreviewGameViewController.swift b/Delta/Emulation/PreviewGameViewController.swift index 42833a4..2089c3c 100644 --- a/Delta/Emulation/PreviewGameViewController.swift +++ b/Delta/Emulation/PreviewGameViewController.swift @@ -41,7 +41,8 @@ class PreviewGameViewController: DeltaCore.GameViewController emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext) - self.preferredContentSize = emulatorCore.preferredRenderingSize + let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0) + self.preferredContentSize = size } } @@ -75,7 +76,7 @@ extension PreviewGameViewController super.viewDidAppear(animated) self.emulatorCoreQueue.async { - self.emulatorCore?.resume() + self.emulatorCore?.start() } } @@ -113,13 +114,9 @@ extension PreviewGameViewController if previousState == .stopped, state == .running { - self.emulatorCoreQueue.sync { - if self.isAppearing - { - // Pause to prevent it from starting before visible (in case user peeked slowly) - self.emulatorCore?.pause() - } - + self.emulatorCoreQueue.async { + // Pause to prevent it from starting before visible (in case user peeked slowly) + self.emulatorCore?.pause() self.preparePreview() } } @@ -172,5 +169,7 @@ private extension PreviewGameViewController // Re-enable emulatorCore to update gameView again self.emulatorCore?.add(self.gameView) + + self.emulatorCore?.resume() } } diff --git a/Delta/Extensions/UIImage+SymbolFallback.swift b/Delta/Extensions/UIImage+SymbolFallback.swift new file mode 100644 index 0000000..e3e5358 --- /dev/null +++ b/Delta/Extensions/UIImage+SymbolFallback.swift @@ -0,0 +1,24 @@ +// +// UIImage+SymbolFallback.swift +// Delta +// +// Created by Riley Testut on 2/5/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import UIKit + +extension UIImage +{ + convenience init?(symbolNameIfAvailable name: String) + { + if #available(iOS 13, *) + { + self.init(systemName: name) + } + else + { + return nil + } + } +} diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index 1eba284..7bd09e7 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -8,6 +8,7 @@ import UIKit import MobileCoreServices +import AVFoundation import DeltaCore @@ -59,8 +60,9 @@ class GameCollectionViewController: UICollectionViewController private let prototypeCell = GridCollectionViewCell() - private var _performing3DTouchTransition = false - private weak var _destination3DTouchTransitionViewController: UIViewController? + private var _performingPreviewTransition = false + private weak var _previewTransitionViewController: PreviewGameViewController? + private weak var _previewTransitionDestinationViewController: UIViewController? private var _renameAction: UIAlertAction? private var _changingArtworkGame: Game? @@ -92,27 +94,31 @@ extension GameCollectionViewController layout.itemWidth = 90 layout.minimumInteritemSpacing = 12 - self.registerForPreviewing(with: self, sourceView: self.collectionView!) - - let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) - self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) + if #available(iOS 13, *) {} + else + { + self.registerForPreviewing(with: self, sourceView: self.collectionView!) + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) + self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) + } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if _performing3DTouchTransition + if _performingPreviewTransition { - _performing3DTouchTransition = false + _performingPreviewTransition = false // Unlike our custom transitions, 3D Touch transition doesn't manually call appearance methods for us // To compensate, we call them ourselves - _destination3DTouchTransitionViewController?.beginAppearanceTransition(true, animated: true) + _previewTransitionDestinationViewController?.beginAppearanceTransition(true, animated: true) self.transitionCoordinator?.animate(alongsideTransition: nil, completion: { (context) in - self._destination3DTouchTransitionViewController?.endAppearanceTransition() - self._destination3DTouchTransitionViewController = nil + self._previewTransitionDestinationViewController?.endAppearanceTransition() + self._previewTransitionDestinationViewController = nil }) } } @@ -176,9 +182,9 @@ extension GameCollectionViewController self.activeSaveState = nil - if _performing3DTouchTransition + if _performingPreviewTransition { - _destination3DTouchTransitionViewController = destinationViewController + _previewTransitionDestinationViewController = destinationViewController } default: break @@ -380,27 +386,27 @@ private extension GameCollectionViewController { let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil) - let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, action: { [unowned self] action in + let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "pencil.and.ellipsis.rectangle"), action: { [unowned self] action in self.rename(game) }) - let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default) { [unowned self] action in + let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "photo")) { [unowned self] action in self.changeArtwork(for: game) } - let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, action: { [unowned self] action in + let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "square.and.arrow.up"), action: { [unowned self] action in self.share(game) }) - let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, action: { [unowned self] action in + let saveStatesAction = Action(title: NSLocalizedString("Save States", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc.on.doc"), action: { [unowned self] action in self.viewSaveStates(for: game) }) - let importSaveFile = Action(title: NSLocalizedString("Import Save File", comment: ""), style: .default) { [unowned self] _ in + let importSaveFile = Action(title: NSLocalizedString("Import Save File", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "tray.and.arrow.down")) { [unowned self] _ in self.importSaveFile(for: game) } - let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in + let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in self.delete(game) }) @@ -687,6 +693,18 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate let game = self.dataSource.item(at: indexPath) + let gameViewController = self.makePreviewGameViewController(for: game) + _previewTransitionViewController = gameViewController + return gameViewController + } + + func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) + { + self.commitPreviewTransition() + } + + func makePreviewGameViewController(for game: Game) -> PreviewGameViewController + { let gameViewController = PreviewGameViewController() gameViewController.game = game @@ -702,11 +720,11 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate return gameViewController } - func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) + func commitPreviewTransition() { - let gameViewController = viewControllerToCommit as! PreviewGameViewController - let game = gameViewController.game as! Game + guard let gameViewController = _previewTransitionViewController else { return } + let game = gameViewController.game as! Game gameViewController.pauseEmulation() let indexPath = self.dataSource.fetchedResultsController.indexPath(forObject: game)! @@ -716,7 +734,7 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate gameViewController.emulatorCore?.stop() - _performing3DTouchTransition = true + _performingPreviewTransition = true self.launchGame(at: indexPath, clearScreen: true, ignoreAlreadyRunningError: true) @@ -805,3 +823,55 @@ extension GameCollectionViewController: UICollectionViewDelegateFlowLayout return size } } + +@available(iOS 13.0, *) +extension GameCollectionViewController +{ + override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? + { + let game = self.dataSource.item(at: indexPath) + let actions = self.actions(for: game) + + return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in + guard let self = self else { return nil } + + let previewViewController = self.makePreviewGameViewController(for: game) + self._previewTransitionViewController = previewViewController + + return previewViewController + }) { suggestedActions in + return UIMenu(title: "", children: actions.menuActions) + } + } + + override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) + { + self.commitPreviewTransition() + } + + override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + guard let indexPath = configuration.identifier as? NSIndexPath else { return nil } + guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? GridCollectionViewCell else { return nil } + + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear + + if let image = cell.imageView.image + { + let artworkFrame = AVMakeRect(aspectRatio: image.size, insideRect: cell.imageView.bounds) + + let bezierPath = UIBezierPath(rect: artworkFrame) + parameters.visiblePath = bezierPath + } + + let preview = UITargetedPreview(view: cell.imageView, parameters: parameters) + return preview + } + + override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + _previewTransitionViewController = nil + return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration) + } +} diff --git a/Delta/Pause Menu/Save States/SaveStatesViewController.swift b/Delta/Pause Menu/Save States/SaveStatesViewController.swift index 85bd353..53d3195 100644 --- a/Delta/Pause Menu/Save States/SaveStatesViewController.swift +++ b/Delta/Pause Menu/Save States/SaveStatesViewController.swift @@ -66,6 +66,8 @@ class SaveStatesViewController: UICollectionViewController private var prototypeCellWidthConstraint: NSLayoutConstraint! private var prototypeHeader = SaveStatesCollectionHeaderView() + private weak var _previewTransitionViewController: PreviewGameViewController? + private let dataSource: RSTFetchedResultsCollectionViewPrefetchingDataSource private var emulatorCoreSaveState: SaveStateProtocol? @@ -115,12 +117,16 @@ extension SaveStatesViewController self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth) self.prototypeCellWidthConstraint.isActive = true - let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(SaveStatesViewController.handleLongPressGesture(_:))) - self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) - self.prepareEmulatorCoreSaveState() - self.registerForPreviewing(with: self, sourceView: self.collectionView!) + if #available(iOS 13, *) {} + else + { + self.registerForPreviewing(with: self, sourceView: self.collectionView!) + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(SaveStatesViewController.handleLongPressGesture(_:))) + self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) + } self.navigationController?.navigationBar.barStyle = .blackTranslucent self.navigationController?.toolbar.barStyle = .blackTranslucent @@ -393,7 +399,18 @@ private extension SaveStatesViewController func updatePreviewSaveState(_ saveState: SaveState?) { - let alertController = UIAlertController(title: NSLocalizedString("Change Preview Save State?", comment: ""), message: NSLocalizedString("The Preview Save State is loaded whenever you preview this game from the Main Menu with 3D Touch. Are you sure you want to change it?", comment: ""), preferredStyle: .alert) + let message: String + + if #available(iOS 13, *) + { + message = NSLocalizedString("The Preview Save State is loaded whenever you long press this game from the Main Menu. Are you sure you want to change it?", comment: "") + } + else + { + message = NSLocalizedString("The Preview Save State is loaded whenever you 3D Touch this game from the Main Menu. Are you sure you want to change it?", comment: "") + } + + let alertController = UIAlertController(title: NSLocalizedString("Change Preview Save State?", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)) alertController.addAction(UIAlertAction(title: NSLocalizedString("Change", comment: ""), style: .default, handler: { (action) in @@ -471,20 +488,31 @@ private extension SaveStatesViewController { guard saveState.type != .auto else { return nil } + let isPreviewAvailable: Bool + + if #available(iOS 13, *) + { + isPreviewAvailable = true + } + else + { + isPreviewAvailable = (self.traitCollection.forceTouchCapability == .available) + } + var actions = [Action]() - if self.traitCollection.forceTouchCapability == .available + if isPreviewAvailable { if saveState.game?.previewSaveState != saveState { - let previewAction = Action(title: NSLocalizedString("Set as Preview Save State", comment: ""), style: .default, action: { [unowned self] action in + let previewAction = Action(title: NSLocalizedString("Set as Preview Save State", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "eye.fill"), action: { [unowned self] action in self.updatePreviewSaveState(saveState) }) actions.append(previewAction) } else { - let previewAction = Action(title: NSLocalizedString("Remove as Preview Save State", comment: ""), style: .default, action: { [unowned self] action in + let previewAction = Action(title: NSLocalizedString("Remove as Preview Save State", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "eye.slash.fill"), action: { [unowned self] action in self.updatePreviewSaveState(nil) }) actions.append(previewAction) @@ -494,7 +522,7 @@ private extension SaveStatesViewController let cancelAction = Action(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, action: nil) actions.append(cancelAction) - let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, action: { [unowned self] action in + let renameAction = Action(title: NSLocalizedString("Rename", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "pencil.and.ellipsis.rectangle"), action: { [unowned self] action in self.renameSaveState(saveState) }) actions.append(renameAction) @@ -504,19 +532,19 @@ private extension SaveStatesViewController case .auto: break case .quick: break case .general: - let lockAction = Action(title: NSLocalizedString("Lock", comment: ""), style: .default, action: { [unowned self] action in + let lockAction = Action(title: NSLocalizedString("Lock", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "lock.fill"), action: { [unowned self] action in self.lockSaveState(saveState) }) actions.append(lockAction) case .locked: - let unlockAction = Action(title: NSLocalizedString("Unlock", comment: ""), style: .default, action: { [unowned self] action in + let unlockAction = Action(title: NSLocalizedString("Unlock", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "lock.open.fill"), action: { [unowned self] action in self.unlockSaveState(saveState) }) actions.append(unlockAction) } - let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, action: { [unowned self] action in + let deleteAction = Action(title: NSLocalizedString("Delete", comment: ""), style: .destructive, image: UIImage(symbolNameIfAvailable: "trash"), action: { [unowned self] action in self.deleteSaveState(saveState) }) actions.append(deleteAction) @@ -608,21 +636,36 @@ extension SaveStatesViewController: UIViewControllerPreviewingDelegate previewingContext.sourceRect = layoutAttributes.frame let saveState = self.dataSource.item(at: indexPath) - let actions = self.actionsForSaveState(saveState)?.previewActions ?? [] - let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path) - let previewGameViewController = PreviewGameViewController() - previewGameViewController.game = self.game - previewGameViewController.overridePreviewActionItems = actions - previewGameViewController.previewSaveState = saveState - previewGameViewController.previewImage = previewImage + let previewGameViewController = self.makePreviewGameViewController(for: saveState) + _previewTransitionViewController = previewGameViewController return previewGameViewController } func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { - let gameViewController = viewControllerToCommit as! PreviewGameViewController + self.commitPreviewTransition() + } + + func makePreviewGameViewController(for saveState: SaveState) -> PreviewGameViewController + { + let previewImage = self.dataSource.prefetchItemCache.object(forKey: saveState) ?? UIImage(contentsOfFile: saveState.imageFileURL.path) + + let gameViewController = PreviewGameViewController() + gameViewController.game = self.game + gameViewController.previewSaveState = saveState + gameViewController.previewImage = previewImage + + let actions = self.actionsForSaveState(saveState)?.previewActions ?? [] + gameViewController.overridePreviewActionItems = actions + + return gameViewController + } + + func commitPreviewTransition() + { + guard let gameViewController = self._previewTransitionViewController else { return } gameViewController.pauseEmulation() let fileURL = FileManager.default.uniqueTemporaryURL() @@ -708,3 +751,47 @@ extension SaveStatesViewController: UICollectionViewDelegateFlowLayout return size } } + +@available(iOS 13.0, *) +extension SaveStatesViewController +{ + override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? + { + let saveState = self.dataSource.item(at: indexPath) + guard let actions = self.actionsForSaveState(saveState) else { return nil } + + return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in + guard let self = self else { return nil } + + let previewGameViewController = self.makePreviewGameViewController(for: saveState) + self._previewTransitionViewController = previewGameViewController + + return previewGameViewController + }) { suggestedActions in + return UIMenu(title: "", children: actions.menuActions) + } + } + + override func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) + { + self.commitPreviewTransition() + } + + override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + guard let indexPath = configuration.identifier as? NSIndexPath else { return nil } + guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? GridCollectionViewCell else { return nil } + + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear + + let preview = UITargetedPreview(view: cell.imageView, parameters: parameters) + return preview + } + + override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? + { + self._previewTransitionViewController = nil + return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration) + } +}