mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-23 20:41:50 +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/applet_creator.cpp
|
||||||
${source_DIR}/skyline/applet/controller_applet.cpp
|
${source_DIR}/skyline/applet/controller_applet.cpp
|
||||||
${source_DIR}/skyline/applet/player_select_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/IHardwareOpusDecoder.cpp
|
||||||
${source_DIR}/skyline/services/codec/IHardwareOpusDecoderManager.cpp
|
${source_DIR}/skyline/services/codec/IHardwareOpusDecoderManager.cpp
|
||||||
${source_DIR}/skyline/services/hid/IHidServer.cpp
|
${source_DIR}/skyline/services/hid/IHidServer.cpp
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="emu.skyline.EmulationActivity"
|
android:name="emu.skyline.EmulationActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<meta-data
|
<meta-data
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include "controller_applet.h"
|
#include "controller_applet.h"
|
||||||
#include "player_select_applet.h"
|
#include "player_select_applet.h"
|
||||||
#include "applet_creator.h"
|
#include "applet_creator.h"
|
||||||
|
#include "swkbd/software_keyboard_applet.h"
|
||||||
|
|
||||||
namespace skyline::applet {
|
namespace skyline::applet {
|
||||||
std::shared_ptr<service::am::IApplet> CreateApplet(
|
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);
|
return std::make_shared<ControllerApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
|
||||||
case AppletId::LibraryAppletPlayerSelect:
|
case AppletId::LibraryAppletPlayerSelect:
|
||||||
return std::make_shared<PlayerSelectApplet>(state, manager, std::move(onAppletStateChanged), std::move(onNormalDataPushFromApplet), std::move(onInteractiveDataPushFromApplet), appletMode);
|
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:
|
default:
|
||||||
throw exception("Unimplemented Applet: 0x{:X} ({})", static_cast<u32>(appletId), ToString(appletId));
|
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;
|
thread_local inline JniEnvironment env;
|
||||||
|
|
||||||
JvmManager::JvmManager(JNIEnv *environ, jobject instance)
|
JvmManager::JvmManager(JNIEnv *environ, jobject instance)
|
||||||
: instance(environ->NewGlobalRef(instance)),
|
: instance{environ->NewGlobalRef(instance)},
|
||||||
instanceClass(reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))),
|
instanceClass{reinterpret_cast<jclass>(environ->NewGlobalRef(environ->GetObjectClass(instance)))},
|
||||||
initializeControllersId(environ->GetMethodID(instanceClass, "initializeControllers", "()V")),
|
initializeControllersId{environ->GetMethodID(instanceClass, "initializeControllers", "()V")},
|
||||||
vibrateDeviceId(environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")),
|
vibrateDeviceId{environ->GetMethodID(instanceClass, "vibrateDevice", "(I[J[I)V")},
|
||||||
clearVibrationDeviceId(environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")),
|
clearVibrationDeviceId{environ->GetMethodID(instanceClass, "clearVibrationDevice", "(I)V")},
|
||||||
getVersionCodeId(environ->GetMethodID(instanceClass, "getVersionCode", "()I")) {
|
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);
|
env.Initialize(environ);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +109,42 @@ namespace skyline {
|
|||||||
env->CallVoidMethod(instance, clearVibrationDeviceId, index);
|
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() {
|
i32 JvmManager::GetVersionCode() {
|
||||||
return env->CallIntMethod(instance, getVersionCodeId);
|
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 {
|
class JvmManager {
|
||||||
public:
|
public:
|
||||||
|
using KeyboardHandle = jobject;
|
||||||
|
using KeyboardConfig = std::array<u8, 0x4C8>;
|
||||||
|
using KeyboardCloseResult = u32;
|
||||||
|
using KeyboardTextCheckResult = u32;
|
||||||
|
|
||||||
|
|
||||||
jobject instance; //!< A reference to the activity
|
jobject instance; //!< A reference to the activity
|
||||||
jclass instanceClass; //!< The class of the activity
|
jclass instanceClass; //!< The class of the activity
|
||||||
|
|
||||||
@ -106,6 +112,26 @@ namespace skyline {
|
|||||||
*/
|
*/
|
||||||
void ClearVibrationDevice(jint index);
|
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
|
* @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
|
* @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 initializeControllersId;
|
||||||
jmethodID vibrateDeviceId;
|
jmethodID vibrateDeviceId;
|
||||||
jmethodID clearVibrationDeviceId;
|
jmethodID clearVibrationDeviceId;
|
||||||
|
jmethodID showKeyboardId;
|
||||||
|
jmethodID waitForSubmitOrCancelId;
|
||||||
|
jmethodID closeKeyboardId;
|
||||||
|
jmethodID showValidationResultId;
|
||||||
jmethodID getVersionCodeId;
|
jmethodID getVersionCodeId;
|
||||||
|
|
||||||
|
jmethodID getIntegerValueId;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class AppDialog : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
private lateinit var binding : AppDialogBinding
|
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
|
* 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.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.databinding.EmuActivityBinding
|
||||||
import emu.skyline.input.*
|
import emu.skyline.input.*
|
||||||
import emu.skyline.loader.getRomFormat
|
import emu.skyline.loader.getRomFormat
|
||||||
|
import emu.skyline.utils.ByteBufferSerializable
|
||||||
import emu.skyline.utils.Settings
|
import emu.skyline.utils.Settings
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.util.concurrent.FutureTask
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
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
|
// Stop forcing 60Hz on exit to allow the skyline UI to run at high refresh rates
|
||||||
getSystemService<DisplayManager>()?.unregisterDisplayListener(this)
|
getSystemService<DisplayManager>()?.unregisterDisplayListener(this)
|
||||||
force60HzRefreshRate(false);
|
force60HzRefreshRate(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceCreated(holder : SurfaceHolder) {
|
override fun surfaceCreated(holder : SurfaceHolder) {
|
||||||
@ -574,6 +582,50 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
vibrators[index]?.cancel()
|
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
|
* @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="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="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>
|
<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 -->
|
<!-- Misc -->
|
||||||
<!--suppress AndroidLintUnusedResources -->
|
<!--suppress AndroidLintUnusedResources -->
|
||||||
<string name="expand_button_title" tools:override="true">Expand</string>
|
<string name="expand_button_title" tools:override="true">Expand</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user