mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 21:11:53 +01:00
Implement Software Keyboard applet
This implements the non-inline version of the Software Keyboard (swkbd) applet, which games use to get text input from the user.
This commit is contained in:
parent
a9ee06914d
commit
f9a0394577
@ -263,6 +263,8 @@ add_library(skyline SHARED
|
||||
${source_DIR}/skyline/applet/applet_creator.cpp
|
||||
${source_DIR}/skyline/applet/controller_applet.cpp
|
||||
${source_DIR}/skyline/applet/player_select_applet.cpp
|
||||
${source_DIR}/skyline/applet/swkbd/software_keyboard_applet.cpp
|
||||
${source_DIR}/skyline/applet/swkbd/software_keyboard_config.cpp
|
||||
${source_DIR}/skyline/services/codec/IHardwareOpusDecoder.cpp
|
||||
${source_DIR}/skyline/services/codec/IHardwareOpusDecoderManager.cpp
|
||||
${source_DIR}/skyline/services/hid/IHidServer.cpp
|
||||
|
@ -65,7 +65,7 @@
|
||||
|
||||
<activity
|
||||
android:name="emu.skyline.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:configChanges="orientation|screenSize|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<meta-data
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "controller_applet.h"
|
||||
#include "player_select_applet.h"
|
||||
#include "applet_creator.h"
|
||||
#include "swkbd/software_keyboard_applet.h"
|
||||
|
||||
namespace skyline::applet {
|
||||
std::shared_ptr<service::am::IApplet> CreateApplet(
|
||||
@ -17,6 +18,8 @@ namespace skyline::applet {
|
||||
return std::make_shared<ControllerApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
|
||||
case AppletId::LibraryAppletPlayerSelect:
|
||||
return std::make_shared<PlayerSelectApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
|
||||
case AppletId::LibraryAppletSwkbd:
|
||||
return std::make_shared<swkbd::SoftwareKeyboardApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
|
||||
default:
|
||||
throw exception("Unimplemented Applet: 0x{:X} ({})", static_cast<u32>(appletId), ToString(appletId));
|
||||
}
|
||||
|
@ -0,0 +1,179 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
// Copyright © 2019-2022 Ryujinx Team and Contributors
|
||||
|
||||
#include <codecvt>
|
||||
#include <services/am/storage/ObjIStorage.h>
|
||||
#include <common/settings.h>
|
||||
#include "software_keyboard_applet.h"
|
||||
#include <jvm.h>
|
||||
|
||||
class Utf8Utf16Converter : public std::codecvt<char16_t, char8_t, std::mbstate_t> {
|
||||
public:
|
||||
~Utf8Utf16Converter() override = default;
|
||||
};
|
||||
|
||||
namespace skyline::applet::swkbd {
|
||||
static void WriteStringToSpan(span<u8> chars, std::u16string_view text, bool useUtf8Storage) {
|
||||
if (useUtf8Storage) {
|
||||
auto u8chars{chars.cast<char8_t>()};
|
||||
Utf8Utf16Converter::state_type convert_state;
|
||||
const char16_t *from_next;
|
||||
char8_t *to_next;
|
||||
Utf8Utf16Converter().out(convert_state, text.data(), text.end(), from_next, u8chars.data(), u8chars.end().base(), to_next);
|
||||
// Null terminate the string, if it isn't out of bounds
|
||||
if (to_next < reinterpret_cast<const char8_t *>(text.end()))
|
||||
*to_next = u8'\0';
|
||||
} else {
|
||||
std::memcpy(chars.data(), text.data(), std::min(text.size() * sizeof(char16_t), chars.size()));
|
||||
// Null terminate the string, if it isn't out of bounds
|
||||
if (text.size() * sizeof(char16_t) < chars.size())
|
||||
*(reinterpret_cast<char16_t *>(chars.data()) + text.size()) = u'\0';
|
||||
}
|
||||
}
|
||||
|
||||
SoftwareKeyboardApplet::ValidationRequest::ValidationRequest(std::u16string_view text, bool useUtf8Storage) : size{sizeof(ValidationRequest)} {
|
||||
WriteStringToSpan(chars, text, useUtf8Storage);
|
||||
}
|
||||
|
||||
SoftwareKeyboardApplet::OutputResult::OutputResult(CloseResult closeResult, std::u16string_view text, bool useUtf8Storage) : closeResult{closeResult} {
|
||||
WriteStringToSpan(chars, text, useUtf8Storage);
|
||||
}
|
||||
|
||||
static std::u16string FillDefaultText(u32 minLength, u32 maxLength) {
|
||||
std::u16string text{u"Skyline"};
|
||||
while (text.size() < minLength)
|
||||
text += u"Emulator" + text;
|
||||
if (text.size() > maxLength)
|
||||
text.resize(maxLength);
|
||||
return text;
|
||||
}
|
||||
|
||||
void SoftwareKeyboardApplet::SendResult() {
|
||||
if (dialog)
|
||||
state.jvm->CloseKeyboard(dialog);
|
||||
PushNormalDataAndSignal(std::make_shared<service::am::ObjIStorage<OutputResult>>(state, manager, OutputResult{currentResult, currentText, config.commonConfig.isUseUtf8}));
|
||||
onAppletStateChanged->Signal();
|
||||
}
|
||||
|
||||
SoftwareKeyboardApplet::SoftwareKeyboardApplet(
|
||||
const DeviceState &state,
|
||||
service::ServiceManager &manager,
|
||||
std::shared_ptr<kernel::type::KEvent> onAppletStateChanged,
|
||||
std::shared_ptr<kernel::type::KEvent> onNormalDataPushFromApplet,
|
||||
std::shared_ptr<kernel::type::KEvent> onInteractiveDataPushFromApplet,
|
||||
service::applet::LibraryAppletMode appletMode)
|
||||
: IApplet{state,
|
||||
manager,
|
||||
std::move(onAppletStateChanged),
|
||||
std::move(onNormalDataPushFromApplet),
|
||||
std::move(onInteractiveDataPushFromApplet),
|
||||
appletMode} {
|
||||
if (appletMode != service::applet::LibraryAppletMode::AllForeground)
|
||||
throw exception("Inline Software Keyboard not implemeted");
|
||||
}
|
||||
|
||||
Result SoftwareKeyboardApplet::Start() {
|
||||
std::scoped_lock lock{inputDataMutex};
|
||||
auto commonArgs{normalInputData.front()->GetSpan().as<service::applet::CommonArguments>()};
|
||||
normalInputData.pop();
|
||||
|
||||
auto configSpan{normalInputData.front()->GetSpan()};
|
||||
normalInputData.pop();
|
||||
config = [&] {
|
||||
if (commonArgs.apiVersion < 0x30007)
|
||||
return KeyboardConfigVB{configSpan.as<KeyboardConfigV0>()};
|
||||
else if (commonArgs.apiVersion < 0x6000B)
|
||||
return KeyboardConfigVB{configSpan.as<KeyboardConfigV7>()};
|
||||
else
|
||||
return configSpan.as<KeyboardConfigVB>();
|
||||
}();
|
||||
Logger::Debug("Swkbd Config:\n* KeyboardMode: {}\n* InvalidCharFlags: {:#09b}\n* TextMaxLength: {}\n* TextMinLength: {}\n* PasswordMode: {}\n* InputFormMode: {}\n* IsUseNewLine: {}\n* IsUseTextCheck: {}",
|
||||
static_cast<u32>(config.commonConfig.keyboardMode),
|
||||
config.commonConfig.invalidCharFlags.raw,
|
||||
config.commonConfig.textMaxLength,
|
||||
config.commonConfig.textMinLength,
|
||||
static_cast<u32>(config.commonConfig.passwordMode),
|
||||
static_cast<u32>(config.commonConfig.inputFormMode),
|
||||
config.commonConfig.isUseNewLine,
|
||||
config.commonConfig.isUseTextCheck
|
||||
);
|
||||
|
||||
auto maxChars{static_cast<u32>(SwkbdTextBytes / (config.commonConfig.isUseUtf8 ? sizeof(char8_t) : sizeof(char16_t)))};
|
||||
config.commonConfig.textMaxLength = std::min(config.commonConfig.textMaxLength, maxChars);
|
||||
if (config.commonConfig.textMaxLength == 0)
|
||||
config.commonConfig.textMaxLength = maxChars;
|
||||
config.commonConfig.textMinLength = std::min(config.commonConfig.textMinLength, config.commonConfig.textMaxLength);
|
||||
|
||||
if (config.commonConfig.textMaxLength > MaxOneLineChars)
|
||||
config.commonConfig.inputFormMode = InputFormMode::MultiLine;
|
||||
|
||||
if (!normalInputData.empty() && config.commonConfig.initialStringLength > 0)
|
||||
currentText = std::u16string(normalInputData.front()->GetSpan().subspan(config.commonConfig.initialStringOffset).cast<char16_t>().data(), config.commonConfig.initialStringLength);
|
||||
|
||||
dialog = state.jvm->ShowKeyboard(*reinterpret_cast<JvmManager::KeyboardConfig *>(&config), currentText);
|
||||
if (!dialog) {
|
||||
Logger::Warn("Couldn't show keyboard dialog, using default text");
|
||||
currentResult = CloseResult::Enter;
|
||||
currentText = FillDefaultText(config.commonConfig.textMinLength, config.commonConfig.textMaxLength);
|
||||
} else {
|
||||
auto result{state.jvm->WaitForSubmitOrCancel(dialog)};
|
||||
currentResult = static_cast<CloseResult>(result.first);
|
||||
currentText = result.second;
|
||||
}
|
||||
if (config.commonConfig.isUseTextCheck && currentResult == CloseResult::Enter) {
|
||||
PushInteractiveDataAndSignal(std::make_shared<service::am::ObjIStorage<ValidationRequest>>(state, manager, ValidationRequest{currentText, config.commonConfig.isUseUtf8}));
|
||||
validationPending = true;
|
||||
} else {
|
||||
SendResult();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Result SoftwareKeyboardApplet::GetResult() {
|
||||
return {};
|
||||
}
|
||||
|
||||
void SoftwareKeyboardApplet::PushNormalDataToApplet(std::shared_ptr<service::am::IStorage> data) {
|
||||
std::scoped_lock lock{inputDataMutex};
|
||||
normalInputData.emplace(data);
|
||||
}
|
||||
|
||||
void SoftwareKeyboardApplet::PushInteractiveDataToApplet(std::shared_ptr<service::am::IStorage> data) {
|
||||
if (validationPending) {
|
||||
auto dataSpan{data->GetSpan()};
|
||||
auto validationResult{dataSpan.as<ValidationResult>()};
|
||||
if (validationResult.result == TextCheckResult::Success) {
|
||||
validationPending = false;
|
||||
SendResult();
|
||||
} else {
|
||||
if (dialog) {
|
||||
if (static_cast<CloseResult>(state.jvm->ShowValidationResult(dialog, static_cast<JvmManager::KeyboardTextCheckResult>(validationResult.result), std::u16string(validationResult.chars.data()))) == CloseResult::Enter) {
|
||||
// Accepted on confirmation dialog
|
||||
validationPending = false;
|
||||
SendResult();
|
||||
} else {
|
||||
// Cancelled or failed validation, go back to waiting for text
|
||||
auto result{state.jvm->WaitForSubmitOrCancel(dialog)};
|
||||
currentResult = static_cast<CloseResult>(result.first);
|
||||
currentText = result.second;
|
||||
if (currentResult == CloseResult::Enter) {
|
||||
PushInteractiveDataAndSignal(std::make_shared<service::am::ObjIStorage<ValidationRequest>>(state, manager, ValidationRequest{currentText, config.commonConfig.isUseUtf8}));
|
||||
} else {
|
||||
SendResult();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::array<u8, SwkbdTextBytes> chars{};
|
||||
WriteStringToSpan(chars, std::u16string(validationResult.chars.data()), true);
|
||||
std::string message{reinterpret_cast<char *>(chars.data())};
|
||||
if (validationResult.result == TextCheckResult::ShowFailureDialog)
|
||||
Logger::Warn("Sending default text despite being rejected by the guest with message: \"{}\"", message);
|
||||
else
|
||||
Logger::Debug("Guest asked to confirm default text with message: \"{}\"", message);
|
||||
PushNormalDataAndSignal(std::make_shared<service::am::ObjIStorage<OutputResult>>(state, manager, OutputResult{CloseResult::Enter, currentText, config.commonConfig.isUseUtf8}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <services/am/applet/IApplet.h>
|
||||
#include <services/applet/common_arguments.h>
|
||||
#include <jvm.h>
|
||||
#include "software_keyboard_config.h"
|
||||
|
||||
namespace skyline::applet::swkbd {
|
||||
static_assert(sizeof(KeyboardConfigVB) == sizeof(JvmManager::KeyboardConfig));
|
||||
|
||||
/**
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard
|
||||
* @brief An implementation for the Software Keyboard (swkbd) Applet which handles translating guest applet transactions to the appropriate host behavior
|
||||
*/
|
||||
class SoftwareKeyboardApplet : public service::am::IApplet {
|
||||
private:
|
||||
/**
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#CloseResult
|
||||
*/
|
||||
enum class CloseResult : u32 {
|
||||
Enter = 0x0,
|
||||
Cancel = 0x1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#TextCheckResult
|
||||
*/
|
||||
enum class TextCheckResult : u32 {
|
||||
Success = 0x0,
|
||||
ShowFailureDialog = 0x1,
|
||||
ShowConfirmDialog = 0x2,
|
||||
};
|
||||
|
||||
static constexpr u32 SwkbdTextBytes{0x7D4}; //!< Size of the returned IStorage buffer that's used to return the input text
|
||||
|
||||
static constexpr u32 MaxOneLineChars{32}; //!< The maximum number of characters for which anything other than InputFormMode::MultiLine is used
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
/**
|
||||
* @brief The final result after the swkbd has closed
|
||||
*/
|
||||
struct OutputResult {
|
||||
CloseResult closeResult;
|
||||
std::array<u8, SwkbdTextBytes> chars{};
|
||||
|
||||
OutputResult(CloseResult closeResult, std::u16string_view text, bool useUtf8Storage);
|
||||
};
|
||||
static_assert(sizeof(OutputResult) == 0x7D8);
|
||||
|
||||
/**
|
||||
* @brief A request for validating a string inside guest code, this is pushed via the interactive queue
|
||||
*/
|
||||
struct ValidationRequest {
|
||||
u64 size;
|
||||
std::array<u8, SwkbdTextBytes> chars{};
|
||||
|
||||
ValidationRequest(std::u16string_view text, bool useUtf8Storage);
|
||||
};
|
||||
static_assert(sizeof(ValidationRequest) == 0x7DC);
|
||||
|
||||
/**
|
||||
* @brief The result of validating text submitted to the guest
|
||||
*/
|
||||
struct ValidationResult {
|
||||
TextCheckResult result;
|
||||
std::array<char16_t, SwkbdTextBytes / sizeof(char16_t)> chars;
|
||||
};
|
||||
static_assert(sizeof(ValidationResult) == 0x7D8);
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
std::mutex inputDataMutex;
|
||||
std::queue<std::shared_ptr<service::am::IStorage>> normalInputData;
|
||||
KeyboardConfigVB config{};
|
||||
bool validationPending{};
|
||||
std::u16string currentText{};
|
||||
CloseResult currentResult{};
|
||||
|
||||
jobject dialog{};
|
||||
|
||||
void SendResult();
|
||||
|
||||
public:
|
||||
SoftwareKeyboardApplet(const DeviceState &state, service::ServiceManager &manager, std::shared_ptr<kernel::type::KEvent> onAppletStateChanged, std::shared_ptr<kernel::type::KEvent> onNormalDataPushFromApplet, std::shared_ptr<kernel::type::KEvent> onInteractiveDataPushFromApplet, service::applet::LibraryAppletMode appletMode);
|
||||
|
||||
Result Start() override;
|
||||
|
||||
Result GetResult() override;
|
||||
|
||||
void PushNormalDataToApplet(std::shared_ptr<service::am::IStorage> data) override;
|
||||
|
||||
void PushInteractiveDataToApplet(std::shared_ptr<service::am::IStorage> data) override;
|
||||
};
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
// Copyright © 2019-2022 Ryujinx Team and Contributors
|
||||
|
||||
#include "software_keyboard_config.h"
|
||||
|
||||
namespace skyline::applet::swkbd {
|
||||
KeyboardConfigVB::KeyboardConfigVB() = default;
|
||||
|
||||
KeyboardConfigVB::KeyboardConfigVB(const KeyboardConfigV7 &v7config) : commonConfig(v7config.commonConfig), separateTextPos(v7config.separateTextPos) {}
|
||||
|
||||
KeyboardConfigVB::KeyboardConfigVB(const KeyboardConfigV0 &v0config) : commonConfig(v0config.commonConfig) {}
|
||||
}
|
187
app/src/main/cpp/skyline/applet/swkbd/software_keyboard_config.h
Normal file
187
app/src/main/cpp/skyline/applet/swkbd/software_keyboard_config.h
Normal file
@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
// Copyright © 2019-2022 Ryujinx Team and Contributors
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <services/account/IAccountServiceForApplication.h>
|
||||
|
||||
namespace skyline::applet::swkbd {
|
||||
/**
|
||||
* @brief Specifies the characters the keyboard should allow you to input
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardMode
|
||||
*/
|
||||
enum class KeyboardMode : u32 {
|
||||
Full = 0x0,
|
||||
Numeric = 0x1,
|
||||
ASCII = 0x2,
|
||||
FullLatin = 0x3,
|
||||
Alphabet = 0x4,
|
||||
SimplifiedChinese = 0x5,
|
||||
TraditionalChinese = 0x6,
|
||||
Korean = 0x7,
|
||||
LanguageSet2 = 0x8,
|
||||
LanguageSet2Latin = 0x9,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies the characters that you shouldn't be allowed to input
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#InvalidCharFlags
|
||||
*/
|
||||
union InvalidCharFlags {
|
||||
u32 raw;
|
||||
struct {
|
||||
u32 _pad_ : 1;
|
||||
u32 space : 1;
|
||||
u32 atMark : 1;
|
||||
u32 percent : 1;
|
||||
u32 slash : 1;
|
||||
u32 backslash : 1;
|
||||
u32 numeric : 1;
|
||||
u32 outsideOfDownloadCode : 1;
|
||||
u32 outsideOfMiiNickName : 1;
|
||||
} flags;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies where the cursor should initially be on the initial string
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#InitialCursorPos
|
||||
*/
|
||||
enum class InitialCursorPos : u32 {
|
||||
First = 0x0,
|
||||
Last = 0x1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#PasswordMode
|
||||
*/
|
||||
enum class PasswordMode : u32 {
|
||||
Show = 0x0,
|
||||
Hide = 0x1, //!< Hides any inputted text to prevent a password from being leaked
|
||||
};
|
||||
|
||||
/**
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#InputFormMode
|
||||
* @note Only applies when 1 <= textMaxLength <= 32, otherwise Multiline is used
|
||||
*/
|
||||
enum class InputFormMode : u32 {
|
||||
OneLine = 0x0,
|
||||
MultiLine = 0x1,
|
||||
Separate = 0x2, //!< Used with separateTextPos
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Specifies the language of custom dictionary entries
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#DictionaryLanguage
|
||||
*/
|
||||
enum class DictionaryLanguage : u16 {
|
||||
Japanese = 0x00,
|
||||
AmericanEnglish = 0x01,
|
||||
CanadianFrench = 0x02,
|
||||
LatinAmericanSpanish = 0x03,
|
||||
Reserved1 = 0x04,
|
||||
BritishEnglish = 0x05,
|
||||
French = 0x06,
|
||||
German = 0x07,
|
||||
Spanish = 0x08,
|
||||
Italian = 0x09,
|
||||
Dutch = 0x0A,
|
||||
Portuguese = 0x0B,
|
||||
Russian = 0x0C,
|
||||
Reserved2 = 0x0D,
|
||||
SimplifiedChinesePinyin = 0x0E,
|
||||
TraditionalChineseCangjie = 0x0F,
|
||||
TraditionalChineseSimplifiedCangjie = 0x10,
|
||||
TraditionalChineseZhuyin = 0x11,
|
||||
Korean = 0x12,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A descriptor of a custom dictionary entry
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#DictionaryInfo
|
||||
*/
|
||||
struct DictionaryInfo {
|
||||
u32 offset;
|
||||
u16 size;
|
||||
DictionaryLanguage language;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The keyboard config that's common across all versions
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
|
||||
*/
|
||||
struct CommonKeyboardConfig {
|
||||
KeyboardMode keyboardMode;
|
||||
std::array<char16_t, 0x9> okText;
|
||||
char16_t leftOptionalSymbolKey;
|
||||
char16_t rightOptionalSymbolKey;
|
||||
bool isPredictionEnabled;
|
||||
u8 _pad0_;
|
||||
InvalidCharFlags invalidCharFlags;
|
||||
InitialCursorPos initialCursorPos;
|
||||
std::array<char16_t, 0x41> headerText;
|
||||
std::array<char16_t, 0x81> subText;
|
||||
std::array<char16_t, 0x101> guideText;
|
||||
u8 _pad2_[0x2];
|
||||
u32 textMaxLength;
|
||||
u32 textMinLength;
|
||||
PasswordMode passwordMode;
|
||||
InputFormMode inputFormMode;
|
||||
bool isUseNewLine;
|
||||
bool isUseUtf8;
|
||||
bool isUseBlurBackground;
|
||||
u8 _pad3_;
|
||||
u32 initialStringOffset;
|
||||
u32 initialStringLength;
|
||||
u32 userDictionaryOffset;
|
||||
u32 userDictionaryNum;
|
||||
bool isUseTextCheck;
|
||||
u8 reserved[0x3];
|
||||
};
|
||||
static_assert(sizeof(CommonKeyboardConfig) == 0x3D4);
|
||||
|
||||
/**
|
||||
* @brief The keyboard config for the first api version
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
|
||||
*/
|
||||
struct KeyboardConfigV0 {
|
||||
CommonKeyboardConfig commonConfig;
|
||||
u8 _pad0_[0x4];
|
||||
u64 textCheckCallback{};
|
||||
};
|
||||
static_assert(sizeof(KeyboardConfigV0) == 0x3E0);
|
||||
|
||||
/**
|
||||
* @brief The keyboard config as of api version 0x30007
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
|
||||
*/
|
||||
struct KeyboardConfigV7 {
|
||||
CommonKeyboardConfig commonConfig;
|
||||
u8 _pad0_[0x4];
|
||||
u64 textCheckCallback;
|
||||
std::array<u32, 0x8> separateTextPos;
|
||||
};
|
||||
static_assert(sizeof(KeyboardConfigV7) == 0x400);
|
||||
|
||||
/**
|
||||
* @brief The keyboard config as of api version 0x6000B
|
||||
* @url https://switchbrew.org/wiki/Software_Keyboard#KeyboardConfig
|
||||
*/
|
||||
struct KeyboardConfigVB {
|
||||
CommonKeyboardConfig commonConfig{};
|
||||
std::array<u32, 0x8> separateTextPos{0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF};
|
||||
std::array<DictionaryInfo, 0x18> customisedDictionaryInfoList{};
|
||||
u8 customisedDictionaryCount{};
|
||||
bool isCancelButtonDisabled{};
|
||||
u8 reserved0[0xD];
|
||||
u8 trigger{};
|
||||
u8 reserved1[0x4];
|
||||
|
||||
KeyboardConfigVB();
|
||||
|
||||
KeyboardConfigVB(const KeyboardConfigV7 &v7config);
|
||||
|
||||
KeyboardConfigVB(const KeyboardConfigV0 &v0config);
|
||||
};
|
||||
static_assert(sizeof(KeyboardConfigVB) == 0x4C8);
|
||||
}
|
@ -54,12 +54,17 @@ namespace skyline {
|
||||
thread_local inline JniEnvironment env;
|
||||
|
||||
JvmManager::JvmManager(JNIEnv *environ, jobject instance)
|
||||
: instance(environ->NewGlobalRef(instance)),
|
||||
instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))),
|
||||
initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")),
|
||||
vibrateDeviceId(environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")),
|
||||
clearVibrationDeviceId(environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")),
|
||||
getVersionCodeId(environ->GetMethodID(instanceClass, "getVersionCode", "()I")) {
|
||||
: instance{environ->NewGlobalRef(instance)},
|
||||
instanceClass{reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))},
|
||||
initializeControllersId{environ->GetMethodID(instanceClass, "initializeControllers", "()V")},
|
||||
vibrateDeviceId{environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")},
|
||||
clearVibrationDeviceId{environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")},
|
||||
showKeyboardId{environ->GetMethodID(instanceClass, "showKeyboard", "(Ljava/nio/ByteBuffer;Ljava/lang/String;)Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;")},
|
||||
waitForSubmitOrCancelId{environ->GetMethodID(instanceClass, "waitForSubmitOrCancel", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)[Ljava/lang/Object;")},
|
||||
closeKeyboardId{environ->GetMethodID(instanceClass, "closeKeyboard", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;)V")},
|
||||
showValidationResultId{environ->GetMethodID(instanceClass, "showValidationResult", "(Lemu/skyline/applet/swkbd/SoftwareKeyboardDialog;ILjava/lang/String;)I")},
|
||||
getVersionCodeId{environ->GetMethodID(instanceClass, "getVersionCode", "()I")},
|
||||
getIntegerValueId{environ->GetMethodID(environ->FindClass("java/lang/Integer"), "intValue", "()I")} {
|
||||
env.Initialize(environ);
|
||||
}
|
||||
|
||||
@ -104,7 +109,42 @@ namespace skyline {
|
||||
env->CallVoidMethod(instance, clearVibrationDeviceId, index);
|
||||
}
|
||||
|
||||
jobject JvmManager::ShowKeyboard(KeyboardConfig &config, std::u16string initialText) {
|
||||
auto buffer{env->NewDirectByteBuffer(&config, sizeof(KeyboardConfig))};
|
||||
auto str{env->NewString(reinterpret_cast<const jchar *>(initialText.data()), static_cast<int>(initialText.length()))};
|
||||
jobject localKeyboardDialog{env->CallObjectMethod(instance, showKeyboardId, buffer, str)};
|
||||
env->DeleteLocalRef(buffer);
|
||||
env->DeleteLocalRef(str);
|
||||
auto keyboardDialog{env->NewGlobalRef(localKeyboardDialog)};
|
||||
|
||||
env->DeleteLocalRef(localKeyboardDialog);
|
||||
return keyboardDialog;
|
||||
}
|
||||
|
||||
std::pair<JvmManager::KeyboardCloseResult, std::u16string> JvmManager::WaitForSubmitOrCancel(jobject keyboardDialog) {
|
||||
auto returnArray{reinterpret_cast<jobjectArray>(env->CallObjectMethod(instance, waitForSubmitOrCancelId, keyboardDialog))};
|
||||
auto buttonInteger{env->GetObjectArrayElement(returnArray, 0)};
|
||||
auto inputJString{reinterpret_cast<jstring>(env->GetObjectArrayElement(returnArray, 1))};
|
||||
auto stringChars{env->GetStringChars(inputJString, nullptr)};
|
||||
std::u16string input{stringChars, stringChars + env->GetStringLength(inputJString)};
|
||||
env->ReleaseStringChars(inputJString, stringChars);
|
||||
|
||||
return {static_cast<KeyboardCloseResult>(env->CallIntMethod(buttonInteger, getIntegerValueId)), input};
|
||||
}
|
||||
|
||||
void JvmManager::CloseKeyboard(jobject dialog) {
|
||||
env->CallVoidMethod(instance, closeKeyboardId, dialog);
|
||||
env->DeleteGlobalRef(dialog);
|
||||
}
|
||||
|
||||
i32 JvmManager::GetVersionCode() {
|
||||
return env->CallIntMethod(instance, getVersionCodeId);
|
||||
}
|
||||
|
||||
JvmManager::KeyboardCloseResult JvmManager::ShowValidationResult(jobject dialog, KeyboardTextCheckResult checkResult, std::u16string message) {
|
||||
auto str{env->NewString(reinterpret_cast<const jchar *>(message.data()), static_cast<int>(message.length()))};
|
||||
auto result{static_cast<KeyboardCloseResult>(env->CallIntMethod(instance, showValidationResultId, dialog, checkResult, str))};
|
||||
env->DeleteLocalRef(str);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,12 @@ namespace skyline {
|
||||
*/
|
||||
class JvmManager {
|
||||
public:
|
||||
using KeyboardHandle = jobject;
|
||||
using KeyboardConfig = std::array<u8, 0x4C8>;
|
||||
using KeyboardCloseResult = u32;
|
||||
using KeyboardTextCheckResult = u32;
|
||||
|
||||
|
||||
jobject instance; //!< A reference to the activity
|
||||
jclass instanceClass; //!< The class of the activity
|
||||
|
||||
@ -106,6 +112,26 @@ namespace skyline {
|
||||
*/
|
||||
void ClearVibrationDevice(jint index);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.showKeyboard in Kotlin
|
||||
*/
|
||||
KeyboardHandle ShowKeyboard(KeyboardConfig &config, std::u16string initialText);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.waitForSubmitOrCancel in Kotlin
|
||||
*/
|
||||
std::pair<KeyboardCloseResult, std::u16string> WaitForSubmitOrCancel(KeyboardHandle dialog);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.closeKeyboard in Kotlin
|
||||
*/
|
||||
void CloseKeyboard(KeyboardHandle dialog);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.showValidationResult in Kotlin
|
||||
*/
|
||||
KeyboardCloseResult ShowValidationResult(KeyboardHandle dialog, KeyboardTextCheckResult checkResult, std::u16string message);
|
||||
|
||||
/**
|
||||
* @brief A call to EmulationActivity.getVersionCode in Kotlin
|
||||
* @return A version code in Vulkan's format with 14-bit patch + 10-bit major and minor components
|
||||
@ -116,6 +142,12 @@ namespace skyline {
|
||||
jmethodID initializeControllersId;
|
||||
jmethodID vibrateDeviceId;
|
||||
jmethodID clearVibrationDeviceId;
|
||||
jmethodID showKeyboardId;
|
||||
jmethodID waitForSubmitOrCancelId;
|
||||
jmethodID closeKeyboardId;
|
||||
jmethodID showValidationResultId;
|
||||
jmethodID getVersionCodeId;
|
||||
|
||||
jmethodID getIntegerValueId;
|
||||
};
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : AppDialogBinding
|
||||
|
||||
private val item by lazy { requireArguments().getSerializable("item") as AppItem }
|
||||
private val item by lazy { requireArguments().getSerializable("item")!! as AppItem }
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
|
@ -20,12 +20,20 @@ import androidx.core.content.getSystemService
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.applet.swkbd.SoftwareKeyboardConfig
|
||||
import emu.skyline.applet.swkbd.SoftwareKeyboardDialog
|
||||
import emu.skyline.databinding.EmuActivityBinding
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import emu.skyline.utils.ByteBufferSerializable
|
||||
import emu.skyline.utils.Settings
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.concurrent.FutureTask
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
@ -365,7 +373,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
|
||||
// Stop forcing 60Hz on exit to allow the skyline UI to run at high refresh rates
|
||||
getSystemService<DisplayManager>()?.unregisterDisplayListener(this)
|
||||
force60HzRefreshRate(false);
|
||||
force60HzRefreshRate(false)
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder : SurfaceHolder) {
|
||||
@ -574,6 +582,50 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
vibrators[index]?.cancel()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun showKeyboard(buffer : ByteBuffer, initialText : String) : SoftwareKeyboardDialog? {
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
val config = ByteBufferSerializable.createFromByteBuffer(SoftwareKeyboardConfig::class, buffer) as SoftwareKeyboardConfig
|
||||
|
||||
val keyboardDialog = SoftwareKeyboardDialog.newInstance(config, initialText)
|
||||
runOnUiThread {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
transaction
|
||||
.add(android.R.id.content, keyboardDialog)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
return keyboardDialog
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun waitForSubmitOrCancel(dialog : SoftwareKeyboardDialog) : Array<Any?> {
|
||||
return dialog.waitForSubmitOrCancel().let { arrayOf(if (it.cancelled) 1 else 0, it.text) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun closeKeyboard(dialog : SoftwareKeyboardDialog) {
|
||||
runOnUiThread { dialog.dismiss() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun showValidationResult(dialog : SoftwareKeyboardDialog, validationResult : Int, message : String) : Int {
|
||||
val confirm = validationResult == SoftwareKeyboardDialog.validationConfirm
|
||||
var accepted = false
|
||||
val validatorResult = FutureTask { return@FutureTask accepted }
|
||||
runOnUiThread {
|
||||
val builder = MaterialAlertDialogBuilder(dialog.requireContext())
|
||||
builder.setMessage(message)
|
||||
builder.setPositiveButton(if (confirm) getString(android.R.string.ok) else getString(android.R.string.cancel)) { _, _ -> accepted = confirm }
|
||||
if (confirm)
|
||||
builder.setNegativeButton(getString(android.R.string.cancel)) { _, _ -> }
|
||||
builder.setOnDismissListener { validatorResult.run() }
|
||||
builder.show()
|
||||
}
|
||||
return if (validatorResult.get()) 0 else 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A version code in Vulkan's format with 14-bit patch + 10-bit major and minor components
|
||||
*/
|
||||
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package emu.skyline.applet.swkbd
|
||||
|
||||
import emu.skyline.utils.*
|
||||
|
||||
fun getCodepointArray(vararg chars : Char) : IntArray {
|
||||
val codepointArray = IntArray(chars.size)
|
||||
for (i in chars.indices)
|
||||
codepointArray[i] = chars[i].code
|
||||
return codepointArray
|
||||
}
|
||||
|
||||
val DownloadCodeCodepoints = getCodepointArray('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X')
|
||||
val OutsideOfMiiNicknameCodepoints = getCodepointArray('@', '%', '\\', 'ō', 'Ō', '₽', '₩', '♥', '♡')
|
||||
|
||||
/**
|
||||
* This data class matches KeyboardMode in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class KeyboardMode(
|
||||
var mode : u32 = 0u
|
||||
) : ByteBufferSerializable {
|
||||
companion object {
|
||||
val Full = KeyboardMode(0u)
|
||||
val Numeric = KeyboardMode(1u)
|
||||
val ASCII = KeyboardMode(2u)
|
||||
val FullLatin = KeyboardMode(3u)
|
||||
val Alphabet = KeyboardMode(4u)
|
||||
val SimplifiedChinese = KeyboardMode(5u)
|
||||
val TraditionalChinese = KeyboardMode(6u)
|
||||
val Korean = KeyboardMode(7u)
|
||||
val LanguageSet2 = KeyboardMode(8u)
|
||||
val LanguageSet2Latin = KeyboardMode(9u)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class matches InvalidCharFlags in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class InvalidCharFlags(
|
||||
var flags : u32 = 0u
|
||||
) : ByteBufferSerializable {
|
||||
// Access each flag as a bool
|
||||
var space : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 1) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 1).inv() or (if (value) 0b1u shl 1 else 0u)
|
||||
}
|
||||
var atMark : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 2) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 2).inv() or (if (value) 0b1u shl 2 else 0u)
|
||||
}
|
||||
var percent : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 3) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 3).inv() or (if (value) 0b1u shl 3 else 0u)
|
||||
}
|
||||
var slash : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 4) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 4).inv() or (if (value) 0b1u shl 4 else 0u)
|
||||
}
|
||||
var backSlash : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 5) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 5).inv() or (if (value) 0b1u shl 5 else 0u)
|
||||
}
|
||||
var numeric : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 6) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 6).inv() or (if (value) 0b1u shl 6 else 0u)
|
||||
}
|
||||
var outsideOfDownloadCode : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 7) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 7).inv() or (if (value) 0b1u shl 7 else 0u)
|
||||
}
|
||||
var outsideOfMiiNickName : Boolean
|
||||
get() : Boolean = (flags and 0b1u shl 8) != 0u
|
||||
set(value) {
|
||||
flags = flags and (0b1u shl 8).inv() or (if (value) 0b1u shl 8 else 0u)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class matches InitialCursorPos in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class InitialCursorPos(
|
||||
var pos : u32 = 0u
|
||||
) : ByteBufferSerializable {
|
||||
companion object {
|
||||
val First = InitialCursorPos(0u)
|
||||
val Last = InitialCursorPos(1u)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class matches PasswordMode in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class PasswordMode(
|
||||
var mode : u32 = 0u
|
||||
) : ByteBufferSerializable {
|
||||
companion object {
|
||||
val Show = PasswordMode(0u)
|
||||
val Hide = PasswordMode(1u)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class matches InputFormMode in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class InputFormMode(
|
||||
var mode : u32 = 0u
|
||||
) : ByteBufferSerializable {
|
||||
companion object {
|
||||
val OneLine = InputFormMode(0u)
|
||||
val MultiLine = InputFormMode(1u)
|
||||
val Separate = InputFormMode(2u)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This data class matches DictionaryLanguage in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class DictionaryLanguage(
|
||||
var lang : u16 = 0u
|
||||
) : ByteBufferSerializable
|
||||
|
||||
/**
|
||||
* This data class matches DictionaryInfo in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class DictionaryInfo(
|
||||
var offset : u32 = 0u,
|
||||
var size : u16 = 0u,
|
||||
var dictionaryLang : DictionaryLanguage = DictionaryLanguage()
|
||||
) : ByteBufferSerializable
|
||||
|
||||
/**
|
||||
* This data class matches KeyboardConfigVB in skyline/applet/swkbd/software_keyboard_config.h
|
||||
*/
|
||||
data class SoftwareKeyboardConfig(
|
||||
var keyboardMode : KeyboardMode = KeyboardMode(),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x9) val okText : CharArray = CharArray(0x9),
|
||||
var leftOptionalSymbolKey : Char = '\u0000',
|
||||
var rightOptionalSymbolKey : Char = '\u0000',
|
||||
var isPredictionEnabled : bool = false,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x1) val _pad0_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
var invalidCharsFlags : InvalidCharFlags = InvalidCharFlags(),
|
||||
var initialCursorPos : InitialCursorPos = InitialCursorPos(),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x41) val headerText : CharArray = CharArray(0x41),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x81) val subText : CharArray = CharArray(0x81),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x101) val guideText : CharArray = CharArray(0x101),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x2) val _pad2_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
var textMaxLength : u32 = 0u,
|
||||
var textMinLength : u32 = 0u,
|
||||
var passwordMode : PasswordMode = PasswordMode(),
|
||||
var inputFormMode : InputFormMode = InputFormMode(),
|
||||
var isUseNewLine : bool = false,
|
||||
var isUseUtf8 : bool = false,
|
||||
var isUseBlurBackground : bool = false,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x1) val _pad3_ : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
var initialStringOffset : u32 = 0u,
|
||||
var initialStringLength : u32 = 0u,
|
||||
var userDictionaryOffset : u32 = 0u,
|
||||
var userDictionaryNum : u32 = 0u,
|
||||
var isUseTextCheck : bool = false,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x3) val reserved0 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x8) val separateTextPos : u32Array = u32Array(0x8),
|
||||
@param:ByteBufferSerializable.ByteBufferSerializableArray(0x18) val customizedDicInfoList : Array<DictionaryInfo> = Array(0x18) { DictionaryInfo() },
|
||||
var customizedDicCount : u8 = 0u,
|
||||
var isCancelButtonDisabled : bool = false,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0xD) val reserved1 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
var trigger : u8 = 0u,
|
||||
@param:ByteBufferSerializable.ByteBufferSerializablePadding(0x4) val reserved2 : ByteBufferSerializable.ByteBufferPadding = ByteBufferSerializable.ByteBufferPadding,
|
||||
) : ByteBufferSerializable {
|
||||
fun isValid(codepoint : Int) : Boolean {
|
||||
when (keyboardMode) {
|
||||
KeyboardMode.Numeric -> {
|
||||
if (!(codepoint in '0'.code..'9'.code || codepoint == leftOptionalSymbolKey.code || codepoint == rightOptionalSymbolKey.code))
|
||||
return false
|
||||
}
|
||||
KeyboardMode.ASCII -> {
|
||||
if (codepoint !in 0x00..0x7F)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (invalidCharsFlags.space && Character.isSpaceChar(codepoint))
|
||||
return false
|
||||
if (invalidCharsFlags.atMark && codepoint == '@'.code)
|
||||
return false
|
||||
if (invalidCharsFlags.slash && codepoint == '/'.code)
|
||||
return false
|
||||
if (invalidCharsFlags.backSlash && codepoint == '\\'.code)
|
||||
return false
|
||||
if (invalidCharsFlags.numeric && codepoint in '0'.code..'9'.code)
|
||||
return false
|
||||
if (invalidCharsFlags.outsideOfDownloadCode && codepoint !in DownloadCodeCodepoints)
|
||||
return false
|
||||
if (invalidCharsFlags.outsideOfMiiNickName && codepoint in OutsideOfMiiNicknameCodepoints)
|
||||
return false
|
||||
if (!isUseNewLine && codepoint == '\n'.code)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun isValid(string : String) : Boolean {
|
||||
if (string.length !in textMinLength.toInt()..textMaxLength.toInt())
|
||||
return false
|
||||
for (codepoint in string.codePoints()) {
|
||||
if (!isValid(codepoint))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun isValid(chars : CharSequence) : Boolean {
|
||||
if (chars.length !in textMinLength.toInt()..textMaxLength.toInt())
|
||||
return false
|
||||
for (codepoint in chars.codePoints()) {
|
||||
if (!isValid(codepoint))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.applet.swkbd
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import emu.skyline.databinding.KeyboardDialogBinding
|
||||
import emu.skyline.utils.stringFromChars
|
||||
import java.util.concurrent.FutureTask
|
||||
|
||||
data class SoftwareKeyboardResult(val cancelled : Boolean, val text : String)
|
||||
|
||||
class SoftwareKeyboardDialog : DialogFragment() {
|
||||
private val config by lazy { requireArguments().getParcelable<SoftwareKeyboardConfig>("config")!! }
|
||||
private val initialText by lazy { requireArguments().getString("initialText")!! }
|
||||
private var stopped = false
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @param config Holds the [SoftwareKeyboardConfig] that will be used to create the keyboard between instances of this dialog
|
||||
* @param initialText Holds the text that was set by the guest when the dialog was created
|
||||
*/
|
||||
fun newInstance(config : SoftwareKeyboardConfig, initialText : String) : SoftwareKeyboardDialog {
|
||||
val args = Bundle()
|
||||
args.putParcelable("config", config)
|
||||
args.putString("initialText", initialText)
|
||||
val fragment = SoftwareKeyboardDialog()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
||||
const val validationConfirm = 2
|
||||
const val validationError = 1
|
||||
}
|
||||
|
||||
private lateinit var binding : KeyboardDialogBinding
|
||||
|
||||
private var cancelled : Boolean = false
|
||||
private var futureResult : FutureTask<SoftwareKeyboardResult> = FutureTask<SoftwareKeyboardResult> { return@FutureTask SoftwareKeyboardResult(cancelled, binding.textInput.text.toString()) }
|
||||
|
||||
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = if (savedInstanceState?.getBoolean("stopped") != true) KeyboardDialogBinding.inflate(inflater).also { binding = it }.root else null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view : View, savedInstanceState : Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (config.inputFormMode == InputFormMode.OneLine) {
|
||||
binding.header.text = stringFromChars(config.headerText)
|
||||
binding.header.visibility = View.VISIBLE
|
||||
binding.sub.text = stringFromChars(config.subText)
|
||||
binding.sub.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (config.keyboardMode == KeyboardMode.Numeric) {
|
||||
binding.textInput.inputType = if (config.passwordMode == PasswordMode.Hide) InputType.TYPE_NUMBER_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER
|
||||
} else {
|
||||
binding.textInput.inputType = if (config.passwordMode == PasswordMode.Hide) InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_TEXT
|
||||
if (config.invalidCharsFlags.outsideOfDownloadCode)
|
||||
binding.textInput.inputType = binding.textInput.inputType or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
if (config.isUseNewLine)
|
||||
binding.textInput.inputType = binding.textInput.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
else
|
||||
binding.textInput.inputType = binding.textInput.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
|
||||
}
|
||||
|
||||
val okText = stringFromChars(config.okText)
|
||||
if (okText.isNotBlank())
|
||||
binding.okButton.text = okText
|
||||
val guideText = stringFromChars(config.guideText)
|
||||
if (guideText.isNotBlank())
|
||||
binding.inputLayout.hint = guideText
|
||||
|
||||
binding.textInput.setText(initialText)
|
||||
binding.textInput.setSelection(if (config.initialCursorPos == InitialCursorPos.First) 0 else initialText.length)
|
||||
binding.textInput.filters = arrayOf(SoftwareKeyboardFilter(config))
|
||||
binding.textInput.doOnTextChanged { text, _, _, _ ->
|
||||
binding.okButton.isEnabled = config.isValid(text!!)
|
||||
binding.lengthStatus.text = "${text.length}/${config.textMaxLength}"
|
||||
}
|
||||
binding.lengthStatus.text = "${initialText.length}/${config.textMaxLength}"
|
||||
binding.okButton.isEnabled = config.isValid(initialText)
|
||||
binding.okButton.setOnClickListener {
|
||||
cancelled = false
|
||||
futureResult.run()
|
||||
}
|
||||
if (config.isCancelButtonDisabled) {
|
||||
binding.cancelButton.visibility = ViewGroup.GONE
|
||||
} else {
|
||||
binding.cancelButton.setOnClickListener {
|
||||
cancelled = true
|
||||
futureResult.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
stopped = false
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
stopped = true
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState : Bundle) {
|
||||
outState.putBoolean("stopped", stopped)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
fun waitForSubmitOrCancel() : SoftwareKeyboardResult {
|
||||
val result = futureResult.get()
|
||||
futureResult = FutureTask<SoftwareKeyboardResult> { return@FutureTask SoftwareKeyboardResult(cancelled, binding.textInput.text.toString()) }
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.applet.swkbd
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
|
||||
class SoftwareKeyboardFilter(val config : SoftwareKeyboardConfig) : InputFilter {
|
||||
override fun filter(source : CharSequence, start : Int, end : Int, dest : Spanned, dstart : Int, dend : Int) : CharSequence? {
|
||||
val filteredStringBuilder = SpannableStringBuilder()
|
||||
val newCharacterIndex = arrayOfNulls<Int>(end - start)
|
||||
var currentIndex : Int
|
||||
|
||||
val sourceCodepoints = source.subSequence(start, end).codePoints().iterator()
|
||||
val replacedLength = dest.subSequence(dstart, dend).length
|
||||
|
||||
var nextIndex = start
|
||||
while (sourceCodepoints.hasNext()) {
|
||||
var codepoint = sourceCodepoints.next()
|
||||
currentIndex = nextIndex
|
||||
nextIndex += Character.charCount(codepoint)
|
||||
|
||||
if (config.invalidCharsFlags.outsideOfDownloadCode)
|
||||
codepoint = Character.toUpperCase(codepoint)
|
||||
if (!config.isValid(codepoint))
|
||||
continue
|
||||
// Check if the string would be over the maximum length after adding this character
|
||||
if (dest.length - replacedLength + filteredStringBuilder.length + Character.charCount(codepoint) > config.textMaxLength.toInt())
|
||||
break
|
||||
else
|
||||
// Keep track where in the new string each character ended up
|
||||
newCharacterIndex[currentIndex - start] = filteredStringBuilder.length
|
||||
filteredStringBuilder.append(String(Character.toChars(codepoint)))
|
||||
}
|
||||
|
||||
if (source is Spanned) {
|
||||
// Copy the spans from the source to the returned SpannableStringBuilder
|
||||
for (span in source.getSpans(start, end, Any::class.java)) {
|
||||
val spanStart = source.getSpanStart(span) - start
|
||||
val spanEnd = source.getSpanEnd(span) - start
|
||||
|
||||
currentIndex = spanStart
|
||||
var newStart = newCharacterIndex[currentIndex]
|
||||
// Make the new span's start be at the first character that was in the span's range and wasn't removed
|
||||
while (newStart == null && currentIndex < spanEnd - 1) {
|
||||
newStart = newCharacterIndex[++currentIndex]
|
||||
}
|
||||
|
||||
currentIndex = spanEnd - 1
|
||||
var newEnd = newCharacterIndex[currentIndex]
|
||||
// Make the span end be at the last character that was in the span's range and wasn't removed
|
||||
while (newEnd == null && currentIndex > spanStart) {
|
||||
newEnd = newCharacterIndex[--currentIndex]
|
||||
}
|
||||
|
||||
if(newStart != null && newEnd != null)
|
||||
filteredStringBuilder.setSpan(span, newStart, newEnd + 1, source.getSpanFlags(span))
|
||||
}
|
||||
}
|
||||
return filteredStringBuilder
|
||||
}
|
||||
}
|
110
app/src/main/res/layout/keyboard_dialog.xml
Normal file
110
app/src/main/res/layout/keyboard_dialog.xml
Normal file
@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/input_dialog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/backgroundColor"
|
||||
app:layout_anchorGravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/horizontal_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.05">
|
||||
|
||||
<Space
|
||||
android:id="@+id/space_left"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.05" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.90">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/sub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/header" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_button"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/grid_padding"
|
||||
android:layout_marginEnd="@dimen/grid_padding"
|
||||
android:text="@android:string/cancel"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ok_button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/length_status" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/ok_button"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/grid_padding"
|
||||
android:text="@android:string/ok"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/length_status" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/grid_padding"
|
||||
android:hint="@string/input_hint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sub">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="flagNoExtractUi|flagNoFullscreen"
|
||||
android:inputType="text"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/length_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/grid_padding"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/input_layout" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<Space
|
||||
android:id="@+id/space_right"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.05" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -166,6 +166,8 @@
|
||||
<string name="open_sans_description">Open Sans is used as our FOSS shared font replacement for Latin, Korean and Chinese</string>
|
||||
<string name="roboto_description">Roboto is used as our FOSS shared font replacement for Nintendo\'s extended character set</string>
|
||||
<string name="source_sans_pro_description">Source Sans Pro is used as our FOSS shared font replacement for Nintendo\'s extended Chinese character set</string>
|
||||
<!-- Software Keyboard -->
|
||||
<string name="input_hint">Input Text</string>
|
||||
<!-- Misc -->
|
||||
<!--suppress AndroidLintUnusedResources -->
|
||||
<string name="expand_button_title" tools:override="true">Expand</string>
|
||||
|
Loading…
Reference in New Issue
Block a user