diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index e4318605..56cb0281 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну" STR_HIDE_BATTERY: "Схаваць % батарэі" STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца" STR_TEXT_AA: "Згладжванне тэксту" -STR_SHORT_PWR_BTN: "Кароткае націсканне PWR" STR_ORIENTATION: "Арыентацыя чытання" -STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі" STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела" STR_FONT_FAMILY: "Шрыфт чытання" STR_FONT_SIZE: "Памер шрыфту інтэрфейсу" @@ -119,15 +117,10 @@ STR_CROP: "Абрэзаць" STR_NEVER: "Ніколі" STR_IN_READER: "У рэжыме чытання" STR_ALWAYS: "Заўсёды" -STR_IGNORE: "Ігнараваць" -STR_SLEEP: "Сон" -STR_PAGE_TURN: "Перагортванне" STR_PORTRAIT: "Партрэт" STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_INVERTED: "Інверсія" STR_LANDSCAPE_CCW: "Ландшафт (CCW)" -STR_PREV_NEXT: "Назад/Наперад" -STR_NEXT_PREV: "Наперад/Назад" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 614821a2..01c0d705 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -70,9 +70,7 @@ STR_IMAGES: "Imatges" STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_PLACEHOLDER: "Text de mostra" STR_IMAGES_SUPPRESS: "Suprimir" -STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_ORIENTATION: "Orientació de lectura" -STR_SIDE_BTN_LAYOUT: "Disposició botons laterals" STR_LONG_PRESS_SKIP: "Pressió llarga omet el capítol" STR_FONT_FAMILY: "Tipus de lletra" STR_FONT_SIZE: "Mida de la lletra (UI)" @@ -124,15 +122,10 @@ STR_CROP: "Retallar" STR_NEVER: "Mai" STR_IN_READER: "Al lector" STR_ALWAYS: "Sempre" -STR_IGNORE: "Ignora" -STR_SLEEP: "Dormir" -STR_PAGE_TURN: "Canvi de pàgina" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horitzontal horari" STR_INVERTED: "Invertit" STR_LANDSCAPE_CCW: "Horitzontal antihorari" -STR_PREV_NEXT: "Anterior/Següent" -STR_NEXT_PREV: "Següent/Anterior" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index b655a34f..5da37a60 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu" STR_HIDE_BATTERY: "Skrýt baterii %" STR_EXTRA_SPACING: "Extra mezery mezi odstavci" STR_TEXT_AA: "Vyhlazování textu" -STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení" STR_ORIENTATION: "Orientace čtení" -STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)" STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu" STR_FONT_FAMILY: "Rodina písem čtečky" STR_FONT_SIZE: "Velikost písma rozhraní" @@ -119,15 +117,10 @@ STR_CROP: "Oříznout" STR_NEVER: "Nikdy" STR_IN_READER: "Ve čtečce" STR_ALWAYS: "Vždy" -STR_IGNORE: "Ignorovat" -STR_SLEEP: "Spánek" -STR_PAGE_TURN: "Otáčení stránek" STR_PORTRAIT: "Na výšku" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_INVERTED: "Invertovaný" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" -STR_PREV_NEXT: "Předchozí/Další" -STR_NEXT_PREV: "Další/Předchozí" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 807d9831..9e540dca 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -70,9 +70,7 @@ STR_IMAGES: "Billeder" STR_IMAGES_DISPLAY: "Vis" STR_IMAGES_PLACEHOLDER: "Pladsholder" STR_IMAGES_SUPPRESS: "Skjul" -STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap" STR_ORIENTATION: "Læseretning" -STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)" STR_LONG_PRESS_SKIP: "Langt tryk spring kapitel over" STR_FONT_FAMILY: "Læser skrifttype" STR_FONT_SIZE: "Læser skriftstørrelse" @@ -124,15 +122,10 @@ STR_CROP: "Beskær" STR_NEVER: "Aldrig" STR_IN_READER: "I læseren" STR_ALWAYS: "Altid" -STR_IGNORE: "Ignorer" -STR_SLEEP: "Hvile" -STR_PAGE_TURN: "Sideskift" STR_PORTRAIT: "Portræt" STR_LANDSCAPE_CW: "Liggende med uret" STR_INVERTED: "Inverteret" STR_LANDSCAPE_CCW: "Liggende mod uret" -STR_PREV_NEXT: "Forrige/Næste" -STR_NEXT_PREV: "Næste/Forrige" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 73f6b510..55d3814f 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Gediffuseerde Bayer" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Maak fallback voor ongeldige inhoudsopgave" -STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop" STR_ORIENTATION: "Leesstand" -STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)" STR_LONG_PRESS_SKIP: "Hoofdstuk overslaan (lang indrukken)" STR_FONT_FAMILY: "Lettertype lezer" STR_FONT_SIZE: "Lettergrootte lezer" @@ -199,15 +197,10 @@ STR_CROP: "Bijsnijden" STR_NEVER: "Nooit" STR_IN_READER: "In lezer" STR_ALWAYS: "Altijd" -STR_IGNORE: "Negeren" -STR_SLEEP: "Slaap" -STR_PAGE_TURN: "Pagina omslaan" STR_PORTRAIT: "Staand" STR_LANDSCAPE_CW: "Liggend (rechtsom)" STR_INVERTED: "Omgekeerd" STR_LANDSCAPE_CCW: "Liggend (linksom)" -STR_PREV_NEXT: "Vorige/Volgende" -STR_NEXT_PREV: "Volgende/Vorige" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 8df785b2..5a929c21 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffused Bayer" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Create fallback for invalid TOC" -STR_SHORT_PWR_BTN: "Short Power Button Click" STR_ORIENTATION: "Reading Orientation" -STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)" STR_LONG_PRESS_SKIP: "Long-press Chapter Skip" STR_FONT_FAMILY: "Reader Font Family" STR_FONT_SIZE: "Reader Font Size" @@ -199,15 +197,10 @@ STR_CROP: "Crop" STR_NEVER: "Never" STR_IN_READER: "In Reader" STR_ALWAYS: "Always" -STR_IGNORE: "Ignore" -STR_SLEEP: "Sleep" -STR_PAGE_TURN: "Page Turn" STR_PORTRAIT: "Portrait" STR_LANDSCAPE_CW: "Landscape CW" STR_INVERTED: "Inverted" STR_LANDSCAPE_CCW: "Landscape CCW" -STR_PREV_NEXT: "Prev/Next" -STR_NEXT_PREV: "Next/Prev" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" @@ -547,3 +540,42 @@ STR_CAPTIVE_PORTAL_DETECTED: "Login Required" STR_CAPTIVE_PORTAL_HINT_1: "Network requires browser login. On another device," STR_CAPTIVE_PORTAL_HINT_2: "visit the URL below to authorize, then press OK." STR_CAPTIVE_PORTAL_DONE: "I'm authorized" +STR_MENU_BTN_ACTIONS: "Button Actions" +STR_MENU_BTN_PHYSICAL: "Physical Buttons" +STR_BTN_SHORT_PRESS: "Short Press" +STR_BTN_DOUBLE_PRESS: "Double Press" +STR_BTN_LONG_PRESS: "Long Press" +STR_BTN_BACK: "Back Button" +STR_BTN_CONFIRM: "Confirm Button" +STR_BTN_LEFT: "Left Button" +STR_BTN_RIGHT: "Right Button" +STR_BTN_PAGE_BACK: "Page Back Button" +STR_BTN_PAGE_FORWARD: "Page Forward Button" +STR_BTN_POWER: "Power Button" +STR_BTN_ACT_DEFAULT: "Default" +STR_BTN_DEF_IGNORE: "Default (ignore)" +STR_BTN_DEF_EXIT_READER: "Default (exit reader)" +STR_BTN_DEF_GO_HOME: "Default (go home)" +STR_BTN_DEF_READER_MENU: "Default (reader menu)" +STR_BTN_DEF_KOREADER_SYNC: "Default (KOReader sync)" +STR_BTN_DEF_PREV_PAGE: "Default (previous page)" +STR_BTN_DEF_NEXT_PAGE: "Default (next page)" +STR_BTN_DEF_CHAPTER_BACK: "Default (chapter back)" +STR_BTN_DEF_CHAPTER_FORWARD: "Default (chapter forward)" +STR_BTN_DEF_SLEEP: "Default (sleep)" +STR_BTN_ACT_PAGE_FORWARD: "Next Page" +STR_BTN_ACT_PAGE_BACK: "Previous Page" +STR_BTN_ACT_PAGE_FORWARD_10: "Skip 10 Pages Forward" +STR_BTN_ACT_PAGE_BACK_10: "Skip 10 Pages Back" +STR_BTN_ACT_GO_HOME: "Go Home" +STR_BTN_ACT_SLEEP: "Sleep" +STR_BTN_ACT_FORCE_REFRESH: "Refresh Screen" +STR_BTN_ACT_OPEN_TOC: "Open Table of Contents" +STR_BTN_ACT_OPEN_BOOKMARKS: "Open Bookmarks" +STR_BTN_ACT_STAR_PAGE: "Star Page" +STR_BTN_ACT_FOOTNOTES: "Footnotes" +STR_BTN_ACT_NEXT_SECTION: "Next Section / Chapter" +STR_BTN_ACT_PREV_SECTION: "Previous Section / Chapter" +STR_BTN_ACT_EXIT_READER: "Exit Reader" +STR_BTN_ACT_READER_MENU: "Reader Menu" +STR_BTN_ACT_KOREADER_SYNC: "KOReader Sync" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index e584d7a2..50923526 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -66,9 +66,7 @@ STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila" STR_HIDE_BATTERY: "Piilota akun %" STR_EXTRA_SPACING: "Kappaleiden lisäväli" STR_TEXT_AA: "Tekstin reunanpehmennys" -STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus" STR_ORIENTATION: "Lukusuunta" -STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)" STR_LONG_PRESS_SKIP: "Pitkä painallus: lukuhyppy" STR_FONT_FAMILY: "Lukijan fonttiperhe" STR_FONT_SIZE: "Käyttöliittymän fonttikoko" @@ -119,15 +117,10 @@ STR_CROP: "Rajaa" STR_NEVER: "Ei koskaan" STR_IN_READER: "Lukijassa" STR_ALWAYS: "Aina" -STR_IGNORE: "Ohita" -STR_SLEEP: "Lepotila" -STR_PAGE_TURN: "Sivunkääntö" STR_PORTRAIT: "Pysty" STR_LANDSCAPE_CW: "Vaaka myötäpäivään" STR_INVERTED: "Käännetty" STR_LANDSCAPE_CCW: "Vaaka vastapäivään" -STR_PREV_NEXT: "Edell/Seur" -STR_NEXT_PREV: "Seur/Edell" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 1622549e..529475a4 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -91,9 +91,7 @@ STR_IMAGES: "Images" STR_IMAGES_DISPLAY: "Affichage" STR_IMAGES_PLACEHOLDER: "Espace réservé" STR_IMAGES_SUPPRESS: "Masquer" -STR_SHORT_PWR_BTN: "Appui court alim." STR_ORIENTATION: "Orientation de lecture" -STR_SIDE_BTN_LAYOUT: "Boutons latéraux" STR_LONG_PRESS_SKIP: "Appui long saut de chapitre" STR_FONT_FAMILY: "Police de caractères du lecteur" STR_FONT_SIZE: "Taille texte interface" @@ -147,15 +145,10 @@ STR_CROP: "Rogné" STR_NEVER: "Jamais" STR_IN_READER: "Dans le lecteur" STR_ALWAYS: "Toujours" -STR_IGNORE: "Ignorer" -STR_SLEEP: "Mise en veille" -STR_PAGE_TURN: "Page suivante" STR_PORTRAIT: "Portrait" STR_LANDSCAPE_CW: "Paysage" STR_INVERTED: "Inversé" STR_LANDSCAPE_CCW: "Paysage inversé" -STR_PREV_NEXT: "Préc/Suiv" -STR_NEXT_PREV: "Suiv/Préc" STR_ABC: "abc" STR_FORCE_REFRESH: "Actualiser l'écran" STR_NEXT: "Suiv" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index d388acf6..883a77f7 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -79,9 +79,7 @@ STR_IMAGE_DITHERING: "Bild-Dithering" STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffundiertes Bayer" -STR_SHORT_PWR_BTN: "An-Taste kurz drücken" STR_ORIENTATION: "Leseausrichtung" -STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap." STR_FONT_FAMILY: "Lese-Schriftfamilie" STR_FONT_SIZE: "Schriftgröße" @@ -138,15 +136,10 @@ STR_CROP: "Zuschnitt" STR_NEVER: "Nie" STR_IN_READER: "Beim Lesen" STR_ALWAYS: "Immer" -STR_IGNORE: "Ignorieren" -STR_SLEEP: "Standby" -STR_PAGE_TURN: "Umblättern" STR_PORTRAIT: "Hochformat" STR_LANDSCAPE_CW: "Querformat rechts" STR_INVERTED: "Invertiert" STR_LANDSCAPE_CCW: "Querformat links" -STR_PREV_NEXT: "Zurück/Weiter" -STR_NEXT_PREV: "Weiter/Zurück" STR_ABC: "abc" STR_FORCE_REFRESH: "Bildschirm aktualisieren" STR_NEXT: "Weiter" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 70b0d8bf..a3090ed3 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -70,9 +70,7 @@ STR_IMAGES: "Képek" STR_IMAGES_DISPLAY: "Megjelenítés" STR_IMAGES_PLACEHOLDER: "Helyőrző" STR_IMAGES_SUPPRESS: "Elnyomás" -STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás" STR_ORIENTATION: "Olvasási irány" -STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)" STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás" STR_FONT_FAMILY: "Olvasó betűkészlet" STR_FONT_SIZE: "Olvasó betűméret" @@ -124,15 +122,10 @@ STR_CROP: "Körbevágás" STR_NEVER: "Soha" STR_IN_READER: "Olvasóban" STR_ALWAYS: "Mindig" -STR_IGNORE: "Mellőzés" -STR_SLEEP: "Alvás" -STR_PAGE_TURN: "Lapozás" STR_PORTRAIT: "Álló" STR_LANDSCAPE_CW: "Fekvő jobbra" STR_INVERTED: "Fordított" STR_LANDSCAPE_CCW: "Fekvő balra" -STR_PREV_NEXT: "Előző/Következő" -STR_NEXT_PREV: "Következő/Előző" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 4d531de3..add4d299 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -91,9 +91,7 @@ STR_IMAGES: "Immagini" STR_IMAGES_DISPLAY: "Visualizza" STR_IMAGES_PLACEHOLDER: "Segnaposto" STR_IMAGES_SUPPRESS: "Nascondi" -STR_SHORT_PWR_BTN: "Pressione breve tasto accensione" STR_ORIENTATION: "Orientamento lettura" -STR_SIDE_BTN_LAYOUT: "Pulsanti laterali (lettore)" STR_LONG_PRESS_SKIP: "Pressione lunga: salta capitolo" STR_FONT_FAMILY: "Font lettore" STR_FONT_SIZE: "Dimensione font lettore" @@ -147,15 +145,10 @@ STR_CROP: "Ritaglia" STR_NEVER: "Mai" STR_IN_READER: "Nel lettore" STR_ALWAYS: "Sempre" -STR_IGNORE: "Ignora" -STR_SLEEP: "Sospendi" -STR_PAGE_TURN: "Cambio pagina" STR_PORTRAIT: "Verticale" STR_LANDSCAPE_CW: "Orizzontale ↻" STR_INVERTED: "Invertito" STR_LANDSCAPE_CCW: "Orizzontale ↺" -STR_PREV_NEXT: "Prec/Succ" -STR_NEXT_PREV: "Succ/Prec" STR_ABC: "abc" STR_FORCE_REFRESH: "Aggiorna schermo" STR_NEXT: "Succ" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 66c07712..f6d87b8a 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -65,9 +65,7 @@ STR_SLEEP_COVER_MODE: "Ұйқы экраны мұқаба режимі" STR_HIDE_BATTERY: "Батарея % жасыру" STR_EXTRA_SPACING: "Қосымша абзац аралығы" STR_TEXT_AA: "Мәтін сырғытпасы" -STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу" STR_ORIENTATION: "Оқу бағдары" -STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)" STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу" STR_FONT_FAMILY: "Оқырман қаріп тобы" STR_FONT_SIZE: "Интерфейс қаріп өлшемі" @@ -118,15 +116,10 @@ STR_CROP: "Кесу" STR_NEVER: "Ешқашан" STR_IN_READER: "Оқырманда" STR_ALWAYS: "Әрқашан" -STR_IGNORE: "Елемеу" -STR_SLEEP: "Ұйқы" -STR_PAGE_TURN: "Бет аудару" STR_PORTRAIT: "Тік бағдар" STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)" STR_INVERTED: "Төңкерілген" STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)" -STR_PREV_NEXT: "Алдыңғы/Келесі" -STR_NEXT_PREV: "Келесі/Алдыңғы" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index bb26d143..29c457db 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -70,9 +70,7 @@ STR_IMAGES: "Paveikslėliai" STR_IMAGES_DISPLAY: "Rodyti" STR_IMAGES_PLACEHOLDER: "Vietaženklis" STR_IMAGES_SUPPRESS: "Slėpti" -STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp." STR_ORIENTATION: "Orientacija" -STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai" STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)" STR_FONT_FAMILY: "Šriftas" STR_FONT_SIZE: "Šrifto dydis" @@ -124,15 +122,10 @@ STR_CROP: "Kirpti" STR_NEVER: "Niekada" STR_IN_READER: "Skaitytuve" STR_ALWAYS: "Visada" -STR_IGNORE: "Nepaisyti" -STR_SLEEP: "Miegas" -STR_PAGE_TURN: "Versti psl." STR_PORTRAIT: "Stačias" STR_LANDSCAPE_CW: "Gulsčias (P)" STR_INVERTED: "Apverstas" STR_LANDSCAPE_CCW: "Gulsčias (A)" -STR_PREV_NEXT: "Atgal/Pirmyn" -STR_NEXT_PREV: "Pirmyn/Atgal" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "O. Dyslexic" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 1ef05908..a20c0fb8 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Dyfundowany Bayer" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Utwórz fallback dla nieprawidłowego spisu treści" -STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania" STR_ORIENTATION: "Układ czytania" -STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych" STR_LONG_PRESS_SKIP: "Przytrzymaj aby przeskoczyć rozdział" STR_FONT_FAMILY: "Czcionka" STR_FONT_SIZE: "Rozmiar czcionki" @@ -199,15 +197,10 @@ STR_CROP: "Przytnij" STR_NEVER: "Nigdy" STR_IN_READER: "W czytniku" STR_ALWAYS: "Zawsze" -STR_IGNORE: "Ignoruj" -STR_SLEEP: "Uśpienie" -STR_PAGE_TURN: "Nast. str." STR_PORTRAIT: "Pionowo" STR_LANDSCAPE_CW: "Poziomo P" STR_INVERTED: "Odwrócony" STR_LANDSCAPE_CCW: "Poziomo L" -STR_PREV_NEXT: "Poprz./Nast." -STR_NEXT_PREV: "Nast./Poprz." STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/portuguese_br.yaml b/lib/I18n/translations/portuguese_br.yaml index ca8bd7aa..366f1210 100644 --- a/lib/I18n/translations/portuguese_br.yaml +++ b/lib/I18n/translations/portuguese_br.yaml @@ -95,9 +95,7 @@ STR_IMAGE_DITHERING: "Dithering de imagem" STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer difuso" -STR_SHORT_PWR_BTN: "Clique curto no botão de ligar" STR_ORIENTATION: "Orientação de leitura" -STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais" STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa" STR_FONT_FAMILY: "Fonte do leitor" STR_FONT_SIZE: "Tam. da fonte da UI" @@ -151,15 +149,10 @@ STR_CROP: "Recortar" STR_NEVER: "Nunca" STR_IN_READER: "No leitor" STR_ALWAYS: "Sempre" -STR_IGNORE: "Ignorar" -STR_SLEEP: "Repouso" -STR_PAGE_TURN: "Virar página" STR_PORTRAIT: "Retrato" STR_LANDSCAPE_CW: "Paisagem H" STR_INVERTED: "Invertido" STR_LANDSCAPE_CCW: "Paisagem AH" -STR_PREV_NEXT: "Ant/Próx" -STR_NEXT_PREV: "Próx/Ant" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/portuguese_pt.yaml b/lib/I18n/translations/portuguese_pt.yaml index 973a1e0b..1394e0d9 100644 --- a/lib/I18n/translations/portuguese_pt.yaml +++ b/lib/I18n/translations/portuguese_pt.yaml @@ -87,9 +87,7 @@ STR_TEXT_AA: "Suavização do texto" STR_TEXT_DARKNESS: "Escuridão do texto" STR_EXTRA_DARK: "Extra escuro" STR_MAX_DARK: "Máximo" -STR_SHORT_PWR_BTN: "Pressão curta do botão de energia" STR_ORIENTATION: "Orientação de leitura" -STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais" STR_LONG_PRESS_SKIP: "Saltar capítulo com pressão longa" STR_FONT_FAMILY: "Tipo de letra do leitor" STR_FONT_SIZE: "Tamanho da letra do leitor" @@ -146,15 +144,10 @@ STR_CROP: "Recortar" STR_NEVER: "Nunca" STR_IN_READER: "No leitor" STR_ALWAYS: "Sempre" -STR_IGNORE: "Ignorar" -STR_SLEEP: "Repouso" -STR_PAGE_TURN: "Virar página" STR_PORTRAIT: "Retrato" STR_LANDSCAPE_CW: "Paisagem H" STR_INVERTED: "Invertido" STR_LANDSCAPE_CCW: "Paisagem AH" -STR_PREV_NEXT: "Ant./Próx." -STR_NEXT_PREV: "Próx./Ant." STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index b41caa60..4d935023 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -70,9 +70,7 @@ STR_IMAGES: "Imagini" STR_IMAGES_DISPLAY: "Afişare" STR_IMAGES_PLACEHOLDER: "Substituent" STR_IMAGES_SUPPRESS: "Suprimare" -STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător" STR_ORIENTATION: "Orientare lectură" -STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)" STR_LONG_PRESS_SKIP: "Sărire capitol la apăsare lungă" STR_FONT_FAMILY: "Familie font lectură" STR_FONT_SIZE: "Dimensiune font" @@ -124,15 +122,10 @@ STR_CROP: "Decupat" STR_NEVER: "Niciodată" STR_IN_READER: "În lectură" STR_ALWAYS: "Întotdeauna" -STR_IGNORE: "Ignoră" -STR_SLEEP: "Repaus" -STR_PAGE_TURN: "Răsfoire pagină" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Orizontal dreapta" STR_INVERTED: "Invers" STR_LANDSCAPE_CCW: "Orizontal stânga" -STR_PREV_NEXT: "Înainte/Înapoi" -STR_NEXT_PREV: "Înapoi/Înainte" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 8268e2ee..a8779835 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -88,9 +88,7 @@ STR_IMAGES: "Изображения" STR_IMAGES_DISPLAY: "Показать" STR_IMAGES_PLACEHOLDER: "Заглушки" STR_IMAGES_SUPPRESS: "Скрыть" -STR_SHORT_PWR_BTN: "Короткое нажатие PWR" STR_ORIENTATION: "Ориентация чтения" -STR_SIDE_BTN_LAYOUT: "Боковые кнопки" STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы" STR_FONT_FAMILY: "Шрифт чтения" STR_FONT_SIZE: "Размер шрифта интерфейса" @@ -144,15 +142,10 @@ STR_CROP: "Обрезать" STR_NEVER: "Никогда" STR_IN_READER: "В режиме чтения" STR_ALWAYS: "Всегда" -STR_IGNORE: "Игнорировать" -STR_SLEEP: "Сон" -STR_PAGE_TURN: "Перелистывание" STR_PORTRAIT: "Портрет" STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_INVERTED: "Инверсия" STR_LANDSCAPE_CCW: "Ландшафт (CCW)" -STR_PREV_NEXT: "Назад/Вперёд" -STR_NEXT_PREV: "Вперёд/Назад" STR_ABC: "abc" STR_FORCE_REFRESH: "Обновить экран" STR_NEXT: "Далее" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 5b9b753e..b8cf33f1 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -84,9 +84,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Difuzni Bayer" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Ustvari rezervo za neveljavno kazalo" -STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop" STR_ORIENTATION: "Orientacija branja" -STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov" STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja" STR_FONT_FAMILY: "Pisava bralnika" STR_FONT_SIZE: "Velikost pisave" @@ -181,15 +179,10 @@ STR_CROP: "Obreži" STR_NEVER: "Nikoli" STR_IN_READER: "V bralniku" STR_ALWAYS: "Vedno" -STR_IGNORE: "Prezri" -STR_SLEEP: "Spanje" -STR_PAGE_TURN: "Obračanje strani" STR_PORTRAIT: "Pokončno" STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" STR_INVERTED: "Obrnjeno" STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)" -STR_PREV_NEXT: "Nazaj/Naprej" -STR_NEXT_PREV: "Naprej/Nazaj" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 047f689e..397ca2e4 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -85,9 +85,7 @@ STR_IMAGES: "Imágenes" STR_IMAGES_DISPLAY: "Mostrar" STR_IMAGES_PLACEHOLDER: "Reemplazar" STR_IMAGES_SUPPRESS: "Ocultar" -STR_SHORT_PWR_BTN: "Toque corto botón encendido" STR_ORIENTATION: "Orientación" -STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)" STR_LONG_PRESS_SKIP: "Saltar capítulo (pulsación larga)" STR_FONT_FAMILY: "Tipografía" STR_FONT_SIZE: "Tamaño" @@ -141,15 +139,10 @@ STR_CROP: "Recortar" STR_NEVER: "Nunca" STR_IN_READER: "En el lector" STR_ALWAYS: "Siempre" -STR_IGNORE: "Ignorar" -STR_SLEEP: "Suspender" -STR_PAGE_TURN: "Pasar página" STR_PORTRAIT: "Vertical" STR_LANDSCAPE_CW: "Horizontal (horario)" STR_INVERTED: "Invertido" STR_LANDSCAPE_CCW: "Horizontal (antihorario)" -STR_PREV_NEXT: "Ant./Sig." -STR_NEXT_PREV: "Sig./Ant." STR_ABC: "abc" STR_FORCE_REFRESH: "Actualizar pantalla" STR_NEXT: "Sig" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index da6099a3..16155cd1 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -96,9 +96,7 @@ STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffus Bayer" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Skapa fallback för ogiltig innehållsförteckning" -STR_SHORT_PWR_BTN: "Kort strömknappsklick" STR_ORIENTATION: "Läsrikting" -STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)" STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning" STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj" STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek" @@ -199,15 +197,10 @@ STR_CROP: "Beskär" STR_NEVER: "Aldrig" STR_IN_READER: "I Eboksläsare" STR_ALWAYS: "Alltid" -STR_IGNORE: "Ignorera" -STR_SLEEP: "Vila" -STR_PAGE_TURN: "Sidvändning" STR_PORTRAIT: "Porträtt" STR_LANDSCAPE_CW: "Landskap medurs" STR_INVERTED: "Inverterad" STR_LANDSCAPE_CCW: "Landskap moturs" -STR_PREV_NEXT: "Förra/Nästa" -STR_NEXT_PREV: "Nästa/Förra" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Öppen Dyslexic" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index f23d2420..1b28ab40 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -65,9 +65,7 @@ STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu" STR_HIDE_BATTERY: "Pil Yüzdesini Gizle" STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu" STR_TEXT_AA: "Metin Yumuşatma (AA)" -STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması" STR_ORIENTATION: "Okuma Yönü" -STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_LONG_PRESS_SKIP: "Uzun Basışla Bölüm Atla" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" STR_FONT_SIZE: "Arayüz Yazı Boyutu" @@ -120,15 +118,10 @@ STR_CROP: "Kırp" STR_NEVER: "Asla" STR_IN_READER: "Okuyucuda" STR_ALWAYS: "Her Zaman" -STR_IGNORE: "Yoksay" -STR_SLEEP: "Uyku" -STR_PAGE_TURN: "Sayfa Çevirme" STR_PORTRAIT: "Dikey" STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" STR_INVERTED: "Ters" STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)" -STR_PREV_NEXT: "Önceki/Sonraki" -STR_NEXT_PREV: "Sonraki/Önceki" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index ef942ccb..0191fc5f 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -95,9 +95,7 @@ STR_IMAGE_DITHERING: "Дітеринг зображення" STR_IMAGE_DITHER_BAYER: "Bayer" STR_IMAGE_DITHER_ATKINSON: "Atkinson" STR_IMAGE_DITHER_DIFFUSED_BAYER: "Дифузний Bayer" -STR_SHORT_PWR_BTN: "Коротке натискання кнопки живлення" STR_ORIENTATION: "Орієнтація читання" -STR_SIDE_BTN_LAYOUT: "Розташування бічних кнопок (читач)" STR_LONG_PRESS_SKIP: "Пропуск розділу при довгому натисканні" STR_FONT_FAMILY: "Сімейство шрифтів" STR_FONT_SIZE: "Розмір шрифту інтерфейсу" @@ -151,15 +149,10 @@ STR_CROP: "Обрізати" STR_NEVER: "Ніколи" STR_IN_READER: "В читачі" STR_ALWAYS: "Завжди" -STR_IGNORE: "Ігнорувати" -STR_SLEEP: "Сон" -STR_PAGE_TURN: "Перегортання сторінки" STR_PORTRAIT: "Портрет" STR_LANDSCAPE_CW: "Альбомний за годинниковою" STR_INVERTED: "Перевернутий" STR_LANDSCAPE_CCW: "Альбомний проти годинникової" -STR_PREV_NEXT: "Попер/Наст" -STR_NEXT_PREV: "Наст/Попер" STR_BOOKERLY: "Bookerly" STR_NOTO_SANS: "Noto Sans" STR_OPEN_DYSLEXIC: "Open Dyslexic" diff --git a/src/ButtonEventManager.cpp b/src/ButtonEventManager.cpp new file mode 100644 index 00000000..af344e09 --- /dev/null +++ b/src/ButtonEventManager.cpp @@ -0,0 +1,117 @@ +#include "ButtonEventManager.h" + +#include "CrossPointSettings.h" + +// Required for constexpr array out-of-class definition (C++14). +constexpr ButtonEventManager::Button ButtonEventManager::ALL_BUTTONS[ButtonEventManager::NUM_BUTTONS]; + +bool ButtonEventManager::hasDoubleAction(const Button button) { + using BA = CrossPointSettings::BUTTON_ACTION; + switch (button) { + case Button::Back: + return SETTINGS.btnDoubleBack != BA::BTN_DEFAULT; + case Button::Confirm: + return SETTINGS.btnDoubleConfirm != BA::BTN_DEFAULT; + case Button::Left: + return SETTINGS.btnDoubleLeft != BA::BTN_DEFAULT; + case Button::Right: + return SETTINGS.btnDoubleRight != BA::BTN_DEFAULT; + case Button::PageBack: + return SETTINGS.btnDoublePageBack != BA::BTN_DEFAULT; + case Button::PageForward: + return SETTINGS.btnDoublePageForward != BA::BTN_DEFAULT; + case Button::Power: + return SETTINGS.btnDoublePower != BA::BTN_DEFAULT; + } + return false; +} + +void ButtonEventManager::pushEvent(const Button button, const PressType type) { + const int next = (eventTail + 1) % EVENT_BUF; + if (next == eventHead) return; // buffer full, drop oldest not possible — just drop newest + eventBuf[eventTail] = {button, type}; + eventTail = next; +} + +bool ButtonEventManager::consumeEvent(ButtonEvent& out) { + if (eventHead == eventTail) return false; + out = eventBuf[eventHead]; + eventHead = (eventHead + 1) % EVENT_BUF; + return true; +} + +void ButtonEventManager::drain() { + for (auto& b : buttons) { + b.state = State::Idle; + b.pressDownTime = 0; + b.releaseTime = 0; + } + eventHead = eventTail = 0; +} + +void ButtonEventManager::processButton(const int idx, const Button btn) { + PerButton& s = buttons[idx]; + const unsigned long now = millis(); + const bool pressed = input.wasPressed(btn); + const bool released = input.wasReleased(btn); + const bool held = input.isPressed(btn); + + switch (s.state) { + case State::Idle: + if (pressed) { + s.state = State::Pressed; + s.pressDownTime = now; + } + break; + + case State::Pressed: + if (released) { + const unsigned long heldMs = now - s.pressDownTime; + if (heldMs >= LONG_PRESS_MS) { + pushEvent(btn, PressType::Long); + s.state = State::Idle; + } else if (hasDoubleAction(btn)) { + // Delay short-press decision until double-click window expires + s.releaseTime = now; + s.state = State::ReleasedOnce; + } else { + // No double action configured — fire immediately + pushEvent(btn, PressType::Short); + s.state = State::Idle; + } + } else if (!held) { + // Button disappeared without wasReleased edge (e.g. after drain) — reset + s.state = State::Idle; + } + break; + + case State::ReleasedOnce: + if (pressed) { + // Second press within window — start tracking it + s.state = State::DoublePressed; + s.pressDownTime = now; + } else if (now - s.releaseTime >= DOUBLE_WINDOW_MS) { + // Window expired without a second press — it was a short press + pushEvent(btn, PressType::Short); + s.state = State::Idle; + } + break; + + case State::DoublePressed: + if (released) { + pushEvent(btn, PressType::Double); + s.state = State::Idle; + } else if (!held) { + // Disappeared without edge — treat as double anyway + pushEvent(btn, PressType::Double); + s.state = State::Idle; + } + break; + } +} + +void ButtonEventManager::update() { + for (int i = 0; i < NUM_BUTTONS; i++) { + processButton(i, ALL_BUTTONS[i]); + } +} diff --git a/src/ButtonEventManager.h b/src/ButtonEventManager.h new file mode 100644 index 00000000..59b7562d --- /dev/null +++ b/src/ButtonEventManager.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "MappedInputManager.h" + +// Forward declaration for the global accessor used by Activity.h. +// Defined in main.cpp alongside the ButtonEventManager instance. +class ButtonEventManager; +ButtonEventManager& globalButtonEvents(); + +// Classifies raw button edges into Short, Double, and Long press events. +// +// Per-button state machines run each loop() tick. The key latency rule: +// - If no double-click action is configured for a button, Short fires immediately +// on release (zero extra wait). +// - If a double-click action IS configured, Short is delayed by DOUBLE_WINDOW_MS +// to allow disambiguation. +// - Long fires on release once hold time >= LONG_PRESS_MS (no extra wait). +// - Double fires on the second release within DOUBLE_WINDOW_MS. +// +// Activities query consumeEvent() each loop tick to receive pending events. +// drain() resets all state machines — call it on activity transitions. + +class ButtonEventManager { + public: + using Button = MappedInputManager::Button; + + enum class PressType { Short, Double, Long }; + + struct ButtonEvent { + Button button; + PressType type; + }; + + // Timing constants (milliseconds) + static constexpr unsigned long LONG_PRESS_MS = 600; + static constexpr unsigned long DOUBLE_WINDOW_MS = 300; + + explicit ButtonEventManager(MappedInputManager& input) : input(input) {} + + // Call once per main loop tick, after MappedInputManager::update(). + void update(); + + // Returns the next pending event, or false if none. Call repeatedly until + // false to drain all events for this tick. + bool consumeEvent(ButtonEvent& out); + + // Reset all per-button FSMs. Call on activity transitions to prevent bleed-through. + void drain(); + + // Returns true if a double-click action is configured for this button. + // ButtonEventManager queries CrossPointSettings internally. + static bool hasDoubleAction(Button button); + + private: + static constexpr int NUM_BUTTONS = 7; + static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = { + Button::Back, Button::Confirm, Button::Left, Button::Right, Button::PageBack, Button::PageForward, Button::Power, + }; + + enum class State { Idle, Pressed, ReleasedOnce, DoublePressed }; + + struct PerButton { + State state = State::Idle; + unsigned long pressDownTime = 0; // when the current (or first) press started + unsigned long releaseTime = 0; // when the first release happened (for double-click window) + }; + + PerButton buttons[NUM_BUTTONS]; + + // Pending events ring buffer (small — at most one event per button per tick) + static constexpr int EVENT_BUF = 16; + ButtonEvent eventBuf[EVENT_BUF] = {}; + int eventHead = 0; + int eventTail = 0; + + MappedInputManager& input; + + void pushEvent(Button button, PressType type); + void processButton(int idx, Button btn); +}; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index aca87bdc..e6719399 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -140,7 +140,10 @@ bool CrossPointSettings::loadFromBinaryFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, extraParagraphSpacing); if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT); + { + uint8_t ignored; + serialization::readPod(inputFile, ignored); + } // legacy shortPwrBtn field if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT); // legacy if (++settingsRead >= fileSettingsCount) break; @@ -148,7 +151,10 @@ bool CrossPointSettings::loadFromBinaryFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; - readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); + { + uint8_t ignored; + serialization::readPod(inputFile, ignored); + } // legacy sideButtonLayout field if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT); if (++settingsRead >= fileSettingsCount) break; @@ -177,7 +183,10 @@ bool CrossPointSettings::loadFromBinaryFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, longPressChapterSkip); + { + uint8_t ignored; + serialization::readPod(inputFile, ignored); + } // was longPressChapterSkip if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); if (++settingsRead >= fileSettingsCount) break; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index fa9f3720..75212422 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -88,11 +88,6 @@ class CrossPointSettings { FRONT_BUTTON_HARDWARE_COUNT }; - // Side button layout options - // Default: Previous, Next - // Swapped: Next, Previous - enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT }; - // Font family options enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT }; // Font size options @@ -127,17 +122,6 @@ class CrossPointSettings { REFRESH_FREQUENCY_COUNT }; - // Short power button press actions - enum SHORT_PWRBTN { - IGNORE = 0, - SLEEP = 1, - PAGE_TURN = 2, - FORCE_REFRESH = 3, - FOOTNOTES = 4, - STAR_PAGE = 5, - SHORT_PWRBTN_COUNT - }; - // Hide battery percentage enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; @@ -214,14 +198,11 @@ class CrossPointSettings { // Text darkness (0 = normal, 1 = dark, 2 = extra dark). Default 1 preserves // historical AA rendering (both grayscale shades drawn in the MSB pass). uint8_t textDarkness = DARKNESS_DARK; - // Short power button click behaviour - uint8_t shortPwrBtn = IGNORE; // EPUB reading orientation settings // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise uint8_t orientation = PORTRAIT; // Button layouts (front layout retained for migration only) uint8_t frontButtonLayout = BACK_CONFIRM_LEFT_RIGHT; - uint8_t sideButtonLayout = PREV_NEXT; // Front button remap (logical -> hardware) // Used by MappedInputManager to translate logical buttons into physical front buttons. uint8_t frontButtonBack = FRONT_HW_BACK; @@ -249,8 +230,6 @@ class CrossPointSettings { char opdsPassword[64] = ""; // Hide battery percentage uint8_t hideBatteryPercentage = HIDE_NEVER; - // Long-press chapter skip on side buttons - uint8_t longPressChapterSkip = 1; // UI Theme uint8_t uiTheme = LYRA; // Sunlight fading compensation @@ -279,14 +258,62 @@ class CrossPointSettings { // Show the Weather home screen menu item (1 = enabled, 0 = hidden) uint8_t useWeather = 1; + // Configurable actions for short / double / long press on each logical button. + // BTN_DEFAULT means "use the button's normal built-in behaviour". + enum BUTTON_ACTION { + BTN_DEFAULT = 0, + BTN_PAGE_FORWARD, + BTN_PAGE_BACK, + BTN_PAGE_FORWARD_10, + BTN_PAGE_BACK_10, + BTN_GO_HOME, + BTN_SLEEP, + BTN_FORCE_REFRESH, + BTN_OPEN_TOC, + BTN_OPEN_BOOKMARKS, + BTN_STAR_PAGE, + BTN_FOOTNOTES, + BTN_NEXT_SECTION, + BTN_PREV_SECTION, + BTN_EXIT_READER, + BTN_READER_MENU, + BTN_KOREADER_SYNC, + BUTTON_ACTION_COUNT + }; + + // Short-press actions (default: built-in) + uint8_t btnShortBack = BTN_DEFAULT; + uint8_t btnShortConfirm = BTN_DEFAULT; + uint8_t btnShortLeft = BTN_DEFAULT; + uint8_t btnShortRight = BTN_DEFAULT; + uint8_t btnShortPageBack = BTN_DEFAULT; + uint8_t btnShortPageForward = BTN_DEFAULT; + uint8_t btnShortPower = BTN_DEFAULT; + + // Double-press actions (default: BTN_DEFAULT = disabled, no disambiguation wait) + uint8_t btnDoubleBack = BTN_DEFAULT; + uint8_t btnDoubleConfirm = BTN_DEFAULT; + uint8_t btnDoubleLeft = BTN_DEFAULT; + uint8_t btnDoubleRight = BTN_DEFAULT; + uint8_t btnDoublePageBack = BTN_DEFAULT; + uint8_t btnDoublePageForward = BTN_DEFAULT; + uint8_t btnDoublePower = BTN_DEFAULT; + + // Long-press actions (default: built-in) + uint8_t btnLongBack = BTN_DEFAULT; + uint8_t btnLongConfirm = BTN_DEFAULT; + uint8_t btnLongLeft = BTN_DEFAULT; + uint8_t btnLongRight = BTN_DEFAULT; + uint8_t btnLongPageBack = BTN_DEFAULT; + uint8_t btnLongPageForward = BTN_DEFAULT; + uint8_t btnLongPower = BTN_DEFAULT; + ~CrossPointSettings() = default; // Get singleton instance static CrossPointSettings& getInstance() { return instance; } - uint16_t getPowerButtonDuration() const { - return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400; - } + static constexpr uint16_t getPowerButtonDuration() { return 400; } int getReaderFontId() const; // If count_only is true, returns the number of settings items that would be written. diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 467c0f75..6679990c 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,55 +2,27 @@ #include "CrossPointSettings.h" -namespace { -using ButtonIndex = uint8_t; - -struct SideLayoutMap { - ButtonIndex pageBack; - ButtonIndex pageForward; -}; - -// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. -constexpr SideLayoutMap kSideLayouts[] = { - {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, - {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, -}; -} // namespace - bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { - const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); - const auto& side = kSideLayouts[sideLayout]; - switch (button) { case Button::Back: - // Logical Back maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonBack); case Button::Confirm: - // Logical Confirm maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonConfirm); case Button::Left: - // Logical Left maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonLeft); case Button::Right: - // Logical Right maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonRight); case Button::Up: - // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: - // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: - // Power button bypasses remapping. return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: - // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageBack); + return (gpio.*fn)(HalGPIO::BTN_UP); case Button::PageForward: - // Reader page navigation uses side buttons and can be swapped via settings. - return (gpio.*fn)(side.pageForward); + return (gpio.*fn)(HalGPIO::BTN_DOWN); } - return false; } diff --git a/src/SettingsList.h b/src/SettingsList.h index 03cb30f6..f106470d 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -130,15 +130,93 @@ inline const std::vector list = { "syntheticTocFallback", StrId::STR_CAT_READER) .withSubcategory(StrId::STR_MENU_READER_TWEAKS), - // --- Controls --- - SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, - {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), - SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", - StrId::STR_CAT_CONTROLS), - SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, - StrId::STR_FOOTNOTES, StrId::STR_STAR_PAGE}, - "shortPwrBtn", StrId::STR_CAT_CONTROLS), +// --- Controls --- +// --- Button Actions (short / double / long press per logical button) --- +// All entries share the same ordered action-label list; the submenu groups them behind +// a single placeholder row in the device UI. +// Shared action options (everything except the first "default" entry). +#define BTN_ACT_OPTIONS \ + StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, StrId::STR_BTN_ACT_PAGE_FORWARD_10, \ + StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, StrId::STR_BTN_ACT_SLEEP, \ + StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, StrId::STR_BTN_ACT_OPEN_BOOKMARKS, \ + StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, StrId::STR_BTN_ACT_NEXT_SECTION, \ + StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, StrId::STR_BTN_ACT_READER_MENU, \ + StrId::STR_BTN_ACT_KOREADER_SYNC + + // Back button: short=exit reader, double=ignore, long=go home + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, + {StrId::STR_BTN_DEF_EXIT_READER, BTN_ACT_OPTIONS}, "btnShortBack", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_MENU_BTN_ACTIONS) + .withSubmenu(StrId::STR_BTN_BACK), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_BACK), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongBack, + {StrId::STR_BTN_DEF_GO_HOME, BTN_ACT_OPTIONS}, "btnLongBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_BACK), + // Confirm button: short=reader menu, double=ignore, long=KOReader sync + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm, + {StrId::STR_BTN_DEF_READER_MENU, BTN_ACT_OPTIONS}, "btnShortConfirm", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_CONFIRM), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_CONFIRM), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongConfirm, + {StrId::STR_BTN_DEF_KOREADER_SYNC, BTN_ACT_OPTIONS}, "btnLongConfirm", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_CONFIRM), + // Left button: short=previous page, double=ignore, long=chapter back + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortLeft, + {StrId::STR_BTN_DEF_PREV_PAGE, BTN_ACT_OPTIONS}, "btnShortLeft", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_LEFT), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleLeft, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleLeft", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_LEFT), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongLeft, + {StrId::STR_BTN_DEF_CHAPTER_BACK, BTN_ACT_OPTIONS}, "btnLongLeft", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_LEFT), + // Right button: short=next page, double=ignore, long=chapter forward + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortRight, + {StrId::STR_BTN_DEF_NEXT_PAGE, BTN_ACT_OPTIONS}, "btnShortRight", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_RIGHT), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleRight, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleRight", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_RIGHT), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongRight, + {StrId::STR_BTN_DEF_CHAPTER_FORWARD, BTN_ACT_OPTIONS}, "btnLongRight", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_RIGHT), + // Page Back button: short=previous page, double=ignore, long=chapter back + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageBack, + {StrId::STR_BTN_DEF_PREV_PAGE, BTN_ACT_OPTIONS}, "btnShortPageBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_BACK), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageBack, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePageBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_BACK), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageBack, + {StrId::STR_BTN_DEF_CHAPTER_BACK, BTN_ACT_OPTIONS}, "btnLongPageBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_BACK), + // Page Forward button: short=next page, double=ignore, long=chapter forward + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageForward, + {StrId::STR_BTN_DEF_NEXT_PAGE, BTN_ACT_OPTIONS}, "btnShortPageForward", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_FORWARD), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageForward, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePageForward", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_FORWARD), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageForward, + {StrId::STR_BTN_DEF_CHAPTER_FORWARD, BTN_ACT_OPTIONS}, "btnLongPageForward", + StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_PAGE_FORWARD), + // Power button: short=ignore, double=ignore, long=sleep (via hold timer, not event system) + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPower, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnShortPower", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_POWER), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePower, + {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoublePower", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_POWER), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPower, {StrId::STR_BTN_DEF_SLEEP}, + "btnLongPower", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_BTN_POWER), + +#undef BTN_ACT_OPTIONS // --- System --- SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", diff --git a/src/activities/Activity.h b/src/activities/Activity.h index c5e4949e..2eebecea 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -8,6 +8,7 @@ #include "ActivityManager.h" // for using the ActivityManager singleton #include "ActivityResult.h" +#include "ButtonEventManager.h" #include "GfxRenderer.h" #include "MappedInputManager.h" #include "RenderLock.h" @@ -19,13 +20,14 @@ class Activity { std::string name; GfxRenderer& renderer; MappedInputManager& mappedInput; + ButtonEventManager& buttonEvents; ActivityResultHandler resultHandler; ActivityResult result; public: explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) - : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} + : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), buttonEvents(globalButtonEvents()) {} virtual ~Activity() = default; const std::string& getName() const { return name; } virtual void onEnter(); @@ -45,6 +47,11 @@ class Activity { virtual bool preventAutoSleep() { return false; } virtual bool isReaderActivity() const { return false; } + // Called by ActivityManager when a globally-configured button action targets the + // current activity. Override in reader activities to handle reader-specific actions. + // Non-reader activities can ignore this (default is no-op). + virtual void onButtonAction(CrossPointSettings::BUTTON_ACTION) {} + // Start a new activity without destroying the current one // Note: requestUpdate() will be invoked automatically once resultHandler finishes void startActivityForResult(std::unique_ptr&& activity, ActivityResultHandler resultHandler); diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index d81cb491..5fe41c90 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -154,6 +154,7 @@ void ActivityManager::loop() { // Arm input drain so the button that triggered the pop doesn't bleed into the // restored activity (or into a new activity the handler just pushed). drainInput = true; + buttonEvents.drain(); // Request an update to ensure the popped activity gets re-rendered if (pendingAction == PendingAction::None) { @@ -204,6 +205,7 @@ void ActivityManager::loop() { // Arm input drain so the button that triggered the transition doesn't bleed // into the new activity. drainInput = true; + buttonEvents.drain(); // onEnter may request another pending action, we will handle it in the next loop iteration continue; @@ -387,6 +389,12 @@ bool ActivityManager::isReaderActivity() const { return currentActivity && curre bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } +void ActivityManager::dispatchButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + if (currentActivity && currentActivity->isReaderActivity()) { + currentActivity->onButtonAction(action); + } +} + void ActivityManager::requestUpdate(bool immediate) { if (immediate) { if (renderTaskHandle) { diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 5ab9d04f..1d763206 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -9,6 +9,8 @@ #include #include +#include "ButtonEventManager.h" +#include "CrossPointSettings.h" #include "GfxRenderer.h" #include "MappedInputManager.h" @@ -52,6 +54,7 @@ class ActivityManager { protected: GfxRenderer& renderer; MappedInputManager& mappedInput; + ButtonEventManager& buttonEvents; std::vector> stackActivities; std::unique_ptr currentActivity; @@ -96,7 +99,10 @@ class ActivityManager { public: explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) - : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { + : renderer(renderer), + mappedInput(mappedInput), + buttonEvents(globalButtonEvents()), + renderingMutex(xSemaphoreCreateMutex()) { assert(renderingMutex != nullptr && "Failed to create rendering mutex"); stackActivities.reserve(10); } @@ -160,6 +166,11 @@ class ActivityManager { bool isReaderActivity() const; bool skipLoopDelay() const; + // Dispatch a globally-configured button action to the current activity. + // Reader-specific actions (page navigation, TOC, bookmarks, footnotes) are forwarded + // only when the current activity is a reader; others are no-ops in other contexts. + void dispatchButtonAction(CrossPointSettings::BUTTON_ACTION action); + // If immediate is true, the update will be triggered immediately. // Otherwise, it will be deferred until the end of the current loop iteration. void requestUpdate(bool immediate = false); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 83bcf765..96019f1b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -295,37 +295,6 @@ void EpubReaderActivity::loop() { return; } - const bool screenshotChordReleased = gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN); - - // Handle short power button press for footnotes - if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FOOTNOTES && - mappedInput.wasReleased(MappedInputManager::Button::Power) && !screenshotChordReleased) { - if (currentPageFootnotes.size() == 1) { - navigateToHref(currentPageFootnotes[0].href, true); - } else if (currentPageFootnotes.size() > 1) { - ReaderUtils::enforceExitFullRefresh(renderer); - startActivityForResult(std::make_unique(renderer, mappedInput, currentPageFootnotes), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& footnoteResult = std::get(result.data); - navigateToHref(footnoteResult.href, true); - } - requestUpdate(); - }); - } - return; - } - - // Star page toggle via short power button press - if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE && - mappedInput.wasReleased(MappedInputManager::Button::Power)) { - if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { - bookmarkStore.toggle(static_cast(currentSpineIndex), static_cast(section->currentPage)); - requestUpdate(); - } - return; - } - auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; @@ -343,7 +312,7 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; + const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs; // Chapter skip navigates by TOC entries, not spine boundaries. // Spine items without their own TOC entry inherit the previous spine's tocIndex @@ -1795,3 +1764,160 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf // No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay return true; } + +void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + switch (action) { + case BA::BTN_PAGE_FORWARD: + pageTurn(true); + break; + case BA::BTN_PAGE_BACK: + pageTurn(false); + break; + case BA::BTN_PAGE_FORWARD_10: + for (int i = 0; i < 10; i++) { + if (!stepPageState(true)) break; + } + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + for (int i = 0; i < 10; i++) { + if (!stepPageState(false)) break; + } + requestUpdate(); + break; + case BA::BTN_STAR_PAGE: + if (section) { + bookmarkStore.toggle(static_cast(currentSpineIndex), static_cast(section->currentPage)); + requestUpdate(); + } + break; + case BA::BTN_FOOTNOTES: + if (!currentPageFootnotes.empty()) { + if (currentPageFootnotes.size() == 1) { + navigateToHref(currentPageFootnotes[0].href, true); + } else { + startActivityForResult( + std::make_unique(renderer, mappedInput, currentPageFootnotes), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& footnoteResult = std::get(result.data); + navigateToHref(footnoteResult.href, true); + } + }); + } + } + break; + case BA::BTN_OPEN_TOC: + if (epub) { + const int spineIdx = currentSpineIndex; + const int tocIdx = section ? section->getTocIndexForPage(section->currentPage) + : epub->getTocIndexForSpineIndex(currentSpineIndex); + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult(std::make_unique(renderer, mappedInput, epub, + epub->getPath(), spineIdx, tocIdx), + [this](const ActivityResult& result) { + if (result.isCancelled) return; + RenderLock lock(*this); + const auto& chapter = std::get(result.data); + auto resolvedPage = + (chapter.tocIndex && chapter.spineIndex == currentSpineIndex && section) + ? section->getPageForTocIndex(*chapter.tocIndex) + : std::nullopt; + if (resolvedPage) { + section->currentPage = *resolvedPage; + } else { + pendingTocIndex = chapter.tocIndex; + currentSpineIndex = chapter.spineIndex; + nextPageNumber = 0; + section.reset(); + } + }); + } + break; + case BA::BTN_NEXT_SECTION: + case BA::BTN_PREV_SECTION: { + const bool forward = (action == BA::BTN_NEXT_SECTION); + { + RenderLock lock(*this); + if (section && section->pageCount > 0) { + const int curTocIndex = section->getTocIndexForPage(section->currentPage); + const int nextTocIndex = forward ? curTocIndex + 1 : curTocIndex - 1; + if (curTocIndex < 0) { + nextPageNumber = 0; + currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1; + section.reset(); + } else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) { + const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex); + if (newSpineIndex == currentSpineIndex) { + if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) { + section->currentPage = *resolvedPage; + } + } else { + pendingTocIndex = nextTocIndex; + nextPageNumber = 0; + currentSpineIndex = newSpineIndex; + section.reset(); + } + } else if (forward) { + nextPageNumber = 0; + currentSpineIndex = epub->getSpineItemsCount(); + section.reset(); + } else { + nextPageNumber = 0; + currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1; + section.reset(); + } + } else { + nextPageNumber = 0; + currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1; + section.reset(); + } + } + requestUpdate(); + break; + } + case BA::BTN_EXIT_READER: + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + break; + case BA::BTN_READER_MENU: + if (epub) { + const int currentPage = section ? section->currentPage + 1 : 0; + const int totalPages = section ? section->pageCount : 0; + float bookProgress = 0.0f; + if (epub->getBookSize() > 0 && section && section->pageCount > 0) { + const float chapterProgress = + static_cast(section->currentPage) / static_cast(section->pageCount); + bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; + } + const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); + const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast(currentSpineIndex), + static_cast(section->currentPage)); + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult( + std::make_unique( + renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, + SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, + bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness, + !bookmarkStore.isEmpty(), isCurrentPageStarred), + [this](const ActivityResult& result) { + const auto& menu = std::get(result.data); + applyOrientation(menu.orientation); + applyTextDarkness(menu.textDarkness); + toggleAutoPageTurn(menu.pageTurnOption); + applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride, + menu.fontSizeOverride); + if (!result.isCancelled) { + onReaderMenuConfirm(static_cast(menu.action)); + } + }); + } + break; + case BA::BTN_KOREADER_SYNC: + launchKOReaderSync(SyncLaunchMode::COMPARE); + break; + default: + break; + } +} diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 8185139f..42c8e332 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -188,6 +188,7 @@ class EpubReaderActivity final : public Activity { void loop() override; void render(RenderLock&& lock) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index 9773a950..5310a938 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -253,7 +253,7 @@ void MdReaderActivity::loop() { return; } - const bool headingSkip = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > HEADING_SKIP_MS; + const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS; if (headingSkip && !headings.empty()) { jumpToHeading(nextTriggered); return; @@ -892,4 +892,70 @@ void MdReaderActivity::savePageIndexCache() const { } LOG_DBG("MDR", "Saved page index cache: %d pages", totalPages); -} \ No newline at end of file +} + +void MdReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + auto clampPage = [this]() { + if (totalPages == 0) { + currentPage = 0; + return; + } + if (currentPage < 0) currentPage = 0; + if (currentPage >= totalPages) currentPage = totalPages - 1; + }; + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage < totalPages - 1) { + currentPage++; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage += 10; + clampPage(); + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage -= 10; + clampPage(); + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + break; + case BA::BTN_NEXT_SECTION: + jumpToHeading(true); + break; + case BA::BTN_PREV_SECTION: + jumpToHeading(false); + break; + case BA::BTN_OPEN_TOC: + if (!headings.empty()) { + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + startActivityForResult( + std::make_unique(renderer, mappedInput, headings, currentHeadingIndex), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + currentPage = std::get(result.data).page; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + }); + } + break; + case BA::BTN_EXIT_READER: + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + break; + default: + break; + } +} diff --git a/src/activities/reader/MdReaderActivity.h b/src/activities/reader/MdReaderActivity.h index 604fc532..c262e9c6 100644 --- a/src/activities/reader/MdReaderActivity.h +++ b/src/activities/reader/MdReaderActivity.h @@ -89,4 +89,5 @@ class MdReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; }; \ No newline at end of file diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 7eb88c32..1e326803 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -31,8 +31,8 @@ inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { // Suppresses input processing on activity entry until the user has released all buttons and a // clean frame (no pending press/release events) has been observed. Without this, the power-button -// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or, with -// longPressChapterSkip enabled, a chapter skip (the wake-hold easily exceeds skipChapterMs). +// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or chapter +// skip (the wake-hold easily exceeds skipChapterMs). // Each reader holds an instance, calls arm() in onEnter(), and calls shouldDrain() at the top // of loop() — returning early when it returns true. struct InputDrainGuard { @@ -62,17 +62,16 @@ struct PageTurnResult { }; inline PageTurnResult detectPageTurn(const MappedInputManager& input) { - const bool usePress = !SETTINGS.longPressChapterSkip; - const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || - input.wasPressed(MappedInputManager::Button::Left)) - : (input.wasReleased(MappedInputManager::Button::PageBack) || - input.wasReleased(MappedInputManager::Button::Left)); - const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - input.wasReleased(MappedInputManager::Button::Power); - const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn || - input.wasPressed(MappedInputManager::Button::Right)) - : (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || - input.wasReleased(MappedInputManager::Button::Right)); + // Only treat wasReleased as a page turn when the button's short-press action is default. + // Non-default short-press actions are dispatched by the global dispatcher in main.cpp; + // counting wasReleased as well would double-fire the action. + using BA = CrossPointSettings::BUTTON_ACTION; + const bool prev = + (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) || + (SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left)); + const bool next = + (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) || + (SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right)); return {prev, next}; } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index f3f5cdc8..14a88241 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -155,16 +155,6 @@ void TxtReaderActivity::loop() { return; } - // Star page toggle via short power button press - if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE && - mappedInput.wasReleased(MappedInputManager::Button::Power)) { - if (currentPage >= 0) { - bookmarkStore.toggle(0, static_cast(currentPage)); - } - requestUpdate(); - return; - } - // Open starred pages list via Confirm button if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) { ReaderUtils::enforceExitFullRefresh(renderer); @@ -782,3 +772,62 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx } return true; } + +void TxtReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + auto clampPage = [this]() { + if (currentPage < 0) currentPage = 0; + if (currentPage >= totalPages) currentPage = totalPages - 1; + }; + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage < totalPages - 1) { + currentPage++; + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage += 10; + clampPage(); + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage -= 10; + clampPage(); + requestUpdate(); + break; + case BA::BTN_STAR_PAGE: + bookmarkStore.toggle(0, static_cast(currentPage)); + requestUpdate(); + break; + case BA::BTN_OPEN_BOOKMARKS: + if (!bookmarkStore.isEmpty()) { + ReaderUtils::enforceExitFullRefresh(renderer); + startActivityForResult(std::make_unique(renderer, mappedInput, bookmarkStore), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& starred = std::get(result.data); + currentPage = starred.pageNumber; + requestUpdate(); + } + }); + } + break; + case BA::BTN_NEXT_SECTION: + case BA::BTN_PREV_SECTION: + // TXT files have no headings/chapters; treat as unsupported (no-op). + break; + case BA::BTN_EXIT_READER: + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + break; + default: + break; + } +} diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 03449fe7..60615e7a 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -58,6 +58,7 @@ class TxtReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index d5a631fc..d65bab38 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -92,19 +92,10 @@ void XtcReaderActivity::loop() { return; } - // When long-press chapter skip is disabled, turn pages on press instead of release. - const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; - const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left)); - const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power); - const bool nextTriggered = usePressForPageTurn - ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasPressed(MappedInputManager::Button::Right)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasReleased(MappedInputManager::Button::Right)); + const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || + mappedInput.wasReleased(MappedInputManager::Button::Right); if (!prevTriggered && !nextTriggered) { return; @@ -121,7 +112,7 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; + const bool skipPages = mappedInput.getHeldTime() > skipPageMs; const int skipAmount = skipPages ? 10 : 1; if (prevTriggered) { @@ -442,3 +433,61 @@ bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx free(pageBuffer); return true; } + +void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + if (!xtc) return; + const uint32_t pageCount = xtc->getPageCount(); + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage + 1 < pageCount) { + currentPage++; + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1; + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage = (currentPage >= 10) ? currentPage - 10 : 0; + requestUpdate(); + break; + case BA::BTN_NEXT_SECTION: + if (xtc->hasChapters()) { + const auto& chapters = xtc->getChapters(); + for (const auto& ch : chapters) { + if (ch.startPage > currentPage) { + currentPage = ch.startPage; + requestUpdate(); + break; + } + } + } + break; + case BA::BTN_PREV_SECTION: + if (xtc->hasChapters()) { + const auto& chapters = xtc->getChapters(); + for (int i = static_cast(chapters.size()) - 1; i >= 0; i--) { + if (chapters[i].startPage < currentPage) { + currentPage = chapters[i].startPage; + requestUpdate(); + break; + } + } + } + break; + case BA::BTN_EXIT_READER: + ReaderUtils::enforceExitFullRefresh(renderer); + finish(); + break; + default: + break; + } +} diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 46fcabf1..b4938a52 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -31,6 +31,7 @@ class XtcReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/activities/settings/SettingInfo.h b/src/activities/settings/SettingInfo.h index 77abbe13..489673ae 100644 --- a/src/activities/settings/SettingInfo.h +++ b/src/activities/settings/SettingInfo.h @@ -264,7 +264,9 @@ inline void SettingInfo::prepareSubmenus(std::vector& items, auto it = std::find_if(preparedSubmenus.begin(), preparedSubmenus.end(), [&item](const SubmenuData& d) { return d.id == item.submenu; }); if (it == preparedSubmenus.end()) { - preparedItems.push_back(SettingInfo::SubmenuEntry(item.submenu)); + auto placeholder = SettingInfo::SubmenuEntry(item.submenu); + placeholder.subcategory = item.subcategory; // inherit so addTo inserts the separator + preparedItems.push_back(std::move(placeholder)); preparedSubmenus.push_back({item.submenu, {}}); it = preparedSubmenus.end() - 1; } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 01020f65..b8e3ffba 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "CrossPointSettings.h" @@ -21,6 +23,7 @@ bool SettingsActivity::isListItemSelectable(int settingIdx) const { void SettingsActivity::onEnter() { Activity::onEnter(); + needsHalfRefresh = true; // Build per-category vectors from the shared settings list. // addTo tracks the last subcategory per vector and automatically inserts a separator @@ -78,6 +81,7 @@ void SettingsActivity::onEnter() { // Device-only ACTION items — subcategory drives separator insertion automatically. controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); + controlsSettings.insert(controlsSettings.begin(), SettingInfo::Separator(StrId::STR_MENU_BTN_PHYSICAL)); addToMoved(readerSettings, lastReaderSub, SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); @@ -215,11 +219,15 @@ void SettingsActivity::toggleCurrentSetting() { if (setting.type == SettingType::ACTION) { auto resultHandler = [this](const ActivityResult& result) { SETTINGS.saveToFile(); + needsHalfRefresh = true; const auto* menuResult = std::get_if(&result.data); if (menuResult && menuResult->action != -1) { auto activity = createActivityForAction(static_cast(menuResult->action), renderer, mappedInput); if (activity) { - startActivityForResult(std::move(activity), [this](const ActivityResult&) { SETTINGS.saveToFile(); }); + startActivityForResult(std::move(activity), [this](const ActivityResult&) { + SETTINGS.saveToFile(); + needsHalfRefresh = true; + }); } } }; @@ -277,6 +285,7 @@ void SettingsActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - // Always use standard refresh for settings screen - renderer.displayBuffer(); + const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh; + needsHalfRefresh = false; + renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH); } diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index a84114b9..9514ae86 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -25,6 +25,7 @@ class SettingsActivity final : public Activity { static const StrId categoryNames[categoryCount]; std::vector submenuData; + bool needsHalfRefresh = false; void enterCategory(int categoryIndex); void toggleCurrentSetting(); diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp index 0fc68ff0..1dc4c5e8 100644 --- a/src/activities/settings/SettingsSubmenuActivity.cpp +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -1,6 +1,8 @@ #include "SettingsSubmenuActivity.h" #include +#include +#include #include #include "CrossPointSettings.h" @@ -11,6 +13,7 @@ void SettingsSubmenuActivity::onEnter() { Activity::onEnter(); + needsHalfRefresh = true; initMenuList(); requestUpdate(); } @@ -63,5 +66,7 @@ void SettingsSubmenuActivity::render(RenderLock&&) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); + const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh; + needsHalfRefresh = false; + renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH); } diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h index 32618941..91b0f4b6 100644 --- a/src/activities/settings/SettingsSubmenuActivity.h +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -12,6 +12,7 @@ class SettingsSubmenuActivity final : public MenuListActivity { StrId titleId; std::function itemValueStringOverride; + bool needsHalfRefresh = false; // MenuListActivity overrides void onEnter() override; diff --git a/src/main.cpp b/src/main.cpp index e79a2bf0..f857808e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include +#include "ButtonEventManager.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "GlobalBookmarkIndex.h" @@ -33,6 +34,8 @@ #include "util/ScreenshotUtil.h" MappedInputManager mappedInputManager(gpio); +ButtonEventManager buttonEventManager(mappedInputManager); +ButtonEventManager& globalButtonEvents() { return buttonEventManager; } GfxRenderer renderer(display); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; @@ -284,6 +287,7 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); + buttonEventManager.update(); HalClock::updatePeriodic(); renderer.setFadingFix(SETTINGS.fadingFix); @@ -346,6 +350,8 @@ void loop() { // Track power button hold for sleep. We require a fresh press edge (wasPressed) // before starting to measure hold time, so that a hold carried over from boot // (wake-up press) is never misinterpreted as a "go to sleep" press. + // The power button long-press is not user-remappable, so this path always owns it. + // Sleep mapped to other buttons is handled by the dispatcher's BTN_SLEEP case below. static unsigned long powerHoldStart = 0; if (gpio.wasPressed(HalGPIO::BTN_POWER)) { powerHoldStart = millis(); @@ -366,14 +372,6 @@ void loop() { } } - // Refresh screen when power button is short-pressed with FORCE_REFRESH setting. - if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FORCE_REFRESH && - mappedInputManager.wasReleased(MappedInputManager::Button::Power)) { - LOG_DBG("MAIN", "Manual screen refresh triggered"); - RenderLock lock; - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - } - if (!gpio.isPressed(HalGPIO::BTN_POWER)) { powerHoldStart = 0; } @@ -384,6 +382,151 @@ void loop() { activityManager.requestUpdate(); } + // Dispatch globally-configured button actions before handing control to the activity. + // Only non-Default actions are intercepted here; Default falls through to the activity. + { + using BA = CrossPointSettings::BUTTON_ACTION; + using B = MappedInputManager::Button; + ButtonEventManager::ButtonEvent ev; + while (buttonEventManager.consumeEvent(ev)) { + auto actionFor = [&](B btn) -> uint8_t { + switch (btn) { + case B::Back: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortBack; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleBack; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongBack; + } + break; + case B::Confirm: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortConfirm; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleConfirm; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongConfirm; + } + break; + case B::Left: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortLeft; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleLeft; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongLeft; + } + break; + case B::Right: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortRight; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoubleRight; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongRight; + } + break; + case B::PageBack: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPageBack; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePageBack; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPageBack; + } + break; + case B::PageForward: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPageForward; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePageForward; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPageForward; + } + break; + case B::Power: + switch (ev.type) { + case ButtonEventManager::PressType::Short: + return SETTINGS.btnShortPower; + case ButtonEventManager::PressType::Double: + return SETTINGS.btnDoublePower; + case ButtonEventManager::PressType::Long: + return SETTINGS.btnLongPower; + } + break; + default: + break; // Up/Down have no FSMs — ButtonEventManager never emits these + } + return BA::BTN_DEFAULT; + }; + + const uint8_t action = actionFor(ev.button); + if (action == BA::BTN_DEFAULT) continue; + + switch (static_cast(action)) { + case BA::BTN_PAGE_FORWARD: + activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD); + break; + case BA::BTN_PAGE_BACK: + activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK); + break; + case BA::BTN_PAGE_FORWARD_10: + activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD_10); + break; + case BA::BTN_PAGE_BACK_10: + activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK_10); + break; + case BA::BTN_GO_HOME: + activityManager.goHome(); + break; + case BA::BTN_SLEEP: + activityManager.goToSleep(); + break; + case BA::BTN_FORCE_REFRESH: { + RenderLock lock; + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + break; + } + case BA::BTN_OPEN_TOC: + activityManager.dispatchButtonAction(BA::BTN_OPEN_TOC); + break; + case BA::BTN_OPEN_BOOKMARKS: + activityManager.goToGlobalBookmarks(); + break; + case BA::BTN_STAR_PAGE: + activityManager.dispatchButtonAction(BA::BTN_STAR_PAGE); + break; + case BA::BTN_FOOTNOTES: + activityManager.dispatchButtonAction(BA::BTN_FOOTNOTES); + break; + case BA::BTN_NEXT_SECTION: + activityManager.dispatchButtonAction(BA::BTN_NEXT_SECTION); + break; + case BA::BTN_PREV_SECTION: + activityManager.dispatchButtonAction(BA::BTN_PREV_SECTION); + break; + case BA::BTN_EXIT_READER: + activityManager.dispatchButtonAction(BA::BTN_EXIT_READER); + break; + case BA::BTN_READER_MENU: + activityManager.dispatchButtonAction(BA::BTN_READER_MENU); + break; + case BA::BTN_KOREADER_SYNC: + activityManager.dispatchButtonAction(BA::BTN_KOREADER_SYNC); + break; + default: + break; + } + } + } + const unsigned long activityStartTime = millis(); activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime;