From e2f09d711dae72f45f5b8a0413d181c100b5718b Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Fri, 9 Aug 2024 18:51:12 +0800 Subject: [PATCH] on screen key test --- Limelight/Input/ControllerSupport.m | 15 + .../Input/CustomOSC/LayoutOnScreenControls.m | 2 +- .../Input/CustomOSC/OSCProfilesManager.m | 4 +- .../Input/CustomOSC/OnScreenButtonState.h | 5 +- .../Input/CustomOSC/OnScreenButtonState.m | 5 +- Limelight/Input/OnScreenControls.m | 2 +- Limelight/Input/OnScreenKeyView.swift | 112 ++++++ .../OnScreenKeyManagerViewController.swift | 339 ++++++++++++++++++ .../StreamFrameViewController.m | 26 ++ Moonlight.xcodeproj/project.pbxproj | 8 + 10 files changed, 512 insertions(+), 6 deletions(-) create mode 100644 Limelight/Input/OnScreenKeyView.swift create mode 100644 Limelight/ViewControllers/OnScreenKeyManagerViewController.swift diff --git a/Limelight/Input/ControllerSupport.m b/Limelight/Input/ControllerSupport.m index dad29d74..86046bed 100644 --- a/Limelight/Input/ControllerSupport.m +++ b/Limelight/Input/ControllerSupport.m @@ -354,6 +354,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; rightStickY = MAX_MAGNITUDE(rightStickY, controller.mergedWithController.lastRightStickY); } + NSLog(@"gamepadMask: %@", [self binaryRepresentationOfInteger:buttonFlags]); // we got the pressed OSC buttons here. + // Player 1 is always present for OSC LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0, [self getActiveGamepadMask], buttonFlags, leftTrigger, rightTrigger, @@ -370,6 +372,19 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; } } + +- (NSString *)binaryRepresentationOfInteger:(int)number { + NSMutableString *binaryString = [NSMutableString string]; + int numBits = sizeof(number) * 8; + + for (int i = numBits - 1; i >= 0; i--) { + [binaryString appendString:((number >> i) & 1) ? @"1" : @"0"]; + } + + return binaryString; +} + + +(BOOL) hasKeyboardOrMouse { if (@available(iOS 14.0, tvOS 14.0, *)) { return GCMouse.mice.count > 0 || GCKeyboard.coalescedKeyboard != nil; diff --git a/Limelight/Input/CustomOSC/LayoutOnScreenControls.m b/Limelight/Input/CustomOSC/LayoutOnScreenControls.m index ffe66a18..4e2605d6 100644 --- a/Limelight/Input/CustomOSC/LayoutOnScreenControls.m +++ b/Limelight/Input/CustomOSC/LayoutOnScreenControls.m @@ -162,7 +162,7 @@ } /* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */ - OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name isHidden:layerBeingDragged.isHidden andPosition:layerBeingDragged.position]; + OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name isKeyboardButton:NO isHidden:layerBeingDragged.isHidden andPosition:layerBeingDragged.position]; [layoutChanges addObject:onScreenButtonState]; [[NSNotificationCenter defaultCenter] postNotificationName:@"OSCLayoutChanged" object:self]; // lets the view controller know whether to fade the undo button in or out depending on whether there are any further OSC layout changes the user is allowed to undo diff --git a/Limelight/Input/CustomOSC/OSCProfilesManager.m b/Limelight/Input/CustomOSC/OSCProfilesManager.m index b30f8971..9b6ea0e2 100644 --- a/Limelight/Input/CustomOSC/OSCProfilesManager.m +++ b/Limelight/Input/CustomOSC/OSCProfilesManager.m @@ -195,7 +195,7 @@ NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init]; for (CALayer *buttonLayer in buttonLayers) { - OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:buttonLayer.name isHidden:buttonLayer.isHidden andPosition:buttonLayer.position]; + OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:buttonLayer.name isKeyboardButton:NO isHidden:buttonLayer.isHidden andPosition:buttonLayer.position]; NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; [buttonStatesEncoded addObject: buttonStateEncoded]; } @@ -220,7 +220,7 @@ NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init]; for (CALayer *buttonLayer in buttonLayers) { - OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:buttonLayer.name isHidden:buttonLayer.isHidden andPosition:buttonLayer.position]; + OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:buttonLayer.name isKeyboardButton:NO isHidden:buttonLayer.isHidden andPosition:buttonLayer.position]; NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; [buttonStatesEncoded addObject: buttonStateEncoded]; } diff --git a/Limelight/Input/CustomOSC/OnScreenButtonState.h b/Limelight/Input/CustomOSC/OnScreenButtonState.h index a08402ba..453fed6a 100644 --- a/Limelight/Input/CustomOSC/OnScreenButtonState.h +++ b/Limelight/Input/CustomOSC/OnScreenButtonState.h @@ -15,13 +15,16 @@ NS_ASSUME_NONNULL_BEGIN We are able to associate this 'OnScreenButtonState' object and its corresponding CALayer onscreen controller button by setting their 'name' properties equal; in our particular case we give them descriptive names such as 'aButton', 'upButton', 'leftStick', etc. By keeping references to the 19 on screen controller buttons (CALayers) in an array, and creating 19 'OnScreenButtonState' objects with names corresponding to these 19 CALayers and keeping them in an array, we can iterate through both arrays to find OSC buttons (CALayers) with the same name as one of the 'OnScreenButtonStateObjects' and then set the CALayer on screen control button's 'position' and 'hidden' property according to the value of the 'OnScreenButtonState' objects 'position' and 'isHidden' properties. Naturally we would like the user to be able to save their controller layout configurations so that they can load them between app launches, so we adopt encoding/decoding related protocols so we encode these 'OnScreenButtonState' objects and save them to NSUserDefaults */ + +// OS Button state info obj to be modified @interface OnScreenButtonState : NSObject @property NSString *name; @property CGPoint position; @property BOOL isHidden; +@property (nonatomic, assign) BOOL isKeyboardButton; -- (id) initWithButtonName:(NSString*)name isHidden:(BOOL)isHidden andPosition:(CGPoint)position; +- (id) initWithButtonName:(NSString*)name isKeyboardButton:(BOOL)isKeyboardButton isHidden:(BOOL)isHidden andPosition:(CGPoint)position; + (BOOL) supportsSecureCoding; - (void) encodeWithCoder:(NSCoder*)encoder; diff --git a/Limelight/Input/CustomOSC/OnScreenButtonState.m b/Limelight/Input/CustomOSC/OnScreenButtonState.m index a6dca53c..aba8338e 100644 --- a/Limelight/Input/CustomOSC/OnScreenButtonState.m +++ b/Limelight/Input/CustomOSC/OnScreenButtonState.m @@ -10,11 +10,12 @@ @implementation OnScreenButtonState -- (id) initWithButtonName:(NSString*)name isHidden:(BOOL)isHidden andPosition:(CGPoint)position { +- (id) initWithButtonName:(NSString*)name isKeyboardButton:(BOOL)isKeyboardButton isHidden:(BOOL)isHidden andPosition:(CGPoint)position { if ((self = [self init])) { self.name = name; self.isHidden = isHidden; self.position = position; + self.isKeyboardButton = isKeyboardButton; } return self; @@ -26,6 +27,7 @@ - (void) encodeWithCoder:(NSCoder*)encoder { [encoder encodeObject:self.name forKey:@"name"]; + [encoder encodeBool:self.isKeyboardButton forKey:@"isKeyboardButton"]; [encoder encodeBool:self.isHidden forKey:@"isHidden"]; [encoder encodeCGPoint:self.position forKey:@"position"]; } @@ -33,6 +35,7 @@ - (id) initWithCoder:(NSCoder*)decoder { if (self = [super init]) { self.name = [decoder decodeObjectForKey:@"name"]; + self.isKeyboardButton = [decoder decodeBoolForKey:@"isKeyboardButton"]; self.isHidden = [decoder decodeBoolForKey:@"isHidden"]; self.position = [decoder decodeCGPointForKey:@"position"]; } diff --git a/Limelight/Input/OnScreenControls.m b/Limelight/Input/OnScreenControls.m index 68d8fde4..0b1fb4b6 100644 --- a/Limelight/Input/OnScreenControls.m +++ b/Limelight/Input/OnScreenControls.m @@ -457,7 +457,7 @@ static float L3_Y; // create A button _aButton.contents = (id) aButtonImage.CGImage; _aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame; - [_view.layer addSublayer:_aButton]; + [_view.layer addSublayer:_aButton]; // rendering OSC Button here // create B button _bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame; diff --git a/Limelight/Input/OnScreenKeyView.swift b/Limelight/Input/OnScreenKeyView.swift new file mode 100644 index 00000000..3a4a9fdb --- /dev/null +++ b/Limelight/Input/OnScreenKeyView.swift @@ -0,0 +1,112 @@ +// +// OnScreenKey.swift +// Moonlight-ZWM +// +// Created by ZWM on 2024/8/4. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +import UIKit + +@objc class OnScreenKeyView: UIView { + @objc static var relocationMode: Bool = false + @objc var keyLabel: String + @objc var keyString: String + private let label: UILabel + private let originalBackgroundColor: UIColor + private var storedLocation: CGPoint = .zero + + + @objc init(keyString: String, keyLabel: String) { + self.keyString = keyString + self.keyLabel = keyLabel + self.label = UILabel() + self.originalBackgroundColor = UIColor(white: 0.2, alpha: 0.7) + + super.init(frame: .zero) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc public func setKeyLocation(xOffset:CGFloat, yOffset:CGFloat) { + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: self.superview!.leadingAnchor, constant: xOffset), + self.topAnchor.constraint(equalTo: self.superview!.topAnchor, constant: yOffset), + ]) + } + + @objc public func enableRelocationMode(enabled: Bool){ + OnScreenKeyView.relocationMode = enabled + } + + private func setupView() { + label.text = self.keyLabel + label.font = UIFont.systemFont(ofSize: 30) + label.textColor = UIColor(white: 1.0, alpha: 0.8) + label.shadowColor = .black + label.translatesAutoresizingMaskIntoConstraints = false + + self.layer.borderColor = UIColor.black.cgColor + self.layer.borderWidth = 2 + self.layer.cornerRadius = 8 + self.backgroundColor = UIColor(white: 0.2, alpha: 0.7) + self.layer.shadowRadius = 4 + self.layer.shadowOpacity = 0.5 + + addSubview(label) + + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: centerXAnchor), + label.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + + NSLayoutConstraint.activate([ + self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width * 0.088), + self.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height * 0.1), + ]) + } + + // Touch event handling + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + //self.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.7) + self.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.7) + + + if !OnScreenKeyView.relocationMode { LiSendKeyboardEvent(CommandManager.keyMappings[self.keyString]!,Int8(KEY_ACTION_DOWN), 0) } + else { + // Store the initial center point of the key + if let touch = touches.first { + let touchLocation = touch.location(in: superview) + storedLocation = touchLocation + } + } + + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + // Move the KeyView based on touch movement in relocation mode + if OnScreenKeyView.relocationMode { + if let touch = touches.first { + let currentLocation = touch.location(in: superview) + let offsetX = currentLocation.x - storedLocation.x + let offsetY = currentLocation.y - storedLocation.y + + center = CGPoint(x: center.x + offsetX, y: center.y + offsetY) + storedLocation = currentLocation // Update initial center for next movement + } + } + } + + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + self.backgroundColor = self.originalBackgroundColor + if !OnScreenKeyView.relocationMode { LiSendKeyboardEvent(CommandManager.keyMappings[self.keyString]!,Int8(KEY_ACTION_UP), 0) } + } +} diff --git a/Limelight/ViewControllers/OnScreenKeyManagerViewController.swift b/Limelight/ViewControllers/OnScreenKeyManagerViewController.swift new file mode 100644 index 00000000..2b0b38e0 --- /dev/null +++ b/Limelight/ViewControllers/OnScreenKeyManagerViewController.swift @@ -0,0 +1,339 @@ +// +// KeyManagerViewController.swift +// Moonlight-ZWM +// +// Created by ZWM on 2024/7/23. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +import UIKit + +@objc public class OnScreenKeyManagerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + + public let tableView = UITableView() + private let addButton = UIButton(type: .system) + private let deleteButton = UIButton(type: .system) + private let editButton = UIButton(type: .system) + private let exitButton = UIButton(type: .system) + private let pinButton = UIButton(type: .system) + private let viewBackgroundColor = UIColor(white: 0.2, alpha: 0.8); + private let highlightColor = UIColor(white: 0.39, alpha: 0.8); + private let titleLabel = UILabel() + + private var viewPinned: Bool = false + private var isEditingMode: Bool = false { + didSet { + updateEditingMode() + } + } + + override public func viewDidLoad() { + super.viewDidLoad() + + setupViews() + updateEditingMode() + + tableView.dataSource = self + tableView.delegate = self + + // Register the cell class + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + //pass self to the CommandManager + //CommandManager.shared.viewController = self + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + setupConstraints() + } + + private func setupViews() { + + // Set corner radius + view.layer.cornerRadius = 20 // Adjust the corner radius + view.layer.masksToBounds = true + + // Set up the title label + titleLabel.text = SwiftLocalizationHelper.localizedString(forKey: "Send Commands") + titleLabel.font = UIFont.boldSystemFont(ofSize: 20) // Adjust font size as needed + titleLabel.textColor = UIColor.white // Adjust color as needed + titleLabel.textAlignment = .center + view.addSubview(titleLabel) + + view.backgroundColor = viewBackgroundColor + tableView.backgroundColor = .clear + tableView.rowHeight = 60 + tableView.separatorColor = highlightColor + // Configure buttons + addButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Add / Duplicate"), for: .normal) + deleteButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Delete"), for: .normal) + editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal) + exitButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Exit"), for: .normal) + pinButton.setTitle("📌", for: .normal) + addButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) // Adjust the size as needed + deleteButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) + editButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) + exitButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) + pinButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) + + // Add subviews + view.addSubview(tableView) + view.addSubview(addButton) + view.addSubview(deleteButton) + view.addSubview(editButton) + view.addSubview(exitButton) + view.addSubview(pinButton) + + // Setup button targets + addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside) + deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside) + editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside) + exitButton.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside) + pinButton.addTarget(self, action: #selector(pinButtonTapped), for: .touchUpInside) + pinButton.contentEdgeInsets = UIEdgeInsets(top: 7, left: 20, bottom: 7, right: 20) + + } + + private func setupConstraints() { + + guard let superview = view.superview else { + return + } + + view.translatesAutoresizingMaskIntoConstraints = false + + tableView.translatesAutoresizingMaskIntoConstraints = false + addButton.translatesAutoresizingMaskIntoConstraints = false + deleteButton.translatesAutoresizingMaskIntoConstraints = false + editButton.translatesAutoresizingMaskIntoConstraints = false + exitButton.translatesAutoresizingMaskIntoConstraints = false + pinButton.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + + // Set the width and height of the view + view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: 1), + view.heightAnchor.constraint(equalTo: view.superview!.heightAnchor, multiplier: 1), + + // Center the view horizontally and vertically + view.centerXAnchor.constraint(equalTo: view.superview!.centerXAnchor), + view.centerYAnchor.constraint(equalTo: view.superview!.centerYAnchor), + + // ViewTitle constrains + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 13.5), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), + titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), + + + // TableView constraints + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60), + + // ExitButton constraints + exitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), + exitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + + // EditButton constraints + editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 50), + editButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + + // AddButton constraints + addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25), + addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + + // DeleteButton constraints + deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -50), + deleteButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + + // DeleteButton constraints + pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -50), + pinButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + ]) + } + + // view controller method + private func updateEditingMode() { + addButton.isEnabled = isEditingMode + deleteButton.isEnabled = isEditingMode + if(isEditingMode){ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Done"), for: .normal) } + else{ editButton.setTitle(SwiftLocalizationHelper.localizedString(forKey: "Edit"), for: .normal) } + } + + // view controller method + @objc private func pinButtonTapped() { + viewPinned = !viewPinned + if viewPinned { + pinButton.backgroundColor = highlightColor + } + else{ + pinButton.backgroundColor = viewBackgroundColor + } + } + + // tobe modified + @objc private func addButtonTapped() { + let previouslySelectedIndexPath = tableView.indexPathForSelectedRow //memorize selected indexpath + let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "New Command"), message: SwiftLocalizationHelper.localizedString(forKey: "Enter a new command and alias"), preferredStyle: .alert) + alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Command") } + alert.addTextField { $0.placeholder = SwiftLocalizationHelper.localizedString(forKey: "Alias (optional)") } + alert.textFields?[0].keyboardType = .asciiCapable + alert.textFields?[0].autocorrectionType = .no + alert.textFields?[0].spellCheckingType = .no + alert.textFields?[1].keyboardType = .asciiCapable + alert.textFields?[1].autocorrectionType = .no + alert.textFields?[1].spellCheckingType = .no + + let submitAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Add"), style: .default) { [unowned alert] _ in + let keyboardCmdString = alert.textFields?[0].text ?? "" + let alias = alert.textFields?[1].text ?? keyboardCmdString + let newCommand = RemoteCommand(keyboardCmdString: keyboardCmdString, alias: alias) + let addCommandSuceeded = CommandManager.shared.addCommand(newCommand) + //self.reloadTableView() // don't know why but this reload has to be called from the CommandManager, it doesn't work here. + + //if previouslySelectedIndexPath == nil { return } + if addCommandSuceeded { + let lastRow = self.tableView.numberOfRows(inSection: 0) - 1 //for now there's only 1 section for the tableview, just use setion 0 + let newEntryIndexPath = IndexPath(row: lastRow, section: 0) + self.tableView.selectRow(at: newEntryIndexPath, animated: true, scrollPosition: .middle) // shift the highlight to the newly added entry + } + else { + self.tableView.selectRow(at: previouslySelectedIndexPath, animated: true, scrollPosition: .middle) // keep the highlight on the previous entry if failed to add command + } + } + + let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey:"Cancel"), style: .cancel) + alert.addAction(submitAction) + alert.addAction(cancelAction) + + if let selectedIndexPath = self.tableView.indexPathForSelectedRow { + let selectedCommand = CommandManager.shared.getAllCommands()[selectedIndexPath.row] + alert.textFields?[0].text = selectedCommand.keyboardCmdString // load selected keyboard cmd string + //alert.textFields?[1].text = selectedCommand.alias // leave the alias input field empty + } + + self.present(alert, animated: true) + } + + // tobe modified + @objc private func deleteButtonTapped() { + guard let selectedIndexPath = tableView.indexPathForSelectedRow else { + return + } + let previouslySelectedIndexPath = selectedIndexPath // memorize selected row before reloading tableview + + CommandManager.shared.deleteCommand(at: selectedIndexPath.row) + reloadTableView() + // target new entry after deletion + var targetRowAfterDel = previouslySelectedIndexPath.row - 1 + if targetRowAfterDel < 0 { targetRowAfterDel = 0 } + let targetIndexPathAfterDel = IndexPath(row: targetRowAfterDel, section: previouslySelectedIndexPath.section) + if targetRowAfterDel < tableView.numberOfRows(inSection: previouslySelectedIndexPath.section) { + tableView.selectRow(at: targetIndexPathAfterDel, animated: true, scrollPosition: .middle) // shift highlight to the target entry after deletion + } + } + + @objc private func editButtonTapped() { + isEditingMode.toggle() + } + + @objc private func exitButtonTapped() { + dismiss(animated: true, completion: nil) + } + + @objc public func reloadTableView() { + tableView.layoutIfNeeded() + tableView.reloadData() + + /* + let previouslySelectedIndexPath = tableView.indexPathForSelectedRow + if let indexPath = previouslySelectedIndexPath { + // Make sure the indexPath is still valid and scroll to the selected indexPath + if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) { + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle) // keep the entry of previous index selected. + } + } */ + } + + // tobe modified + // UITableViewDataSource + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return CommandManager.shared.getAllCommands().count + } + + + // tobe modified + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + // Configure cell appearance + cell.backgroundColor = .clear // Set background color of the cell + cell.textLabel?.textColor = UIColor.white // Set font color of the text + + + // Set selected background view + let selectedBackgroundView = UIView() + selectedBackgroundView.backgroundColor = highlightColor // Color for the selected state + cell.selectedBackgroundView = selectedBackgroundView + + let command = CommandManager.shared.getAllCommands()[indexPath.row] + cell.textLabel?.text = command.alias + return cell + } + + + // UITableViewDelegate + // tobe modified + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + if let cell = tableView.cellForRow(at: indexPath) { + // Set the cell background color to the flashing color + // Animate the flash effect + UIView.animate(withDuration: 0.1, animations: { + cell.selectedBackgroundView?.backgroundColor = .clear + }) { _ in + // Reset the cell background color after the animation + UIView.animate(withDuration: 0.1) { + cell.selectedBackgroundView?.backgroundColor = self.highlightColor + } + } + } + + if !isEditingMode { + // Sending keyboard command + let command = CommandManager.shared.getAllCommands()[indexPath.row] + sendKeyboardCommand(command) + if !viewPinned { dismiss(animated: false, completion: nil) } // dimiss the view in sending mode & the view is not pinned + } + } + + private func sendKeyboardCommand(_ cmd: RemoteCommand) { + print("Sending key-value") + let keyboardCmdStrings = CommandManager.shared.extractKeyStrings(from: cmd.keyboardCmdString) + sendKeyDownEventWithDelay(keyboardCmdStrings: keyboardCmdStrings!) + } + + private func sendKeyDownEventWithDelay(keyboardCmdStrings: [String], delay: TimeInterval = 0.05, index: Int = 0) { + guard index < keyboardCmdStrings.count else { + for keyStr in keyboardCmdStrings { + LiSendKeyboardEvent(CommandManager.keyMappings[keyStr]!,Int8(KEY_ACTION_UP), 0) + } + return + } // 如果已经处理完所有键,释放所有按键 + + let keyCode = CommandManager.keyMappings[keyboardCmdStrings[index]] + guard let keyCode = keyCode else { + print("No mapping found for \(keyboardCmdStrings[index])") + sendKeyDownEventWithDelay(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index+1) // 跳过当前键,继续下一个 + return + } + // 发送当前键的键盘按下事件 + LiSendKeyboardEvent(keyCode,Int8(KEY_ACTION_DOWN), 0) + // 使用 asyncAfter 在指定异步延迟后发送下一个键的键盘按下事件 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + self.sendKeyDownEventWithDelay(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index+1) // 跳过当前键,继续下一个 + } + } +} + diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m index 22183d6a..b2ec501f 100644 --- a/Limelight/ViewControllers/StreamFrameViewController.m +++ b/Limelight/ViewControllers/StreamFrameViewController.m @@ -15,6 +15,7 @@ #import "CustomEdgeSwipeGestureRecognizer.h" #import "CustomTapGestureRecognizer.h" #import "LocalizationHelper.h" +#import "Moonlight-Swift.h" #include #include @@ -162,6 +163,31 @@ { [super viewDidAppear:animated]; + + + + OnScreenKeyView *keyView = [[OnScreenKeyView alloc] initWithKeyString:@"WIN" keyLabel:@"WIN"]; + keyView.translatesAutoresizingMaskIntoConstraints = NO; + + // Add the KeyView to the view controller's view + [self.view addSubview:keyView]; + + // Configure the KeyView's layout + + CGFloat xOffset = 50; + CGFloat yOffset = 550; + CGFloat spacing = 10.0; + + [keyView setKeyLocationWithXOffset:500 yOffset:(CGFloat)550]; + + + + + + + + + #if !TARGET_OS_TV [[self revealViewController] setPrimaryViewController:self]; [[NSNotificationCenter defaultCenter] addObserver:self diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index f425804b..01879216 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; + 84203F232C5F24C00032EEED /* OnScreenKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84203F222C5F24C00032EEED /* OnScreenKeyView.swift */; }; 84351B0E2C29ACCB00A77989 /* LayoutOnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */; }; 84351B0F2C29ACCB00A77989 /* OnScreenButtonState.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B092C29ACCA00A77989 /* OnScreenButtonState.m */; }; 84351B102C29ACCB00A77989 /* OSCProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 84351B0A2C29ACCA00A77989 /* OSCProfile.m */; }; @@ -23,6 +24,7 @@ 8454AC662C31907F00F0A302 /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC682C31907F00F0A302 /* iPad.storyboard */; }; 8454AC6A2C319BDA00F0A302 /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8454AC6C2C319BDA00F0A302 /* iPhone.storyboard */; }; 84591A512BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m */; }; + 84866ACC2C63A6340037EB63 /* OnScreenKeyManagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84866ACB2C63A6340037EB63 /* OnScreenKeyManagerViewController.swift */; }; 84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; }; 84E1CDEF2C4F688E00547904 /* CommandManagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEE2C4F688E00547904 /* CommandManagerViewController.swift */; }; 84E1CDFA2C4FE83400547904 /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; }; @@ -125,6 +127,7 @@ 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomTapGestureRecognizer.h; sourceTree = ""; }; 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = ""; }; + 84203F222C5F24C00032EEED /* OnScreenKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnScreenKeyView.swift; sourceTree = ""; }; 84351B062C29ACCA00A77989 /* OnScreenButtonState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnScreenButtonState.h; sourceTree = ""; }; 84351B072C29ACCA00A77989 /* LayoutOnScreenControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LayoutOnScreenControls.m; sourceTree = ""; }; 84351B082C29ACCA00A77989 /* OSCProfilesManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSCProfilesManager.h; sourceTree = ""; }; @@ -153,6 +156,7 @@ 8454AC6D2C319BDA00F0A302 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/iPhone.xcstrings; sourceTree = ""; }; 84591A4F2BE16240006AAB10 /* CustomEdgeSwipeGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomEdgeSwipeGestureRecognizer.h; sourceTree = ""; }; 84591A502BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomEdgeSwipeGestureRecognizer.m; sourceTree = ""; }; + 84866ACB2C63A6340037EB63 /* OnScreenKeyManagerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnScreenKeyManagerViewController.swift; sourceTree = ""; }; 84D8EB932C5146AD00FC10A9 /* Moonlight-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Moonlight-Bridging-Header.h"; sourceTree = ""; }; 84E1CDEB2C4F67D300547904 /* CommandManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandManager.swift; sourceTree = ""; }; 84E1CDEE2C4F688E00547904 /* CommandManagerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandManagerViewController.swift; sourceTree = ""; }; @@ -623,6 +627,7 @@ 988FCD3F293B091B003050E2 /* KeyboardInputField.h */, 988FCD40293B091B003050E2 /* KeyboardInputField.m */, 84E1CDEB2C4F67D300547904 /* CommandManager.swift */, + 84203F222C5F24C00032EEED /* OnScreenKeyView.swift */, ); path = Input; sourceTree = ""; @@ -687,6 +692,7 @@ FB4A23B61A9D3637004D2EF2 /* LoadingFrameViewController.h */, FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */, 84E1CDEE2C4F688E00547904 /* CommandManagerViewController.swift */, + 84866ACB2C63A6340037EB63 /* OnScreenKeyManagerViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1017,6 +1023,7 @@ FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */, FB89462F19F646E200339C8A /* Connection.m in Sources */, FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */, + 84866ACC2C63A6340037EB63 /* OnScreenKeyManagerViewController.swift in Sources */, 843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */, FB4678FF1A565DAC00377732 /* WakeOnLanManager.m in Sources */, 84E1CDEC2C4F67D300547904 /* CommandManager.swift in Sources */, @@ -1028,6 +1035,7 @@ FBDE86E019F7A837001C18A8 /* UIComputerView.m in Sources */, FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */, FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */, + 84203F232C5F24C00032EEED /* OnScreenKeyView.swift in Sources */, 84FD0B512C31338D006B5BE5 /* LocalizationHelper.m in Sources */, FB1D59971BBCCB6400F482CA /* ComputerScrollView.m in Sources */, FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */,