[UI] Android ImGui touch and mouse input

This commit is contained in:
Triang3l 2022-07-14 21:13:40 +03:00
parent 3a065c35f0
commit 7b8281aee0
11 changed files with 467 additions and 23 deletions

View File

@ -1,8 +1,10 @@
package jp.xenia.emulator; package jp.xenia.emulator;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.os.Bundle; import android.os.Bundle;
import android.view.MotionEvent;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.View; import android.view.View;
@ -16,10 +18,7 @@ public abstract class WindowedAppActivity extends Activity {
System.loadLibrary("xenia-app"); System.loadLibrary("xenia-app");
} }
private final WindowSurfaceOnLayoutChangeListener mWindowSurfaceOnLayoutChangeListener = private final WindowSurfaceListener mWindowSurfaceListener = new WindowSurfaceListener();
new WindowSurfaceOnLayoutChangeListener();
private final WindowSurfaceHolderCallback mWindowSurfaceHolderCallback =
new WindowSurfaceHolderCallback();
// May be 0 while destroying (mainly while the superclass is). // May be 0 while destroying (mainly while the superclass is).
private long mAppContext = 0; private long mAppContext = 0;
@ -35,6 +34,8 @@ public abstract class WindowedAppActivity extends Activity {
private native void onWindowSurfaceLayoutChange( private native void onWindowSurfaceLayoutChange(
long appContext, int left, int top, int right, int bottom); long appContext, int left, int top, int right, int bottom);
private native boolean onWindowSurfaceMotionEvent(long appContext, MotionEvent event);
private native void onWindowSurfaceChanged(long appContext, Surface windowSurface); private native void onWindowSurfaceChanged(long appContext, Surface windowSurface);
private native void paintWindow(long appContext, boolean forcePaint); private native void paintWindow(long appContext, boolean forcePaint);
@ -48,8 +49,10 @@ public abstract class WindowedAppActivity extends Activity {
// Detach from the old surface. // Detach from the old surface.
if (mWindowSurfaceView != null) { if (mWindowSurfaceView != null) {
mWindowSurfaceView.getHolder().removeCallback(mWindowSurfaceHolderCallback); mWindowSurfaceView.getHolder().removeCallback(mWindowSurfaceListener);
mWindowSurfaceView.removeOnLayoutChangeListener(mWindowSurfaceOnLayoutChangeListener); mWindowSurfaceView.setOnTouchListener(null);
mWindowSurfaceView.setOnGenericMotionListener(null);
mWindowSurfaceView.removeOnLayoutChangeListener(mWindowSurfaceListener);
mWindowSurfaceView = null; mWindowSurfaceView = null;
if (mAppContext != 0) { if (mAppContext != 0) {
onWindowSurfaceChanged(mAppContext, null); onWindowSurfaceChanged(mAppContext, null);
@ -61,12 +64,12 @@ public abstract class WindowedAppActivity extends Activity {
} }
mWindowSurfaceView = windowSurfaceView; mWindowSurfaceView = windowSurfaceView;
// The native window code assumes that, when the surface exists, it covers the entire
// window.
// FIXME(Triang3l): This doesn't work if the layout has already been performed. // FIXME(Triang3l): This doesn't work if the layout has already been performed.
mWindowSurfaceView.addOnLayoutChangeListener(mWindowSurfaceOnLayoutChangeListener); mWindowSurfaceView.addOnLayoutChangeListener(mWindowSurfaceListener);
mWindowSurfaceView.setOnGenericMotionListener(mWindowSurfaceListener);
mWindowSurfaceView.setOnTouchListener(mWindowSurfaceListener);
final SurfaceHolder windowSurfaceHolder = mWindowSurfaceView.getHolder(); final SurfaceHolder windowSurfaceHolder = mWindowSurfaceView.getHolder();
windowSurfaceHolder.addCallback(mWindowSurfaceHolderCallback); windowSurfaceHolder.addCallback(mWindowSurfaceListener);
// If setting after the creation of the surface. // If setting after the creation of the surface.
if (mAppContext != 0) { if (mAppContext != 0) {
final Surface windowSurface = windowSurfaceHolder.getSurface(); final Surface windowSurface = windowSurfaceHolder.getSurface();
@ -115,7 +118,11 @@ public abstract class WindowedAppActivity extends Activity {
super.onDestroy(); super.onDestroy();
} }
private class WindowSurfaceOnLayoutChangeListener implements View.OnLayoutChangeListener { private class WindowSurfaceListener implements
View.OnGenericMotionListener,
View.OnLayoutChangeListener,
View.OnTouchListener,
SurfaceHolder.Callback2 {
@Override @Override
public void onLayoutChange( public void onLayoutChange(
final View v, final int left, final int top, final int right, final int bottom, final View v, final int left, final int top, final int right, final int bottom,
@ -124,9 +131,24 @@ public abstract class WindowedAppActivity extends Activity {
onWindowSurfaceLayoutChange(mAppContext, left, top, right, bottom); onWindowSurfaceLayoutChange(mAppContext, left, top, right, bottom);
} }
} }
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
if (mAppContext == 0) {
return false;
}
return onWindowSurfaceMotionEvent(mAppContext, event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mAppContext == 0) {
return false;
}
return onWindowSurfaceMotionEvent(mAppContext, event);
} }
private class WindowSurfaceHolderCallback implements SurfaceHolder.Callback2 {
@Override @Override
public void surfaceCreated(final SurfaceHolder holder) { public void surfaceCreated(final SurfaceHolder holder) {
if (mAppContext == 0) { if (mAppContext == 0) {

View File

@ -43,7 +43,8 @@ ImGuiDrawer::~ImGuiDrawer() {
window_->RemoveInputListener(this); window_->RemoveInputListener(this);
if (internal_state_) { if (internal_state_) {
ImGui::SetCurrentContext(internal_state_); ImGui::SetCurrentContext(internal_state_);
if (ImGui::IsAnyMouseDown()) { if (touch_pointer_id_ == TouchEvent::kPointerIDNone &&
ImGui::IsAnyMouseDown()) {
window_->ReleaseMouse(); window_->ReleaseMouse();
} }
} }
@ -213,6 +214,9 @@ void ImGuiDrawer::Initialize() {
frame_time_tick_frequency_ = double(Clock::QueryHostTickFrequency()); frame_time_tick_frequency_ = double(Clock::QueryHostTickFrequency());
last_frame_time_ticks_ = Clock::QueryHostTickCount(); last_frame_time_ticks_ = Clock::QueryHostTickCount();
touch_pointer_id_ = TouchEvent::kPointerIDNone;
reset_mouse_position_after_next_frame_ = false;
} }
void ImGuiDrawer::SetupFontTexture() { void ImGuiDrawer::SetupFontTexture() {
@ -310,6 +314,11 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
RenderDrawLists(draw_data, ui_draw_context); RenderDrawLists(draw_data, ui_draw_context);
} }
if (reset_mouse_position_after_next_frame_) {
reset_mouse_position_after_next_frame_ = false;
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
}
if (dialogs_.empty()) { if (dialogs_.empty()) {
// All dialogs have removed themselves during the draw, detach. // All dialogs have removed themselves during the draw, detach.
presenter_->RemoveUIDrawerFromUIThread(this); presenter_->RemoveUIDrawerFromUIThread(this);
@ -382,7 +391,7 @@ void ImGuiDrawer::OnKeyChar(KeyEvent& e) {
} }
void ImGuiDrawer::OnMouseDown(MouseEvent& e) { void ImGuiDrawer::OnMouseDown(MouseEvent& e) {
UpdateMousePosition(e); SwitchToPhysicalMouseAndUpdateMousePosition(e);
auto& io = GetIO(); auto& io = GetIO();
int button = -1; int button = -1;
switch (e.button()) { switch (e.button()) {
@ -409,10 +418,12 @@ void ImGuiDrawer::OnMouseDown(MouseEvent& e) {
} }
} }
void ImGuiDrawer::OnMouseMove(MouseEvent& e) { UpdateMousePosition(e); } void ImGuiDrawer::OnMouseMove(MouseEvent& e) {
SwitchToPhysicalMouseAndUpdateMousePosition(e);
}
void ImGuiDrawer::OnMouseUp(MouseEvent& e) { void ImGuiDrawer::OnMouseUp(MouseEvent& e) {
UpdateMousePosition(e); SwitchToPhysicalMouseAndUpdateMousePosition(e);
auto& io = GetIO(); auto& io = GetIO();
int button = -1; int button = -1;
switch (e.button()) { switch (e.button()) {
@ -440,14 +451,48 @@ void ImGuiDrawer::OnMouseUp(MouseEvent& e) {
} }
void ImGuiDrawer::OnMouseWheel(MouseEvent& e) { void ImGuiDrawer::OnMouseWheel(MouseEvent& e) {
UpdateMousePosition(e); SwitchToPhysicalMouseAndUpdateMousePosition(e);
auto& io = GetIO(); auto& io = GetIO();
io.MouseWheel += float(e.scroll_y()) / float(MouseEvent::kScrollPerDetent); io.MouseWheel += float(e.scroll_y()) / float(MouseEvent::kScrollPerDetent);
} }
void ImGuiDrawer::OnTouchEvent(TouchEvent& e) {
auto& io = GetIO();
TouchEvent::Action action = e.action();
uint32_t pointer_id = e.pointer_id();
if (action == TouchEvent::Action::kDown) {
// The latest pointer needs to be controlling the ImGui mouse.
if (touch_pointer_id_ == TouchEvent::kPointerIDNone) {
// Switching from the mouse to touch input.
if (ImGui::IsAnyMouseDown()) {
std::memset(io.MouseDown, 0, sizeof(io.MouseDown));
window_->ReleaseMouse();
}
}
touch_pointer_id_ = pointer_id;
} else {
if (pointer_id != touch_pointer_id_) {
return;
}
}
UpdateMousePosition(e.x(), e.y());
if (action == TouchEvent::Action::kUp ||
action == TouchEvent::Action::kCancel) {
io.MouseDown[0] = false;
touch_pointer_id_ = TouchEvent::kPointerIDNone;
// Make sure that after a touch, the ImGui mouse isn't hovering over
// anything.
reset_mouse_position_after_next_frame_ = true;
} else {
io.MouseDown[0] = true;
reset_mouse_position_after_next_frame_ = false;
}
}
void ImGuiDrawer::ClearInput() { void ImGuiDrawer::ClearInput() {
auto& io = GetIO(); auto& io = GetIO();
if (ImGui::IsAnyMouseDown()) { if (touch_pointer_id_ == TouchEvent::kPointerIDNone &&
ImGui::IsAnyMouseDown()) {
window_->ReleaseMouse(); window_->ReleaseMouse();
} }
io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX);
@ -460,6 +505,8 @@ void ImGuiDrawer::ClearInput() {
io.KeySuper = false; io.KeySuper = false;
std::memset(io.KeysDown, 0, sizeof(io.KeysDown)); std::memset(io.KeysDown, 0, sizeof(io.KeysDown));
io.ClearInputCharacters(); io.ClearInputCharacters();
touch_pointer_id_ = TouchEvent::kPointerIDNone;
reset_mouse_position_after_next_frame_ = false;
} }
void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) { void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) {
@ -487,12 +534,27 @@ void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) {
} }
} }
void ImGuiDrawer::UpdateMousePosition(const MouseEvent& e) { void ImGuiDrawer::UpdateMousePosition(float x, float y) {
auto& io = GetIO(); auto& io = GetIO();
float physical_to_logical = float physical_to_logical =
float(window_->GetMediumDpi()) / float(window_->GetDpi()); float(window_->GetMediumDpi()) / float(window_->GetDpi());
io.MousePos.x = e.x() * physical_to_logical; io.MousePos.x = x * physical_to_logical;
io.MousePos.y = e.y() * physical_to_logical; io.MousePos.y = y * physical_to_logical;
}
void ImGuiDrawer::SwitchToPhysicalMouseAndUpdateMousePosition(
const MouseEvent& e) {
if (touch_pointer_id_ != TouchEvent::kPointerIDNone) {
touch_pointer_id_ = TouchEvent::kPointerIDNone;
auto& io = GetIO();
std::memset(io.MouseDown, 0, sizeof(io.MouseDown));
// Nothing needs to be done regarding CaptureMouse and ReleaseMouse - all
// buttons as well as mouse capture have been released when switching to
// touch input, the mouse is never captured during touch input, and now
// resetting to no buttons down (therefore not capturing).
}
reset_mouse_position_after_next_frame_ = false;
UpdateMousePosition(float(e.x()), float(e.y()));
} }
} // namespace ui } // namespace ui

View File

@ -59,6 +59,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
void OnMouseMove(MouseEvent& e) override; void OnMouseMove(MouseEvent& e) override;
void OnMouseUp(MouseEvent& e) override; void OnMouseUp(MouseEvent& e) override;
void OnMouseWheel(MouseEvent& e) override; void OnMouseWheel(MouseEvent& e) override;
void OnTouchEvent(TouchEvent& e) override;
// For now, no need for OnDpiChanged because redrawing is done continuously. // For now, no need for OnDpiChanged because redrawing is done continuously.
private: private:
@ -70,7 +71,8 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
void ClearInput(); void ClearInput();
void OnKey(KeyEvent& e, bool is_down); void OnKey(KeyEvent& e, bool is_down);
void UpdateMousePosition(const MouseEvent& e); void UpdateMousePosition(float x, float y);
void SwitchToPhysicalMouseAndUpdateMousePosition(const MouseEvent& e);
Window* window_; Window* window_;
size_t z_order_; size_t z_order_;
@ -92,6 +94,16 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
// detaching the presenter. // detaching the presenter.
std::unique_ptr<ImmediateTexture> font_texture_; std::unique_ptr<ImmediateTexture> font_texture_;
// If there's an active pointer, the ImGui mouse is controlled by this touch.
// If it's TouchEvent::kPointerIDNone, the ImGui mouse is controlled by the
// mouse.
uint32_t touch_pointer_id_ = TouchEvent::kPointerIDNone;
// Whether after the next frame (since the mouse up event needs to be handled
// with the correct mouse position still), the ImGui mouse position should be
// reset (for instance, after releasing a touch), so it's not hovering over
// anything.
bool reset_mouse_position_after_next_frame_ = false;
double frame_time_tick_frequency_; double frame_time_tick_frequency_;
uint64_t last_frame_time_ticks_; uint64_t last_frame_time_ticks_;
}; };

View File

@ -10,6 +10,7 @@
#ifndef XENIA_UI_UI_EVENT_H_ #ifndef XENIA_UI_UI_EVENT_H_
#define XENIA_UI_UI_EVENT_H_ #define XENIA_UI_UI_EVENT_H_
#include <cstdint>
#include <filesystem> #include <filesystem>
#include "xenia/ui/virtual_key.h" #include "xenia/ui/virtual_key.h"
@ -156,6 +157,46 @@ class MouseEvent : public UIEvent {
int32_t scroll_y_ = 0; int32_t scroll_y_ = 0;
}; };
class TouchEvent : public UIEvent {
public:
enum class Action {
kDown,
kUp,
// Should be treated as an up event, but without performing the usual action
// for releasing.
kCancel,
kMove,
};
// Can be used by event listeners as the value for when there's no current
// pointer, for example.
static constexpr uint32_t kPointerIDNone = UINT32_MAX;
explicit TouchEvent(Window* target, uint32_t pointer_id, Action action,
float x, float y)
: UIEvent(target),
pointer_id_(pointer_id),
action_(action),
x_(x),
y_(y) {}
bool is_handled() const { return handled_; }
void set_handled(bool value) { handled_ = value; }
uint32_t pointer_id() { return pointer_id_; }
Action action() const { return action_; }
// Can be outside the boundaries of the surface.
float x() const { return x_; }
float y() const { return y_; }
private:
bool handled_ = false;
uint32_t pointer_id_;
Action action_;
float x_;
float y_;
};
} // namespace ui } // namespace ui
} // namespace xe } // namespace xe

View File

@ -645,6 +645,19 @@ void Window::OnMouseWheel(MouseEvent& e,
} }
} }
void Window::OnTouchEvent(TouchEvent& e,
WindowDestructionReceiver& destruction_receiver) {
PropagateEventThroughInputListeners(
[&e](auto listener) {
listener->OnTouchEvent(e);
return e.is_handled();
},
destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return;
}
}
void Window::SendEventToListeners( void Window::SendEventToListeners(
std::function<void(WindowListener*)> fn, std::function<void(WindowListener*)> fn,
WindowDestructionReceiver& destruction_receiver) { WindowDestructionReceiver& destruction_receiver) {

View File

@ -597,6 +597,9 @@ class Window {
void OnMouseWheel(MouseEvent& e, void OnMouseWheel(MouseEvent& e,
WindowDestructionReceiver& destruction_receiver); WindowDestructionReceiver& destruction_receiver);
void OnTouchEvent(TouchEvent& e,
WindowDestructionReceiver& destruction_receiver);
private: private:
struct ListenerIterationContext { struct ListenerIterationContext {
explicit ListenerIterationContext(ListenerIterationContext* outer_context, explicit ListenerIterationContext(ListenerIterationContext* outer_context,

View File

@ -9,10 +9,16 @@
#include "xenia/ui/window_android.h" #include "xenia/ui/window_android.h"
#include <android/input.h>
#include <jni.h>
#include <algorithm>
#include <cstdint>
#include <memory> #include <memory>
#include <utility> #include <utility>
#include "xenia/base/assert.h" #include "xenia/base/assert.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/ui/surface_android.h" #include "xenia/ui/surface_android.h"
#include "xenia/ui/windowed_app_context_android.h" #include "xenia/ui/windowed_app_context_android.h"
@ -55,6 +61,181 @@ void AndroidWindow::OnActivitySurfaceLayoutChange() {
} }
} }
bool AndroidWindow::OnActivitySurfaceMotionEvent(jobject event) {
auto& android_app_context =
static_cast<const AndroidWindowedAppContext&>(app_context());
JNIEnv* jni_env = android_app_context.ui_thread_jni_env();
const AndroidWindowedAppContext::JniIDs& jni_ids =
android_app_context.jni_ids();
int32_t source =
jni_env->CallIntMethod(event, jni_ids.motion_event_get_source);
switch (source) {
case AINPUT_SOURCE_TOUCHSCREEN: {
// Returning true for all touch events regardless of whether they have
// been handled to keep receiving touch events for the pointers in the
// event (if returning false for a down event, no more events will be sent
// for the pointers in it), and also because there are multiple pointers
// in a single event, and different handler invocations may result in
// different is_handled.
WindowDestructionReceiver destruction_receiver(this);
int32_t action_and_pointer_index =
jni_env->CallIntMethod(event, jni_ids.motion_event_get_action);
int32_t action = action_and_pointer_index & AMOTION_EVENT_ACTION_MASK;
// For pointer ACTION_POINTER_DOWN and ACTION_POINTER_UP.
int32_t pointer_index = (action_and_pointer_index &
AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
if (action == AMOTION_EVENT_ACTION_POINTER_DOWN ||
action == AMOTION_EVENT_ACTION_POINTER_UP) {
int32_t touch_pointer_index =
(action_and_pointer_index &
AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
TouchEvent e(
this,
jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id,
touch_pointer_index),
(action == AMOTION_EVENT_ACTION_POINTER_DOWN)
? TouchEvent::Action::kDown
: TouchEvent::Action::kUp,
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x,
pointer_index),
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y,
pointer_index));
OnTouchEvent(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
} else {
TouchEvent::Action touch_event_action;
switch (action) {
case AMOTION_EVENT_ACTION_DOWN:
touch_event_action = TouchEvent::Action::kDown;
break;
case AMOTION_EVENT_ACTION_UP:
touch_event_action = TouchEvent::Action::kUp;
break;
case AMOTION_EVENT_ACTION_MOVE:
touch_event_action = TouchEvent::Action::kMove;
break;
case AMOTION_EVENT_ACTION_CANCEL:
touch_event_action = TouchEvent::Action::kCancel;
break;
default:
return true;
}
int32_t touch_pointer_count = jni_env->CallIntMethod(
event, jni_ids.motion_event_get_pointer_count);
for (int32_t i = 0; i < touch_pointer_count; ++i) {
TouchEvent e(
this,
jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id,
i),
touch_event_action,
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x, i),
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y, i));
OnTouchEvent(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
}
}
return true;
} break;
case AINPUT_SOURCE_MOUSE: {
WindowDestructionReceiver destruction_receiver(this);
// X and Y can be outside the View (have negative coordinates, or beyond
// the size of the element), and not only for ACTION_HOVER_EXIT (it's
// predeced by ACTION_HOVER_MOVE, at least on Android API level 30, also
// with out-of-bounds coordinates), when moving the mouse outside the
// View, or when starting moving the mouse when the pointer was previously
// outside the View in some cases.
int32_t mouse_x = int32_t(
std::min(float(GetActualPhysicalWidth()),
std::max(0.0f, jni_env->CallFloatMethod(
event, jni_ids.motion_event_get_x, 0))) +
0.5f);
int32_t mouse_y = int32_t(
std::min(float(GetActualPhysicalHeight()),
std::max(0.0f, jni_env->CallFloatMethod(
event, jni_ids.motion_event_get_y, 0))) +
0.5f);
static const MouseEvent::Button kMouseEventButtons[] = {
MouseEvent::Button::kLeft, MouseEvent::Button::kRight,
MouseEvent::Button::kMiddle, MouseEvent::Button::kX1,
MouseEvent::Button::kX2,
};
static constexpr uint32_t kUsedMouseButtonMask =
(UINT32_C(1) << xe::countof(kMouseEventButtons)) - 1;
uint32_t new_mouse_button_state = uint32_t(
jni_env->CallIntMethod(event, jni_ids.motion_event_get_button_state));
// OnMouseUp.
uint32_t mouse_buttons_remaining =
mouse_button_state_ & ~new_mouse_button_state & kUsedMouseButtonMask;
uint32_t mouse_button_index;
while (
xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) {
mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index);
MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x,
mouse_y);
OnMouseUp(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
}
// Generic OnMouseMove regardless of the action since any event can
// provide new coordinates.
{
MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y);
OnMouseMove(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
}
// OnMouseWheel.
// The axis value may be outside -1...1 if multiple scrolls have occurred
// quickly.
int32_t scroll_x = int32_t(
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value,
AMOTION_EVENT_AXIS_HSCROLL, 0) *
float(MouseEvent::kScrollPerDetent));
int32_t scroll_y = int32_t(
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value,
AMOTION_EVENT_AXIS_VSCROLL, 0) *
float(MouseEvent::kScrollPerDetent));
if (scroll_x || scroll_y) {
MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y,
scroll_x, scroll_y);
OnMouseWheel(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
}
// OnMouseDown.
mouse_buttons_remaining =
new_mouse_button_state & ~mouse_button_state_ & kUsedMouseButtonMask;
while (
xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) {
mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index);
MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x,
mouse_y);
OnMouseDown(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return true;
}
}
// Update the button state for state differences.
mouse_button_state_ = new_mouse_button_state;
return true;
} break;
}
return false;
}
uint32_t AndroidWindow::GetLatestDpiImpl() const { uint32_t AndroidWindow::GetLatestDpiImpl() const {
auto& android_app_context = auto& android_app_context =
static_cast<const AndroidWindowedAppContext&>(app_context()); static_cast<const AndroidWindowedAppContext&>(app_context());
@ -62,6 +243,9 @@ uint32_t AndroidWindow::GetLatestDpiImpl() const {
} }
bool AndroidWindow::OpenImpl() { bool AndroidWindow::OpenImpl() {
// Reset the input.
mouse_button_state_ = 0;
// The window is a proxy between the main activity and Xenia, so there can be // The window is a proxy between the main activity and Xenia, so there can be
// only one open window for an activity. // only one open window for an activity.
auto& android_app_context = auto& android_app_context =

View File

@ -10,6 +10,8 @@
#ifndef XENIA_UI_WINDOW_ANDROID_H_ #ifndef XENIA_UI_WINDOW_ANDROID_H_
#define XENIA_UI_WINDOW_ANDROID_H_ #define XENIA_UI_WINDOW_ANDROID_H_
#include <jni.h>
#include "xenia/ui/menu_item.h" #include "xenia/ui/menu_item.h"
#include "xenia/ui/window.h" #include "xenia/ui/window.h"
@ -30,6 +32,7 @@ class AndroidWindow : public Window {
uint32_t GetMediumDpi() const override { return 160; } uint32_t GetMediumDpi() const override { return 160; }
void OnActivitySurfaceLayoutChange(); void OnActivitySurfaceLayoutChange();
bool OnActivitySurfaceMotionEvent(jobject event);
void OnActivitySurfaceChanged() { OnSurfaceChanged(true); } void OnActivitySurfaceChanged() { OnSurfaceChanged(true); }
void PaintActivitySurface(bool force_paint) { OnPaint(force_paint); } void PaintActivitySurface(bool force_paint) { OnPaint(force_paint); }
@ -42,6 +45,9 @@ class AndroidWindow : public Window {
std::unique_ptr<Surface> CreateSurfaceImpl( std::unique_ptr<Surface> CreateSurfaceImpl(
Surface::TypeFlags allowed_types) override; Surface::TypeFlags allowed_types) override;
void RequestPaintImpl() override; void RequestPaintImpl() override;
private:
uint32_t mouse_button_state_ = 0;
}; };
// Dummy for the menu item - menus are controlled by the layout. // Dummy for the menu item - menus are controlled by the layout.

View File

@ -48,6 +48,8 @@ class WindowInputListener {
virtual void OnMouseMove(MouseEvent& e) {} virtual void OnMouseMove(MouseEvent& e) {}
virtual void OnMouseUp(MouseEvent& e) {} virtual void OnMouseUp(MouseEvent& e) {}
virtual void OnMouseWheel(MouseEvent& e) {} virtual void OnMouseWheel(MouseEvent& e) {}
virtual void OnTouchEvent(TouchEvent& e) {}
}; };
} // namespace ui } // namespace ui

View File

@ -20,6 +20,7 @@
#include <unistd.h> #include <unistd.h>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <cstring>
#include "xenia/base/assert.h" #include "xenia/base/assert.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
@ -130,6 +131,12 @@ void AndroidWindowedAppContext::JniActivityOnWindowSurfaceLayoutChange(
} }
} }
bool AndroidWindowedAppContext::JniActivityOnWindowSurfaceMotionEvent(
jobject event) {
return activity_window_ &&
activity_window_->OnActivitySurfaceMotionEvent(event);
}
void AndroidWindowedAppContext::JniActivityOnWindowSurfaceChanged( void AndroidWindowedAppContext::JniActivityOnWindowSurfaceChanged(
jobject window_surface_object) { jobject window_surface_object) {
// Detach from the old surface. // Detach from the old surface.
@ -250,6 +257,62 @@ bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env,
android_base_initialized_ = true; android_base_initialized_ = true;
ui_thread_jni_env_->DeleteLocalRef(application_context_init_ref); ui_thread_jni_env_->DeleteLocalRef(application_context_init_ref);
// Initialize common windowed app JNI IDs.
{
jclass motion_event_class_local_ref =
ui_thread_jni_env_->FindClass("android/view/MotionEvent");
if (!motion_event_class_local_ref) {
XELOGE(
"AndroidWindowedAppContext: Failed to find the motion event class");
Shutdown();
return false;
}
motion_event_class_ =
reinterpret_cast<jclass>(ui_thread_jni_env_->NewGlobalRef(
reinterpret_cast<jobject>(motion_event_class_local_ref)));
ui_thread_jni_env_->DeleteLocalRef(
reinterpret_cast<jobject>(motion_event_class_local_ref));
}
if (!motion_event_class_) {
XELOGE(
"AndroidWindowedAppContext: Failed to create a global reference to the "
"motion event class");
Shutdown();
return false;
}
bool motion_event_ids_obtained = true;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_action = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getAction", "()I")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_axis_value = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getAxisValue", "(II)F")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_button_state = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getButtonState", "()I")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_pointer_id = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getPointerId", "(I)I")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_pointer_count =
ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getPointerCount", "()I")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_source = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getSource", "()I")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_x = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getX", "(I)F")) != nullptr;
motion_event_ids_obtained &=
(jni_ids_.motion_event_get_y = ui_thread_jni_env_->GetMethodID(
motion_event_class_, "getY", "(I)F")) != nullptr;
if (!motion_event_ids_obtained) {
XELOGE(
"AndroidWindowedAppContext: Failed to get the motion event class IDs");
Shutdown();
return false;
}
// Initialize interfacing with the WindowedAppActivity. // Initialize interfacing with the WindowedAppActivity.
activity_ = ui_thread_jni_env_->NewGlobalRef(activity); activity_ = ui_thread_jni_env_->NewGlobalRef(activity);
if (!activity_) { if (!activity_) {
@ -343,6 +406,12 @@ void AndroidWindowedAppContext::Shutdown() {
activity_ = nullptr; activity_ = nullptr;
} }
std::memset(&jni_ids_, 0, sizeof(jni_ids_));
if (motion_event_class_) {
ui_thread_jni_env_->DeleteGlobalRef(motion_event_class_);
motion_event_class_ = nullptr;
}
if (android_base_initialized_) { if (android_base_initialized_) {
xe::ShutdownAndroidAppFromMainThread(); xe::ShutdownAndroidAppFromMainThread();
android_base_initialized_ = false; android_base_initialized_ = false;
@ -495,6 +564,14 @@ Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceLayoutChange(
->JniActivityOnWindowSurfaceLayoutChange(left, top, right, bottom); ->JniActivityOnWindowSurfaceLayoutChange(left, top, right, bottom);
} }
JNIEXPORT jboolean JNICALL
Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceMotionEvent(
JNIEnv* jni_env, jobject activity, jlong app_context_ptr, jobject event) {
return jboolean(
reinterpret_cast<xe::ui::AndroidWindowedAppContext*>(app_context_ptr)
->JniActivityOnWindowSurfaceMotionEvent(event));
}
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceChanged( Java_jp_xenia_emulator_WindowedAppActivity_onWindowSurfaceChanged(
JNIEnv* jni_env, jobject activity, jlong app_context_ptr, JNIEnv* jni_env, jobject activity, jlong app_context_ptr,

View File

@ -28,17 +28,35 @@ class WindowedApp;
class AndroidWindowedAppContext final : public WindowedAppContext { class AndroidWindowedAppContext final : public WindowedAppContext {
public: public:
// Precached JNI references and IDs that may be used for windowed app purposes
// by external code.
struct JniIDs {
// android.view.MotionEvent.
jmethodID motion_event_get_action;
jmethodID motion_event_get_axis_value;
jmethodID motion_event_get_button_state;
jmethodID motion_event_get_pointer_count;
jmethodID motion_event_get_pointer_id;
jmethodID motion_event_get_source;
jmethodID motion_event_get_x;
jmethodID motion_event_get_y;
};
WindowedApp* app() const { return app_.get(); } WindowedApp* app() const { return app_.get(); }
void NotifyUILoopOfPendingFunctions() override; void NotifyUILoopOfPendingFunctions() override;
void PlatformQuitFromUIThread() override; void PlatformQuitFromUIThread() override;
JNIEnv* ui_thread_jni_env() const { return ui_thread_jni_env_; }
uint32_t GetPixelDensity() const { uint32_t GetPixelDensity() const {
return configuration_ ? uint32_t(AConfiguration_getDensity(configuration_)) return configuration_ ? uint32_t(AConfiguration_getDensity(configuration_))
: 160; : 160;
} }
const JniIDs& jni_ids() const { return jni_ids_; }
int32_t window_surface_layout_left() const { int32_t window_surface_layout_left() const {
return window_surface_layout_left_; return window_surface_layout_left_;
} }
@ -70,6 +88,7 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
void JniActivityOnDestroy(); void JniActivityOnDestroy();
void JniActivityOnWindowSurfaceLayoutChange(jint left, jint top, jint right, void JniActivityOnWindowSurfaceLayoutChange(jint left, jint top, jint right,
jint bottom); jint bottom);
bool JniActivityOnWindowSurfaceMotionEvent(jobject event);
void JniActivityOnWindowSurfaceChanged(jobject window_surface_object); void JniActivityOnWindowSurfaceChanged(jobject window_surface_object);
void JniActivityPaintWindow(bool force_paint); void JniActivityPaintWindow(bool force_paint);
@ -117,6 +136,9 @@ class AndroidWindowedAppContext final : public WindowedAppContext {
bool android_base_initialized_ = false; bool android_base_initialized_ = false;
jclass motion_event_class_ = nullptr;
JniIDs jni_ids_ = {};
jobject activity_ = nullptr; jobject activity_ = nullptr;
jmethodID activity_method_finish_ = nullptr; jmethodID activity_method_finish_ = nullptr;
jmethodID activity_method_post_invalidate_window_surface_ = nullptr; jmethodID activity_method_post_invalidate_window_surface_ = nullptr;