diff --git a/VoidLink/GamepadNavigationIllustrationHud.swift b/VoidLink/GamepadNavigationIllustrationHud.swift index 0bc02456..5aa00725 100644 --- a/VoidLink/GamepadNavigationIllustrationHud.swift +++ b/VoidLink/GamepadNavigationIllustrationHud.swift @@ -19,7 +19,7 @@ final class GamepadNavigationIllustrationHud: UIView { private struct Hint { let control: ControllerElement let keyContent: KeyContent - let title: String + let titles: [String] let isInAction: Bool } @@ -39,10 +39,7 @@ final class GamepadNavigationIllustrationHud: UIView { private static let hudScale: CGFloat = PublicUtils.isIPhone ? 0.74 : 1 private static let hudWidth: CGFloat = 235 private static let edgeMargin: CGFloat = PublicUtils.isIPhone ? 5 : 24 - private var hints: [Hint] = [ - // Hint(control: .leftStick, keyContent: .text("L"), title: "浏览", isInAction: false), - // Hint(control: .a, keyContent: .text("A"), title: "选择", isInAction: false) - ] + private var hints: [Hint] = [] override init(frame: CGRect) { super.init(frame: frame) @@ -83,10 +80,10 @@ final class GamepadNavigationIllustrationHud: UIView { currentHud = hud hud.attach(to: window) - if let mainFrameVC = ControllerNavigator.radialMenuDelegate as? MainFrameViewController { + if let mainFrameVC = ControllerNavigator.radialMenuDelegate as? MainFrameViewController, ControllerNavigator.radialMenuView == nil { DispatchQueue.main.asyncAfter(deadline: .now()+3) { let streamFrameVC = StreamFrameViewController.sharedInstance() - if ControllerNavigator.radialMenuView == nil, mainFrameVC.isStreaming(), !mainFrameVC.settingsExpandedInStreamView, streamFrameVC?.hasNoPresentedVC == true { + if ControllerNavigator.radialMenuView == nil, mainFrameVC.isStreaming(), !mainFrameVC.settingsExpandedInStreamView, streamFrameVC?.hasNoPresentedVC == true, !ControllerNavigator.controllerMouseEnabled { requestHudDetachKeepingMinimumActionDuration() } } @@ -171,7 +168,7 @@ final class GamepadNavigationIllustrationHud: UIView { guard hints.indices.contains(index), let rowStack = row as? UIStackView, let keyView = rowStack.arrangedSubviews.first, - let titleLabel = rowStack.arrangedSubviews.last as? UILabel else { + let titleView = rowStack.arrangedSubviews.last else { continue } @@ -192,7 +189,7 @@ final class GamepadNavigationIllustrationHud: UIView { imageView.tintColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83) } } - titleLabel.textColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.69 : 0.6) + updateTitleColors(in: titleView, color: ThemeManager.textColor.withAlphaComponent(isDark ? 0.69 : 0.6)) } } @@ -223,7 +220,7 @@ final class GamepadNavigationIllustrationHud: UIView { } private func updateHints(with elements: [ControllerNavigationElement]) { - let nextHints = elements.compactMap { hint(for: $0) } + let nextHints = hints(for: elements) hints = nextHints.isEmpty ? [] : nextHints guard !hints.isEmpty else {return} reloadHintRows() @@ -236,7 +233,7 @@ final class GamepadNavigationIllustrationHud: UIView { Hint( control: hint.control, keyContent: hint.keyContent, - title: hint.title, + titles: hint.titles, isInAction: Self.isControlActive(hint.control) ) } @@ -252,12 +249,35 @@ final class GamepadNavigationIllustrationHud: UIView { hints.forEach { contentStackView.addArrangedSubview(makeRow(for: $0)) } } - private func hint(for navigationElement: ControllerNavigationElement) -> Hint? { + private func hints(for navigationElements: [ControllerNavigationElement]) -> [Hint] { + var groupedElements: [ControllerElement: [ControllerNavigationElement]] = [:] + var orderedControls: [ControllerElement] = [] + + for navigationElement in navigationElements { + if groupedElements[navigationElement.control] == nil { + orderedControls.append(navigationElement.control) + groupedElements[navigationElement.control] = [] + } + groupedElements[navigationElement.control]?.append(navigationElement) + } + + return orderedControls.flatMap { control -> [Hint] in + guard let elements = groupedElements[control] else { return [] } + if elements.count == 2 { + return [hint(for: elements)] + } + + return elements.map { hint(for: [$0]) } + } + } + + private func hint(for navigationElements: [ControllerNavigationElement]) -> Hint { + let control = navigationElements.first?.control ?? .null return Hint( - control: navigationElement.control, - keyContent: keyContent(for: navigationElement.control), - title: navigationElement.action.localized, - isInAction: navigationElement.isInAction || Self.isControlActive(navigationElement.control) + control: control, + keyContent: keyContent(for: control), + titles: navigationElements.map { $0.action.localized }, + isInAction: navigationElements.contains(where: { $0.isInAction }) || Self.isControlActive(control) ) } @@ -309,22 +329,48 @@ final class GamepadNavigationIllustrationHud: UIView { private func makeRow(for hint: Hint) -> UIStackView { let keyView = makeKeyView(for: hint.keyContent) + let titleView = makeTitleView(for: hint.titles) - let titleLabel = UILabel() - titleLabel.text = hint.title - titleLabel.font = UIFont.roundedSystemFont(ofSize: 14, weight: .medium) - titleLabel.adjustsFontSizeToFitWidth = true - titleLabel.minimumScaleFactor = 0.2 - titleLabel.numberOfLines = 1 - titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) - - let rowStackView = UIStackView(arrangedSubviews: [keyView, titleLabel]) + let rowStackView = UIStackView(arrangedSubviews: [keyView, titleView]) rowStackView.axis = .horizontal rowStackView.alignment = .center rowStackView.spacing = 8 return rowStackView } + private func makeTitleView(for titles: [String]) -> UIView { + if titles.count == 2 { + let stackView = UIStackView(arrangedSubviews: titles.map { makeTitleLabel(text: $0, isMergedTitle: true) }) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 1 + stackView.setContentCompressionResistancePriority(.required, for: .horizontal) + return stackView + } + + return makeTitleLabel(text: titles.first ?? "", isMergedTitle: false) + } + + private func makeTitleLabel(text: String, isMergedTitle: Bool) -> UILabel { + let titleLabel = UILabel() + titleLabel.text = text + titleLabel.font = UIFont.roundedSystemFont(ofSize: isMergedTitle ? 12.5 : 14, weight: .medium) + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.minimumScaleFactor = isMergedTitle ? 0.55 : 0.2 + titleLabel.numberOfLines = 1 + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + return titleLabel + } + + private func updateTitleColors(in view: UIView, color: UIColor) { + if let label = view as? UILabel { + label.textColor = color + return + } + + view.subviews.forEach { updateTitleColors(in: $0, color: color) } + } + private func makeKeyView(for content: KeyContent) -> UIView { let keyContainerView = UIView() keyContainerView.translatesAutoresizingMaskIntoConstraints = false @@ -423,7 +469,7 @@ final class GamepadNavigationIllustrationHud: UIView { } private static func isControlActive(_ control: ControllerElement) -> Bool { - !relatedControls(for: control).isDisjoint(with: activeControls) + !activeControls(forHintControl: control).isDisjoint(with: activeControls) } private static func requestActionStateEnd(for control: ControllerElement) { @@ -486,12 +532,16 @@ final class GamepadNavigationIllustrationHud: UIView { } } - private static func relatedControls(for control: ControllerElement) -> Set { + private static func activeControls(forHintControl control: ControllerElement) -> Set { switch control { case .leftStick, .leftStickX, .leftStickY: return [.leftStick, .leftStickX, .leftStickY] case .rightStick, .rightStickX, .rightStickY: return [.rightStick, .rightStickX, .rightStickY] + case .dpad: + return [.dpad, .dpadUp, .dpadDown, .dpadLeft, .dpadRight] + case .abxy: + return [.abxy, .a, .b, .x, .y] default: return [control] } diff --git a/VoidLink/Input/ControllerNavigator.swift b/VoidLink/Input/ControllerNavigator.swift index 657ed4cc..a5325fef 100644 --- a/VoidLink/Input/ControllerNavigator.swift +++ b/VoidLink/Input/ControllerNavigator.swift @@ -24,8 +24,14 @@ final class ControllerNavigationElement: NSObject { private var controllerNavigationSelectedIndexPathKey: UInt8 = 0 private var controllerNavigationPersistTokenKey: UInt8 = 0 private var controllerNavigationHighlightGenerationKey: UInt8 = 0 +private var controllerMouseCurvePreviewViewKey: UInt8 = 0 +private var controllerMouseCurvePreviewDismissWorkItemKey: UInt8 = 0 private let settingsControllerNavigationHighlightedIdentifierKey = "SettingsControllerNavigationHighlightedIdentifier" private let hostControllerNavigationHighlightedUUIDKey = "HostControllerNavigationHighlightedUUID" +private let settingsExcludedControllerNavigationSectionIdentifiers: Set = [ + "SettingsSectionTouch&Controller", + "SettingsSectionPencil" +] @available(iOS 13.0, *) private final class ControllerMouseDisplayLinkTarget: NSObject { @@ -34,6 +40,125 @@ private final class ControllerMouseDisplayLinkTarget: NSObject { } } +@available(iOS 13.0, *) +private final class ControllerMouseCurvePreviewView: UIView { + var expo: CGFloat = 1.0 { + didSet { + setNeedsDisplay() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + isOpaque = false + layer.cornerRadius = 14 + layer.cornerCurve = .continuous + layer.masksToBounds = true + layer.borderWidth = 1 + } + + override func draw(_ rect: CGRect) { + let isDark = ThemeManager.userInterfaceStyle() == .dark + let backgroundColor = isDark + ? UIColor(white: 0.08, alpha: 0.88) + : UIColor.white.withAlphaComponent(0.92) + let axisColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.62 : 0.48) + let textColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.78 : 0.68) + let curveColor = ThemeManager.appPrimaryColor + layer.borderColor = ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.32 : 0.22).cgColor + + guard let context = UIGraphicsGetCurrentContext() else { return } + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let footerHeight: CGFloat = 22 + let xLabelHeight: CGFloat = 24 + let plotHorizontalMargin: CGFloat = 34 + let chartRect = bounds.inset(by: UIEdgeInsets(top: 18, left: 0, bottom: footerHeight + xLabelHeight, right: 0)) + let plotSide = min(bounds.width - plotHorizontalMargin * 2, chartRect.height) + let plotRect = CGRect( + x: bounds.midX - plotSide / 2, + y: chartRect.midY - plotSide / 2, + width: plotSide, + height: plotSide + ) + guard plotRect.width > 1, plotRect.height > 1 else { return } + + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.roundedSystemFont(ofSize: 11, weight: .medium), + .foregroundColor: textColor + ] + + ("Cursor Speed".localized as NSString).draw(at: CGPoint(x: plotRect.minX + 8, y: plotRect.minY + 8), withAttributes: textAttributes) + let xAxisTitle = "Stick Offset".localized as NSString + let xAxisTitleSize = xAxisTitle.size(withAttributes: textAttributes) + xAxisTitle.draw(at: CGPoint(x: plotRect.midX - xAxisTitleSize.width / 2, y: plotRect.maxY + 10), withAttributes: textAttributes) + + context.setStrokeColor(axisColor.cgColor) + context.setLineWidth(1) + context.move(to: CGPoint(x: plotRect.minX, y: plotRect.minY)) + context.addLine(to: CGPoint(x: plotRect.minX, y: plotRect.maxY)) + context.addLine(to: CGPoint(x: plotRect.maxX, y: plotRect.maxY)) + context.strokePath() + + ("0" as NSString).draw(at: CGPoint(x: plotRect.minX - 10, y: plotRect.maxY - 8), withAttributes: textAttributes) + ("1" as NSString).draw(at: CGPoint(x: plotRect.minX - 10, y: plotRect.minY - 7), withAttributes: textAttributes) + ("1" as NSString).draw(at: CGPoint(x: plotRect.maxX - 4, y: plotRect.maxY + 10), withAttributes: textAttributes) + + let guidePath = UIBezierPath() + guidePath.move(to: CGPoint(x: plotRect.minX, y: plotRect.maxY)) + guidePath.addLine(to: CGPoint(x: plotRect.maxX, y: plotRect.minY)) + context.saveGState() + context.setLineDash(phase: 0, lengths: [4, 4]) + axisColor.withAlphaComponent(0.35).setStroke() + guidePath.lineWidth = 1 + guidePath.stroke() + context.restoreGState() + + let curvePath = UIBezierPath() + for step in 0...120 { + let normalizedX = CGFloat(step) / 120 + let speed = abs(PublicUtils.controllerMouseExpoMappedOffset(normalizedX, expo: expo)) + let point = CGPoint( + x: plotRect.minX + normalizedX * plotRect.width, + y: plotRect.maxY - min(max(speed, 0), 1) * plotRect.height + ) + if step == 0 { + curvePath.move(to: point) + } else { + curvePath.addLine(to: point) + } + } + + curveColor.setStroke() + curvePath.lineWidth = 3 + curvePath.lineCapStyle = .round + curvePath.lineJoinStyle = .round + curvePath.stroke() + + let footerParagraphStyle = NSMutableParagraphStyle() + footerParagraphStyle.alignment = .center + let footerAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.roundedSystemFont(ofSize: 10, weight: .medium), + .foregroundColor: textColor.withAlphaComponent(0.76), + .paragraphStyle: footerParagraphStyle + ] + ("Inspired by RC hobby transmitters".localized as NSString).draw( + in: CGRect(x: 12, y: bounds.maxY - footerHeight + 1, width: bounds.width - 24, height: footerHeight - 2), + withAttributes: footerAttributes + ) + } +} + @available(iOS 13.0, *) @objc protocol ControllerNavigatorRadialMenuDelegate: AnyObject { func controllerNavigatorDidSelect(item: RadialMenuItem) @@ -508,11 +633,11 @@ final class ControllerNavigator: NSObject { } case .exit: exit(0) - case .shortcuts: + case .toolbox: DispatchQueue.main.async { radialMenuView?.dismiss() radialMenuView = nil - StreamFrameViewController.sharedInstance().bringUpToolboxMenu() + StreamFrameViewController.sharedInstance().bringUpToolboxMenuWithoutWidgetLayoutTool() } default: break @@ -544,7 +669,7 @@ final class ControllerNavigator: NSObject { if radialMenuState == .moreOptions { if mainFrameVC.isStreaming() { RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "Game Profiles".localized, subtitle: "", symbol:PublicUtils.iOS18Available ? "gamecontroller.circle" : "gamecontroller.fill", item: .gameProfiles)) - RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "=shortcuts".localized, subtitle: "", symbol: "apple.terminal", item: .shortcuts)) + RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "=toolbox".localized, subtitle: "", symbol: "apple.terminal", item: .toolbox)) } else { if !mainFrameVC.isInAppView(), !mainFrameVC.settingsViewExpanded {RadialMenuOverlayView.menuSectors.append(RadialMenuSector(title: "About".localized, subtitle: "", symbol: "questionmark.circle", item: .aboutView))} @@ -655,6 +780,94 @@ final class ControllerNavigator: NSObject { } } + private static func listenToNavigationCluster() { + guard let mainFrameVC = radialMenuDelegate as? MainFrameViewController else {return} + + let upNavButton: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .dpadUp : .y + let downNavButton: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .dpadDown : .a + let leftNavButton: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .dpadLeft : .x + let rightNavButton: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .dpadRight : .b + + if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is ToolboxViewController || uiNavigationDelegate is ProfileSelectorViewController { + let verticalNavigationAxis: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .leftStickY : .rightStickY + ControllerUtil.listenPrimaryControllerStickAxis(verticalNavigationAxis, threshold: 0.6) {state in + GamepadNavigationIllustrationHud.updateActionState(for: verticalNavigationAxis, isInAction: state != .orderedSame) + guard radialMenuView == nil else {return} + switch state { + case .orderedAscending: + if navigationTimer?.isRunning() != true { + if uiNavigationDelegate is SettingsViewController { + navigateContinuously(downward: true) + } + if uiNavigationDelegate is ControllerCollectionNavigationDelegate { + navigateContinuously(downward: true) + } + } + case .orderedDescending: + if navigationTimer?.isRunning() != true { + if uiNavigationDelegate is SettingsViewController { + navigateContinuously(downward: false) + } + if uiNavigationDelegate is ControllerCollectionNavigationDelegate { + navigateContinuously(downward: false) + } + } + case .orderedSame: + navigationTimer?.clean() + } + } + ControllerUtil.listenPrimaryControllerButton(upNavButton) {pressed in + GamepadNavigationIllustrationHud.updateActionState(for: upNavButton, isInAction: pressed) + if pressed { + uiNavigationDelegate.navigateByController(downward: false) + } + } + ControllerUtil.listenPrimaryControllerButton(downNavButton) {pressed in + GamepadNavigationIllustrationHud.updateActionState(for: downNavButton, isInAction: pressed) + if pressed { + uiNavigationDelegate.navigateByController(downward: true) + } + } + } + + if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || uiNavigationDelegate is ToolboxViewController || uiNavigationDelegate is ProfileSelectorViewController { + let horizontalNavigationAxis: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .leftStickX : .rightStickX + ControllerUtil.listenPrimaryControllerStickAxis(horizontalNavigationAxis, threshold: 0.6) {state in + GamepadNavigationIllustrationHud.updateActionState(for: horizontalNavigationAxis, isInAction: state != .orderedSame) + guard radialMenuView == nil else {return} + switch state { + case .orderedAscending: + if navigationTimer?.isRunning() != true { + if uiNavigationDelegate is ControllerCollectionNavigationDelegate { + navigateContinuously(forward: false) + } + } + case .orderedDescending: + if navigationTimer?.isRunning() != true { + if uiNavigationDelegate is ControllerCollectionNavigationDelegate { + navigateContinuously(forward: true) + } + } + case .orderedSame: + navigationTimer?.clean() + } + } + ControllerUtil.listenPrimaryControllerButton(leftNavButton) {pressed in + GamepadNavigationIllustrationHud.updateActionState(for: leftNavButton, isInAction: pressed) + if pressed { + uiNavigationDelegate.navigateByController(forward: false) + } + } + ControllerUtil.listenPrimaryControllerButton(rightNavButton) {pressed in + GamepadNavigationIllustrationHud.updateActionState(for: rightNavButton, isInAction: pressed) + if pressed { + uiNavigationDelegate.navigateByController(forward: true) + } + } + } + + } + @objc static func listenToControllerMouse() { guard radialMenuState == .mouseModeEnabled else { return } ControllerUtil.listenPrimaryControllerButton(controllerMouseLeftButton){ pressed in @@ -693,9 +906,9 @@ final class ControllerNavigator: NSObject { listenToRadialMenuButton() listenToRadialMenuStick() - + listenToNavigationCluster() - + /* if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is ToolboxViewController || uiNavigationDelegate is ProfileSelectorViewController { let verticalNavigationAxis: ControllerElement = ControllerNavigator.radialMenuButtonPosition == .right ? .leftStickY : .rightStickY ControllerUtil.listenPrimaryControllerStickAxis(verticalNavigationAxis, threshold: 0.6) {state in @@ -749,6 +962,7 @@ final class ControllerNavigator: NSObject { } } } + */ if let uiNavigationDelegate = uiNavigationDelegate, !mainFrameVC.isStreaming() || mainFrameVC.settingsExpandedInStreamView || uiNavigationDelegate is UIAlertController || uiNavigationDelegate is ProfileSelectorViewController || uiNavigationDelegate is ToolboxViewController { let navigations = uiNavigationDelegate.getNavigationElements() @@ -792,6 +1006,13 @@ final class ControllerNavigator: NSObject { @available(iOS 13.0, *) extension SettingsViewController: ControllerUINavigationDelegate { + @objc(showControllerMouseCurvePreviewWithExpo:) + func showControllerMouseCurvePreview(expo: CGFloat) { + PublicUtils.runOnMain { [weak self] in + self?.showControllerMouseCurvePreviewOnMain(expo: expo) + } + } + @objc func persistControllerNavigationHighlight() { guard let highlightedView = ControllerNavigator.controllerNavigationHighlightedView, let identifier = controllerNavigationPersistenceIdentifier(for: highlightedView) else { @@ -801,6 +1022,64 @@ extension SettingsViewController: ControllerUINavigationDelegate { UserDefaults.standard.set(identifier, forKey: settingsControllerNavigationHighlightedIdentifierKey) } + private var controllerMouseCurvePreviewView: ControllerMouseCurvePreviewView? { + get { + objc_getAssociatedObject(self, &controllerMouseCurvePreviewViewKey) as? ControllerMouseCurvePreviewView + } + set { + objc_setAssociatedObject(self, &controllerMouseCurvePreviewViewKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + private var controllerMouseCurvePreviewDismissWorkItem: DispatchWorkItem? { + get { + objc_getAssociatedObject(self, &controllerMouseCurvePreviewDismissWorkItemKey) as? DispatchWorkItem + } + set { + objc_setAssociatedObject(self, &controllerMouseCurvePreviewDismissWorkItemKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + private func showControllerMouseCurvePreviewOnMain(expo: CGFloat) { + let previewView = controllerMouseCurvePreviewView ?? ControllerMouseCurvePreviewView() + controllerMouseCurvePreviewView = previewView + previewView.expo = expo + previewView.translatesAutoresizingMaskIntoConstraints = false + + if previewView.superview == nil { + previewView.alpha = 0 + view.addSubview(previewView) + let preferredWidth = previewView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.72) + preferredWidth.priority = .defaultHigh + NSLayoutConstraint.activate([ + previewView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + previewView.widthAnchor.constraint(lessThanOrEqualToConstant: 360), + preferredWidth, + previewView.heightAnchor.constraint(equalToConstant: 190), + previewView.bottomAnchor.constraint(equalTo: controllerMouseExpoStack.topAnchor, constant: -10) + ]) + } + + view.bringSubviewToFront(previewView) + UIView.animate(withDuration: 0.12) { + previewView.alpha = 1 + } + + controllerMouseCurvePreviewDismissWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self, weak previewView] in + UIView.animate(withDuration: 0.18, animations: { + previewView?.alpha = 0 + }, completion: { _ in + previewView?.removeFromSuperview() + if self?.controllerMouseCurvePreviewView === previewView { + self?.controllerMouseCurvePreviewView = nil + } + }) + } + controllerMouseCurvePreviewDismissWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem) + } + @objc func restoreControllerNavigationHighlight() { guard ControllerNavigator.enabled, ControllerUtil.primaryGCController != nil else {return} PublicUtils.runOnMain { [weak self] in @@ -853,23 +1132,25 @@ extension SettingsViewController: ControllerUINavigationDelegate { } elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButton, action: "radialMenu")) elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .rightStickY : .leftStickY, action: "menuNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .abxy : .dpad, action: "menuNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadLeft : .x, action: "widgetOperationBackward")) elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .b, action: "widgetOperationForward")) return elements } - func navigateByController(forward:Bool) { + func navigateByController(downward:Bool) { guard self.hasNoPresentedVC else {return} if currentSettingsMenuMode == .FavoriteSettings, ControllerNavigator.settingsFavoriteReorderActive { - moveHighlightedFavoriteSettingStack(by: forward ? 1 : -1) + moveHighlightedFavoriteSettingStack(by: downward ? 1 : -1) return } - performControllerNavigationSelectionMove(by: forward ? 1 : -1) + performControllerNavigationSelectionMove(by: downward ? 1 : -1) } - func navigateByController(downward:Bool) { + func navigateByController(forward:Bool) { } func uiWidgetActionForControllerNavigator(forward: Bool, from navigation:ControllerNavigationElement) { @@ -1270,6 +1551,7 @@ extension SettingsViewController: ControllerUINavigationDelegate { targets.append(headerView) } + guard !shouldSkipControllerNavigationSection(sectionView) else { continue } guard let rootStackView = sectionView.rootStackView else { continue } guard isControllerNavigationVisible(rootStackView, within: sectionView) else { continue } @@ -1282,6 +1564,14 @@ extension SettingsViewController: ControllerUINavigationDelegate { return targets } + private func shouldSkipControllerNavigationSection(_ sectionView: MenuSectionView) -> Bool { + guard let identifier = sectionView.identifier else { + return false + } + + return settingsExcludedControllerNavigationSectionIdentifiers.contains(identifier) + } + private func favoriteSettingsControllerNavigationTargets(in parentStack: UIStackView, skippingDisabledStacks: Bool) -> [UIView] { var targets: [UIView] = [] for arrangedSubview in parentStack.arrangedSubviews { @@ -1673,10 +1963,11 @@ extension ControllerCollectionNavigationDelegate { var elements: [ControllerNavigationElement] = [] elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButton, action: "radialMenu")) elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .rightStick : .leftStick, action: "focusNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .abxy : .dpad, action: "focusNavigation")) if self is HostCollectionViewController {elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "applications"))} if self is HostCollectionViewController {elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .x, action: "launchPairWake"))} if self is MainFrameViewController {elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "launch"))} - if self is MainFrameViewController {elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .x, action: "quitApp"))} + if self is MainFrameViewController {elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .b, action: "quitApp"))} return elements } @@ -2055,13 +2346,28 @@ extension UIAlertController: ControllerUINavigationDelegate { guard enabledActions.count > 1, let lastAction = enabledActions.last else { guard let title = firstAction.title, !title.isEmpty else { return [] } - return [ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .x, action: title)] + return [ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: title)] } - return [ - ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadLeft : .x, action: firstAction.title ?? ""), - ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .b, action: lastAction.title ?? "") - ].filter { !$0.action.isEmpty } + + + + var elements: [ControllerNavigationElement] = [] + + if ControllerNavigator.radialMenuButtonPosition == .left { + elements.append(ControllerNavigationElement(control: firstAction.style == .default ? .dpadRight : .dpadUp, action: firstAction.title ?? "")) + if firstAction.style != lastAction.style { + elements.append(ControllerNavigationElement(control: lastAction.style == .cancel ? .dpadUp : .dpadRight, action: lastAction.title ?? "")) + } + } + else { + elements.append(ControllerNavigationElement(control: firstAction.style == .default ? .a : .b, action: firstAction.title ?? "")) + if firstAction.style != lastAction.style { + elements.append(ControllerNavigationElement(control: lastAction.style == .cancel ? .b : .a, action: lastAction.title ?? "")) + } + } + + return elements } func navigateByController(forward: Bool) {} diff --git a/VoidLink/Input/ControllerUtil.swift b/VoidLink/Input/ControllerUtil.swift index f34588ca..19fe37ca 100644 --- a/VoidLink/Input/ControllerUtil.swift +++ b/VoidLink/Input/ControllerUtil.swift @@ -17,6 +17,7 @@ import Foundation case stick case stickAxis case touchpad + case cluster case undefined } @@ -70,6 +71,9 @@ import Foundation case rightStick = 0x800020 case rightStickX = 0x800021 case rightStickY = 0x800022 + + case dpad = 0x800013 + case abxy = 0x800023 case null = 0xFFFFFF @@ -111,6 +115,9 @@ import Foundation case .rightStick: return "RStick".localized case .rightStickX: return "RStickX".localized case .rightStickY: return "RStickY".localized + + case .dpad: return "DPad".localized + case .abxy: return "ABXY".localized case .null: return "Null".localized } @@ -130,6 +137,9 @@ import Foundation case .dpadRight: if #available(iOS 15.0, *) {return "dpad.right.filled"} else {return ""} + case .dpad: + if #available(iOS 14.0, *) {return "dpad"} + else {return ""} case .a: if #available(iOS 15.0, *) {return "circle.grid.cross.down.filled"} else {return ""} @@ -142,6 +152,10 @@ import Foundation case .y: if #available(iOS 15.0, *) {return "circle.grid.cross.up.filled"} else {return ""} + case .abxy: + if #available(iOS 14.0, *) {return "circle.grid.cross"} + else {return ""} + default: return "" } } @@ -184,6 +198,9 @@ import Foundation case .rightStick: return .stick case .rightStickX: return .stickAxis case .rightStickY: return .stickAxis + + case .dpad: return .cluster + case .abxy: return .cluster case .null: return .undefined } @@ -227,6 +244,9 @@ import Foundation case .rightStick: return .right case .rightStickX: return .right case .rightStickY: return .right + + case .abxy: return .right + case .dpad: return .left case .null: return .undefined } diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents index a3223a03..26d5a515 100644 --- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents +++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents @@ -43,7 +43,7 @@ - + diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index dd0cefc7..7c7a4dc8 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -4569,29 +4569,6 @@ } } }, - "All Settings" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "全部设置" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "所有設定" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "所有設定" - } - } - } - }, "All Profiles" : { "extractionState" : "manual", "localizations" : { @@ -4615,6 +4592,29 @@ } } }, + "All Settings" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全部设置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有設定" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "所有設定" + } + } + } + }, "Alpha" : { "extractionState" : "manual", "localizations" : { @@ -6390,6 +6390,52 @@ } } }, + "Current Profile" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前配置" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前設定檔" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "目前設定檔" + } + } + } + }, + "Cursor Speed" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "光标速度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "游標速度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "游標速度" + } + } + } + }, "Custom Resolution Selected" : { "extractionState" : "manual", "localizations" : { @@ -6534,29 +6580,6 @@ } } }, - "Current Profile" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "当前配置" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "目前設定檔" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "目前設定檔" - } - } - } - }, "Del" : { "extractionState" : "manual", "localizations" : { @@ -6632,29 +6655,6 @@ } } }, - "Duplicate" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "复制" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "複製" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "複製" - } - } - } - }, "Delete Buttons Here" : { "extractionState" : "manual", "localizations" : { @@ -7496,6 +7496,29 @@ } } }, + "Duplicate" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } + } + } + }, "e.g. ctrl, lswheel, wasdpad..." : { "extractionState" : "manual", "localizations" : { @@ -8211,29 +8234,6 @@ } } }, - "Export" : { - "extractionState" : "manual", - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "导出" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "匯出" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "匯出" - } - } - } - }, "Expands setting menu during streaming" : { "extractionState" : "manual", "localizations" : { @@ -8280,6 +8280,29 @@ } } }, + "Export" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯出" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "匯出" + } + } + } + }, "externalDisplayModeStackDoc" : { "extractionState" : "manual", "localizations" : { @@ -10229,6 +10252,29 @@ } } }, + "Inspired by RC hobby transmitters" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "灵感来自航模遥控发射机" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "靈感來自航模遙控發射機" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "靈感來自航模遙控發射機" + } + } + } + }, "Interval of Multi-Touch Move Events: %d μs" : { "extractionState" : "manual", "localizations" : { @@ -16814,6 +16860,29 @@ } } }, + "Stick Offset" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "摇杆偏移量" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "搖桿偏移量" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "搖桿偏移量" + } + } + } + }, "stickWheelLink" : { "extractionState" : "manual", "localizations" : { @@ -18977,4 +19046,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift index 262c671e..76416033 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/Table/ProfileSelectorViewController.swift @@ -1201,9 +1201,10 @@ final class ProfileSelectorViewController: UIViewController, UICollectionViewDat func getNavigationElements() -> [ControllerNavigationElement] { var elements: [ControllerNavigationElement] = [] elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .rightStick : .leftStick, action: "focusNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .abxy : .dpad, action: "focusNavigation")) elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "holdToReorder")) - elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "doublePressToDelete")) - elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "exit")) + // elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "doublePressToDelete")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .b, action: "exit")) return elements } diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 4954c876..f514b7f0 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -14,6 +14,7 @@ @class LayoutOnScreenControlsViewController; @class MainFrameViewController; +@class MenuSectionView; @class MicHandler; @class WidgetPickerViewController; @@ -105,6 +106,7 @@ @property (weak, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC; @property (nonatomic, weak) MainFrameViewController *mainFrameViewController; @property (strong, nonatomic) UIView *controllerNavigationHighlightOverlayView; +@property (nonatomic, strong, readonly) MenuSectionView *touchControlSection; @property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 5fe31049..3c762ef0 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -69,6 +69,10 @@ @dynamic overrideUserInterfaceStyle; @synthesize controllerNavigationHighlightOverlayView = _controllerNavigationHighlightOverlayView; +- (MenuSectionView *)touchControlSection { + return touchControlSection; +} + //static NSString* bitrateFormat; static const int bitrateTable[] = { @@ -1839,8 +1843,8 @@ BOOL isCustomResolution(int resolutionSelected) { [tipsAlertController setValue:attributedMessage forKey:@"attributedMessage"]; */ - UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Online Documentation"] - style:UIAlertActionStyleDefault + UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Learn More"] + style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){ [PublicUtils openUrl:onlineDocLink]; }]; @@ -2540,7 +2544,7 @@ BOOL isCustomResolution(int resolutionSelected) { */ UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] - style:UIAlertActionStyleDefault + style:UIAlertActionStyleCancel handler:nil]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"] @@ -3342,6 +3346,9 @@ BOOL isCustomResolution(int resolutionSelected) { - (void) controllerMouseExpoSliderMoved:(UISlider* )sender { [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f ", sender.value]; + if (@available(iOS 13.0, *)) if(!settingsViewJustLoaded) { + [self showControllerMouseCurvePreviewWithExpo:sender.value]; + } } - (uint32_t) getScreenEdgeFromSelector { diff --git a/VoidLink/ViewControllers/StreamFrameViewController.h b/VoidLink/ViewControllers/StreamFrameViewController.h index 1e54bc9e..309614ee 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.h +++ b/VoidLink/ViewControllers/StreamFrameViewController.h @@ -63,5 +63,6 @@ - (void)resetMagnifierStreamViewWithAnimated:(BOOL)animated; - (void)returnToMainFrame; - (void)bringUpToolboxMenu; +- (void)bringUpToolboxMenuWithoutWidgetLayoutTool; @end diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index 389eb41c..02c99953 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -307,11 +307,20 @@ static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil } - (void)bringUpToolboxMenu{ + [self bringUpToolboxMenuWithoutWidgetLayoutTool:NO]; +} + +- (void)bringUpToolboxMenuWithoutWidgetLayoutTool{ + [self bringUpToolboxMenuWithoutWidgetLayoutTool:YES]; +} + +- (void)bringUpToolboxMenuWithoutWidgetLayoutTool:(BOOL)hideWidgetLayoutTool{ [self prepareGameProfileSelector]; ToolboxViewController* oldToolboxVC = toolBoxViewController; toolBoxViewController = [[ToolboxViewController alloc] init]; toolBoxViewController.specialEntryDelegate = self; - toolBoxViewController.specialEntries = oldToolboxVC.specialEntries; + toolBoxViewController.specialEntries = [oldToolboxVC.specialEntries mutableCopy]; + if(hideWidgetLayoutTool) [toolBoxViewController.specialEntries removeObject:@"widgetLayoutTool"]; toolBoxViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext; [self presentViewController:toolBoxViewController animated:YES completion:^{ //[self->toolBoxViewController setupConstraints]; diff --git a/VoidLink/ViewControllers/ToolboxViewController.swift b/VoidLink/ViewControllers/ToolboxViewController.swift index d7a97722..5bd78eb1 100644 --- a/VoidLink/ViewControllers/ToolboxViewController.swift +++ b/VoidLink/ViewControllers/ToolboxViewController.swift @@ -946,6 +946,7 @@ extension ToolboxViewController: ControllerCollectionNavigationDelegate { @objc func getNavigationElements() -> [ControllerNavigationElement] { var elements: [ControllerNavigationElement] = [] elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .rightStick : .leftStick, action: "focusNavigation")) + elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .abxy : .dpad, action: "focusNavigation")) elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "execute")) elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .a, action: "doublePressPinExecute")) elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "holdToReorder")) diff --git a/VoidLinkPreview/RadialMenuView.swift b/VoidLinkPreview/RadialMenuView.swift index 80ea6ca7..5a55ae6c 100644 --- a/VoidLinkPreview/RadialMenuView.swift +++ b/VoidLinkPreview/RadialMenuView.swift @@ -25,7 +25,7 @@ import UIKit case more case navigationSettings case exit - case shortcuts + case toolbox } @objc public enum RadialMenuState:Int {