controller navigation improvements and fixes

This commit is contained in:
True Zhuanjia
2026-07-30 01:05:51 +08:00
parent ef5d839f5e
commit b0c16922a2
12 changed files with 609 additions and 143 deletions
+77 -27
View File
@@ -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<ControllerElement> {
private static func activeControls(forHintControl control: ControllerElement) -> Set<ControllerElement> {
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]
}
+321 -15
View File
@@ -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<String> = [
"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) {}
+20
View File
@@ -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
}
@@ -43,7 +43,7 @@
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.6" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="8" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="12" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseStick" attributeType="Integer 32" defaultValueString="8388640" usesScalarValueType="NO" syncable="YES"/>
<attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="customLocalRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
+162 -93
View File
@@ -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"
}
}
@@ -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
}
@@ -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;
@@ -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 {
@@ -63,5 +63,6 @@
- (void)resetMagnifierStreamViewWithAnimated:(BOOL)animated;
- (void)returnToMainFrame;
- (void)bringUpToolboxMenu;
- (void)bringUpToolboxMenuWithoutWidgetLayoutTool;
@end
@@ -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];
@@ -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"))
+1 -1
View File
@@ -25,7 +25,7 @@ import UIKit
case more
case navigationSettings
case exit
case shortcuts
case toolbox
}
@objc public enum RadialMenuState:Int {