Is there a way to receive macOS events while users hold down a mouse button? - swift

Having a SwiftUI slider.
The user keeps on dragging it around.
While dragging, the user also presses the Option key.
I want to change the user interface based on the keyboard modifier flag change (e.g. the Option key). However, it seems the main event loop is blocked while dragging the slider or even when having a mouse button pressed.
When using the NSEvent.addLocalMonitorForEvents(mathing:handler) to get notified of the Option keypress, the handler does not even get called while the user drags the slider.
Is there any way I can achieve that?
I would also love to understand why the problem exists in the first place.

Eventually, I ended up using a timer block checking and publishing changes of the keyboard modifier flags.
import Cocoa
import Combine
/**
Publishers for keyboard events.
The modifiers are checked regularly by a Timer running on the Main `RunLoop` in `.common` mode
to ensure the modifiers are detected even when having mouse button pressed down
(which normally blocks the run loops on the main thread which are not in `common` mode).
The `NSEvent.addLocalMonitorForEvents` cannot be used because the handler is not executed
for nested-tracking loops, such as control tracking (e.g. when a mouse button is down).
*/
public class KeyboardEvents {
/// Broadcasts keyboard flag changes (e.g. keys like Option, Command, Control, Shift, etc.)
public let modifierFlagsPublisher: AnyPublisher<NSEvent.ModifierFlags?, Never>
private let modifierFlagsSubject = PassthroughSubject<NSEvent.ModifierFlags?, Never>()
private var timer: Timer!
public init() {
modifierFlagsPublisher = modifierFlagsSubject.removeDuplicates().eraseToAnyPublisher()
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [unowned self] _ in
modifierFlagsSubject.send(NSApp.currentEvent?.modifierFlags)
}
RunLoop.current.add(timer, forMode: .common)
}
deinit {
timer.invalidate()
}
}

Related

Unity Dynamic UI number selection with plus and minus buttons

while programming a game in Unity, I had troubles with incrementing this new feature: a dynamic UI number selection with plus and minus buttons.
What I want precisely:
1. three buttons, one blank in the middle displays the number, two with plus and minus signs which, when clicked, increment or decrement number by 1. WORKS OK!
image of what I did
2. (this is where it gets tricky) When user presses for more than, say, .2s, then it increments the value of the central button pretty fast as long as the user is still pressing on the button. Would avoid player from pressing hundred times the button because it increments only by 1.
3. Eventually add an acceleration phase of the increase (at the start increases by 3/s for example and at the max by 20/s or something like that)
Some help on this would be really great, thanks for those who will take time answering :)
edit: found somebody who asked same question on another post -->https://stackoverflow.com/questions/22816917/faster-increments-with-longer-uibutton-hold-duration?rq=1 but I don't understand a single inch of code...(doesn't look like c#) ;( help!
Are you using the button's OnClick() Event? It only calls the function once even if the user is holding down the key.
If you are not sure how to configure it. Here is a tutorial, you can use.
https://vionixstudio.com/2022/05/21/making-a-simple-button-in-unity/
Also the middle button can be a text field.
edit: found the solution myself. The principal issue I encountered when trying to make the button was a way to know if the button was being pressed (a bool variable). I already knew when it was clicked and when it was released with the OnPointerUp and OnPointerDown methods in the event trigger component. I then found the Input.GetButton() funct, which returns true if the button passed in parameter (here the mouse's left click) is pressed.
Here's the code (I didn't make an acceleration phase 'cause I was bored but it can be done pretty easily once you've got the Input.GetButton statement):
public class PlusAndMinus : MonoBehaviour
{
[SerializeField] private GameManager gameManagerScript;
[SerializeField] private int amount;
private bool isPressedForLong=false;
[SerializeField] private float IncreasePerSecWhenPressingLonger;
public void PointerDown()
{
if (!gameManagerScript.isGameActive)
{
StartCoroutine(PointerDownCoroutine());
}
}
private IEnumerator PointerDownCoroutine()
{
yield return new WaitForSeconds(.1f);#.2f might work better
if (Input.GetMouseButton(0))
{
isPressedForLong = true;
}
}
public void PointerUp()
{
if (!gameManagerScript.isGameActive)
{
isPressedForLong = false;
gameManagerScript.UpdateNumberOfBalls("Expected", amount);
}
}
private void Update()
{
if (isPressedForLong)
{
gameManagerScript.UpdateNumberOfBalls("Expected", amount * IncreasePerSecWhenPressingLonger * Time.deltaTime);
}
}
}
The PointerDown event in the event trigger component calls the PointerDown function in the script (same for PointerUp).
The value of "amount" var is set to 1 for the plus button and -1 for the minus button.
The two if statements checking if game is not active are for my game's use but aren't necessary.
Finally, the gameManagerScript.UpdateNumberOfBalls("expected",amount); statements call a function that updates the text in the middle by the amount specified. Here's my code ("expected" argument is for my game's use):
#inside the function
else if (startOrEndOrExpected == "Expected")
{
if (!((numberOfBallsExpected +amount) < 0) & !((numberOfBallsExpected +amount) > numberOfBallsThisLevel))
{
if (Math.Round(numberOfBallsExpected + amount) != Math.Round(numberOfBallsExpected))
{
AudioManager.Play("PlusAndMinus");
}
numberOfBallsExpected += amount;
numberOfBallsExpectedText.text = Math.Round(numberOfBallsExpected).ToString();
}
}
0 and numberOfBallsThisLevel are the boundaries of the number displayed.
Second if statement avoids the button click sound to be played every frame when the user presses for long on the button (only plays when number increments or decrements by 1 or more).
Hope this helps!

swiftUI Problem using onDrag an onDrop in LazyGrid

Implement the proposed solution to do drag and drop in this thread:
SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?
The problem I have is that I want to play a vibration when the drag starts and I don't get it.
I did the following:
.onDrag(){
let impact = UIImpactFeedbackGenerator(style: .light)
impact.impactOccurred()
self.dragging = module
return NSItemProvider(object: String(module.id) as NSString)
}
But if I drop the item on itself (to cancel or by accident). The next time I try to drag it, the onDrag() method is not executed.
How could I fix it?

Randomized Attack Animation RPG

First of all, I'm quite noob with animator and animation systems in unity.
What I'm trying to achieve (and I was trying with Animator component) is a randomized attack only while I keep my mouse button pressed on the enemy and which completes the execution of the attack clip that is playing even if i release the button meanwhile.
I tried adding my 2 attack animations to a list and play it, with something like
anim.Play(Random.Range(0, list.Count))
...but I don'tknow if the problem is that while I keep pressed one animation cancels the other or what.
Therefore I prefer to ask, because I'm probably doing things in the wrong way.
Thank you.
Yes the issue is probably what you said: You have to wait until one Animation finished before starting a new one otherwise you would start a new animation every frame.
You could use a Coroutine (also check the API) to do that.
Ofcourse the same thing could be implemented also only in Update without using a Coroutine but most of the times that becomes really cluttered and sometimes even more complicated to handle. And there is not really any loss or gain (regarding performance) in simply "exporting" it to a Coroutine.
// Reference those in the Inspector or get them elsewhere
public List<AnimationClip> Clips;
public AnimationClip Idle;
private Animator _anim;
// A flag to make sure you can never start the Coroutine multiple times
private bool _isAnimating;
private void Awake()
{
_anim = GetComponent<Animator>();
}
private void Update()
{
if(Input.GetMouseButtonDown(0)
{
// To make sure there is only one routine running
if(!_isAnimating)
{
StartCoroutine(RandomAnimations());
}
}
// This would immediately interrupt the animations when mouse is not pressed anymore
// uncomment if you prefer this otherwise the Coroutine waits for the last animation to finish
// and returns to Idle state afterwards
//else if(Input.GetMouseButtonUp(0))
//{
// // Interrupts the coroutine
// StopCoroutine (RandomAnimations());
//
// // and returns to Idle state
// _anim.Play(Idle.name);
//
// // Reset flag
// _isAnimating = false;
//}
}
private IEnumerator RandomAnimations()
{
// Set flag to prevent another start of this routine
_isAnimating = true;
// Go on picking clips while mouse stays pressed
while(Input.GetMouseButton(0))
{
// Pick random clip from list
var randClip = Clips[Random.Range(0, Clips.Count)];
// Switch to the random clip
_anim.Play(randClip.name);
// Wait until clip finished before picking next one
yield return new WaitForSeconds(randClip.length);
}
// Even if MouseButton not pressed anymore waits until the last animation finished
// then returns to the Idle state
// If you rather want to interrupt immediately if the button is released
// skip this and uncomment the else part in Update
_anim.Play(Idle.name);
// Reset flag
_isAnimating = false;
}
Note that this way of randomizing does not provide things like "Don't play the same animation twice in a row" or "Play all animations before repeating one".
If you want this checkout this answer to a very similar question. There I used a randomized list to run through so there are no doubles

How to suspend a work item on the main queue

I want to know if it is possible to suspend and then resume a work item on the main queue whilst maintaining the '.asyncAfter' time. If not, is there a workaround to achieve this?
At a certain point, I queue up the following DispatchWorkItem:
dispatchWorkItem = DispatchWorkItem(qos: .userInteractive, block: {
self.view.backgroundColor = UIColor.workoutBackgroundColor
self.runTimer()
self.timerButton.animateableTrackLayer.removeAnimation(forKey: "strokeEndAnimation")
self.isRestState = false
})
I queue this up using:
DispatchQueue.main.asyncAfter(deadline: delayTime, execute: self.dispatchWorkItem))
(delayTime being a parameter to the function)
Now, the problem I am running into is how can I suspend this work item if the user performs a 'pause' action in my app.
I have tried using the DispatchQueue.main.suspend() method but the work item continues to execute after the specified delay time. From what I have read, this method should suspend the queue and this queued work item since it is not being executed. (Please correct me if I am wrong there!)
What I need to achieve is the work item is 'paused' until the user performs the 'resume' action in the app which will resume the work item from where the delay time left off.
This works on background queues that I have created when I do not need to make UI updates; however, on the main queue is appears to falter.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
Thanks in advance!
On that, is it possible to create a background queue that on execution, executes a work item on the main queue?
You are suggesting something like this:
var q = DispatchQueue(label: "myqueue")
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
self.q.asyncAfter(deadline: .now() + seconds, execute: {
DispatchQueue.main.async(execute: handler())
})
}
func pause() {
self.q.suspend()
}
func resume() {
self.q.resume()
}
But my actual tests seem to show that that won't work as you desire; the countdown doesn't resume from where it was suspended.
One workaround I have considered is when the user performs the pause action, storing the time left until the work item was going to be executed and re-adding the work item to the queue with that time on the resume action. This seems like a poor quality approach and I feel there is a more appropriate method to this.
It isn't poor quality. There is no built-in mechanism for pausing a dispatch timer countdown, or for introspecting the timer, so if you want to do the whole thing on the main queue your only recourse is just what you said: maintain your own timer and the necessary state variables. Here is a rather silly mockup I hobbled together:
class PausableTimer {
var t : DispatchSourceTimer!
var d : Date!
var orig : TimeInterval = 0
var diff : TimeInterval = 0
var f : (()->Void)!
func configAndStart(seconds:TimeInterval, handler:#escaping ()->Void) {
orig = seconds
f = handler
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
d = Date()
t.resume()
}
func pause() {
t.cancel()
diff = Date().timeIntervalSince(d)
}
func resume() {
orig = orig-diff
t = DispatchSource.makeTimerSource()
t.schedule(deadline: DispatchTime.now()+orig, repeating: .never)
t.setEventHandler(handler: f)
t.resume()
}
}
That worked in my crude testing, and seems to be interruptible (pausable) as desired, but don't quote me; I didn't spend much time on it. The details are left as an exercise for the reader!

