From f1ae9f71b00ba3ac7a89d0c8fbd36de4da4845f3 Mon Sep 17 00:00:00 2001 From: Nicholaos Mouzourakis Date: Thu, 27 Jun 2024 23:17:31 -0400 Subject: [PATCH] Adding text editor functionality to text viewer. --- README.md | 3 +- arm9/source/common/swkbd.c | 100 +++++-- arm9/source/common/swkbd.h | 32 +- arm9/source/common/ui.c | 34 ++- arm9/source/common/ui.h | 4 + arm9/source/godmode.c | 5 +- arm9/source/language.inl | 8 +- arm9/source/utils/scripting.c | 492 +++++++++++++++++++++++-------- arm9/source/utils/scripting.h | 2 +- resources/languages/source.json | 8 +- resources/sample/HelloScript.gm9 | 2 +- 11 files changed, 504 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index ea9462e7f..3f104b6c0 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ With the possibilites GodMode9 provides, not everything may be obvious at first * __Search drives and folders__: Just press R+A on the drive / folder you want to search. * __Compare and verify files__: Press the A button on the first file, select `Calculate SHA-256`. Do the same for the second file. If the two files are identical, you will get a message about them being identical. On the SDCARD drive (`0:`) you can also write an SHA file, so you can check for any modifications at a later point. * __Hexview and hexedit any file__: Press the A button on a file and select `Show in Hexeditor`. A button again enables edit mode, hold the A button and press arrow buttons to edit bytes. You will get an additional confirmation prompt to take over changes. Take note that for certain files, write permissions can't be enabled. -* __View text files in a text viewer__: Press the A button on a file and select `Show in Textviewer` (only shows up for actual text files). You can enable wordwrapped mode via R+Y, and navigate around the file via R+X and the dpad. +* __View/edit text files in a text editor__: Press the A button on a file and select `Show in Text Editor` (only shows up for actual text files). You can enable wordwrapped mode via R+Y, and navigate around the file via R+X and the dpad. * __Chainload FIRM payloads__: Press the A button on a FIRM file, select `FIRM options` -> `Boot FIRM`. Keep in mind you should not run FIRMs from dubious sources and that the write permissions system is no longer in place after booting a payload. * __Chainload FIRM payloads from a neat menu__: The `payloads` menu is found inside the HOME button menu. It provides any FIRM found in `0:/gm9/payloads` for quick chainloading. * __Inject a file to another file__: Put exactly one file (the file to be injected from) into the clipboard (via the Y button). Press A on the file to be injected to. There will be an option to inject the first file into it. @@ -203,6 +203,7 @@ This tool would not have been possible without the help of numerous people. Than * **Lilith Valentine** for testing and helpful advice * **Project Nayuki** for [qrcodegen](https://github.com/nayuki/QR-Code-generator) * **Amazingmax fonts** for the Amazdoom font +* **nevumx** for turning the text viewer into a text editor with UTF-8 and LF/CRLF support * The fine folks on **the official GodMode9 IRC channel and Discord server** * The fine folks on **freenode #Cakey** * All **[3dbrew.org](https://www.3dbrew.org/wiki/Main_Page) editors** diff --git a/arm9/source/common/swkbd.c b/arm9/source/common/swkbd.c index f410812d5..a1fd63c96 100644 --- a/arm9/source/common/swkbd.c +++ b/arm9/source/common/swkbd.c @@ -3,7 +3,6 @@ #include "language.h" #include "swkbd.h" #include "timer.h" -#include "hid.h" #include "utf.h" @@ -12,7 +11,7 @@ static inline char to_uppercase(char c) { return c; } -static bool BuildKeyboard(TouchBox* swkbd, const char* keys, const u8* layout) { +bool BuildKeyboard(TouchBox* swkbd, const char* keys, const u8* layout, bool multi_line) { // count # of rows u32 n_rows = 0; for (u32 i = 0;; i++) { @@ -26,16 +25,18 @@ static bool BuildKeyboard(TouchBox* swkbd, const char* keys, const u8* layout) { u32 height = (n_rows) ? (n_rows * SWKBD_STDKEY_HEIGHT) + ((n_rows-1) * SWKDB_KEY_SPACING) : 0; u32 p_y = SCREEN_HEIGHT - height - SWKBD_STDKEY_HEIGHT - SWKDB_KEY_SPACING; - // set up the textbox - TouchBox* txtbox = swkbd; - txtbox->x = (SCREEN_WIDTH_BOT - SWKBD_TEXTBOX_WIDTH) / 2; - txtbox->y = p_y - 30; - txtbox->w = SWKBD_TEXTBOX_WIDTH; - txtbox->h = 30; - txtbox->id = KEY_TXTBOX; - // set button positions - TouchBox* tb = swkbd + 1; + TouchBox* tb = swkbd; + if (!multi_line) { + // set up the textbox + TouchBox* txtbox = tb++; + txtbox->x = (SCREEN_WIDTH_BOT - SWKBD_TEXTBOX_WIDTH) / 2; + txtbox->y = p_y - 30; + txtbox->w = SWKBD_TEXTBOX_WIDTH; + txtbox->h = 30; + txtbox->id = KEY_TXTBOX; + } + for (u32 l = 0, k = 0; layout[l] != 0; ) { // calculate width of current row u32 n_keys = layout[l++]; @@ -70,8 +71,9 @@ static bool BuildKeyboard(TouchBox* swkbd, const char* keys, const u8* layout) { return true; } -static void DrawKey(const TouchBox* key, const bool pressed, const u32 uppercase) { +static void DrawKey(const TouchBox* key, const bool pressed, const u32 uppercase, const bool multi_line) { const char* keystrs[] = { SWKBD_KEYSTR }; + const char* ml_keystrs[] = { SWKBD_ML_KEYSTR }; const u32 color = (pressed) ? COLOR_SWKBD_PRESSED : (key->id == KEY_ENTER) ? COLOR_SWKBD_ENTER : ((key->id == KEY_CAPS) && (uppercase > 1)) ? COLOR_SWKBD_CAPS : @@ -81,7 +83,7 @@ static void DrawKey(const TouchBox* key, const bool pressed, const u32 uppercase if (key->id == KEY_TXTBOX) return; char keystr[16]; - if (key->id >= 0x80) snprintf(keystr, sizeof(keystr), "%s", keystrs[key->id - 0x80]); + if (key->id >= 0x80) snprintf(keystr, sizeof(keystr), "%s", (multi_line ? ml_keystrs : keystrs)[key->id - 0x80]); else { keystr[0] = (uppercase) ? to_uppercase(key->id) : key->id; keystr[1] = 0; @@ -111,12 +113,12 @@ static void DrawKeyBoardBox(TouchBox* swkbd, u32 color) { DrawRectangle(BOT_SCREEN, x0-1, y0-1, x1-x0+2, y1-y0+2, color); } -static void DrawKeyBoard(TouchBox* swkbd, const u32 uppercase) { +static void DrawKeyBoard(TouchBox* swkbd, const u32 uppercase, const bool multi_line) { // we need to make sure to skip the textbox here(first entry) // draw keyboard - for (TouchBox* tb = swkbd + 1; tb->id != 0; tb++) { - DrawKey(tb, false, uppercase); + for (TouchBox* tb = swkbd + (multi_line ? 0 : 1); tb->id != 0; tb++) { + DrawKey(tb, false, uppercase, multi_line); } } @@ -209,22 +211,26 @@ static void MoveTextBoxCursor(const TouchBox* txtbox, const char* inputstr, cons } } -static char KeyboardWait(TouchBox* swkbd, bool uppercase) { +static char KeyboardWait(TouchBox* swkbd, bool uppercase, const bool multi_line) { u32 id = 0; u16 x, y; // wait for touch input (handle key input, too) while (true) { u32 pressed = InputWait(0); - if (pressed & BUTTON_B) return KEY_ESCAPE; + if (multi_line && pressed & TIMEOUT_HID) return 0; + else if (pressed & BUTTON_B) return KEY_ESCAPE; else if (pressed & BUTTON_A) return KEY_ENTER; else if (pressed & BUTTON_X) return KEY_BKSPC; - else if (pressed & BUTTON_Y) return KEY_INSERT; - else if (pressed & BUTTON_R1) return KEY_CAPS; + else if (pressed & BUTTON_Y) return multi_line ? KEY_CAPS : KEY_INSERT; + else if (!multi_line && pressed & BUTTON_R1) return KEY_CAPS; else if (pressed & BUTTON_RIGHT) return KEY_RIGHT; else if (pressed & BUTTON_LEFT) return KEY_LEFT; - else if (pressed & BUTTON_SELECT) return KEY_SWITCH; + else if (multi_line && pressed & BUTTON_UP) return KEY_UP; + else if (multi_line && pressed & BUTTON_DOWN) return KEY_DOWN; + else if (!multi_line && pressed & BUTTON_SELECT) return KEY_SWITCH; else if (pressed & BUTTON_TOUCH) break; + else if (multi_line && pressed & BUTTON_ANY) return KEY_DUMMY; } // process touch input @@ -232,9 +238,9 @@ static char KeyboardWait(TouchBox* swkbd, bool uppercase) { const TouchBox* tb = TouchBoxGet(&id, x, y, swkbd, 0); if (tb) { if (id == KEY_TXTBOX) break; // immediately break on textbox - DrawKey(tb, true, uppercase); + DrawKey(tb, true, uppercase, multi_line); while(HID_ReadTouchState(&x, &y) && (tb == TouchBoxGet(NULL, x, y, swkbd, 0))); - DrawKey(tb, false, uppercase); + DrawKey(tb, false, uppercase, multi_line); } } @@ -261,9 +267,9 @@ bool ShowKeyboard(char* inputstr, const u32 max_size, const char *format, ...) { } // generate keyboards - if (!BuildKeyboard(swkbd_alphabet, keys_alphabet, layout_alphabet)) return false; - if (!BuildKeyboard(swkbd_special, keys_special, layout_special)) return false; - if (!BuildKeyboard(swkbd_numpad, keys_numpad, layout_numpad)) return false; + if (!BuildKeyboard(swkbd_alphabet, keys_alphabet, layout_alphabet, false)) return false; + if (!BuildKeyboard(swkbd_special, keys_special, layout_special, false)) return false; + if (!BuildKeyboard(swkbd_numpad, keys_numpad, layout_numpad, false)) return false; // (instructional) text char str[512]; // arbitrary limit, should be more than enough @@ -292,19 +298,19 @@ bool ShowKeyboard(char* inputstr, const u32 max_size, const char *format, ...) { // draw keyboard if required if (swkbd != swkbd_prev) { DrawKeyBoardBox(swkbd, COLOR_SWKBD_BOX); - DrawKeyBoard(swkbd, uppercase); + DrawKeyBoard(swkbd, uppercase, false); DrawTextBox(textbox, inputstr, cursor, &scroll); swkbd_prev = swkbd; } // handle user input - char key = KeyboardWait(swkbd, uppercase); + char key = KeyboardWait(swkbd, uppercase, false); if (key == KEY_INSERT) key = ' '; // impromptu replacement if (key == KEY_TXTBOX) { MoveTextBoxCursor(textbox, inputstr, max_size, &cursor, &scroll); } else if (key == KEY_CAPS) { uppercase = (uppercase + 1) % 3; - DrawKeyBoard(swkbd, uppercase); + DrawKeyBoard(swkbd, uppercase, false); continue; } else if (key == KEY_ENTER) { ret = true; @@ -375,7 +381,7 @@ bool ShowKeyboard(char* inputstr, const u32 max_size, const char *format, ...) { } if (uppercase == 1) { uppercase = 0; - DrawKeyBoard(swkbd, uppercase); + DrawKeyBoard(swkbd, uppercase, false); } } @@ -385,4 +391,38 @@ bool ShowKeyboard(char* inputstr, const u32 max_size, const char *format, ...) { ClearScreen(BOT_SCREEN, COLOR_STD_BG); return ret; +} + +char ShowMultiLineKeyboard(TouchBox* swkbd_alphabet, TouchBox* swkbd_special, TouchBox* swkbd_numpad, TouchBox** swkbd, TouchBox** swkbd_prev, u32* uppercase) { + if (!*swkbd) { + u32 str_width = GetDrawStringWidth(STR_TEXTEDITOR_CONTROLS_KEYBOARD); + if (str_width < (24 * FONT_WIDTH_EXT)) str_width = 24 * FONT_WIDTH_EXT; + u32 str_x = (str_width >= SCREEN_WIDTH_BOT) ? 0 : (SCREEN_WIDTH_BOT - str_width) / 2; + ClearScreen(BOT_SCREEN, COLOR_STD_BG); + DrawStringF(BOT_SCREEN, str_x, 20, COLOR_STD_FONT, COLOR_STD_BG, "%s", STR_TEXTEDITOR_CONTROLS_KEYBOARD); + *swkbd = swkbd_alphabet; + } + + // handle keyboard + while (true) { + // draw keyboard if required + if (*swkbd != *swkbd_prev) { + DrawKeyBoardBox(*swkbd, COLOR_SWKBD_BOX); + DrawKeyBoard(*swkbd, *uppercase, true); + *swkbd_prev = *swkbd; + } + + // handle user input + char key = KeyboardWait(*swkbd, *uppercase, true); + if (key == KEY_ALPHA) { + *swkbd = swkbd_alphabet; + } else if (key == KEY_SPECIAL) { + *swkbd = swkbd_special; + } else if (key == KEY_NUMPAD) { + *swkbd = swkbd_numpad; + } else if (key == KEY_CAPS) { + *uppercase = (*uppercase + 1) % 3; + DrawKeyBoard(*swkbd, *uppercase, true); + } else return key; + } } \ No newline at end of file diff --git a/arm9/source/common/swkbd.h b/arm9/source/common/swkbd.h index 86f93d440..00d18a180 100644 --- a/arm9/source/common/swkbd.h +++ b/arm9/source/common/swkbd.h @@ -1,6 +1,7 @@ #pragma once #include "common.h" +#include "hid.h" #include "ui.h" #include "touchcal.h" @@ -20,11 +21,15 @@ enum { KEY_ESCAPE = 0x8A, KEY_SWITCH = 0x8B, KEY_UNICODE = 0x8C, + KEY_UP = 0x8D, + KEY_DOWN = 0x8E, KEY_TXTBOX = 0xFF }; // special key strings -#define SWKBD_KEYSTR "", "DEL", "INS", "SUBMIT", "CAPS", "#$@", "123", "ABC", "←", "→", "ESC", "SWITCH", "U+" +#define SWKBD_KEYSTR "", "DEL", "INS", "SUBMIT", "CAPS", "#$@", "123", "ABC", "←", "→", "ESC", "SWITCH", "U+", "↑", "↓" +// multiline special key stings +#define SWKBD_ML_KEYSTR "", "DEL", "INS", "ENTER", "CAPS", "#$@", "123", "ABC", "←", "→", "ESC", "SWITCH", "U+", "↑", "↓" #define COLOR_SWKBD_NORMAL COLOR_GREY #define COLOR_SWKBD_PRESSED COLOR_LIGHTGREY @@ -45,6 +50,13 @@ enum { 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '_', '#', '!', \ KEY_CAPS, ' ', KEY_NUMPAD, KEY_SPECIAL, KEY_LEFT, KEY_RIGHT +#define SWKBD_KEYS_ML_ALPHABET \ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', KEY_BKSPC, \ + 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '&', KEY_ENTER, \ + 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '(', ')', '[', ']', \ + 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '_', '#', '!', \ + KEY_CAPS, ' ', KEY_NUMPAD, KEY_SPECIAL, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN + #define SWKBD_KEYS_SPECIAL \ '(', ')', '{', '}', '[', ']', \ '.', ',', '?', '!', '`', '\'', \ @@ -53,9 +65,9 @@ enum { KEY_ALPHA, ' ', KEY_BKSPC #define SWKBD_KEYS_NUMPAD \ - '7', '8', '9', 'F', 'E', \ - '4', '5', '6', 'D', 'C', \ - '3', '2', '1', 'B', 'A', \ + '7', '8', '9', 'E', 'F', \ + '4', '5', '6', 'C', 'D', \ + '1', '2', '3', 'A', 'B', \ '0', '.', '_', KEY_LEFT, KEY_RIGHT, \ KEY_ALPHA, KEY_UNICODE, ' ', KEY_BKSPC @@ -68,12 +80,20 @@ enum { 6, 32, 123, 32, 32, 18, 18, 0, \ 0 +#define SWKBD_LAYOUT_ML_ALPHABET \ + 13, 32, 0, \ + 12, 51, 0, \ + 13, 0, \ + 12, 0, \ + 8, 32, 85, 32, 32, 18, 18, 18, 18, 0, \ + 0 + #define SWKBD_LAYOUT_SPECIAL \ 6, 0, \ 6, 0, \ 6, 0, \ 6, 0, \ - 3, 32, 46, 32, 0, \ + 3, 32, 47, 32, 0, \ 0 #define SWKBD_LAYOUT_NUMPAD \ @@ -87,3 +107,5 @@ enum { #define ShowKeyboardOrPrompt (TouchIsCalibrated() ? ShowKeyboard : ShowStringPrompt) bool PRINTF_ARGS(3) ShowKeyboard(char* inputstr, u32 max_size, const char *format, ...); +bool BuildKeyboard(TouchBox* swkbd, const char* keys, const u8* layout, bool multi_line); +char ShowMultiLineKeyboard(TouchBox* swkbd_alphabet, TouchBox* swkbd_special, TouchBox* swkbd_numpad, TouchBox** swkbd, TouchBox** swkbd_prev, u32* uppercase); diff --git a/arm9/source/common/ui.c b/arm9/source/common/ui.c index e2de1ebad..67656003f 100644 --- a/arm9/source/common/ui.c +++ b/arm9/source/common/ui.c @@ -489,22 +489,34 @@ u32 GetDrawStringHeight(const char* str) { return height; } -u32 GetCharSize(const char* str) { - const char *start = str; - do { - str++; - } while ((*str & 0xC0) == 0x80); +static inline bool IsIntermediateByte(const char* chr) { + return (*chr & 0xC0) == 0x80 || (chr[-1] == '\r' && chr[0] == '\n'); +} + +const char* GetNextChar(const char* chr) { + do ++chr; while (IsIntermediateByte(chr)); + return chr; +} + +const char* GetPrevChar(const char* chr) { + do --chr; while (IsIntermediateByte(chr)); + return chr; +} - return str - start; +u32 GetCharSize(const char* str) { + return GetNextChar(str) - str; } u32 GetPrevCharSize(const char* str) { - const char *start = str; - do { - str--; - } while ((*str & 0xC0) == 0x80); + return str - GetPrevChar(str); +} + +void IncChar(const char** chr) { + *chr = GetNextChar(*chr); +} - return start - str; +void DecChar(const char** chr) { + *chr = GetPrevChar(*chr); } u32 GetDrawStringWidth(const char* str) { diff --git a/arm9/source/common/ui.h b/arm9/source/common/ui.h index 4f51c0bb4..770f2c364 100644 --- a/arm9/source/common/ui.h +++ b/arm9/source/common/ui.h @@ -70,6 +70,10 @@ void PRINTF_ARGS(4) DrawStringCenter(u16 *screen, u32 color, u32 bgcolor, const u32 GetCharSize(const char* str); u32 GetPrevCharSize(const char* str); +const char* GetNextChar(const char* chr); +const char* GetPrevChar(const char* chr); +void IncChar(const char** chr); +void DecChar(const char** chr); u32 GetDrawStringHeight(const char* str); u32 GetDrawStringWidth(const char* str); diff --git a/arm9/source/godmode.c b/arm9/source/godmode.c index 886ae3706..1c16d08ec 100644 --- a/arm9/source/godmode.c +++ b/arm9/source/godmode.c @@ -1225,7 +1225,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan int n_opt = 0; int special = (special_opt) ? ++n_opt : -1; int hexviewer = ++n_opt; - int textviewer = (filetype & TXT_GENERIC) ? ++n_opt : -1; + int textviewer = (filetype & TXT_GENERIC || FileGetSize(file_path) == 0) ? ++n_opt : -1; int calcsha256 = ++n_opt; int calcsha1 = ++n_opt; int calccmac = (CheckCmacPath(file_path) == 0) ? ++n_opt : -1; @@ -1316,6 +1316,7 @@ u32 FileHandlerMenu(char* current_path, u32* cursor, u32* scroll, PaneData** pan } else if (user_select == textviewer) { // -> show in text viewer FileTextViewer(file_path, scriptable); + GetDirContents(current_dir, current_path); return 0; } else if (user_select == calcsha256) { // -> calculate SHA-256 @@ -2340,7 +2341,7 @@ u32 HomeMoreMenu(char* current_path) { char* sysinfo_txt = (char*) malloc(STD_BUFFER_SIZE); if (!sysinfo_txt) return 1; MyriaSysinfo(sysinfo_txt); - MemTextViewer(sysinfo_txt, strnlen(sysinfo_txt, STD_BUFFER_SIZE), 1, false); + MemTextViewer(sysinfo_txt, strnlen(sysinfo_txt, STD_BUFFER_SIZE), 1, false, 0, NULL); free(sysinfo_txt); return 0; } diff --git a/arm9/source/language.inl b/arm9/source/language.inl index 547f10b2c..a465f887a 100644 --- a/arm9/source/language.inl +++ b/arm9/source/language.inl @@ -191,7 +191,7 @@ STRING(SHOW_IN_HEXEDITOR, "Show in Hexeditor") STRING(CALCULATE_SHA256, "Calculate SHA-256") STRING(CALCULATE_SHA1, "Calculate SHA-1") STRING(SHOW_FILE_INFO, "Show file info") -STRING(SHOW_IN_TEXTVIEWER, "Show in Textviewer") +STRING(SHOW_IN_TEXTVIEWER, "Show in Text Editor") STRING(CALCULATE_CMAC, "Calculate CMAC") STRING(COPY_TO_OUT, "Copy to %s") STRING(DUMP_TO_OUT, "Dump to %s") @@ -773,7 +773,7 @@ STRING(SCRIPTERR_UNKNOWN_FILE, "unknown file") STRING(SCRIPTERR_APPLY_IPS_FAILD, "apply IPS failed") STRING(SCRIPTERR_APPLY_BPS_FAILED, "apply BPS failed") STRING(SCRIPTERR_APPLY_BPM_FAILED, "apply BPM failed") -STRING(SCRIPTERR_TEXTVIEWER_FAILED, "textviewer failed") +STRING(SCRIPTERR_TEXTVIEWER_FAILED, "text editor failed") STRING(SCRIPTERR_BAD_DUMPSIZE, "bad dumpsize") STRING(SCRIPTERR_CART_INIT_FAIL, "cart init fail") STRING(SCRIPTERR_CART_DUMP_FAILED, "cart dump failed") @@ -787,7 +787,11 @@ STRING(SCRIPTERR_CONTROL_FLOW_ERROR, "control flow error") STRING(SCRIPTERR_UNCLOSED_CONDITIONAL, "unclosed conditional") STRING(SCRIPTERR_ERROR_MESSAGE_FAIL, "error message fail") STRING(ERROR_INVALID_TEXT_DATA, "Error: Invalid text data") +STRING(ERROR_TEXT_FILE_TOO_BIG, "Error: Text file is too large.\nText file size is %u bytes.\nMax file size is %i bytes.") STRING(TEXTVIEWER_CONTROLS_DETAILS, "Textviewer Controls:\n \n↑↓→←(+R) - Scroll\nR+Y - Toggle wordwrap\nR+X - Goto line #\nB - Exit\n") +STRING(TEXTEDITOR_CONTROLS_DETAILS, "Text Editor Controls:\n \n↑↓→←(+R) - Scroll\nR+Y - Toggle wordwrap\nR+X - Goto line #\nA - Enter edit mode\nB - Exit\n") +STRING(TEXTEDITOR_CONTROLS_KEYBOARD, "Text Editor Controls:\n \n↑↓→←(+R) - Move cursor\nY - Caps / Capslock\nX - Delete char\nA - Insert newline\nB - Enter view mode\n") +STRING(TEXT_EDITS_SAVE_CHANGES, "You made text edits.\nWrite changes to file?") STRING(CURRENT_LINE_N_ENTER_NEW_LINE_BELOW, "Current line: %i\nEnter new line below.") STRING(PREVIEW_DISABLED, "(preview disabled)") STRING(PATH_LINE_N_ERR_LINE, "%s\nline %lu: %s\n%s") diff --git a/arm9/source/utils/scripting.c b/arm9/source/utils/scripting.c index 41ebe07a1..cce9c7082 100644 --- a/arm9/source/utils/scripting.c +++ b/arm9/source/utils/scripting.c @@ -18,6 +18,7 @@ #include "ips.h" #include "bps.h" #include "pxi.h" +#include "utf.h" #define _MAX_ARGS 4 @@ -56,6 +57,7 @@ #define TV_NLIN_DISP (SCREEN_HEIGHT / (FONT_HEIGHT_EXT + (2*TV_VPAD))) #define TV_LLEN_DISP (((SCREEN_WIDTH_TOP - (2*TV_HPAD)) / FONT_WIDTH_EXT) - (TV_LNOS + 1)) +#define MAX_CHAR_SIZE 4 // Max number of bytes needed for a UTF-8 character. // some useful macros #define IS_WHITESPACE(c) ((c == ' ') || (c == '\t') || (c == '\r') || (c == '\n')) @@ -205,6 +207,10 @@ static const Gm9ScriptCmd cmd_list[] = { { CMD_ID_BKPT , "bkpt" , 0, 0 } }; +// off-screen string indicators +static const char al_str[] = "<< "; +static const char ar_str[] = " >>"; + // global vars for preview static u32 preview_mode = 0; // 0 -> off 1 -> quick 2 -> full static u32 script_color_active = 0; @@ -259,86 +265,125 @@ static inline u32 hexntostr(const u8* hex, char* str, u32 len) { return len; } -static inline u32 line_len(const char* text, u32 len, u32 ww, const char* line, char** eol) { - u32 last = ((text + len) - line); +static inline bool is_crlf(const char* str) { + u32 crlf = 0, lf = 0; + do if (str[0] == '\n') ++lf; else if (str[0] == '\r' && str[1] == '\n') ++crlf, ++str; + while (*str++); + return crlf > lf; +} + +static inline bool is_newline(const char* chr) { + return chr[0] == '\n' || (chr[0] == '\r' && chr[1] == '\n'); +} + +static inline u32 bytes_in_chars_u32(const char* str, u32 nchars) { + u32 bytes = 0; + for (u32 i = 0; str[bytes] && i < nchars; bytes += GetCharSize(str + bytes), ++i); + return bytes; +} + +static inline int bytes_in_chars_int(const char* str, int nchars) { + int bytes = 0; + for (int i = 0; str[bytes] && i < nchars; bytes += GetCharSize(str + bytes), ++i); + return bytes; +} + +static inline int chars_in_bytes(const char* str, int nbytes) { + int chars = 0; + for (int i = 0; str[chars] && i < nbytes; i += GetCharSize(str + i), ++chars); + return chars; +} + +static inline u32 chars_between_pointers(const char* start, const char* end) { + u32 chars = 0; + for (const char* i = start; *i && i < end; IncChar(&i), ++chars); + return chars; +} + +static inline u32 line_len_chars(const char* text, u32 len, u32 ww, const char* line, const char** eol) { u32 llen = 0; - char* lf = NULL; - char* spc = NULL; + const char* lf = NULL; + const char* spc = NULL; + u32 spc_len = 0; + const char* lptr = line; - if (line >= (text + len)) + if (line > text + len) return 0; // early exit // search line feeds, spaces (only relevant for wordwrapped) - for (llen = 0; !ww || (llen < ww); llen++) { - if (ww && (line[llen] == ' ')) spc = (char*) (line + llen); - if (!line[llen] || (line[llen] == '\n') || (llen >= last)) { - lf = (char*) (line + llen); + for (; !ww || (llen < ww); IncChar(&lptr), ++llen) { + if (ww && (*lptr == ' ')) { + spc = lptr; + spc_len = llen; + } + if (is_newline(lptr) || lptr >= text + len) { + lf = lptr; break; } } // line feed found, truncate trailing "empty" chars // for wordwrapped, stop line after last space (if any) - if (lf) for (; (llen > 0) && (line[llen-1] <= ' '); llen--); - else if (ww && spc) llen = (spc - line) + 1; + if (lf) for (; lptr > line && *GetPrevChar(lptr) < ' '; DecChar(&lptr), --llen); + else if (ww && spc) llen = spc_len + 1; // signal eol if required if (eol) *eol = lf; return llen; } -static inline char* line_seek(const char* text, u32 len, u32 ww, const char* line, int add) { - // safety checks / - if (line < text) return NULL; - if ((line >= (text + len)) && (add >= 0)) return (char*) line; +static inline const char* line_seek_chars(const char* text, u32 len, u32 ww, const char* line, int add) { + // safety check + if ((line <= text && add <= 0) || (line >= text + len && add >= 0)) return line; + + const char* l0 = line; if (!ww) { // non wordwrapped mode - char* lf = ((char*) line - 1); + for (; add < 0 && l0 > text; add++) + for (DecChar(&l0); l0 > text && l0[-1] != '\n'; DecChar(&l0)); - // ensure we are at the start of the line - while ((lf > text) && (*lf != '\n')) lf--; + for (; add > 0 && l0 < text + len; add--) + for (IncChar(&l0); l0 < text + len && l0[-1] != '\n'; IncChar(&l0)); - // handle backwards search - for (; (add < 0) && (lf >= text); add++) - for (lf--; (lf >= text) && (*lf != '\n'); lf--); - - // handle forwards search - for (; (add > 0) && (lf < text + len); add--) - for (lf++; (lf < text + len) && (*lf != '\n'); lf++); - - return lf + 1; + return l0; } else { // wordwrapped mode - char* l0 = (char*) line; - // handle forwards wordwrapped search - for (; (add > 0) && (l0 < text + len); add--) { - char* eol = NULL; - u32 llenww = line_len(text, len, ww, l0, &eol); - if (eol || !llenww) l0 = line_seek(text, len, 0, l0, 1); - else l0 += llenww; + for (; add > 0 && l0 < text + len; add--) { + const char* eol = NULL; + u32 llenww_chars = line_len_chars(text, len, ww, l0, &eol); + if (eol || !llenww_chars) l0 = line_seek_chars(text, len, 0, l0, 1); + else l0 += bytes_in_chars_u32(l0, llenww_chars); } // handle backwards wordwrapped search - while ((add < 0) && (l0 > text)) { - char* l1 = line_seek(text, len, 0, l0, -1); - char* l0_minus1 = l1; + while (add < 0 && l0 > text) { + const char* l1 = line_seek_chars(text, len, 0, l0, -1); + if (l0 > text + len) { + l0 = text + len; + if (l1 == l0) ++add; + } + const char* l0_minus1 = l1; int nlww = 0; // no of wordwrapped lines in paragraph - for (char* ld = l1; ld < l0; ld = line_seek(text, len, ww, ld, 1), nlww++) + for (const char* ld = l1; ld < l0; ld = line_seek_chars(text, len, ww, ld, 1), nlww++) l0_minus1 = ld; + if (line > text + len && l0 == text + len && chars_in_bytes(l0_minus1, l0 - l0_minus1) == (int) ww) ++add; if (add + nlww < 0) { // haven't reached the desired line yet add += nlww; l0 = l1; } else { // reached the desired line - l0 = (add == -1) ? l0_minus1 : line_seek(text, len, ww, l1, nlww + add); + l0 = (add == -1) ? l0_minus1 : line_seek_chars(text, len, ww, l1, nlww + add); add = 0; } } - return l0; } } +static inline const char* line_start(const char* text, u32 len, u32 ww, const char* ptr) { + return line_seek_chars(text, len, ww, GetNextChar(ptr), -1); +} + static inline u32 get_lno(const char* text, u32 len, const char* line) { u32 lno = 1; @@ -1614,7 +1659,7 @@ bool run_line(const char* line_start, const char* line_end, u32* flags, char* er // checks for illegal ASCII symbols bool ValidateText(const char* text, u32 len) { - if (!len) return false; + if (!len) return true; for (u32 i = 0; i < len; i++) { char c = text[i]; if ((c == '\r') && ((i+1) < len) && (text[i+1] != '\n')) return false; // CR without LF @@ -1624,57 +1669,89 @@ bool ValidateText(const char* text, u32 len) { return true; } -void MemTextView(const char* text, u32 len, char* line0, int off_disp, int lno, u32 ww, u32 mno, bool is_script) { +void MemTextView(const char* text, u32 len, const char* line0, int off_disp_chars, int lno, u32 ww, u32 mno, bool is_script, const char* cursor) { // block placements - const char* al_str = "<< "; - const char* ar_str = " >>"; u32 x_txt = (TV_LNOS >= 0) ? TV_HPAD + ((TV_LNOS+1)*FONT_WIDTH_EXT) : TV_HPAD; u32 x_lno = TV_HPAD; - u32 p_al = 0; - u32 p_ar = TV_LLEN_DISP - strlen(ar_str); - u32 x_al = x_txt + (p_al * FONT_WIDTH_EXT); - u32 x_ar = x_txt + (p_ar * FONT_WIDTH_EXT); + u32 x_al = x_txt; + u32 x_ar = x_txt + ((TV_LLEN_DISP - strlen(ar_str)) * FONT_WIDTH_EXT); // display text on screen - char txtstr[TV_LLEN_DISP + 1]; - char* ptr = line0; + char txtstr[TV_LLEN_DISP * MAX_CHAR_SIZE + 1]; + const char* ptr = line0; u32 nln = lno; + bool last_empty_line_drawn = false; for (u32 y = TV_VPAD; y < SCREEN_HEIGHT; y += FONT_HEIGHT_EXT + (2*TV_VPAD)) { - char* ptr_next = line_seek(text, len, ww, ptr, 1); - u32 llen = line_len(text, len, ww, ptr, NULL); - u32 ncpy = ((int) llen < off_disp) ? 0 : (llen - off_disp); - if (ncpy > TV_LLEN_DISP) ncpy = TV_LLEN_DISP; - bool al = !ww && off_disp && (ptr != ptr_next); - bool ar = !ww && (llen > off_disp + TV_LLEN_DISP); + int off_disp_bytes = bytes_in_chars_int(ptr, off_disp_chars); + const char* ptr_next = line_seek_chars(text, len, ww, ptr, 1); + u32 llen_chars = line_len_chars(text, len, ww, ptr, NULL); + u32 llen_bytes = bytes_in_chars_u32(ptr, llen_chars); + u32 tv_llen_disp_chars = llen_chars; + if (tv_llen_disp_chars > TV_LLEN_DISP) tv_llen_disp_chars = TV_LLEN_DISP; + u32 tv_llen_disp_bytes = bytes_in_chars_u32(ptr + off_disp_bytes, tv_llen_disp_chars); + u32 ncpy_bytes = ((int) llen_bytes < off_disp_bytes) ? 0 : (llen_bytes - off_disp_bytes); + if (ncpy_bytes > tv_llen_disp_bytes) ncpy_bytes = tv_llen_disp_bytes; + bool al = !ww && off_disp_chars && (ptr != ptr_next); + bool ar = !ww && (llen_chars + 1 > off_disp_chars + TV_LLEN_DISP); // set text color / find start of comment of scripts u32 color_text = (nln == mno) ? script_color_active : (is_script) ? script_color_code : (u32) COLOR_TVTEXT; - int cmt_start = TV_LLEN_DISP; // start of comment in current displayed line (may be negative) + int cmt_start_bytes = TV_LLEN_DISP; // start of comment in current displayed line (may be negative) + int cmt_start_chars = 0; if (is_script && (nln != mno)) { - char* hash = line_seek(text, len, 0, ptr, 0); - for (; *hash != '#' && (hash - ptr < (int) llen); hash++); - cmt_start = (hash - ptr) - off_disp; + const char* hash = line_start(text, len, 0, ptr); + for (; *hash != '#' && hash - ptr < (int) llen_bytes; hash++); + cmt_start_bytes = hash - (ptr + off_disp_bytes); + if (cmt_start_bytes <= 0) color_text = script_color_comment; + else cmt_start_chars = chars_in_bytes(ptr + off_disp_bytes, cmt_start_bytes); } - if (cmt_start <= 0) color_text = script_color_comment; // build text string - snprintf(txtstr, sizeof(txtstr), "%-*.*s", (int) TV_LLEN_DISP, (int) TV_LLEN_DISP, ""); - if (ncpy) memcpy(txtstr, ptr + off_disp, ncpy); + snprintf(txtstr, sizeof(txtstr), "%-*.*s", (int) (TV_LLEN_DISP * MAX_CHAR_SIZE), (int) (TV_LLEN_DISP * MAX_CHAR_SIZE), ""); + if (ncpy_bytes) { + memcpy(txtstr, ptr + off_disp_bytes, ncpy_bytes); + } for (char* d = txtstr; *d; d++) if (*d < ' ') *d = ' '; - if (al) memcpy(txtstr + p_al, al_str, strlen(al_str)); - if (ar) memcpy(txtstr + p_ar, ar_str, strlen(ar_str)); + if (ar) { + char* textstr_end = txtstr + ncpy_bytes; + u32 txtstr_ar_bytes = 0; + for (int txtstr_ar_i = 0; txtstr_ar_i < (int) strlen(ar_str); txtstr_ar_bytes += GetPrevCharSize(textstr_end - txtstr_ar_bytes), ++txtstr_ar_i); + memcpy(textstr_end - txtstr_ar_bytes, ar_str, sizeof(ar_str)); + } + if (al) { + u32 txtstr_al_bytes = bytes_in_chars_u32(txtstr, strlen(al_str)); + memmove(txtstr + strlen(al_str), txtstr + txtstr_al_bytes, sizeof(txtstr) - txtstr_al_bytes); + memcpy(txtstr, al_str, strlen(al_str)); + } // draw line number & text DrawString(TOP_SCREEN, txtstr, x_txt, y, color_text, COLOR_STD_BG); if (TV_LNOS > 0) { // line number - if (ptr != ptr_next) - DrawStringF(TOP_SCREEN, x_lno, y, ((ptr == text) || (*(ptr-1) == '\n')) ? COLOR_TVOFFS : COLOR_TVOFFSL, COLOR_STD_BG, "%0*lu", TV_LNOS, nln); + bool prev_ww_line_full = ww && ww == chars_between_pointers(line_seek_chars(text, len, ww, ptr, -1), ptr); + bool last_line_empty = (ptr == text + len && (!len || ptr[-1] == '\n' || prev_ww_line_full) && !last_empty_line_drawn); + if (ptr != ptr_next || last_line_empty) { + DrawStringF(TOP_SCREEN, x_lno, y, ((ptr == text) || (ptr[-1] == '\n')) ? COLOR_TVOFFS : COLOR_TVOFFSL, COLOR_STD_BG, "%0*lu", TV_LNOS, nln); + if (last_line_empty) last_empty_line_drawn = true; + } else DrawStringF(TOP_SCREEN, x_lno, y, COLOR_TVOFFSL, COLOR_STD_BG, "%*.*s", TV_LNOS, TV_LNOS, " "); } + if (cursor) { + u32 cursor_line_offset_chars = chars_between_pointers(ptr + off_disp_bytes, cursor); + if (cursor >= ptr + off_disp_bytes && cursor <= ptr + off_disp_bytes + ncpy_bytes && cursor_line_offset_chars < TV_LLEN_DISP + && (cursor != ptr + off_disp_bytes + ncpy_bytes || is_newline(cursor) || cursor == text + len)) { + DrawRectangle(TOP_SCREEN, x_txt + cursor_line_offset_chars * FONT_WIDTH_EXT, y, FONT_WIDTH_EXT, 1, COLOR_RED); + DrawRectangle(TOP_SCREEN, x_txt + (cursor_line_offset_chars + 1) * FONT_WIDTH_EXT - ((cursor_line_offset_chars == TV_LLEN_DISP - 1) ? 1 : 0), y, 1, FONT_HEIGHT_EXT, COLOR_RED); + DrawRectangle(TOP_SCREEN, x_txt + cursor_line_offset_chars * FONT_WIDTH_EXT, y, 1, FONT_HEIGHT_EXT, COLOR_RED); + DrawRectangle(TOP_SCREEN, x_txt + cursor_line_offset_chars * FONT_WIDTH_EXT, y + FONT_HEIGHT_EXT - 1, FONT_WIDTH_EXT, 1, COLOR_RED); + cursor = NULL; // prevent cursor from being drawn multiple times at the end of a file + } + } + // colorize comment if is_script - if ((cmt_start > 0) && ((u32) cmt_start < TV_LLEN_DISP)) { - memset(txtstr, ' ', cmt_start); + if (cmt_start_chars > 0 && cmt_start_chars < (int) TV_LLEN_DISP) { + memset(txtstr, ' ', cmt_start_bytes); + memmove(txtstr + cmt_start_chars, txtstr + cmt_start_bytes, sizeof(txtstr) - cmt_start_bytes); DrawString(TOP_SCREEN, txtstr, x_txt, y, script_color_comment, COLOR_TRANSPARENT); } @@ -1683,12 +1760,12 @@ void MemTextView(const char* text, u32 len, char* line0, int off_disp, int lno, if (ar) DrawStringF(TOP_SCREEN, x_ar, y, COLOR_TVOFFS, COLOR_TRANSPARENT, "%s", ar_str); // advance pointer / line number - for (char* c = ptr; c < ptr_next; c++) if (*c == '\n') ++nln; + for (const char* c = ptr; c < ptr_next; c++) if (*c == '\n') ++nln; ptr = ptr_next; } } -bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script) { +bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script, u32 max_len, const char* save_path) { u32 ww = TV_LLEN_DISP; // check if this really is text @@ -1697,11 +1774,37 @@ bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script) { return false; } - // clear screens - ClearScreenF(true, true, COLOR_STD_BG); + static const char keys_ml_alphabet[] = { SWKBD_KEYS_ML_ALPHABET }; + static const char keys_special[] = { SWKBD_KEYS_SPECIAL }; + static const char keys_numpad[] = { SWKBD_KEYS_NUMPAD }; + static const u8 layout_ml_alphabet[] = { SWKBD_LAYOUT_ML_ALPHABET }; + static const u8 layout_special[] = { SWKBD_LAYOUT_SPECIAL }; + static const u8 layout_numpad[] = { SWKBD_LAYOUT_NUMPAD }; + TouchBox swkbd_alphabet[64]; + TouchBox swkbd_special[32]; + TouchBox swkbd_numpad[32]; + + u32 uppercase = 0; + TouchBox* swkbd = NULL; + TouchBox* swkbd_prev = NULL; - // instructions - ShowString("%s", STR_TEXTVIEWER_CONTROLS_DETAILS); + // generate keyboards + if (!BuildKeyboard(swkbd_alphabet, keys_ml_alphabet, layout_ml_alphabet, true)) return false; + if (!BuildKeyboard(swkbd_special, keys_special, layout_special, true)) return false; + if (!BuildKeyboard(swkbd_numpad, keys_numpad, layout_numpad, true)) return false; + + char* text_cpy = NULL; + u32 text_cpy_len = 0; + if (max_len) { + // create a copy to check for changes against on exit + text_cpy = malloc(len + 1); + text_cpy_len = len; + if (!text_cpy) return false; + memcpy(text_cpy, text, len + 1); + } + + // clear screens + ClearScreen(TOP_SCREEN, COLOR_STD_BG); // set script colors if (as_script) { @@ -1710,56 +1813,181 @@ bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script) { script_color_code = COLOR_TVCMD; } - // find maximum line len - u32 llen_max = 0; - for (char* ptr = (char*) text; ptr < (text + len); ptr = line_seek(text, len, 0, ptr, 1)) { - u32 llen = line_len(text, len, 0, ptr, NULL); - if (llen > llen_max) llen_max = llen; - } - - // find last allowed lines (ww and nonww) - char* llast_nww = line_seek(text, len, 0, text + len, -TV_NLIN_DISP); - char* llast_ww = line_seek(text, len, TV_LLEN_DISP, text + len, -TV_NLIN_DISP); - - char* line0 = (char*) text; - int lcurr = 1; - int off_disp = 0; - for (; lcurr < (int) start; line0 = line_seek(text, len, 0, line0, 1), lcurr++); + bool crlf = is_crlf(text); + bool display_view_instructions = true; + const char* cursor = NULL; + const char* line0 = text; + int lcurr = 1; // Current line number + int off_disp_chars = 0; // non-word-wrapped offset + for (; lcurr < (int) start; line0 = line_seek_chars(text, len, 0, line0, 1), lcurr++); while (true) { // display text on screen - MemTextView(text, len, line0, off_disp, lcurr, ww, 0, as_script); + MemTextView(text, len, line0, off_disp_chars, lcurr, ww, 0, as_script, cursor); - // handle user input - u32 pad_state = InputWait(0); - char* line0_next = line0; - u32 step_ud = (pad_state & BUTTON_R1) ? TV_NLIN_DISP : 1; - u32 step_lr = (pad_state & BUTTON_R1) ? TV_LLEN_DISP : 1; - bool switched = (pad_state & BUTTON_R1); - if (pad_state & BUTTON_DOWN) line0_next = line_seek(text, len, ww, line0, step_ud); - else if (pad_state & BUTTON_UP) line0_next = line_seek(text, len, ww, line0, -step_ud); - else if (pad_state & BUTTON_RIGHT) off_disp += step_lr; - else if (pad_state & BUTTON_LEFT) off_disp -= step_lr; - else if (switched && (pad_state & BUTTON_X)) { - u64 lnext64 = ShowNumberPrompt(lcurr, STR_CURRENT_LINE_N_ENTER_NEW_LINE_BELOW, lcurr); - if (lnext64 && (lnext64 != (u64) -1)) line0_next = line_seek(text, len, 0, line0, (int) lnext64 - lcurr); - ShowString("%s", STR_TEXTVIEWER_CONTROLS_DETAILS); - } else if (switched && (pad_state & BUTTON_Y)) { - ww = ww ? 0 : TV_LLEN_DISP; - line0_next = line_seek(text, len, ww, line0, 0); - } else if (pad_state & (BUTTON_B|BUTTON_START)) break; + const char* line0_next = line0; + + if (!cursor) { // view mode + if (display_view_instructions) { + ClearScreen(BOT_SCREEN, COLOR_STD_BG); + ShowString("%s", max_len ? STR_TEXTEDITOR_CONTROLS_DETAILS : STR_TEXTVIEWER_CONTROLS_DETAILS); + display_view_instructions = false; + } + + // handle user input + u32 pad_state = InputWait(0); + u32 step_ud = (pad_state & BUTTON_R1) ? TV_NLIN_DISP : 1; + u32 step_lr = (pad_state & BUTTON_R1) ? TV_LLEN_DISP : 1; + bool switched = (pad_state & BUTTON_R1); + if (pad_state & BUTTON_DOWN) line0_next = line_seek_chars(text, len, ww, line0, step_ud); + else if (pad_state & BUTTON_UP) line0_next = line_seek_chars(text, len, ww, line0, -step_ud); + else if (pad_state & BUTTON_RIGHT) off_disp_chars += step_lr; + else if (pad_state & BUTTON_LEFT) off_disp_chars -= step_lr; + else if (max_len && pad_state & BUTTON_A) { + cursor = line0; + off_disp_chars = 0; + uppercase = 0; + swkbd = NULL; + swkbd_prev = NULL; + } + else if (switched && pad_state & BUTTON_X) { + u64 lnext64 = ShowNumberPrompt(lcurr, STR_CURRENT_LINE_N_ENTER_NEW_LINE_BELOW, lcurr); + if (lnext64 && (lnext64 != (u64) -1)) + line0_next = line_seek_chars(text, len, 0, line_start(text, len, 0, line0), (int) lnext64 - lcurr); + ShowString("%s", STR_TEXTVIEWER_CONTROLS_DETAILS); + } else if (switched && pad_state & BUTTON_Y) { + ww = ww ? 0 : TV_LLEN_DISP; + line0_next = line_start(text, len, ww, line0); + } else if (pad_state & (BUTTON_B|BUTTON_START)) break; + } else { // edit mode + char key_pressed = ShowMultiLineKeyboard(swkbd_alphabet, swkbd_special, swkbd_numpad, &swkbd, &swkbd_prev, &uppercase); + char key_character = 0; + bool switched = HID_ReadState() & BUTTON_R1; + if (key_pressed == KEY_ESCAPE) { + cursor = NULL; + display_view_instructions = true; + } else if (key_pressed == KEY_DOWN) { + const char* cursor_line_start = line_start(text, len, ww, cursor); + u32 cursor_chars_from_line_start = chars_between_pointers(cursor_line_start, cursor); + cursor = line_seek_chars(text, len, ww, cursor_line_start, switched ? TV_NLIN_DISP : 1); + cursor = line_start(text, len, ww, cursor); + const char* next_line_start = line_seek_chars(text, len, ww, cursor, 1); + for (u32 i = 0; GetNextChar(cursor) < next_line_start && i < cursor_chars_from_line_start; IncChar(&cursor), ++i); + } else if (key_pressed == KEY_UP) { + const char* cursor_line_start = line_start(text, len, ww, cursor); + u32 cursor_chars_from_line_start = chars_between_pointers(cursor_line_start, cursor); + cursor = line_seek_chars(text, len, ww, cursor_line_start, -(switched ? TV_NLIN_DISP : 1)); + const char* next_line_start = line_seek_chars(text, len, ww, cursor, 1); + for (u32 i = 0; GetNextChar(cursor) < next_line_start && i < cursor_chars_from_line_start; IncChar(&cursor), ++i); + } else if (key_pressed == KEY_RIGHT) { + if (switched) { + const char* cursor_line_start = line_start(text, len, ww, cursor); + const char* next_line_start = line_seek_chars(text, len, ww, cursor_line_start, 1); + if (next_line_start == text + len && (!ww || chars_between_pointers(cursor_line_start, next_line_start) != TV_LLEN_DISP)) IncChar(&next_line_start); + while (GetNextChar(cursor) < next_line_start && !is_newline(cursor)) IncChar(&cursor); + } + else if (cursor < text + len) IncChar(&cursor); + } else if (key_pressed == KEY_LEFT) { + if (switched) { + const char* cursor_line_start = line_start(text, len, ww, cursor); + while (cursor > cursor_line_start) DecChar(&cursor); + } + else if (cursor > text) DecChar(&cursor); + } else if (key_pressed == KEY_BKSPC) { + if (cursor > text) { + u32 size = GetPrevCharSize(cursor); + memmove((char *) cursor - size, cursor, text + len - cursor + 1); + len -= size; + cursor -= size; + } + } else if (key_pressed == KEY_UNICODE) { + if (cursor >= text + 4 && cursor <= text + len) { + u16 codepoint = 0; + for (const char *c = cursor - 4; c < cursor; c++) { + if ((*c >= '0' && *c <= '9') || (*c >= 'A' && *c <= 'F') || (*c >= 'a' && *c <= 'f')) { + codepoint <<= 4; + codepoint |= *c - (*c <= '9' ? '0' : ((*c <= 'F' ? 'A' : 'a') - 10)); + } else { + codepoint = 0; + break; + } + } + + if (codepoint != 0) { + char character[5] = {0}; + u16 input[2] = {codepoint, 0}; + utf16_to_utf8((u8*) character, input, 4, 1); + + u32 char_size = GetCharSize(character); + memmove((char *) cursor - 4 + char_size, cursor, text + len - cursor + 1); + memcpy((char *) cursor - 4, character, char_size); + cursor -= 4 - char_size; + len -= 4 - char_size; + } + } + } else if (key_pressed == KEY_ENTER) key_character = crlf ? '\r' : '\n'; + else if (key_pressed < 0x80) key_character = key_pressed; + + if (key_character && len + (key_character == '\r' ? 1 : 0) < max_len) { + if (uppercase == 1) { + uppercase = 0; + } + memmove((char *) cursor + 1, cursor, text + len++ - cursor + 1); + *((char *) cursor++) = key_character; + if (key_character == '\r') { + memmove((char *) cursor + 1, cursor, text + len++ - cursor + 1); + *((char *) cursor++) = '\n'; + } + } + + if (cursor && !ww) { + const char* cursor_line_start = line_start(text, len, ww, cursor); + u32 cursor_chars_from_line_start = chars_between_pointers(cursor_line_start, cursor); + if (cursor_chars_from_line_start < off_disp_chars + strlen(al_str)) off_disp_chars = cursor_chars_from_line_start - strlen(al_str); + if (cursor_chars_from_line_start >= off_disp_chars + TV_LLEN_DISP - strlen(ar_str)) off_disp_chars = cursor_chars_from_line_start + strlen(ar_str) - TV_LLEN_DISP + 1; + } + while (cursor && cursor < line0_next) line0_next = line_seek_chars(text, len, ww, line0_next, -1); + while (cursor && line0_next < line_seek_chars(text, len, ww, GetNextChar(cursor), -TV_NLIN_DISP)) line0_next = line_seek_chars(text, len, ww, line0_next, 1); + } + + // find last allowed lines (ww and nonww) + const char* llast_nww = line_seek_chars(text, len, 0, text + len + 1, -TV_NLIN_DISP); + const char* llast_ww = line_seek_chars(text, len, TV_LLEN_DISP, text + len + 1, -TV_NLIN_DISP); // check for problems, apply changes if (!ww && (line0_next > llast_nww)) line0_next = llast_nww; else if (ww && (line0_next > llast_ww)) line0_next = llast_ww; if (line0_next < line0) { // fix line number for decrease - do if (*(--line0) == '\n') lcurr--; + do { + DecChar(&line0); + if (is_newline(line0)) lcurr--; + } while (line0 > line0_next); } else { // fix line number for increase / same - for (; line0_next > line0; line0++) - if (*line0 == '\n') lcurr++; + for (; line0_next > line0; IncChar(&line0)) if (is_newline(line0)) lcurr++; + } + + // find maximum line length + u32 llen_max = 0; + for (const char* ptr = text; ptr < text + len; ptr = line_seek_chars(text, len, 0, ptr, 1)) { + u32 llen = line_len_chars(text, len, 0, ptr, NULL) + 1; + if (llen > llen_max) llen_max = llen; } - if (off_disp + TV_LLEN_DISP > llen_max) off_disp = llen_max - TV_LLEN_DISP; - if ((off_disp < 0) || ww) off_disp = 0; + + if (off_disp_chars + TV_LLEN_DISP > llen_max) off_disp_chars = llen_max - TV_LLEN_DISP; + if (off_disp_chars < 0 || ww) off_disp_chars = 0; + } + + // check for user edits + if (text_cpy) { + if (save_path) { + bool diffs = false; + if (len != text_cpy_len) diffs = true; + else for (u32 i = 0; i < len; ++i) if (text[i] != text_cpy[i]) { diffs = true; break; } + if (diffs && ShowPrompt(true, "%s", STR_TEXT_EDITS_SAVE_CHANGES) && !FileSetData(save_path, text, len, 0, true)) + ShowPrompt(false, "%s", STR_FAILED_WRITING_TO_FILE); + } + + free(text_cpy); } // clear screens @@ -1772,7 +2000,7 @@ bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script) { // (misses safety checks for wider compatibility) bool MemToCViewer(const char* text, u32 len, const char* title) { const u32 max_captions = 24; // we assume this is enough - char* captions[max_captions]; + const char* captions[max_captions]; u32 lineno[max_captions]; u32 ww = TV_LLEN_DISP; @@ -1784,13 +2012,13 @@ bool MemToCViewer(const char* text, u32 len, const char* title) { // clear screens / view start of readme on top ClearScreenF(true, true, COLOR_STD_BG); - MemTextView(text, len, (char*) text, 0, 1, ww, 0, false); + MemTextView(text, len, text, 0, 1, ww, 0, false, NULL); // parse text for markdown captions u32 n_captions = 0; - char* ptr = (char*) text; + const char* ptr = text; for (u32 lno = 1;; lno++) { - char* ptr_next = line_seek(text, len, 0, ptr, 1); + const char* ptr_next = line_seek_chars(text, len, 0, ptr, 1); if (ptr == ptr_next) break; if (*ptr == '#') { captions[n_captions] = ptr; @@ -1810,7 +2038,7 @@ bool MemToCViewer(const char* text, u32 len, const char* title) { y0 += 2 * (FONT_HEIGHT_EXT + (2*TV_VPAD)); for (u32 i = 0; (i < n_captions) && (y0 < SCREEN_HEIGHT); i++) { u32 text_color = ((int) i == cursor) ? COLOR_TVRUN : COLOR_TVTEXT; - char* caption = captions[i]; + const char* caption = captions[i]; u32 len = 0; u32 lvl = 0; for (; *caption == '#'; caption++, lvl++); @@ -1824,16 +2052,16 @@ bool MemToCViewer(const char* text, u32 len, const char* title) { // handle user input u32 pad_state = InputWait(0); if ((cursor >= 0) && (pad_state & BUTTON_A)) { - if (!MemTextViewer(text, len, lineno[cursor], false)) return false; - MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false); + if (!MemTextViewer(text, len, lineno[cursor], false, 0, NULL)) return false; + MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false, NULL); } else if (pad_state & BUTTON_B) { break; } else if (pad_state & BUTTON_UP) { cursor = (cursor <= 0) ? ((int) n_captions - 1) : cursor - 1; - MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false); + MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false, NULL); } else if (pad_state & BUTTON_DOWN) { if (++cursor >= (int) n_captions) cursor = 0; - MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false); + MemTextView(text, len, captions[cursor], 0, lineno[cursor], ww, 0, false, NULL); } } @@ -1846,18 +2074,20 @@ bool MemToCViewer(const char* text, u32 len, const char* title) { bool FileTextViewer(const char* path, bool as_script) { // load text file (completely into memory) // text file needs to fit inside the STD_BUFFER_SIZE - u32 flen, len; + size_t fileSize = FileGetSize(path); + if (fileSize >= STD_BUFFER_SIZE) { + ShowPrompt(false, STR_ERROR_TEXT_FILE_TOO_BIG, fileSize, STD_BUFFER_SIZE - 1); + return false; + } char* text = malloc(STD_BUFFER_SIZE); if (!text) return false; - flen = FileGetData(path, text, STD_BUFFER_SIZE - 1, 0); - + u32 flen = FileGetData(path, text, STD_BUFFER_SIZE - 1, 0); text[flen] = '\0'; - len = (ptrdiff_t)memchr(text, '\0', flen + 1) - (ptrdiff_t)text; // let MemTextViewer take over - bool result = MemTextViewer(text, len, 1, as_script); + bool result = MemTextViewer(text, flen, 1, as_script, STD_BUFFER_SIZE - 1, path); free(text); return result; @@ -1966,11 +2196,11 @@ bool ExecuteGM9Script(const char* path_script) { } if (show_preview) { if (lno <= (TV_NLIN_DISP/2)) { - MemTextView(script, script_size, script, 0, 1, 0, lno, true); + MemTextView(script, script_size, script, 0, 1, 0, lno, true, NULL); } else { - char* ptr_view = line_seek(script, script_size, 0, ptr, -(TV_NLIN_DISP/2)); + const char* ptr_view = line_seek_chars(script, script_size, 0, ptr, -(TV_NLIN_DISP/2)); u32 lno_view = lno - (TV_NLIN_DISP/2); - MemTextView(script, script_size, ptr_view, 0, lno_view, 0, lno, true); + MemTextView(script, script_size, ptr_view, 0, lno_view, 0, lno, true, NULL); } } } diff --git a/arm9/source/utils/scripting.h b/arm9/source/utils/scripting.h index ad1aa5267..9ee6da008 100644 --- a/arm9/source/utils/scripting.h +++ b/arm9/source/utils/scripting.h @@ -6,7 +6,7 @@ #define SCRIPT_MAX_SIZE STD_BUFFER_SIZE bool ValidateText(const char* text, u32 size); -bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script); +bool MemTextViewer(const char* text, u32 len, u32 start, bool as_script, u32 max_len, const char* save_path); bool MemToCViewer(const char* text, u32 len, const char* title); bool FileTextViewer(const char* path, bool as_script); bool ExecuteGM9Script(const char* path_script); diff --git a/resources/languages/source.json b/resources/languages/source.json index 79be425ff..dca43475c 100644 --- a/resources/languages/source.json +++ b/resources/languages/source.json @@ -189,7 +189,7 @@ "CALCULATE_SHA256": "Calculate SHA-256", "CALCULATE_SHA1": "Calculate SHA-1", "SHOW_FILE_INFO": "Show file info", - "SHOW_IN_TEXTVIEWER": "Show in Textviewer", + "SHOW_IN_TEXTVIEWER": "Show in Text Editor", "CALCULATE_CMAC": "Calculate CMAC", "COPY_TO_OUT": "Copy to %s", "DUMP_TO_OUT": "Dump to %s", @@ -771,7 +771,7 @@ "SCRIPTERR_APPLY_IPS_FAILD": "apply IPS failed", "SCRIPTERR_APPLY_BPS_FAILED": "apply BPS failed", "SCRIPTERR_APPLY_BPM_FAILED": "apply BPM failed", - "SCRIPTERR_TEXTVIEWER_FAILED": "textviewer failed", + "SCRIPTERR_TEXTVIEWER_FAILED": "text editor failed", "SCRIPTERR_BAD_DUMPSIZE": "bad dumpsize", "SCRIPTERR_CART_INIT_FAIL": "cart init fail", "SCRIPTERR_CART_DUMP_FAILED": "cart dump failed", @@ -785,7 +785,11 @@ "SCRIPTERR_UNCLOSED_CONDITIONAL": "unclosed conditional", "SCRIPTERR_ERROR_MESSAGE_FAIL": "error message fail", "ERROR_INVALID_TEXT_DATA": "Error: Invalid text data", + "ERROR_TEXT_FILE_TOO_BIG": "Error: Text file is too large.\nText file size is %u bytes.\nMax file size is %i bytes.", "TEXTVIEWER_CONTROLS_DETAILS": "Textviewer Controls:\n \n↑↓→←(+R) - Scroll\nR+Y - Toggle wordwrap\nR+X - Goto line #\nB - Exit\n", + "TEXTEDITOR_CONTROLS_DETAILS": "Text Editor Controls:\n \n↑↓→←(+R) - Scroll\nR+Y - Toggle wordwrap\nR+X - Goto line #\nA - Enter edit mode\nB - Exit\n", + "TEXTEDITOR_CONTROLS_KEYBOARD": "Text Editor Controls:\n \n↑↓→←(+R) - Move cursor\nY - Caps / Capslock\nX - Delete char\nA - Insert newline\nB - Enter view mode\n", + "TEXT_EDITS_SAVE_CHANGES": "You made text edits.\nWrite changes to file?", "CURRENT_LINE_N_ENTER_NEW_LINE_BELOW": "Current line: %i\nEnter new line below.", "PREVIEW_DISABLED": "(preview disabled)", "PATH_LINE_N_ERR_LINE": "%s\nline %lu: %s\n%s", diff --git a/resources/sample/HelloScript.gm9 b/resources/sample/HelloScript.gm9 index a78af5599..df715ccd5 100644 --- a/resources/sample/HelloScript.gm9 +++ b/resources/sample/HelloScript.gm9 @@ -312,7 +312,7 @@ verify S:/firm1.bin # applybpm 0:/example/patch.bpm 0:/data/originalfolder 0:/game/moddedfolder # 'textview' COMMAND -# This will show a text file on screen, in a dedicated text viewer. Size restrictions apply (max 1MiB) +# This will show a text file on screen, in a dedicated text editor. Size restrictions apply (max 1MiB) # textview 0:/sometext.txt # 'boot' COMMAND