How to prevent mouse clicks from passing through GUI controls and playing animation in Unity3D [duplicate]

This question already has answers here:
Detect mouse clicked on GUI
(3 answers)
Closed 4 years ago.
Description
Hi Guys need your help I faced problems with mouse clicks which pass through UI panel in Unity, that is, I have created pause menu and when I click Resume button, the game gets unpaused and the player plays Attack animation which is undesirable.What I want is when I click Resume button, Attack animation should not be played. The same problem if I just click on panel not necessarily a button and the more I click on UI panel the more Attack animation is played after I exit pause menu. Moreover, I have searched for solutions to this issue and was suggeted to use event system and event triggers but since my knowledge of Unity is at beginner level I could not properly implement it. Please guys help and sorry for my English if it is not clear)) Here is the code that I use:
The code:
using UnityEngine;
using UnityEngine.EventSystems;
public class PauseMenu : MonoBehaviour {
public static bool IsPaused = false;
public GameObject pauseMenuUI;
public GameObject Player;
private bool state;
private void Update() {
//When Escape button is clicked, the game has to freeze and pause menu has to pop up
if (Input.GetKeyDown(KeyCode.Escape)) {
if (IsPaused) {
Resume();
}
else {
Pause();
}
}
}
//Code for Resume button
public void Resume() {
//I was suggested to use event system but no result Attack animation still plays once I exit pause menu
if (EventSystem.current.IsPointerOverGameObject()) {
Player.GetComponent<Animator>().ResetTrigger("Attack");
}
pauseMenuUI.SetActive(false);
Time.timeScale = 1f;
IsPaused = false;
}
//this method is responsible for freezing the game and showing UI panel
private void Pause() {
pauseMenuUI.SetActive(true);
Time.timeScale = 0f;
IsPaused = true;
}
//The code for Quit button
public void QuitGame() {
Application.Quit();
}
}
Im not sure if i understood your problem, but it sounds like somewhere in your code you start an attack when the player does a left click.
Now your problem is that this code is also executed when the player clicks on a UI element, for example in this case the Resume button?
You tried to fix this problem, by resetting the attack trigger of the animator, i think it would be a better solution to prevent the attack from starting instead of trying to reset it later.
EventSystem.current.IsPointerOverGameObject() returns true if the mouse is over an UI element.
So you can use it to modify your code where you start your attack:
... add this check in your code where you want to start the attack
if(EventSystem.current.IsPointerOverGameObject() == false)
{
// add your code to start your attack
}
...
Now your attack will only start if you are not over a UI element