From 0693688f7314a715319fdd555c7a64f9479d39c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 24 Jun 2026 01:42:12 -0300 Subject: [PATCH 1/9] Cache cursors in local sessions, this will restore last cursor position of a recently closed file. --- src/tools/ecode/ecode.cpp | 48 ++++++++++++++++++++++++++++++++++++--- src/tools/ecode/ecode.hpp | 7 ++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 6ee61776c..376bb3e83 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -2851,6 +2851,12 @@ bool App::loadFileFromPath( openFileFromPath( path ); } else { UITab* tab = mSplitter->isDocumentOpen( path ); + auto onLoadedRestoreState = [this, onLoaded]( UICodeEditor* codeEditor, + const std::string& path ) { + restoreClosedDocumentState( codeEditor, path ); + if ( onLoaded ) + onLoaded( codeEditor, path ); + }; if ( tab && tab->getOwnedWidget()->isType( UI_TYPE_CODEEDITOR ) ) { UICodeEditor* editor = tab->getOwnedWidget()->asType(); @@ -2863,9 +2869,9 @@ bool App::loadFileFromPath( } } else { if ( inNewTab ) { - mSplitter->loadAsyncFileFromPathInNewTab( path, onLoaded ); + mSplitter->loadAsyncFileFromPathInNewTab( path, onLoadedRestoreState ); } else { - mSplitter->loadAsyncFileFromPath( path, codeEditor, onLoaded ); + mSplitter->loadAsyncFileFromPath( path, codeEditor, onLoadedRestoreState ); } } } @@ -3180,13 +3186,14 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { updateNonUniqueTabTitles(); } ); - editor->on( Event::OnDocumentClosed, [this]( const Event* event ) { + editor->on( Event::OnDocumentClosed, [this, editor]( const Event* event ) { if ( !appInstance ) return; const DocEvent* docEvent = static_cast( event ); std::string dir( FileSystem::fileRemoveFileName( docEvent->getDoc()->getFilePath() ) ); if ( dir.empty() ) return; + rememberClosedDocumentState( editor ); mRecentClosedFiles.push( docEvent->getDoc()->getFilePath() ); mSettings->updatedReopenClosedFileState(); Lock l( mWatchesLock ); @@ -3458,6 +3465,40 @@ void App::reopenClosedTab() { loadFileFromPath( prevTabPath ); } +std::string App::closedDocumentStateKey( const std::string& path ) const { + if ( path.empty() ) + return {}; + return FileSystem::fileExists( path ) ? FileSystem::getRealPath( path ) : path; +} + +void App::rememberClosedDocumentState( UICodeEditor* editor ) { + if ( !editor || !editor->getDocument().hasFilepath() ) + return; + + std::string key( closedDocumentStateKey( editor->getDocument().getFilePath() ) ); + if ( key.empty() ) + return; + + mClosedDocumentState[key] = editor->getDocument().getSelections(); +} + +void App::restoreClosedDocumentState( UICodeEditor* editor, const std::string& path ) { + if ( !editor ) + return; + + std::string key( closedDocumentStateKey( path ) ); + if ( key.empty() ) + return; + + auto it = mClosedDocumentState.find( key ); + if ( it == mClosedDocumentState.end() ) + return; + + editor->getDocument().setSelection( it->second ); + editor->scrollToCursor(); + mClosedDocumentState.erase( it ); +} + void App::updateEditorState() { if ( mSplitter->curEditorExistsAndFocused() ) { updateEditorTitle( mSplitter->getCurEditor() ); @@ -4149,6 +4190,7 @@ void App::loadFolder( std::string path, bool forceNewWindow ) { mSplitter->removeTabWithOwnedWidgetId( "welcome_ecode" ); mStatusBar->setVisible( mConfig.ui.showStatusBar ); } + mClosedDocumentState.clear(); if ( !mProjectTreeView ) { showSidePanel( mConfig.ui.showSidePanel ); diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 36bdf514e..8209abb76 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -666,6 +666,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UITextView* mDocInfo{ nullptr }; std::vector mRecentFiles; std::stack mRecentClosedFiles; + std::unordered_map mClosedDocumentState; std::vector mRecentFolders; AppConfig mConfig; UISplitter* mProjectSplitter{ nullptr }; @@ -804,6 +805,12 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { void updateEditorState(); + std::string closedDocumentStateKey( const std::string& path ) const; + + void rememberClosedDocumentState( UICodeEditor* editor ); + + void restoreClosedDocumentState( UICodeEditor* editor, const std::string& path ); + void saveDoc(); void loadKeybindings(); From e720b708638ae403b2561c91bdb8d90cca9c3005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 25 Jun 2026 11:26:31 -0300 Subject: [PATCH 2/9] =?UTF-8?q?=E2=80=A2=20Fixed=20the=20keybinding=20merg?= =?UTF-8?q?e=20bug.=20The=20helper=20was=20restoring=20missing=20default?= =?UTF-8?q?=20commands=20=20=20by=20overwriting=20an=20occupied=20default?= =?UTF-8?q?=20shortcut,=20which=20treated=20user=20bindings=20like=20mod+d?= =?UTF-8?q?=3Dduplicate-line-or-selection=20as=20stale=20=20=20because=20t?= =?UTF-8?q?hat=20command=20is=20not=20in=20the=20default=20keybinding=20ta?= =?UTF-8?q?ble.=20Now=20it=20only=20adds=20a=20default=20when=20the=20defa?= =?UTF-8?q?ult=20shortcut=20is=20actually=20=20=20free.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a regression test in src/tests/unit_tests/uicodeeditor_tests.cpp:108 covering the reported case: mod+d reassigned, then an extra mod+e=show-markdown-preview binding exists. This was reported in SpartanJ/ecode#914. • Implemented the splitter-level focus state cache, it will remember closed documents cursor state when reopened and it will restore cursor state per-code editor and not per-document (SpartanJ/ecode#893). --- .../eepp/ui/tools/uicodeeditorsplitter.hpp | 11 +++ src/eepp/ui/tools/uicodeeditorsplitter.cpp | 52 +++++++++++- src/eepp/ui/uicodeeditor.cpp | 9 +- src/tests/unit_tests/uicodeeditor_tests.cpp | 85 +++++++++++++++++++ src/tools/ecode/appconfig.cpp | 4 + src/tools/ecode/appconfig.hpp | 1 + src/tools/ecode/ecode.cpp | 1 + src/tools/ecode/keybindingshelper.cpp | 48 ----------- src/tools/ecode/settingsmenu.cpp | 14 +++ 9 files changed, 172 insertions(+), 53 deletions(-) diff --git a/include/eepp/ui/tools/uicodeeditorsplitter.hpp b/include/eepp/ui/tools/uicodeeditorsplitter.hpp index 3bad13769..0a8148de2 100644 --- a/include/eepp/ui/tools/uicodeeditorsplitter.hpp +++ b/include/eepp/ui/tools/uicodeeditorsplitter.hpp @@ -9,6 +9,7 @@ #include #include +#include using namespace EE::UI::Doc; @@ -387,6 +388,10 @@ class EE_API UICodeEditorSplitter { bool openDocumentsInMainSplit() const { return mOpenDocumentsInMainSplit; } + void setRestoreEditorSelectionOnFocus( bool restore ); + + bool getRestoreEditorSelectionOnFocus() const { return mRestoreEditorSelectionOnFocus; } + UITabWidget* getFirstTabWidget() const; UITabWidget* getPreferredTabWidget() const; @@ -413,6 +418,7 @@ class EE_API UICodeEditorSplitter { bool mFirstCodeEditor{ true }; bool mVisualSplitting{ true }; bool mOpenDocumentsInMainSplit{ false }; + bool mRestoreEditorSelectionOnFocus{ true }; UICodeEditor* mAboutToAddEditor{ nullptr }; UIMessageBox* mTryCloseMsgBox{ nullptr }; Mutex mTabWidgetMutex; @@ -424,6 +430,7 @@ class EE_API UICodeEditorSplitter { size_t mNavigationHistoryMaxSize{ 100 }; std::vector mNavigationHistory; size_t mNavigationHistoryPos{ std::numeric_limits::max() }; + std::unordered_map mEditorSelections; std::function mOnTabWidgetCreateCb; Float mVisualSplitEdgePercent{ 0.1 }; TabTryCloseCallback mTabTryCloseCb; @@ -436,6 +443,10 @@ class EE_API UICodeEditorSplitter { virtual void onTabClosed( const TabEvent* tabEvent ); + void saveEditorSelection( UICodeEditor* editor ); + + void restoreEditorSelection( UICodeEditor* editor ); + void closeAllTabs( std::vector tabs, UITabWidget::FocusTabBehavior focusTabBehavior ); UITabWidget* createTabWidget( Node* parent ); diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index c87e8766c..1c36a7cd2 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -257,7 +257,18 @@ UICodeEditor* UICodeEditorSplitter::createCodeEditor() { /* Splitter commands */ editor->on( Event::OnFocus, [this]( const Event* event ) { - setCurrentWidget( event->getNode()->asType() ); + UICodeEditor* editor = event->getNode()->asType(); + UICodeEditor* prevEditor = mCurEditor; + if ( mRestoreEditorSelectionOnFocus && prevEditor && prevEditor != editor && + !prevEditor->hasFocus() ) + saveEditorSelection( prevEditor ); + setCurrentWidget( editor ); + if ( mRestoreEditorSelectionOnFocus && prevEditor && prevEditor != editor ) + restoreEditorSelection( editor ); + } ); + editor->on( Event::OnFocusLoss, [this]( const Event* event ) { + if ( mRestoreEditorSelectionOnFocus ) + saveEditorSelection( event->getNode()->asType() ); } ); editor->on( Event::OnTextChanged, [this]( const Event* event ) { mClient->onDocumentModified( event->getNode()->asType(), @@ -559,8 +570,9 @@ UICodeEditorSplitter::createCodeEditorInTabWidget( UITabWidget* tabWidget ) { UICodeEditor* editor = createCodeEditor(); mAboutToAddEditor = editor; editor->on( Event::OnDocumentChanged, [this]( const Event* event ) { - mClient->onDocumentStateChanged( event->getNode()->asType(), - event->getNode()->asType()->getDocument() ); + UICodeEditor* editor = event->getNode()->asType(); + mEditorSelections.erase( editor ); + mClient->onDocumentStateChanged( editor, editor->getDocument() ); } ); UITab* tab = tabWidget->add( editor->getDocument().getFilename(), editor ); editor->setData( (UintPtr)tab ); @@ -1716,6 +1728,38 @@ void UICodeEditorSplitter::clearNavigationHistory() { mNavigationHistoryPos = std::numeric_limits::max(); } +void UICodeEditorSplitter::setRestoreEditorSelectionOnFocus( bool restore ) { + if ( mRestoreEditorSelectionOnFocus == restore ) + return; + + mRestoreEditorSelectionOnFocus = restore; + if ( !mRestoreEditorSelectionOnFocus ) + mEditorSelections.clear(); +} + +void UICodeEditorSplitter::saveEditorSelection( UICodeEditor* editor ) { + if ( editor && editor->hasDocument() ) + mEditorSelections[editor] = editor->getDocument().getSelections(); +} + +void UICodeEditorSplitter::restoreEditorSelection( UICodeEditor* editor ) { + if ( !editor || !editor->hasDocument() ) + return; + + auto it = mEditorSelections.find( editor ); + if ( it == mEditorSelections.end() ) + return; + + TextRanges selection = editor->getDocument().sanitizeRange( it->second ); + if ( selection.empty() ) + return; + + // resetSelection() drops stale extra cursors; setSelection() updates the active cursor index. + editor->getDocument().resetSelection( selection ); + editor->getDocument().setSelection( selection ); + editor->scrollToCursor(); +} + std::shared_ptr UICodeEditorSplitter::getThreadPool() const { return mThreadPool; } @@ -1798,6 +1842,8 @@ void UICodeEditorSplitter::closeSplitter( UISplitter* splitter ) { void UICodeEditorSplitter::onTabClosed( const TabEvent* tabEvent ) { UIWidget* widget = tabEvent->getTab()->getOwnedWidget()->asType(); UITabWidget* tabWidget = tabEvent->getTab()->getTabWidget(); + if ( widget && widget->isType( UI_TYPE_CODEEDITOR ) ) + mEditorSelections.erase( widget->asType() ); if ( tabWidget->getTabCount() == 0 ) { UISplitter* splitter = splitterFromWidget( widget ); if ( splitter ) { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 5b151ec2c..8b97e0c73 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1605,6 +1605,11 @@ Uint32 UICodeEditor::onMouseDown( const Vector2i& position, const Uint32& flags if ( localPos.y < mPluginsTopSpace ) return UIWidget::onMouseDown( position, flags ); + const auto resetSelectionTo = [this]( const TextPosition& pos ) { + mDoc->resetSelection(); + mDoc->setSelection( pos ); + }; + bool downOverGutter = localPos.x < mPaddingPx.Left + getGutterWidth(); if ( flags & EE_BUTTON_LMASK ) { @@ -1620,14 +1625,14 @@ Uint32 UICodeEditor::onMouseDown( const Vector2i& position, const Uint32& flags return UIWidget::onMouseDown( position, flags ); if ( !downOverGutter || mAllowSelectingTextFromGutter ) - mDoc->setSelection( textScreenPos ); + resetSelectionTo( textScreenPos ); if ( downOverGutter && mAllowSelectingTextFromGutter ) mDoc->selectLine(); } } else if ( !downOverGutter && tryExecuteMouseBinding( shortcut ) ) { return UIWidget::onMouseDown( position, flags ); } else if ( !mDoc->hasSelection() ) { - mDoc->setSelection( textScreenPos ); + resetSelectionTo( textScreenPos ); } } else if ( !( flags & ( EE_BUTTON_LMASK | EE_BUTTON_RMASK ) ) ) { tryExecuteMouseBinding( shortcut ); diff --git a/src/tests/unit_tests/uicodeeditor_tests.cpp b/src/tests/unit_tests/uicodeeditor_tests.cpp index bb7fc7493..30527ba39 100644 --- a/src/tests/unit_tests/uicodeeditor_tests.cpp +++ b/src/tests/unit_tests/uicodeeditor_tests.cpp @@ -2,14 +2,18 @@ #include #include #include +#include #include #include #include +#include "../../tools/ecode/keybindingshelper.cpp" + using namespace EE; using namespace EE::UI; using namespace EE::UI::Doc; using namespace EE::Scene; +using namespace EE::System; static const std::string userCode = R"objcpp(#import "common.h" #import @@ -101,6 +105,87 @@ OF_APPLICATION_DELEGATE(test) } \ } +UTEST( KeybindingsHelper, PreservesUserShortcutWhenAddingBinding ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - KeybindingsHelper Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + const std::string path = Sys::getTempPath() + "eepp_keybindingshelper.cfg"; + const std::string statePath = Sys::getTempPath() + "eepp_keybindingshelper_state.cfg"; + FileSystem::fileRemove( path ); + FileSystem::fileRemove( statePath ); + + IniFile ini( path, false ); + IniFile iniState( statePath, false ); + const std::string group( "editor" ); + const std::string modD( "mod+d" ); + const std::string modX( "mod+x" ); + const std::string modE( "mod+e" ); + ini.setValue( group, modD, std::string( "duplicate-line-or-selection" ) ); + ini.setValue( group, modX, std::string( "cut" ) ); + ini.setValue( group, modE, std::string( "show-markdown-preview" ) ); + + std::unordered_map keybindings; + std::unordered_map invertedKeybindings; + const std::map defaultKeybindings{ + { { KEY_D, KeyMod::getDefaultModifier() }, "select-word" }, + { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, + }; + + ecode::KeybindingsHelper::updateKeybindings( ini, group, app.getWindow()->getInput(), + keybindings, invertedKeybindings, + defaultKeybindings, false, {}, iniState ); + + ASSERT_TRUE( keybindings.find( modD ) != keybindings.end() ); + const std::string savedModD = ini.getValue( group, modD, "" ); + EXPECT_STREQ( keybindings[modD].c_str(), "duplicate-line-or-selection" ); + EXPECT_STREQ( savedModD.c_str(), "duplicate-line-or-selection" ); + EXPECT_STREQ( invertedKeybindings["duplicate-line-or-selection"].c_str(), modD.c_str() ); + + FileSystem::fileRemove( path ); + FileSystem::fileRemove( statePath ); +} + +UTEST( KeybindingsHelper, RestoresMissingCommandWhenShortcutIsFree ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - KeybindingsHelper Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + const std::string path = Sys::getTempPath() + "eepp_keybindingshelper_restore.cfg"; + const std::string statePath = Sys::getTempPath() + "eepp_keybindingshelper_restore_state.cfg"; + FileSystem::fileRemove( path ); + FileSystem::fileRemove( statePath ); + + IniFile ini( path, false ); + IniFile iniState( statePath, false ); + const std::string group( "editor" ); + const std::string modD( "mod+d" ); + const std::string modX( "mod+x" ); + ini.setValue( group, modX, std::string( "cut" ) ); + + std::unordered_map keybindings; + std::unordered_map invertedKeybindings; + const std::map defaultKeybindings{ + { { KEY_D, KeyMod::getDefaultModifier() }, "select-word" }, + { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, + }; + + ecode::KeybindingsHelper::updateKeybindings( ini, group, app.getWindow()->getInput(), + keybindings, invertedKeybindings, + defaultKeybindings, false, {}, iniState ); + + ASSERT_TRUE( keybindings.find( modD ) != keybindings.end() ); + const std::string savedModD = ini.getValue( group, modD, "" ); + EXPECT_STREQ( keybindings[modD].c_str(), "select-word" ); + EXPECT_STREQ( savedModD.c_str(), "select-word" ); + EXPECT_STREQ( invertedKeybindings["select-word"].c_str(), modD.c_str() ); + + FileSystem::fileRemove( path ); + FileSystem::fileRemove( statePath ); +} + UTEST( UICodeEditor, DocumentViewStressTest ) { UIApplication app( WindowSettings( 800, 600, "eepp - Stress Test", WindowStyle::Default, diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index b31cfe148..4669a6024 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -196,6 +196,8 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, editor.hideTabBarOnSingleTab = ini.getValueB( "editor", "hide_tab_bar_on_single_tab", false ); editor.hideTabBar = ini.getValueB( "editor", "hide_tab_bar", false ); editor.tabSwitcher = ini.getValueB( "editor", "tab_switcher", false ); + editor.restoreEditorSelectionOnFocus = + ini.getValueB( "editor", "restore_editor_selection_on_focus", true ); editor.tabJumpMode = UITabWidget::tabJumpModefromString( ini.getValue( "editor", "tab_jump_mode", "linear" ) ); @@ -388,6 +390,8 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "editor", "hide_tab_bar_on_single_tab", editor.hideTabBarOnSingleTab ); ini.setValueB( "editor", "hide_tab_bar", editor.hideTabBar ); ini.setValueB( "editor", "tab_switcher", editor.tabSwitcher ); + ini.setValueB( "editor", "restore_editor_selection_on_focus", + editor.restoreEditorSelectionOnFocus ); ini.setValue( "editor", "tab_jump_mode", UITabWidget::tabJumpModeToString( editor.tabJumpMode ) ); ini.setValue( "editor", "new_tab_position", NewTabPosition::toString( editor.newTabPosition ) ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index af1f7889b..77ac3a468 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -118,6 +118,7 @@ struct CodeEditorConfig { bool hideTabBar{ false }; bool tabSwitcher{ false }; bool openDocumentsInMainSplit{ false }; + bool restoreEditorSelectionOnFocus{ true }; UITabWidget::TabJumpMode tabJumpMode{ UITabWidget::TabJumpMode::Linear }; NewTabPosition::Position newTabPosition{ NewTabPosition::Last }; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 376bb3e83..1f956b912 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -4966,6 +4966,7 @@ void App::init( InitParameters& params ) { mSplitter->setHideTabBarOnSingleTab( mConfig.editor.hideTabBarOnSingleTab ); mSplitter->setHideTabBar( mConfig.editor.hideTabBar ); mSplitter->setOpenDocumentsInMainSplit( mConfig.editor.openDocumentsInMainSplit ); + mSplitter->setRestoreEditorSelectionOnFocus( mConfig.editor.restoreEditorSelectionOnFocus ); mSplitter->setOnTabWidgetCreateCb( [this]( UITabWidget* tabWidget ) { tabWidget->getTabBar()->onDoubleClick( [this]( const MouseEvent* ) { mSplitter->createEditorInNewTab(); } ); diff --git a/src/tools/ecode/keybindingshelper.cpp b/src/tools/ecode/keybindingshelper.cpp index 940539806..bff72cc1c 100644 --- a/src/tools/ecode/keybindingshelper.cpp +++ b/src/tools/ecode/keybindingshelper.cpp @@ -58,22 +58,6 @@ void KeybindingsHelper::updateKeybindings( invertedKeybindings[key.second] = shortcutStr; ini.setValue( group, shortcutStr, key.second ); added = true; - } else if ( foundCmd == invertedKeybindings.end() ) { - // Override the shortcut if the command that holds that - // shortcut does not exists anymore - auto kb = keybindings.find( shortcutStr ); - if ( kb != keybindings.end() ) { - bool found = false; - for ( const auto& val : defKeybindings ) - if ( val.second == kb->second ) - found = true; - if ( !found ) { - keybindings[shortcutStr] = key.second; - invertedKeybindings[key.second] = shortcutStr; - ini.setValue( group, shortcutStr, key.second ); - added = true; - } - } } } } @@ -147,22 +131,6 @@ void KeybindingsHelper::updateKeybindings( invertedKeybindings[key.second] = shortcutStr; ini.setValue( group, shortcutStr, key.second ); added = true; - } else if ( foundCmd == invertedKeybindings.end() ) { - // Override the shortcut if the command that holds that - // shortcut does not exists anymore - auto kb = keybindings.find( shortcutStr ); - if ( kb != keybindings.end() ) { - bool found = false; - for ( const auto& val : defKeybindings ) - if ( val.second == kb->second ) - found = true; - if ( !found ) { - keybindings[shortcutStr] = key.second; - invertedKeybindings[key.second] = shortcutStr; - ini.setValue( group, shortcutStr, key.second ); - added = true; - } - } } } } @@ -235,22 +203,6 @@ void KeybindingsHelper::updateKeybindings( invertedKeybindings[key.second] = shortcutStr; ini.setValue( group, shortcutStr, key.second ); added = true; - } else if ( foundCmd == invertedKeybindings.end() ) { - // Override the shortcut if the command that holds that - // shortcut does not exists anymore - auto kb = keybindings.find( shortcutStr ); - if ( kb != keybindings.end() ) { - bool found = false; - for ( const auto& val : defKeybindings ) - if ( val.second == kb->second ) - found = true; - if ( !found ) { - keybindings[shortcutStr] = key.second; - invertedKeybindings[key.second] = shortcutStr; - ini.setValue( group, shortcutStr, key.second ); - added = true; - } - } } } } diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 1e9a1df14..67fbaa674 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -2077,6 +2077,15 @@ UIMenu* SettingsMenu::createViewMenu() { "Synchronizes the current focused document as the selected\nfile in the " "directory tree." ) ) ->setId( "sync-project-tree" ); + mViewMenu + ->addCheckBox( + i18n( "restore_editor_selection_on_focus", "Restore editor selection on focus" ) ) + ->setActive( mApp->getConfig().editor.restoreEditorSelectionOnFocus ) + ->setTooltipText( + i18n( "restore_editor_selection_on_focus_tooltip", + "Restores each editor split's last cursor and selection state when it regains " + "focus." ) ) + ->setId( "restore-editor-selection-on-focus" ); mViewMenu->addSeparator(); mViewMenu @@ -2210,6 +2219,11 @@ UIMenu* SettingsMenu::createViewMenu() { } else if ( item->getId() == "sync-project-tree" ) { mApp->getConfig().editor.syncProjectTreeWithEditor = item->asType()->isActive(); + } else if ( item->getId() == "restore-editor-selection-on-focus" ) { + mApp->getConfig().editor.restoreEditorSelectionOnFocus = + item->asType()->isActive(); + mSplitter->setRestoreEditorSelectionOnFocus( + mApp->getConfig().editor.restoreEditorSelectionOnFocus ); } else { String text = String( event->getNode()->asType()->getId() ).toLower(); String::replaceAll( text, " ", "-" ); From fba521d366a2ee37a4c37f8ea6cf6a28dc380775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 27 Jun 2026 01:19:35 -0300 Subject: [PATCH 3/9] Update efsw --- src/thirdparty/efsw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/thirdparty/efsw b/src/thirdparty/efsw index 495963460..8cefba85b 160000 --- a/src/thirdparty/efsw +++ b/src/thirdparty/efsw @@ -1 +1 @@ -Subproject commit 49596346078f5903af250b39d9b183e9f3db9733 +Subproject commit 8cefba85b400e1d6d2f581efe9a49d7728093668 From 3a1c75f4f645c2b20705726946a303ef23da4565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 28 Jun 2026 01:52:14 -0300 Subject: [PATCH 4/9] Added Insert Date / Time functionality (Settings -> Edit -> Insert Date, plus commands for each option). Fixed a use after free bug in UICodeEditorSplitter. --- .../eepp/ui/tools/uicodeeditorsplitter.hpp | 1 + src/eepp/ui/iconmanager.cpp | 1 + src/eepp/ui/tools/uicodeeditorsplitter.cpp | 125 ++++++++++++----- src/tools/ecode/appconfig.cpp | 2 + src/tools/ecode/appconfig.hpp | 1 + src/tools/ecode/datetimecontroller.cpp | 128 ++++++++++++++++++ src/tools/ecode/datetimecontroller.hpp | 37 +++++ src/tools/ecode/ecode.cpp | 4 + src/tools/ecode/ecode.hpp | 2 + .../ecode/plugins/plugincontextprovider.hpp | 3 + src/tools/ecode/settingsmenu.cpp | 53 ++++++++ src/tools/ecode/settingsmenu.hpp | 3 + 12 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 src/tools/ecode/datetimecontroller.cpp create mode 100644 src/tools/ecode/datetimecontroller.hpp diff --git a/include/eepp/ui/tools/uicodeeditorsplitter.hpp b/include/eepp/ui/tools/uicodeeditorsplitter.hpp index 0a8148de2..047832f9c 100644 --- a/include/eepp/ui/tools/uicodeeditorsplitter.hpp +++ b/include/eepp/ui/tools/uicodeeditorsplitter.hpp @@ -435,6 +435,7 @@ class EE_API UICodeEditorSplitter { Float mVisualSplitEdgePercent{ 0.1 }; TabTryCloseCallback mTabTryCloseCb; std::function mCanCreateSplitFn; + std::unordered_map> mEventCbs; UICodeEditorSplitter( UICodeEditorSplitter::Client* client, UISceneNode* sceneNode, std::shared_ptr threadPool, diff --git a/src/eepp/ui/iconmanager.cpp b/src/eepp/ui/iconmanager.cpp index 4ff387476..94e8ce8f7 100644 --- a/src/eepp/ui/iconmanager.cpp +++ b/src/eepp/ui/iconmanager.cpp @@ -119,6 +119,7 @@ UIIconTheme* IconManager::init( const std::string& iconThemeName, FontTrueType* { "input-field", 0xF47A }, { "sun", 0xF1BC }, { "moon", 0xEF72 }, + { "calendar-2", 0xEB21 }, } ) { iconTheme->add( UIGlyphIcon::New( icon.first, remixIconFont, icon.second ) ); } diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index 1c36a7cd2..cdd1a79aa 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -110,7 +110,42 @@ UICodeEditorSplitter::UICodeEditorSplitter( UICodeEditorSplitter::Client* client } } -UICodeEditorSplitter::~UICodeEditorSplitter() {} +UICodeEditorSplitter::~UICodeEditorSplitter() { + std::vector tabWidgets; + { + Lock l( mTabWidgetMutex ); + tabWidgets = mTabWidgets; + mTabWidgets.clear(); + } + + for ( UITabWidget* tabWidget : tabWidgets ) { + if ( nullptr == tabWidget ) + continue; + + tabWidget->setTabTryCloseCallback( nullptr ); + tabWidget->setSplitFunction( nullptr, mVisualSplitEdgePercent ); + auto tabWidgetCbsIt = mEventCbs.find( tabWidget ); + if ( tabWidgetCbsIt != mEventCbs.end() ) + tabWidget->removeEventListener( tabWidgetCbsIt->second ); + + for ( size_t i = 0; i < tabWidget->getTabCount(); ++i ) { + UITab* tab = tabWidget->getTab( i ); + if ( nullptr == tab || nullptr == tab->getOwnedWidget() || + !tab->getOwnedWidget()->isWidget() ) + continue; + + UIWidget* widget = tab->getOwnedWidget()->asType(); + auto widgetCbsIt = mEventCbs.find( widget ); + if ( widgetCbsIt != mEventCbs.end() ) + widget->removeEventListener( widgetCbsIt->second ); + } + } + + mEventCbs.clear(); + mCurEditor = nullptr; + mCurWidget = nullptr; + mClient = nullptr; +} UITabWidget* UICodeEditorSplitter::tabWidgetFromEditor( UICodeEditor* editor ) const { if ( editor && editor->getData() != 0 ) @@ -256,7 +291,7 @@ UICodeEditor* UICodeEditorSplitter::createCodeEditor() { registerSplitterCommands( doc ); /* Splitter commands */ - editor->on( Event::OnFocus, [this]( const Event* event ) { + mEventCbs[editor].push_back( editor->on( Event::OnFocus, [this]( const Event* event ) { UICodeEditor* editor = event->getNode()->asType(); UICodeEditor* prevEditor = mCurEditor; if ( mRestoreEditorSelectionOnFocus && prevEditor && prevEditor != editor && @@ -265,29 +300,32 @@ UICodeEditor* UICodeEditorSplitter::createCodeEditor() { setCurrentWidget( editor ); if ( mRestoreEditorSelectionOnFocus && prevEditor && prevEditor != editor ) restoreEditorSelection( editor ); - } ); - editor->on( Event::OnFocusLoss, [this]( const Event* event ) { + } ) ); + mEventCbs[editor].push_back( editor->on( Event::OnFocusLoss, [this]( const Event* event ) { if ( mRestoreEditorSelectionOnFocus ) saveEditorSelection( event->getNode()->asType() ); - } ); - editor->on( Event::OnTextChanged, [this]( const Event* event ) { + } ) ); + mEventCbs[editor].push_back( editor->on( Event::OnTextChanged, [this]( const Event* event ) { mClient->onDocumentModified( event->getNode()->asType(), event->getNode()->asType()->getDocument() ); - } ); - editor->on( Event::OnSelectionChanged, [this]( const Event* event ) { - mClient->onDocumentSelectionChange( - event->getNode()->asType(), - event->getNode()->asType()->getDocument() ); - } ); - editor->on( Event::OnCursorPosChange, [this]( const Event* event ) { - mClient->onDocumentCursorPosChange( - event->getNode()->asType(), - event->getNode()->asType()->getDocument() ); - } ); - editor->on( Event::OnDocumentUndoRedo, [this]( const Event* event ) { - mClient->onDocumentUndoRedo( event->getNode()->asType(), - event->getNode()->asType()->getDocument() ); - } ); + } ) ); + mEventCbs[editor].push_back( + editor->on( Event::OnSelectionChanged, [this]( const Event* event ) { + mClient->onDocumentSelectionChange( + event->getNode()->asType(), + event->getNode()->asType()->getDocument() ); + } ) ); + mEventCbs[editor].push_back( + editor->on( Event::OnCursorPosChange, [this]( const Event* event ) { + mClient->onDocumentCursorPosChange( + event->getNode()->asType(), + event->getNode()->asType()->getDocument() ); + } ) ); + mEventCbs[editor].push_back( + editor->on( Event::OnDocumentUndoRedo, [this]( const Event* event ) { + mClient->onDocumentUndoRedo( event->getNode()->asType(), + event->getNode()->asType()->getDocument() ); + } ) ); editor->addKeyBinds( getLocalDefaultKeybindings() ); editor->addUnlockedCommands( getUnlockedCommands() ); @@ -569,11 +607,12 @@ UICodeEditorSplitter::createCodeEditorInTabWidget( UITabWidget* tabWidget ) { return std::make_pair( (UITab*)nullptr, (UICodeEditor*)nullptr ); UICodeEditor* editor = createCodeEditor(); mAboutToAddEditor = editor; - editor->on( Event::OnDocumentChanged, [this]( const Event* event ) { - UICodeEditor* editor = event->getNode()->asType(); - mEditorSelections.erase( editor ); - mClient->onDocumentStateChanged( editor, editor->getDocument() ); - } ); + mEventCbs[editor].push_back( + editor->on( Event::OnDocumentChanged, [this]( const Event* event ) { + UICodeEditor* editor = event->getNode()->asType(); + mEditorSelections.erase( editor ); + mClient->onDocumentStateChanged( editor, editor->getDocument() ); + } ) ); UITab* tab = tabWidget->add( editor->getDocument().getFilename(), editor ); editor->setData( (UintPtr)tab ); DocEvent docEvent( editor, &editor->getDocument(), Event::OnEditorTabReady ); @@ -616,13 +655,13 @@ UICodeEditorSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* widget->setData( (UintPtr)tab ); // We use both events because there was an strange behavior that sometimes OnFocusWithin was not // enough, so this is just in case. - widget->on( Event::OnFocus, [this]( const Event* event ) { + mEventCbs[widget].push_back( widget->on( Event::OnFocus, [this]( const Event* event ) { setCurrentWidget( event->getNode()->asType() ); - } ); - widget->on( Event::OnFocusWithin, [this]( const Event* event ) { + } ) ); + mEventCbs[widget].push_back( widget->on( Event::OnFocusWithin, [this]( const Event* event ) { setCurrentWidget( event->getNode()->asType() ); - } ); - widget->on( Event::OnTitleChange, [this]( const Event* event ) { + } ) ); + mEventCbs[widget].push_back( widget->on( Event::OnTitleChange, [this]( const Event* event ) { const TextEvent* tevent = static_cast( event ); UIWidget* widget = event->getNode()->asType(); UITabWidget* tabWidget = tabWidgetFromWidget( widget ); @@ -630,7 +669,7 @@ UICodeEditorSplitter::createWidgetInTabWidget( UITabWidget* tabWidget, UIWidget* if ( !tab ) return; tab->setText( tevent->getText() ); - } ); + } ) ); if ( focus ) tabWidget->setTabSelected( tab ); mClient->onTabCreated( tab, widget ); @@ -713,7 +752,8 @@ UITabWidget* UICodeEditorSplitter::createTabWidget( Node* parent ) { }, mVisualSplitEdgePercent ); } - tabWidget->on( Event::OnTabSelected, [this]( const Event* event ) { + mEventCbs[tabWidget].push_back( tabWidget->on( Event::OnTabSelected, [this]( + const Event* event ) { UITabWidget* tabWidget = event->getNode()->asType(); eeASSERT( nullptr != tabWidget && nullptr != tabWidget->getTabSelected() && nullptr != tabWidget->getTabSelected()->getOwnedWidget() ); @@ -727,7 +767,7 @@ UITabWidget* UICodeEditorSplitter::createTabWidget( Node* parent ) { } else { setCurrentWidget( tabWidget->getTabSelected()->getOwnedWidget()->asType() ); } - } ); + } ) ); tabWidget->setTabTryCloseCallback( [this]( UITab* tab, UITabWidget::FocusTabBehavior focusTabBehavior ) -> bool { if ( tab->getOwnedWidget() && @@ -737,9 +777,10 @@ UITabWidget* UICodeEditorSplitter::createTabWidget( Node* parent ) { } return false; } ); - tabWidget->on( Event::OnTabClosed, [this]( const Event* event ) { - onTabClosed( static_cast( event ) ); - } ); + mEventCbs[tabWidget].push_back( + tabWidget->on( Event::OnTabClosed, [this]( const Event* event ) { + onTabClosed( static_cast( event ) ); + } ) ); if ( mOnTabWidgetCreateCb ) mOnTabWidgetCreateCb( tabWidget ); Lock l( mTabWidgetMutex ); @@ -1842,12 +1883,22 @@ void UICodeEditorSplitter::closeSplitter( UISplitter* splitter ) { void UICodeEditorSplitter::onTabClosed( const TabEvent* tabEvent ) { UIWidget* widget = tabEvent->getTab()->getOwnedWidget()->asType(); UITabWidget* tabWidget = tabEvent->getTab()->getTabWidget(); + auto widgetCbsIt = mEventCbs.find( widget ); + if ( widgetCbsIt != mEventCbs.end() ) { + widget->removeEventListener( widgetCbsIt->second ); + mEventCbs.erase( widgetCbsIt ); + } if ( widget && widget->isType( UI_TYPE_CODEEDITOR ) ) mEditorSelections.erase( widget->asType() ); if ( tabWidget->getTabCount() == 0 ) { UISplitter* splitter = splitterFromWidget( widget ); if ( splitter ) { if ( splitter->isFull() ) { + auto tabWidgetCbsIt = mEventCbs.find( tabWidget ); + if ( tabWidgetCbsIt != mEventCbs.end() ) { + tabWidget->removeEventListener( tabWidgetCbsIt->second ); + mEventCbs.erase( tabWidgetCbsIt ); + } tabWidget->close(); auto itWidget = std::find( mTabWidgets.begin(), mTabWidgets.end(), tabWidget ); if ( itWidget != mTabWidgets.end() ) { diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 4669a6024..a0fb7d418 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -177,6 +177,7 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, TextFormat::stringToLineEnding( ini.getValue( "document", "line_endings", "LF" ) ); editor.newTabPosition = NewTabPosition::fromString( ini.getValue( "editor", "new_tab_position", "last" ) ); + editor.customDateFormat = ini.getValue( "editor", "custom_date_format", "%d.%m.%Y %H:%M:%S" ); // Migrate old data if ( ini.keyValueExists( "document", "windows_line_endings" ) && !ini.keyValueExists( "document", "line_endings" ) && @@ -395,6 +396,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValue( "editor", "tab_jump_mode", UITabWidget::tabJumpModeToString( editor.tabJumpMode ) ); ini.setValue( "editor", "new_tab_position", NewTabPosition::toString( editor.newTabPosition ) ); + ini.setValue( "editor", "custom_date_format", editor.customDateFormat ); ini.setValueB( "editor", "single_click_tree_navigation", editor.singleClickNavigation ); ini.setValueB( "editor", "sync_project_tree_with_editor", editor.syncProjectTreeWithEditor ); ini.setValueB( "editor", "auto_close_xml_tags", editor.autoCloseXMLTags ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index 77ac3a468..ef2786ace 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -121,6 +121,7 @@ struct CodeEditorConfig { bool restoreEditorSelectionOnFocus{ true }; UITabWidget::TabJumpMode tabJumpMode{ UITabWidget::TabJumpMode::Linear }; NewTabPosition::Position newTabPosition{ NewTabPosition::Last }; + std::string customDateFormat{ "%d.%m.%Y %H:%M:%S" }; bool singleClickNavigation{ false }; bool syncProjectTreeWithEditor{ true }; diff --git a/src/tools/ecode/datetimecontroller.cpp b/src/tools/ecode/datetimecontroller.cpp new file mode 100644 index 000000000..087b9f764 --- /dev/null +++ b/src/tools/ecode/datetimecontroller.cpp @@ -0,0 +1,128 @@ +#include "datetimecontroller.hpp" +#include "appconfig.hpp" +#include "plugins/plugincontextprovider.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ecode { + +DateTimeController::DateTimeController( PluginContextProvider* context ) : mContext( context ) {} + +void DateTimeController::registerCommands( EE::UI::Doc::TextDocument& doc ) { + doc.setCommand( "insert-date-dd-mm-yyyy", [this] { insertDate( "%d.%m.%Y" ); } ); + doc.setCommand( "insert-date-mm-dd-yyyy", [this] { insertDate( "%m.%d.%Y" ); } ); + doc.setCommand( "insert-date-yyyy-mm-dd", [this] { insertDate( "%Y/%m/%d" ); } ); + doc.setCommand( "insert-date-time-dd-mm-yyyy", [this] { insertDate( "%d.%m.%Y %H:%M:%S" ); } ); + doc.setCommand( "insert-date-time-mm-dd-yyyy", [this] { insertDate( "%m.%d.%Y %H:%M:%S" ); } ); + doc.setCommand( "insert-date-time-yyyy-mm-dd", [this] { insertDate( "%Y/%m/%d %H:%M:%S" ); } ); + doc.setCommand( "insert-date-custom", + [this] { insertDate( mContext->getConfig().editor.customDateFormat ); } ); + doc.setCommand( "set-custom-date-format", [this] { setCustomDateFormat(); } ); +} + +bool DateTimeController::isValidDateFormat( const std::string& format ) { + if ( String::trim( format ).empty() ) + return false; + + for ( size_t i = 0; i < format.size(); ++i ) { + if ( format[i] != '%' ) + continue; + + if ( ++i >= format.size() ) + return false; + + while ( i < format.size() && ( format[i] == '_' || format[i] == '-' || format[i] == '0' || + format[i] == '^' || format[i] == '#' ) ) + ++i; + + while ( i < format.size() && std::isdigit( static_cast( format[i] ) ) ) + ++i; + + if ( i < format.size() && ( format[i] == 'E' || format[i] == 'O' ) ) + ++i; + + if ( i >= format.size() ) + return false; + + static constexpr std::string_view VALID_SPECIFIERS = + "aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%"; + if ( VALID_SPECIFIERS.find( format[i] ) == std::string_view::npos ) + return false; + } + + return true; +} + +std::string DateTimeController::formatCurrentDate( const std::string& format ) { + std::time_t rawtime; + std::time( &rawtime ); + std::tm* timeinfo = std::localtime( &rawtime ); + if ( nullptr == timeinfo ) + return {}; + + char buf[256]; + if ( std::strftime( buf, sizeof( buf ), format.c_str(), timeinfo ) == 0 ) + return {}; + return std::string( buf ); +} + +void DateTimeController::insertDate( const std::string& format ) { + if ( !mContext->getSplitter()->curEditorExistsAndFocused() ) + return; + + UICodeEditor* editor = mContext->getSplitter()->getCurEditor(); + if ( nullptr == editor || editor->isLocked() ) + return; + + if ( !isValidDateFormat( format ) ) + return; + + std::string date( formatCurrentDate( format ) ); + if ( !date.empty() ) + editor->getDocument().textInput( String::fromUtf8( date ) ); +} + +void DateTimeController::setCustomDateFormat() { + UIMessageBox* msgBox = + UIMessageBox::New( UIMessageBox::INPUT, + mContext + ->i18n( "set_custom_date_format_message", + "Set Custom Date Format:\nUses strftime format specifiers." ) + .unescape() ); + msgBox->setTitle( mContext->i18n( "set_custom_date_format", "Set Custom Date Format" ) ); + msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); + msgBox->getTextInput()->setText( mContext->getConfig().editor.customDateFormat ); + msgBox->getTextInput()->on( Event::OnTextChanged, [msgBox]( const Event* ) { + msgBox->getTextInput()->removeClass( "error" ); + } ); + msgBox->showWhenReady(); + msgBox->on( Event::OnConfirm, [this, msgBox]( const Event* ) { + std::string format( msgBox->getTextInput()->getText().toUtf8() ); + if ( !isValidDateFormat( format ) || formatCurrentDate( format ).empty() ) { + msgBox->getTextInput()->addClass( "error" ); + mContext->errorMsgBox( + mContext->i18n( "invalid_date_format", "Invalid date format." ) ); + return; + } + mContext->getConfig().editor.customDateFormat = std::move( format ); + msgBox->closeWindow(); + } ); + setFocusEditorOnClose( msgBox ); +} + +void DateTimeController::setFocusEditorOnClose( EE::UI::UIMessageBox* msgBox ) { + UICodeEditor* editor = mContext->getSplitter()->getCurEditor(); + msgBox->on( Event::OnWindowClose, [editor]( const Event* ) { + if ( editor && SceneManager::existsSingleton() && + !SceneManager::instance()->isShuttingDown() ) + editor->setFocus(); + } ); +} + +} // namespace ecode diff --git a/src/tools/ecode/datetimecontroller.hpp b/src/tools/ecode/datetimecontroller.hpp new file mode 100644 index 000000000..ce7bce51b --- /dev/null +++ b/src/tools/ecode/datetimecontroller.hpp @@ -0,0 +1,37 @@ +#ifndef ECODE_DATETIMECONTROLLER_HPP +#define ECODE_DATETIMECONTROLLER_HPP + +#include +#include + +namespace EE { namespace UI { +class UIMessageBox; +}} // namespace EE::UI + +namespace ecode { + +class PluginContextProvider; + +class DateTimeController { + public: + explicit DateTimeController( PluginContextProvider* context ); + + void registerCommands( EE::UI::Doc::TextDocument& doc ); + + private: + PluginContextProvider* mContext{ nullptr }; + + static bool isValidDateFormat( const std::string& format ); + + static std::string formatCurrentDate( const std::string& format ); + + void insertDate( const std::string& format ); + + void setCustomDateFormat(); + + void setFocusEditorOnClose( EE::UI::UIMessageBox* msgBox ); +}; + +} // namespace ecode + +#endif diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 1f956b912..31ac7a91c 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1,6 +1,7 @@ #include "ecode.hpp" #include "colorschemetranslator.hpp" #include "customwidgets.hpp" +#include "datetimecontroller.hpp" #include "featureshealth.hpp" #include "keybindingshelper.hpp" #include "pathhelper.hpp" @@ -1107,6 +1108,7 @@ App::App( const size_t& jobs, const std::vector& args ) : mArgs( args ), mThreadPool( ThreadPool::createShared( jobs > 0 ? jobs : eemax( 4, Sys::getCPUCount() ) ) ), + mDateTimeController( std::make_unique( this ) ), mSettingsActions( std::make_unique( this ) ) {} static void fsRemoveAll( const std::string& fpath ) { @@ -2311,6 +2313,7 @@ std::vector App::getUnlockedCommands() { "maximize-tab-widget", "restore-maximized-tab-widget", "close-folder", + "set-custom-date-format", }; } @@ -3112,6 +3115,7 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { } } } ); + mDateTimeController->registerCommands( doc ); registerUnlockedCommands( doc ); editor->on( Event::OnDocumentSave, [this]( const Event* event ) { diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 8209abb76..587fb6c1e 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -31,6 +31,7 @@ namespace ecode { class AutoCompletePlugin; class LinterPlugin; class FormatterPlugin; +class DateTimeController; class SettingsMenu; class UITreeViewFS; @@ -748,6 +749,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { std::unique_ptr mTerminalManager; std::unique_ptr mPluginManager; std::unique_ptr mSettings; + std::unique_ptr mDateTimeController; std::string mFileToOpen; UITheme* mTheme{ nullptr }; UIStatusBar* mStatusBar{ nullptr }; diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index 0e4c116b0..4bfd92c08 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -22,6 +22,7 @@ class UISplitter; class UITabWidget; class UISceneNode; class UICodeEditor; +class UIMessageBox; namespace Doc { class SyntaxColorScheme; @@ -94,6 +95,8 @@ class PluginContextProvider { virtual String i18n( const std::string& key, const String& def ) = 0; + virtual UIMessageBox* errorMsgBox( const String& msg ) = 0; + virtual const std::string& getWindowTitle() const = 0; virtual EE::Window::Window* getWindow() const = 0; diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 67fbaa674..460245f41 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -1340,6 +1340,15 @@ UIMenu* SettingsMenu::createEditMenu() { getKeybind( "delete-to-next-char" ) ) ->setId( "delete-to-next-char" ); mEditMenu->addSeparator(); + mDateMenu = UIPopUpMenu::New(); + auto* dateMenuItem = mEditMenu->addSubMenu( i18n( "insert_date", "Insert Date" ), + findIcon( "calendar-2" ), mDateMenu ); + dateMenuItem->setId( "insert_date_menu" ); + dateMenuItem->on( Event::OnMenuShow, [this, dateMenuItem]( const Event* ) { + mDateMenu->setOwnerNode( dateMenuItem ); + updateDateMenu(); + } ); + mEditMenu->addSeparator(); mEditMenu ->add( i18n( "select_all", "Select All" ), findIcon( "select-all" ), getKeybind( "select-all" ) ) @@ -1395,6 +1404,7 @@ UIMenu* SettingsMenu::createEditMenu() { mEditMenu->getItemId( "redo" )->setEnabled( false ); mEditMenu->getItemId( "copy" )->setEnabled( false ); mEditMenu->getItemId( "cut" )->setEnabled( false ); + mEditMenu->getItemId( "insert_date_menu" )->setEnabled( false ); mEditMenu->getItemId( "open-containing-folder" )->setVisible( false ); mEditMenu->getItemId( "copy-containing-folder-path" )->setVisible( false ); moveSep->setEnabled( false )->setVisible( false ); @@ -1409,6 +1419,7 @@ UIMenu* SettingsMenu::createEditMenu() { mEditMenu->getItemId( "redo" )->setEnabled( doc->hasRedo() ); mEditMenu->getItemId( "copy" )->setEnabled( doc->hasSelection() ); mEditMenu->getItemId( "cut" )->setEnabled( doc->hasSelection() ); + mEditMenu->getItemId( "insert_date_menu" )->setEnabled( true ); mEditMenu->getItemId( "open-containing-folder" )->setVisible( doc->hasFilepath() ); mEditMenu->getItemId( "copy-containing-folder-path" )->setVisible( doc->hasFilepath() ); moveSep->setEnabled( true )->setVisible( true ); @@ -1420,6 +1431,48 @@ UIMenu* SettingsMenu::createEditMenu() { return mEditMenu; } +void SettingsMenu::updateDateMenu() { + if ( mDateMenu->getCount() != 0 ) + return; + mDateMenu->removeAll(); + mDateMenu->removeEventsOfType( Event::OnItemClicked ); + + struct DateFormatCommand { + const char* command; + const char* i18nKey; + const char* label; + }; + + static constexpr DateFormatCommand DATE_COMMANDS[] = { + { "insert-date-dd-mm-yyyy", "insert_date_dd_mm_yyyy", "dd.mm.yyyy" }, + { "insert-date-mm-dd-yyyy", "insert_date_mm_dd_yyyy", "mm.dd.yyyy" }, + { "insert-date-yyyy-mm-dd", "insert_date_yyyy_mm_dd", "yyyy/mm/dd" }, + { "insert-date-time-dd-mm-yyyy", "insert_date_time_dd_mm_yyyy", "dd.mm.yyyy hh:mm:ss" }, + { "insert-date-time-mm-dd-yyyy", "insert_date_time_mm_dd_yyyy", "mm.dd.yyyy hh:mm:ss" }, + { "insert-date-time-yyyy-mm-dd", "insert_date_time_yyyy_mm_dd", "yyyy/mm/dd hh:mm:ss" }, + }; + + for ( const auto& cmd : DATE_COMMANDS ) { + mDateMenu->add( i18n( cmd.i18nKey, cmd.label ), nullptr, getKeybind( cmd.command ) ) + ->setId( cmd.command ); + } + + mDateMenu->addSeparator(); + mDateMenu + ->add( i18n( "use_custom_date_format", "Use Custom Date Format" ), nullptr, + getKeybind( "insert-date-custom" ) ) + ->setId( "insert-date-custom" ); + mDateMenu + ->add( i18n( "set_custom_date_format", "Set Custom Date Format" ), nullptr, + getKeybind( "set-custom-date-format" ) ) + ->setId( "set-custom-date-format" ); + + mDateMenu->on( Event::OnItemClicked, [this]( const Event* event ) { + if ( event->getNode()->isType( UI_TYPE_MENUITEM ) ) + runCommand( event->getNode()->getId() ); + } ); +} + UIMenu* SettingsMenu::createWindowMenu() { mWindowMenu = UIPopUpMenu::New(); auto shouldCloseCb = []( UIMenuItem* ) -> bool { return false; }; diff --git a/src/tools/ecode/settingsmenu.hpp b/src/tools/ecode/settingsmenu.hpp index 1a13158e9..f6dd2c4b7 100644 --- a/src/tools/ecode/settingsmenu.hpp +++ b/src/tools/ecode/settingsmenu.hpp @@ -117,6 +117,7 @@ class SettingsMenu { UIPopUpMenu* mProjectMenu{ nullptr }; UIPopUpMenu* mHExtLanguageTypeMenu{ nullptr }; UIPopUpMenu* mEditMenu{ nullptr }; + UIPopUpMenu* mDateMenu{ nullptr }; UIPopUpMenu* mHelpMenu{ nullptr }; UIPopUpMenu* mLineWrapMenu{ nullptr }; UIPopUpMenu* mCodeFoldingMenu{ nullptr }; @@ -132,6 +133,8 @@ class SettingsMenu { void forEachTerminal( const std::function fn ); + void updateDateMenu(); + UITerminal* getCurrentTerminal() const; }; From 57cdab7b7f2f37048a6a3f5e1afc271cd559745e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 29 Jun 2026 20:32:08 -0300 Subject: [PATCH 5/9] Implemented UIFontPickerDialog. Fixed alignment in UILinearLayout when padding is used. --- .ecode/project_build.json | 6 + include/eepp/graphics/fonttruetype.hpp | 2 + include/eepp/scene/event.hpp | 1 + include/eepp/ui.hpp | 3 +- include/eepp/ui/tools/uifontpickerdialog.hpp | 192 ++++ include/eepp/ui/uihelper.hpp | 1 + premake4.lua | 6 + premake5.lua | 6 + src/eepp/graphics/fontmanager.cpp | 3 +- src/eepp/ui/tools/uifontpickerdialog.cpp | 845 ++++++++++++++++++ src/eepp/ui/uilinearlayout.cpp | 14 +- src/eepp/ui/uiwidget.cpp | 30 +- .../ui_font_picker/ui_font_picker.cpp | 34 + .../unit_tests/uifontpickerdialog_tests.cpp | 90 ++ src/tests/unit_tests/uilayout_tests.cpp | 82 ++ 15 files changed, 1298 insertions(+), 17 deletions(-) create mode 100644 include/eepp/ui/tools/uifontpickerdialog.hpp create mode 100644 src/eepp/ui/tools/uifontpickerdialog.cpp create mode 100644 src/examples/ui_font_picker/ui_font_picker.cpp create mode 100644 src/tests/unit_tests/uifontpickerdialog_tests.cpp create mode 100644 src/tests/unit_tests/uilayout_tests.cpp diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 9e6098a1d..4486118bc 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -381,6 +381,12 @@ "command": "${project_root}/bin/eepp-ui-html-debug", "name": "eepp-ui-html-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-font-picker-debug", + "name": "eepp-ui-font-picker-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/include/eepp/graphics/fonttruetype.hpp b/include/eepp/graphics/fonttruetype.hpp index 30fd8bd25..149798b8f 100644 --- a/include/eepp/graphics/fonttruetype.hpp +++ b/include/eepp/graphics/fonttruetype.hpp @@ -35,6 +35,8 @@ class EE_API FontTrueType : public Font { const Font::Info& getInfo() const; + const Uint32& getFaceIndex() const { return mFaceIndex; } + Glyph getGlyph( Uint32 codePoint, unsigned int characterSize, bool bold, bool italic, Float outlineThickness = 0 ) const; diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp index 3e0acb855..3be68ae37 100644 --- a/include/eepp/scene/event.hpp +++ b/include/eepp/scene/event.hpp @@ -131,6 +131,7 @@ class EE_API Event { OnFocusWithin, OnFocusWithinLoss, OnItemsCountChange, + OnApply, NoEvent = eeINDEX_NOT_FOUND }; diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 908beb8f6..ee836c590 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -74,6 +74,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +142,6 @@ #include #include #include -#include #include #include #include @@ -172,6 +172,7 @@ #include #include #include +#include #include #include #include diff --git a/include/eepp/ui/tools/uifontpickerdialog.hpp b/include/eepp/ui/tools/uifontpickerdialog.hpp new file mode 100644 index 000000000..a8cce3b35 --- /dev/null +++ b/include/eepp/ui/tools/uifontpickerdialog.hpp @@ -0,0 +1,192 @@ +#ifndef EE_UI_TOOLS_UIFONTPICKERDIALOG_HPP +#define EE_UI_TOOLS_UIFONTPICKERDIALOG_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace EE { namespace UI { +class UICheckBox; +class UIFileDialog; +class UILoader; +class UIListView; +class UIPushButton; +class UITextInput; +class UITextView; +}} // namespace EE::UI + +namespace EE { namespace UI { namespace Tools { + +class UIColorPicker; + +struct UIFontSelection { + FontDesc font; + Uint32 size{ 12 }; + bool underline{ false }; + bool strikeThrough{ false }; + bool antialiasing{ true }; + Color color{ Color::White }; +}; + +class EE_API UIFontPickerDialog : public UIWindow { + public: + using FontPickedCb = std::function; + + enum Flags { + MonospaceOnly = 1 << 0, + ShowSize = 1 << 1, + ShowEffects = 1 << 2, + ShowColor = 1 << 3, + ShowApplyButton = 1 << 4, + DefaultFlags = ShowSize | ShowEffects | ShowColor, + }; + + struct FontStyleEntry { + std::string label; + FontDesc desc; + }; + + static UIFontPickerDialog* New( Uint32 flags = DefaultFlags ); + + virtual ~UIFontPickerDialog(); + + virtual Uint32 getType() const; + + virtual bool isType( const Uint32& type ) const; + + virtual void setTheme( UITheme* theme ); + + void setSelectedFont( const FontDesc& desc ); + + void setSelectedFont( const std::string& path, Uint32 faceIndex = 0 ); + + const UIFontSelection& getSelection() const; + + void setSelection( const UIFontSelection& selection ); + + void setOnFontPicked( FontPickedCb cb ); + + UIPushButton* getButtonOK() const; + + UIPushButton* getButtonCancel() const; + + UIPushButton* getButtonApply() const; + + UIPushButton* getButtonBrowse() const; + + UITextInput* getSearchInput() const; + + UIListView* getFamilyList() const; + + UIListView* getStyleList() const; + + UIListView* getSizeList() const; + + const KeyBindings::Shortcut& getCloseShortcut() const; + + void setCloseShortcut( const KeyBindings::Shortcut& closeShortcut ); + + virtual bool show(); + + protected: + Uint32 mFlags{ DefaultFlags }; + KeyBindings::Shortcut mCloseShortcut{ KEY_UNKNOWN }; + UIFontSelection mSelection; + FontPickedCb mFontPickedCb; + std::vector mFonts; + std::vector mFamilies; + std::vector mStyles; + std::vector mSizes; + std::shared_ptr mFamilyModel; + std::shared_ptr mStyleModel; + std::shared_ptr mSizeModel; + UIWidget* mFontContent{ nullptr }; + UILoader* mFontLoader{ nullptr }; + UITextInput* mSearchInput{ nullptr }; + UIListView* mFamilyList{ nullptr }; + UIListView* mStyleList{ nullptr }; + UIListView* mSizeList{ nullptr }; + UITextView* mPreviewText{ nullptr }; + UITextInput* mPreviewInput{ nullptr }; + UITextView* mDetailsText{ nullptr }; + UIColorPicker* mColorPicker{ nullptr }; + UIFileDialog* mBrowseDialog{ nullptr }; + Uint32 mColorPickerCloseCb{ 0 }; + Uint32 mBrowseDialogCloseCb{ 0 }; + Uint32 mBrowseDialogOpenCb{ 0 }; + UICheckBox* mMonospaceOnly{ nullptr }; + UICheckBox* mAntialiasing{ nullptr }; + UICheckBox* mUnderline{ nullptr }; + UICheckBox* mStrikeThrough{ nullptr }; + UIPushButton* mColorButton{ nullptr }; + UIPushButton* mButtonOK{ nullptr }; + UIPushButton* mButtonCancel{ nullptr }; + UIPushButton* mButtonApply{ nullptr }; + UIPushButton* mButtonBrowse{ nullptr }; + bool mUpdating{ false }; + bool mLoadingFonts{ false }; + Uint64 mLoadFontsTaskId{ 0 }; + + struct LoadFontsRequest { + std::atomic alive{ true }; + std::atomic dialog{ nullptr }; + }; + std::shared_ptr mLoadFontsRequest; + + UIFontPickerDialog( Uint32 flags = DefaultFlags ); + + virtual void onWindowReady(); + + virtual Uint32 onKeyUp( const KeyEvent& event ); + + virtual Uint32 onMessage( const NodeMessage* msg ); + + void loadWidgets(); + + void loadFonts(); + + void setFonts( std::vector fonts ); + + void sortFonts(); + + void setFontsLoading( bool loading ); + + void updateFamilies(); + + void updateStyles(); + + void updateSelectionFromLists(); + + void updatePreview(); + + void selectInitialRows(); + + void selectFamily( const std::string& family ); + + void selectStyle( const FontDesc& desc ); + + void selectSize( Uint32 size ); + + void emitPicked(); + + void pickColor(); + + void browseFont(); + + bool addExternalFont( const std::string& path, Uint32 faceIndex = 0 ); + + void clearBrowseDialog(); + + bool wantsMonospaceOnly() const; +}; + +}}} // namespace EE::UI::Tools + +#endif diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp index d71fd0af3..9566aca49 100644 --- a/include/eepp/ui/uihelper.hpp +++ b/include/eepp/ui/uihelper.hpp @@ -138,6 +138,7 @@ enum UINodeType { UI_TYPE_WEBVIEW, UI_TYPE_SVG, UI_TYPE_TEXTNODE, + UI_TYPE_FONTPICKERDIALOG, UI_TYPE_MODULES = 10000, UI_TYPE_TERMINAL = 10001, UI_TYPE_USER = 200000, diff --git a/premake4.lua b/premake4.lua index 5b524fb29..f80b8a1d1 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1669,6 +1669,12 @@ solution "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-font-picker" + set_kind() + language "C++" + files { "src/examples/ui_font_picker/*.cpp" } + build_link_configuration( "eepp-ui-font-picker", true ) + project "eepp-ui-dropdownmodellist" set_kind() language "C++" diff --git a/premake5.lua b/premake5.lua index 2a7c981c2..481a8f2e8 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1662,6 +1662,12 @@ workspace "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-font-picker" + set_kind() + language "C++" + files { "src/examples/ui_font_picker/*.cpp" } + build_link_configuration( "eepp-ui-font-picker", true ) + project "eepp-ui-dropdownmodellist" set_kind() language "C++" diff --git a/src/eepp/graphics/fontmanager.cpp b/src/eepp/graphics/fontmanager.cpp index 0e131e2da..9d785ec6d 100644 --- a/src/eepp/graphics/fontmanager.cpp +++ b/src/eepp/graphics/fontmanager.cpp @@ -111,7 +111,8 @@ FontTrueType* FontManager::getOrLoadSystemFallbackFont( const FontDesc& desc ) { for ( auto* font : mSystemFallbackFonts ) { if ( font->getType() == FontType::TTF ) { auto* ttf = static_cast( font ); - if ( ttf->getInfo().fontpath + ttf->getInfo().filename == desc.path ) + if ( ttf->getInfo().fontpath + ttf->getInfo().filename == desc.path && + ttf->getFaceIndex() == desc.faceIndex ) return ttf; } } diff --git a/src/eepp/ui/tools/uifontpickerdialog.cpp b/src/eepp/ui/tools/uifontpickerdialog.cpp new file mode 100644 index 000000000..7fc3794d6 --- /dev/null +++ b/src/eepp/ui/tools/uifontpickerdialog.cpp @@ -0,0 +1,845 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE::UI::Abstract; +using namespace EE::UI::Models; + +namespace EE { namespace UI { namespace Tools { + +static const Uint32 FONT_PICKER_STYLE_MARKER = String::hash( "UIFontPickerDialog" ); + +static const char* FONT_PICKER_STYLE = R"css( +#font_picker { + margin: 8dp; + clip: none; +} +#font_picker .column_label { + margin-bottom: 4dp; +} +#font_picker .footer { + margin-top: 8dp; + clip: none; +} +#font_picker .preview_text { + padding-left: 24dp; + padding-right: 24dp; + word-wrap: true; +} +#font_picker .details { + margin-top: 4dp; +} +#font_picker PushButton.footer_button { + min-width: 80dp; + margin-left: 8dp; +} +#font_picker .separator { + background-color: #FFFFFF44; +} +#font_picker .color_button { + min-width: 48dp; +} +#font_picker .options_panel { + background-color: var(--list-back); + border-color: var(--button-border); + border-radius: var(--button-radius); + border-width: var(--border-width); + padding-bottom: 4dp; + padding-left: 8dp; + padding-right: 8dp; + padding-top: 4dp; +} +)css"; + +static const char* FONT_PICKER_LAYOUT = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + +class StyleListModel final : public Model { + public: + explicit StyleListModel( const std::vector* data ) : + mData( data ) {} + + size_t rowCount( const ModelIndex& ) const override { return mData ? mData->size() : 0; } + + size_t columnCount( const ModelIndex& ) const override { return 1; } + + ModelIndex index( int row, int column, + const ModelIndex& parent = ModelIndex() ) const override { + if ( row >= static_cast( rowCount( parent ) ) || column >= 1 ) + return {}; + return Model::index( row, column, parent ); + } + + Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const override { + if ( role == ModelRole::Display && mData && + index.row() < static_cast( mData->size() ) ) + return Variant( ( *mData )[index.row()].label ); + return {}; + } + + private: + const std::vector* mData{ nullptr }; +}; + +static Uint64 loadFontsTaskTag( const UIFontPickerDialog* dialog ) { + return reinterpret_cast( dialog ); +} + +static std::string styleLabel( const FontDesc& desc ) { + std::string label; + if ( desc.weight == FontWeight::Normal ) { + label = desc.italic ? "Italic" : "Regular"; + } else { + label = Text::fontWeightToString( desc.weight ); + std::replace( label.begin(), label.end(), '-', ' ' ); + if ( !label.empty() ) { + label[0] = static_cast( std::toupper( label[0] ) ); + for ( size_t i = 1; i < label.size(); i++ ) { + if ( label[i - 1] == ' ' ) + label[i] = static_cast( std::toupper( label[i] ) ); + } + } + if ( desc.italic ) + label += " Italic"; + } + if ( desc.faceIndex != 0 ) + label += " #" + String::toString( desc.faceIndex ); + return label; +} + +static Uint32 styleFlags( const UIFontSelection& selection ) { + Uint32 style = selection.font.italic ? Text::Italic : Text::Regular; + if ( selection.font.weight >= FontWeight::SemiBold ) + style |= Text::Bold; + if ( selection.underline ) + style |= Text::Underlined; + if ( selection.strikeThrough ) + style |= Text::StrikeThrough; + return style; +} + +UIFontPickerDialog* UIFontPickerDialog::New( Uint32 flags ) { + return eeNew( UIFontPickerDialog, ( flags ) ); +} + +UIFontPickerDialog::UIFontPickerDialog( Uint32 flags ) : UIWindow(), mFlags( flags ) { + mVisible = false; + mStyleConfig.WinFlags = UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL; + updateWinFlags(); + + mSizes = { 8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 32, 48, 64, 72 }; + mSelection.size = 12; + + setTitle( i18n( "font_picker_select_font", "Select Font" ) ); + + const Sizef sceneSize( getUISceneNode()->getSize() ); + const Sizef maxSize( eemax( 320.f, sceneSize.getWidth() - PixelDensity::dpToPx( 32 ) ), + eemax( 320.f, sceneSize.getHeight() - PixelDensity::dpToPx( 32 ) ) ); + setMinWindowSize( + Sizef( eemin( 720.f, maxSize.getWidth() ), eemin( 440.f, maxSize.getHeight() ) ) ); + setSizeWithDecoration( + Sizef( eemin( 940.f, maxSize.getWidth() ), eemin( 620.f, maxSize.getHeight() ) ) ); + + loadWidgets(); + if ( getUISceneNode()->getUIThemeManager()->getDefaultTheme() ) + setTheme( getUISceneNode()->getUIThemeManager()->getDefaultTheme() ); + selectInitialRows(); + loadFonts(); +} + +UIFontPickerDialog::~UIFontPickerDialog() { + if ( mLoadFontsRequest ) { + mLoadFontsRequest->alive = false; + mLoadFontsRequest->dialog.store( nullptr ); + } + if ( mLoadFontsTaskId && getUISceneNode()->hasThreadPool() ) + getUISceneNode()->getThreadPool()->removeWithTag( loadFontsTaskTag( this ) ); + if ( mColorPicker && mColorPickerCloseCb ) + mColorPicker->getUIWindow()->removeEventListener( mColorPickerCloseCb ); + if ( mColorPicker && !SceneManager::instance()->isShuttingDown() ) + mColorPicker->closePicker(); + mColorPicker = nullptr; + mColorPickerCloseCb = 0; + clearBrowseDialog(); +} + +Uint32 UIFontPickerDialog::getType() const { + return UI_TYPE_FONTPICKERDIALOG; +} + +bool UIFontPickerDialog::isType( const Uint32& type ) const { + return type == UI_TYPE_FONTPICKERDIALOG ? true : UIWindow::isType( type ); +} + +void UIFontPickerDialog::setTheme( UITheme* theme ) { + UIWindow::setTheme( theme ); + + if ( mButtonOK ) { + if ( Drawable* icon = + getUISceneNode()->findIconDrawable( "ok", PixelDensity::dpToPxI( 16 ) ) ) + mButtonOK->setIcon( icon ); + } + + if ( mButtonCancel ) { + if ( Drawable* icon = + getUISceneNode()->findIconDrawable( "cancel", PixelDensity::dpToPxI( 16 ) ) ) + mButtonCancel->setIcon( icon ); + } + + if ( mButtonBrowse ) { + if ( Drawable* icon = getUISceneNode()->findIconDrawable( "document-open", + PixelDensity::dpToPxI( 16 ) ) ) + mButtonBrowse->setIcon( icon ); + } + + onThemeLoaded(); +} + +void UIFontPickerDialog::loadWidgets() { + UISceneNode* sceneNode = getUISceneNode(); + if ( !sceneNode->getStyleSheet().markerExists( FONT_PICKER_STYLE_MARKER ) ) { + CSS::StyleSheetParser parser; + parser.loadFromString( std::string_view{ FONT_PICKER_STYLE } ); + parser.getStyleSheet().setMarker( FONT_PICKER_STYLE_MARKER ); + sceneNode->combineStyleSheet( parser.getStyleSheet() ); + } + + UIWidget* root = sceneNode->loadLayoutFromString( FONT_PICKER_LAYOUT, mContainer ); + root->bind( "font_content", mFontContent ); + root->bind( "font_loader", mFontLoader ); + root->bind( "search_input", mSearchInput ); + root->bind( "family_list", mFamilyList ); + root->bind( "style_list", mStyleList ); + root->bind( "size_list", mSizeList ); + root->bind( "preview_text", mPreviewText ); + root->bind( "preview_input", mPreviewInput ); + root->bind( "details_text", mDetailsText ); + root->bind( "monospace_only", mMonospaceOnly ); + root->bind( "antialiasing", mAntialiasing ); + root->bind( "underline", mUnderline ); + root->bind( "strike_through", mStrikeThrough ); + root->bind( "color_button", mColorButton ); + root->bind( "ok_button", mButtonOK ); + root->bind( "cancel_button", mButtonCancel ); + root->bind( "apply_button", mButtonApply ); + root->bind( "browse_button", mButtonBrowse ); + + mSearchInput->setHint( i18n( "font_picker_search_fonts_ellipsis", "Search fonts..." ) ); + mPreviewInput->setText( "The quick brown fox jumps over the lazy dog" ); + mAntialiasing->setChecked( true ); + mMonospaceOnly->setChecked( ( mFlags & MonospaceOnly ) != 0 ); + mMonospaceOnly->setEnabled( ( mFlags & MonospaceOnly ) == 0 ); + + if ( ( mFlags & ShowSize ) == 0 ) { + if ( auto sizeColumn = root->find( "size_column" ) ) + sizeColumn->setVisible( false )->setEnabled( false ); + } + + if ( ( mFlags & ShowEffects ) == 0 ) { + mAntialiasing->setVisible( false )->setEnabled( false ); + mUnderline->setVisible( false )->setEnabled( false ); + mStrikeThrough->setVisible( false )->setEnabled( false ); + } + + if ( ( mFlags & ShowColor ) == 0 ) { + if ( auto colorLabel = root->find( "color_label" ) ) + colorLabel->setVisible( false )->setEnabled( false ); + mColorButton->setVisible( false )->setEnabled( false ); + } + + if ( ( mFlags & ShowApplyButton ) == 0 ) + mButtonApply->setVisible( false )->setEnabled( false ); + + mFamilyList->setHeadersVisible( false ); + mStyleList->setHeadersVisible( false ); + mSizeList->setHeadersVisible( false ); + mFamilyList->setAutoExpandOnSingleColumn( true ); + mStyleList->setAutoExpandOnSingleColumn( true ); + mSizeList->setAutoExpandOnSingleColumn( true ); + + mSearchInput->on( Event::OnTextChanged, [this]( const Event* ) { updateFamilies(); } ); + mFamilyList->setOnSelectionChange( [this] { + if ( !mUpdating ) { + updateStyles(); + updateSelectionFromLists(); + updatePreview(); + } + } ); + mStyleList->setOnSelectionChange( [this] { + if ( !mUpdating ) { + updateSelectionFromLists(); + updatePreview(); + } + } ); + mSizeList->setOnSelectionChange( [this] { + if ( !mUpdating ) { + updateSelectionFromLists(); + updatePreview(); + } + } ); + + mMonospaceOnly->on( Event::OnValueChange, [this]( const Event* ) { updateFamilies(); } ); + mAntialiasing->on( Event::OnValueChange, [this]( const Event* ) { + mSelection.antialiasing = mAntialiasing->isChecked(); + updatePreview(); + } ); + mUnderline->on( Event::OnValueChange, [this]( const Event* ) { + mSelection.underline = mUnderline->isChecked(); + updatePreview(); + } ); + mStrikeThrough->on( Event::OnValueChange, [this]( const Event* ) { + mSelection.strikeThrough = mStrikeThrough->isChecked(); + updatePreview(); + } ); + mPreviewInput->on( Event::OnTextChanged, [this]( const Event* ) { updatePreview(); } ); + mColorButton->on( Event::MouseClick, [this]( const Event* ) { pickColor(); } ); + mButtonApply->on( Event::MouseClick, [this]( const Event* ) { + emitPicked(); + sendCommonEvent( Event::OnApply ); + } ); + mButtonBrowse->on( Event::MouseClick, [this]( const Event* ) { browseFont(); } ); +} + +void UIFontPickerDialog::loadFonts() { + setFontsLoading( true ); + + if ( getUISceneNode()->hasThreadPool() ) { + auto request = std::make_shared(); + request->dialog.store( this ); + mLoadFontsRequest = request; + UISceneNode* sceneNode = getUISceneNode(); + mLoadFontsTaskId = sceneNode->getThreadPool()->run( + [request, sceneNode] { + auto fonts = SystemFontResolver::instance()->enumerate(); + UIFontPickerDialog* dialog = request->dialog.load(); + sceneNode->runOnMainThread( + [request, fonts = std::move( fonts )]() mutable { + UIFontPickerDialog* dialog = request->dialog.load(); + if ( !request->alive || dialog == nullptr ) + return; + dialog->mLoadFontsTaskId = 0; + dialog->setFonts( std::move( fonts ) ); + }, + Time::Zero, loadFontsTaskTag( dialog ) ); + }, + []( const Uint64& ) {}, loadFontsTaskTag( this ) ); + return; + } + + setFonts( SystemFontResolver::instance()->enumerate() ); +} + +void UIFontPickerDialog::setFonts( std::vector fonts ) { + FontDesc selectedFont = mSelection.font; + for ( const auto& font : mFonts ) { + auto found = std::find_if( fonts.begin(), fonts.end(), [&]( const auto& desc ) { + return desc.path == font.path && desc.faceIndex == font.faceIndex; + } ); + if ( found == fonts.end() ) + fonts.push_back( font ); + } + + mFonts = std::move( fonts ); + sortFonts(); + setFontsLoading( false ); + updateFamilies(); + if ( !selectedFont.path.empty() || !selectedFont.family.empty() ) + setSelectedFont( selectedFont ); + else + updatePreview(); +} + +void UIFontPickerDialog::sortFonts() { + std::sort( mFonts.begin(), mFonts.end(), []( const auto& lhs, const auto& rhs ) { + if ( lhs.family != rhs.family ) + return String::toLower( lhs.family ) < String::toLower( rhs.family ); + if ( lhs.weight != rhs.weight ) + return lhs.weight < rhs.weight; + if ( lhs.italic != rhs.italic ) + return !lhs.italic && rhs.italic; + if ( lhs.path != rhs.path ) + return lhs.path < rhs.path; + return lhs.faceIndex < rhs.faceIndex; + } ); +} + +void UIFontPickerDialog::setFontsLoading( bool loading ) { + mLoadingFonts = loading; + if ( mFontContent ) + mFontContent->setVisible( !loading )->setEnabled( !loading ); + if ( mFontLoader ) + mFontLoader->setVisible( loading )->setEnabled( loading ); + if ( mSearchInput ) + mSearchInput->setEnabled( !loading ); + if ( mButtonBrowse ) + mButtonBrowse->setEnabled( !loading ); + if ( mButtonOK ) + mButtonOK->setEnabled( !loading ); + if ( mButtonApply ) + mButtonApply->setEnabled( !loading && ( mFlags & ShowApplyButton ) != 0 ); +} + +bool UIFontPickerDialog::wantsMonospaceOnly() const { + return mMonospaceOnly && mMonospaceOnly->isChecked(); +} + +void UIFontPickerDialog::updateFamilies() { + std::string previousFamily = mSelection.font.family; + if ( !mFamilyList->getSelection().isEmpty() && + mFamilyList->getSelection().first().row() < static_cast( mFamilies.size() ) ) + previousFamily = mFamilies[mFamilyList->getSelection().first().row()]; + + const std::string query = String::toLower( mSearchInput->getText().toUtf8() ); + std::set families; + for ( const auto& font : mFonts ) { + if ( wantsMonospaceOnly() && !font.monospace ) + continue; + if ( !query.empty() && String::toLower( font.family ).find( query ) == std::string::npos ) + continue; + families.insert( font.family ); + } + mFamilies.assign( families.begin(), families.end() ); + mFamilyModel = ItemListModel::create( mFamilies ); + + mUpdating = true; + mFamilyList->setModel( mFamilyModel ); + mUpdating = false; + + if ( !previousFamily.empty() ) + selectFamily( previousFamily ); + if ( mFamilyList->getSelection().isEmpty() && !mFamilies.empty() ) + mFamilyList->setSelection( mFamilyModel->index( 0 ) ); + + updateStyles(); + updateSelectionFromLists(); + updatePreview(); +} + +void UIFontPickerDialog::updateStyles() { + mStyles.clear(); + if ( !mFamilyList->getSelection().isEmpty() ) { + const Int64 row = mFamilyList->getSelection().first().row(); + if ( row >= 0 && row < static_cast( mFamilies.size() ) ) { + const std::string& family = mFamilies[row]; + for ( const auto& font : mFonts ) { + if ( font.family == family && ( !wantsMonospaceOnly() || font.monospace ) ) + mStyles.push_back( { styleLabel( font ), font } ); + } + } + } + mStyleModel = std::make_shared( &mStyles ); + + mUpdating = true; + mStyleList->setModel( mStyleModel ); + mUpdating = false; + + if ( !mStyles.empty() ) { + selectStyle( mSelection.font ); + if ( mStyleList->getSelection().isEmpty() ) + mStyleList->setSelection( mStyleModel->index( 0 ) ); + } +} + +void UIFontPickerDialog::updateSelectionFromLists() { + if ( !mStyleList->getSelection().isEmpty() ) { + const Int64 row = mStyleList->getSelection().first().row(); + if ( row >= 0 && row < static_cast( mStyles.size() ) ) + mSelection.font = mStyles[row].desc; + } + if ( !mSizeList->getSelection().isEmpty() ) { + const Int64 row = mSizeList->getSelection().first().row(); + if ( row >= 0 && row < static_cast( mSizes.size() ) ) + mSelection.size = mSizes[row]; + } + mSelection.antialiasing = mAntialiasing->isChecked(); + mSelection.underline = mUnderline->isChecked(); + mSelection.strikeThrough = mStrikeThrough->isChecked(); +} + +void UIFontPickerDialog::updatePreview() { + if ( !mPreviewText ) + return; + + FontTrueType* font = FontManager::instance()->getOrLoadSystemFallbackFont( mSelection.font ); + if ( font ) { + mPreviewText->setFont( font ); + mPreviewInput->setFont( font ); + } + + mPreviewText->setFontSize( PixelDensity::dpToPxI( mSelection.size * 2 ) ); + mPreviewInput->setFontSize( PixelDensity::dpToPxI( 12 ) ); + mPreviewText->setFontStyle( styleFlags( mSelection ) ); + mPreviewInput->setFontStyle( styleFlags( mSelection ) ); + mPreviewText->setFontColor( mSelection.color ); + mPreviewInput->setFontColor( mSelection.color ); + if ( !mPreviewInput->getText().empty() ) + mPreviewText->setText( mPreviewInput->getText() ); + + if ( mColorButton ) + mColorButton->setBackgroundColor( mSelection.color ); + + if ( mDetailsText ) { + std::string faceIndexSuffix; + if ( mSelection.font.faceIndex != 0 ) + faceIndexSuffix = " #" + String::toString( mSelection.font.faceIndex ); + mDetailsText->setText( + String::format( "%s %s - %u pt - %s%s", mSelection.font.family.c_str(), + styleLabel( mSelection.font ).c_str(), mSelection.size, + mSelection.font.path.c_str(), faceIndexSuffix.c_str() ) ); + } +} + +void UIFontPickerDialog::selectInitialRows() { + mSizeModel = ItemListModel::create( mSizes ); + mSizeList->setModel( mSizeModel ); + selectSize( mSelection.size ); + if ( mSizeList->getSelection().isEmpty() ) + mSizeList->setSelection( mSizeModel->index( 4 ) ); +} + +void UIFontPickerDialog::selectFamily( const std::string& family ) { + if ( family.empty() || !mFamilyModel ) + return; + for ( size_t i = 0; i < mFamilies.size(); i++ ) { + if ( mFamilies[i] == family ) { + mFamilyList->setSelection( mFamilyModel->index( i ) ); + return; + } + } +} + +void UIFontPickerDialog::selectStyle( const FontDesc& desc ) { + if ( desc.family.empty() || !mStyleModel ) + return; + for ( size_t i = 0; i < mStyles.size(); i++ ) { + const auto& style = mStyles[i].desc; + if ( style.path == desc.path && style.faceIndex == desc.faceIndex && + style.weight == desc.weight && style.italic == desc.italic ) { + mStyleList->setSelection( mStyleModel->index( i ) ); + return; + } + } +} + +void UIFontPickerDialog::selectSize( Uint32 size ) { + if ( !mSizeModel ) + return; + for ( size_t i = 0; i < mSizes.size(); i++ ) { + if ( mSizes[i] == size ) { + mSizeList->setSelection( mSizeModel->index( i ) ); + return; + } + } +} + +void UIFontPickerDialog::emitPicked() { + updateSelectionFromLists(); + if ( mFontPickedCb ) + mFontPickedCb( mSelection ); +} + +void UIFontPickerDialog::pickColor() { + if ( mColorPicker ) { + mColorPicker->getUIWindow()->toFront(); + return; + } + + mColorPicker = UIColorPicker::NewWindow( [this]( Color color ) { + mSelection.color = color; + updatePreview(); + } ); + mColorPicker->setColor( mSelection.color ); + mColorPickerCloseCb = + mColorPicker->getUIWindow()->on( Event::OnWindowClose, [this]( const Event* ) { + mColorPicker = nullptr; + mColorPickerCloseCb = 0; + } ); +} + +void UIFontPickerDialog::browseFont() { + if ( mBrowseDialog ) { + mBrowseDialog->toFront(); + return; + } + + mBrowseDialog = + UIFileDialog::New( UIFileDialog::DefaultFlags, "*.ttf; *.otf; *.woff; *.woff2; *.otb; " + "*.bdf; *.ttc" ); + mBrowseDialog->setWindowFlags( UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL ); + mBrowseDialog->setTitle( i18n( "font_picker_browse_font", "Browse Font" ) ); + mBrowseDialog->setCloseShortcut( KEY_ESCAPE ); + mBrowseDialogOpenCb = mBrowseDialog->on( Event::OpenFile, [this]( const Event* event ) { + auto* dialog = event->getNode()->asType(); + const std::string path( dialog->getFullPath() ); + if ( addExternalFont( path ) ) + dialog->closeWindow(); + } ); + mBrowseDialogCloseCb = mBrowseDialog->on( Event::OnWindowClose, [this]( const Event* ) { + mBrowseDialog = nullptr; + mBrowseDialogOpenCb = 0; + mBrowseDialogCloseCb = 0; + } ); + mBrowseDialog->center(); + mBrowseDialog->show(); +} + +bool UIFontPickerDialog::addExternalFont( const std::string& path, Uint32 faceIndex ) { + if ( path.empty() ) + return false; + + auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& desc ) { + return desc.path == path && desc.faceIndex == faceIndex; + } ); + + if ( found != mFonts.end() ) { + if ( !mSearchInput->getText().empty() ) + mSearchInput->setText( "" ); + setSelectedFont( *found ); + return true; + } + + const std::string fontName( + FileSystem::fileRemoveExtension( FileSystem::fileNameFromPath( path ) ) ); + FontTrueType* font = FontTrueType::New( fontName ); + if ( !font || !font->loadFromFile( path, faceIndex ) ) { + eeSAFE_DELETE( font ); + return false; + } + + FontDesc desc; + desc.family = font->getInfo().family.empty() ? fontName : font->getInfo().family; + desc.path = path; + desc.faceIndex = font->getFaceIndex(); + desc.weight = font->isBold() || font->isBoldItalic() ? FontWeight::Bold : FontWeight::Normal; + desc.italic = font->isItalic() || font->isBoldItalic(); + desc.monospace = font->isIdentifiedAsMonospace(); + eeSAFE_DELETE( font ); + + mFonts.push_back( desc ); + sortFonts(); + + if ( !mSearchInput->getText().empty() ) + mSearchInput->setText( "" ); + updateFamilies(); + setSelectedFont( desc ); + return true; +} + +void UIFontPickerDialog::clearBrowseDialog() { + if ( !mBrowseDialog ) + return; + if ( mBrowseDialogOpenCb ) + mBrowseDialog->removeEventListener( mBrowseDialogOpenCb ); + if ( mBrowseDialogCloseCb ) + mBrowseDialog->removeEventListener( mBrowseDialogCloseCb ); + if ( !SceneManager::instance()->isShuttingDown() ) + mBrowseDialog->closeWindow(); + mBrowseDialog = nullptr; + mBrowseDialogOpenCb = 0; + mBrowseDialogCloseCb = 0; +} + +void UIFontPickerDialog::setSelectedFont( const FontDesc& desc ) { + FontDesc selection = desc; + if ( !desc.path.empty() ) { + auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& font ) { + return font.path == desc.path && font.faceIndex == desc.faceIndex; + } ); + if ( found == mFonts.end() && addExternalFont( desc.path, desc.faceIndex ) ) + return; + if ( found != mFonts.end() ) + selection = *found; + } + + mSelection.font = selection; + if ( selection.monospace && mMonospaceOnly ) + mMonospaceOnly->setChecked( true ); + selectFamily( selection.family ); + updateStyles(); + selectStyle( selection ); + updatePreview(); +} + +void UIFontPickerDialog::setSelectedFont( const std::string& path, Uint32 faceIndex ) { + auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& desc ) { + return desc.path == path && desc.faceIndex == faceIndex; + } ); + if ( found != mFonts.end() ) { + setSelectedFont( *found ); + return; + } + + addExternalFont( path, faceIndex ); +} + +const UIFontSelection& UIFontPickerDialog::getSelection() const { + return mSelection; +} + +void UIFontPickerDialog::setSelection( const UIFontSelection& selection ) { + mSelection = selection; + if ( mAntialiasing ) + mAntialiasing->setChecked( selection.antialiasing ); + if ( mUnderline ) + mUnderline->setChecked( selection.underline ); + if ( mStrikeThrough ) + mStrikeThrough->setChecked( selection.strikeThrough ); + selectSize( selection.size ); + setSelectedFont( selection.font ); +} + +void UIFontPickerDialog::setOnFontPicked( FontPickedCb cb ) { + mFontPickedCb = std::move( cb ); +} + +UIPushButton* UIFontPickerDialog::getButtonOK() const { + return mButtonOK; +} + +UIPushButton* UIFontPickerDialog::getButtonCancel() const { + return mButtonCancel; +} + +UIPushButton* UIFontPickerDialog::getButtonApply() const { + return mButtonApply; +} + +UIPushButton* UIFontPickerDialog::getButtonBrowse() const { + return mButtonBrowse; +} + +UITextInput* UIFontPickerDialog::getSearchInput() const { + return mSearchInput; +} + +UIListView* UIFontPickerDialog::getFamilyList() const { + return mFamilyList; +} + +UIListView* UIFontPickerDialog::getStyleList() const { + return mStyleList; +} + +UIListView* UIFontPickerDialog::getSizeList() const { + return mSizeList; +} + +const KeyBindings::Shortcut& UIFontPickerDialog::getCloseShortcut() const { + return mCloseShortcut; +} + +void UIFontPickerDialog::setCloseShortcut( const KeyBindings::Shortcut& closeShortcut ) { + mCloseShortcut = closeShortcut; +} + +bool UIFontPickerDialog::show() { + bool ret = UIWindow::show(); + if ( mSearchInput && mSearchInput->isEnabled() ) + mSearchInput->setFocus(); + return ret; +} + +void UIFontPickerDialog::onWindowReady() { + UIWindow::onWindowReady(); + if ( mSearchInput && mSearchInput->isEnabled() ) + mSearchInput->setFocus(); +} + +Uint32 UIFontPickerDialog::onKeyUp( const KeyEvent& event ) { + if ( mCloseShortcut && event.getKeyCode() == mCloseShortcut && + ( mCloseShortcut.mod == 0 || ( event.getMod() & mCloseShortcut.mod ) ) ) { + sendCommonEvent( Event::OnDiscard ); + sendCommonEvent( Event::OnCancel ); + closeWindow(); + } + + return UIWindow::onKeyUp( event ); +} + +Uint32 UIFontPickerDialog::onMessage( const NodeMessage* msg ) { + switch ( msg->getMsg() ) { + case NodeMessage::MouseClick: + if ( msg->getFlags() & EE_BUTTON_LMASK ) { + if ( msg->getSender() == mButtonOK ) { + emitPicked(); + sendCommonEvent( Event::OnConfirm ); + closeWindow(); + } else if ( msg->getSender() == mButtonCancel ) { + sendCommonEvent( Event::OnDiscard ); + sendCommonEvent( Event::OnCancel ); + closeWindow(); + } + } + break; + } + + return UIWindow::onMessage( msg ); +} + +}}} // namespace EE::UI::Tools diff --git a/src/eepp/ui/uilinearlayout.cpp b/src/eepp/ui/uilinearlayout.cpp index 0f5838bc2..2eb90a4fd 100644 --- a/src/eepp/ui/uilinearlayout.cpp +++ b/src/eepp/ui/uilinearlayout.cpp @@ -208,12 +208,13 @@ void UILinearLayout::packVertical() { switch ( Font::getHorizontalAlign( widget->getLayoutGravity() ) ) { case UI_HALIGN_CENTER: - pos.x = ( getPixelsSize().getWidth() - mPaddingPx.Left - mPaddingPx.Right - + pos.x = mPaddingPx.Left + + ( getPixelsSize().getWidth() - mPaddingPx.Left - mPaddingPx.Right - widget->getPixelsSize().getWidth() ) / - 2; + 2; break; case UI_HALIGN_RIGHT: - pos.x = getPixelsSize().getWidth() - mPaddingPx.Left - mPaddingPx.Right - + pos.x = getPixelsSize().getWidth() - mPaddingPx.Right - widget->getPixelsSize().getWidth() - widget->getLayoutPixelsMargin().Right; break; @@ -342,12 +343,13 @@ void UILinearLayout::packHorizontal() { switch ( Font::getVerticalAlign( widget->getLayoutGravity() ) ) { case UI_VALIGN_CENTER: - pos.y = ( getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom - + pos.y = mPaddingPx.Top + + ( getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom - widget->getPixelsSize().getHeight() ) / - 2; + 2; break; case UI_VALIGN_BOTTOM: - pos.y = getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom - + pos.y = getPixelsSize().getHeight() - mPaddingPx.Bottom - widget->getPixelsSize().getHeight() - widget->getLayoutPixelsMargin().Bottom; break; diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index da315b2e4..14a78c728 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -816,34 +816,46 @@ void UIWidget::updateAnchors( const Vector2f& sizeChange ) { } void UIWidget::alignAgainstLayout() { - Vector2f pos = mDpPos; + Vector2f pos = mPosition; + const Sizef parentSize( getParent()->getPixelsSize() ); + const Sizef size( getPixelsSize() ); + Rectf parentContentOffset = Rectf::Zero; + + if ( getParent()->isWidget() ) + parentContentOffset = getParent()->asType()->getPixelsContentOffset(); + + const Float parentContentWidth = + parentSize.getWidth() - parentContentOffset.Left - parentContentOffset.Right; + const Float parentContentHeight = + parentSize.getHeight() - parentContentOffset.Top - parentContentOffset.Bottom; switch ( Font::getHorizontalAlign( mLayoutGravity ) ) { case UI_HALIGN_CENTER: - pos.x = ( getParent()->getSize().getWidth() - getSize().getWidth() ) / 2; + pos.x = parentContentOffset.Left + ( parentContentWidth - size.getWidth() ) / 2; break; case UI_HALIGN_RIGHT: - pos.x = getParent()->getSize().getWidth() - getSize().getWidth() - mLayoutMargin.Right; + pos.x = parentSize.getWidth() - parentContentOffset.Right - size.getWidth() - + mLayoutMarginPx.Right; break; case UI_HALIGN_LEFT: - pos.x = mLayoutMargin.Left; + pos.x = parentContentOffset.Left + mLayoutMarginPx.Left; break; } switch ( Font::getVerticalAlign( mLayoutGravity ) ) { case UI_VALIGN_CENTER: - pos.y = ( getParent()->getSize().getHeight() - getSize().getHeight() ) / 2; + pos.y = parentContentOffset.Top + ( parentContentHeight - size.getHeight() ) / 2; break; case UI_VALIGN_BOTTOM: - pos.y = - getParent()->getSize().getHeight() - getSize().getHeight() - mLayoutMargin.Bottom; + pos.y = parentSize.getHeight() - parentContentOffset.Bottom - size.getHeight() - + mLayoutMarginPx.Bottom; break; case UI_VALIGN_TOP: - pos.y = mLayoutMargin.Top; + pos.y = parentContentOffset.Top + mLayoutMarginPx.Top; break; } - setPosition( pos ); + setPixelsPosition( pos ); } void UIWidget::reportStyleStateChange( bool disableAnimations, bool forceReApplyStyles ) { diff --git a/src/examples/ui_font_picker/ui_font_picker.cpp b/src/examples/ui_font_picker/ui_font_picker.cpp new file mode 100644 index 000000000..112343102 --- /dev/null +++ b/src/examples/ui_font_picker/ui_font_picker.cpp @@ -0,0 +1,34 @@ +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 1280, 720, "eepp - UIFontPickerDialog Example" } ); + + std::shared_ptr threadPool( + ThreadPool::createShared( eemax( 4, Sys::getCPUCount() ) ) ); + + if ( !app.getWindow()->isOpen() ) + return EXIT_FAILURE; + + app.getUI()->setThreadPool( threadPool ); + app.getUI()->getUIIconThemeManager()->setCurrentTheme( + IconManager::init( "icons", FontTrueType::New( "icon", "assets/fonts/remixicon.ttf" ), + FontTrueType::New( "nonicons", "assets/fonts/nonicons.ttf" ), + FontTrueType::New( "codicon", "assets/fonts/codicon.ttf" ) ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + dialog->setCloseShortcut( KEY_ESCAPE ); + dialog->setOnFontPicked( []( const UIFontSelection& selection ) { + Log::info( "Selected font: %s (%s, face index: %u, size: %u)", + selection.font.family.c_str(), selection.font.path.c_str(), + selection.font.faceIndex, selection.size ); + } ); + dialog->center(); + dialog->show(); + + app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) + UIWidgetInspector::create( app.getUI() ); + } ); + + return app.run(); +} diff --git a/src/tests/unit_tests/uifontpickerdialog_tests.cpp b/src/tests/unit_tests/uifontpickerdialog_tests.cpp new file mode 100644 index 000000000..79e717a74 --- /dev/null +++ b/src/tests/unit_tests/uifontpickerdialog_tests.cpp @@ -0,0 +1,90 @@ +#include "utest.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; +using namespace EE::UI::Tools; + +template static void pumpUntil( UISceneNode* sceneNode, Predicate predicate ) { + for ( size_t i = 0; i < 1000 && !predicate(); i++ ) { + sceneNode->update( Time::Zero ); + Sys::sleep( Milliseconds( 5 ) ); + } +} + +UTEST( UIFontPickerDialog, PreselectsExternalFontPath ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + const std::string fontPath = Sys::getProcessPath() + "assets/fonts/NotoSansKR-Regular.ttf"; + ASSERT_TRUE( FileSystem::fileExists( fontPath ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + dialog->setSelectedFont( fontPath ); + + EXPECT_STDSTREQ( fontPath, dialog->getSelection().font.path ); + EXPECT_FALSE( dialog->getSelection().font.family.empty() ); + EXPECT_FALSE( dialog->getFamilyList()->getSelection().isEmpty() ); + EXPECT_FALSE( dialog->getStyleList()->getSelection().isEmpty() ); + + UIFontPickerDialog* selectionDialog = UIFontPickerDialog::New(); + UIFontSelection selection; + selection.font.path = fontPath; + selectionDialog->setSelection( selection ); + + EXPECT_STDSTREQ( fontPath, selectionDialog->getSelection().font.path ); + EXPECT_FALSE( selectionDialog->getSelection().font.family.empty() ); +} + +UTEST( UIFontPickerDialog, AsyncLoadPreservesExternalFontPreselection ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + app.getUI()->setThreadPool( ThreadPool::createShared( 1 ) ); + + const std::string fontPath = Sys::getProcessPath() + "assets/fonts/NotoSansKR-Regular.ttf"; + ASSERT_TRUE( FileSystem::fileExists( fontPath ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + EXPECT_FALSE( dialog->getButtonOK()->isEnabled() ); + + dialog->setSelectedFont( fontPath ); + pumpUntil( app.getUI(), [dialog] { return dialog->getButtonOK()->isEnabled(); } ); + + EXPECT_TRUE( dialog->getButtonOK()->isEnabled() ); + EXPECT_STDSTREQ( fontPath, dialog->getSelection().font.path ); + EXPECT_FALSE( dialog->getSelection().font.family.empty() ); + EXPECT_FALSE( dialog->getFamilyList()->getSelection().isEmpty() ); + EXPECT_FALSE( dialog->getStyleList()->getSelection().isEmpty() ); +} + +UTEST( UIFontPickerDialog, ApplyButtonEmitsOnApply ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New( UIFontPickerDialog::DefaultFlags | + UIFontPickerDialog::ShowApplyButton ); + bool applied = false; + bool picked = false; + bool confirmed = false; + dialog->on( Event::OnApply, [&applied]( const Event* ) { applied = true; } ); + dialog->on( Event::OnConfirm, [&confirmed]( const Event* ) { confirmed = true; } ); + dialog->setOnFontPicked( [&picked]( const UIFontSelection& ) { picked = true; } ); + + dialog->getButtonApply()->sendCommonEvent( Event::MouseClick ); + + EXPECT_TRUE( applied ); + EXPECT_TRUE( picked ); + EXPECT_FALSE( confirmed ); +} diff --git a/src/tests/unit_tests/uilayout_tests.cpp b/src/tests/unit_tests/uilayout_tests.cpp new file mode 100644 index 000000000..54eb87017 --- /dev/null +++ b/src/tests/unit_tests/uilayout_tests.cpp @@ -0,0 +1,82 @@ +#include "utest.hpp" +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::UI; + +UTEST( UILinearLayout, AlignAgainstLayoutUsesParentPadding ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UILinearLayout Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + UIWidget* parent = UIWidget::New(); + parent->setPixelsSize( 200, 120 ); + parent->setPadding( { 10, 20, 30, 40 } ); + parent->setParent( app.getUI()->getRoot() ); + + UILinearLayout* layout = UILinearLayout::NewVertical(); + layout->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + layout->setPixelsSize( 50, 20 ); + layout->setLayoutGravity( UI_HALIGN_CENTER | UI_VALIGN_CENTER ); + layout->setParent( parent ); + + app.getUI()->updateDirtyLayouts(); + + EXPECT_NEAR( 65.f, layout->getPixelsPosition().x, 0.1f ); + EXPECT_NEAR( 40.f, layout->getPixelsPosition().y, 0.1f ); +} + +UTEST( UILinearLayout, CrossAxisAlignmentUsesOwnPadding ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UILinearLayout Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + UILinearLayout* vertical = UILinearLayout::NewVertical(); + vertical->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + vertical->setPixelsSize( 200, 120 ); + vertical->setPadding( { 10, 20, 30, 40 } ); + vertical->setParent( app.getUI()->getRoot() ); + + UIWidget* centered = UIWidget::New(); + centered->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + centered->setPixelsSize( 50, 20 ); + centered->setLayoutGravity( UI_HALIGN_CENTER ); + centered->setParent( vertical ); + + UIWidget* right = UIWidget::New(); + right->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + right->setPixelsSize( 50, 20 ); + right->setLayoutGravity( UI_HALIGN_RIGHT ); + right->setParent( vertical ); + + UILinearLayout* horizontal = UILinearLayout::NewHorizontal(); + horizontal->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + horizontal->setPixelsSize( 200, 120 ); + horizontal->setPadding( { 10, 20, 30, 40 } ); + horizontal->setParent( app.getUI()->getRoot() ); + + UIWidget* middle = UIWidget::New(); + middle->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + middle->setPixelsSize( 50, 20 ); + middle->setLayoutGravity( UI_VALIGN_CENTER ); + middle->setParent( horizontal ); + + UIWidget* bottom = UIWidget::New(); + bottom->setLayoutSizePolicy( SizePolicy::Fixed, SizePolicy::Fixed ); + bottom->setPixelsSize( 50, 20 ); + bottom->setLayoutGravity( UI_VALIGN_BOTTOM ); + bottom->setParent( horizontal ); + + app.getUI()->updateDirtyLayouts(); + + EXPECT_NEAR( 65.f, centered->getPixelsPosition().x, 0.1f ); + EXPECT_NEAR( 120.f, right->getPixelsPosition().x, 0.1f ); + EXPECT_NEAR( 40.f, middle->getPixelsPosition().y, 0.1f ); + EXPECT_NEAR( 60.f, bottom->getPixelsPosition().y, 0.1f ); +} From c0c28b9497f6a9d30555a4ada344d19f97e2bcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 29 Jun 2026 21:02:42 -0300 Subject: [PATCH 6/9] Try fix Android runner. --- .github/workflows/eepp-android-build-check.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/eepp-android-build-check.yml b/.github/workflows/eepp-android-build-check.yml index ce8a4205f..682e6f43c 100644 --- a/.github/workflows/eepp-android-build-check.yml +++ b/.github/workflows/eepp-android-build-check.yml @@ -13,15 +13,8 @@ jobs: with: distribution: temurin java-version: 17 - - uses: android-actions/setup-android@v3 - - name: Accept licenses - run: yes | sdkmanager --licenses > /dev/null - - name: Install SDK + NDK components - run: | - sdkmanager "platform-tools" \ - "platforms;android-34" \ - "build-tools;34.0.0" \ - "ndk;29.0.14206865" + - name: Accept Android SDK licenses + run: yes | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null - name: Configure local properties run: | cd projects/android-project From 2810501cba14ae32525805a6a16bbfd0a9ea2d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 30 Jun 2026 00:05:29 -0300 Subject: [PATCH 7/9] Added UIFilePickerDialog support for ecode (SpartanJ/ecode#903) and improved the dialog. --- include/eepp/graphics/fonttruetype.hpp | 4 + include/eepp/graphics/systemfontresolver.hpp | 26 +++ include/eepp/ui/tools/uifontpickerdialog.hpp | 29 ++- src/eepp/graphics/fonttruetype.cpp | 14 ++ src/eepp/ui/tools/uifontpickerdialog.cpp | 200 ++++++++++++++---- .../unit_tests/uifontpickerdialog_tests.cpp | 115 ++++++++++ src/tools/ecode/ecode.cpp | 87 +------- src/tools/ecode/ecode.hpp | 4 + src/tools/ecode/fontpickercontroller.cpp | 161 ++++++++++++++ src/tools/ecode/fontpickercontroller.hpp | 24 +++ 10 files changed, 541 insertions(+), 123 deletions(-) create mode 100644 src/tools/ecode/fontpickercontroller.cpp create mode 100644 src/tools/ecode/fontpickercontroller.hpp diff --git a/include/eepp/graphics/fonttruetype.hpp b/include/eepp/graphics/fonttruetype.hpp index 149798b8f..41b164d42 100644 --- a/include/eepp/graphics/fonttruetype.hpp +++ b/include/eepp/graphics/fonttruetype.hpp @@ -13,6 +13,8 @@ class IOStream; namespace EE { namespace Graphics { +struct FontDesc; + class EE_API FontTrueType : public Font { public: static FontTrueType* New( const std::string& FontName ); @@ -35,6 +37,8 @@ class EE_API FontTrueType : public Font { const Font::Info& getInfo() const; + bool getFontDesc( FontDesc& desc ) const; + const Uint32& getFaceIndex() const { return mFaceIndex; } Glyph getGlyph( Uint32 codePoint, unsigned int characterSize, bool bold, bool italic, diff --git a/include/eepp/graphics/systemfontresolver.hpp b/include/eepp/graphics/systemfontresolver.hpp index 5b3577d9a..358f1e9d1 100644 --- a/include/eepp/graphics/systemfontresolver.hpp +++ b/include/eepp/graphics/systemfontresolver.hpp @@ -62,6 +62,32 @@ struct FontDesc { FontStretch stretch{ FontStretch::Normal }; bool italic{ false }; bool monospace{ false }; + + std::string getFileKey() const { return path + "#" + String::toString( faceIndex ); } + + std::string getStyleKey() const { + return String::toLower( family ) + "#" + String::toString( static_cast( weight ) ) + + "#" + String::toString( static_cast( stretch ) ) + "#" + + String::toString( italic ? 1 : 0 ) + "#" + String::toString( faceIndex ); + } + + bool sameFile( const FontDesc& other ) const { + return path == other.path && faceIndex == other.faceIndex; + } + + bool sameFile( const std::string& otherPath, Uint32 otherFaceIndex = 0 ) const { + return path == otherPath && faceIndex == otherFaceIndex; + } + + bool sameStyle( const FontDesc& other ) const { return getStyleKey() == other.getStyleKey(); } + + bool operator==( const FontDesc& other ) const { + return family == other.family && path == other.path && faceIndex == other.faceIndex && + weight == other.weight && stretch == other.stretch && italic == other.italic && + monospace == other.monospace; + } + + bool operator!=( const FontDesc& other ) const { return !( *this == other ); } }; class EE_API SystemFontResolver { diff --git a/include/eepp/ui/tools/uifontpickerdialog.hpp b/include/eepp/ui/tools/uifontpickerdialog.hpp index a8cce3b35..58d1858ed 100644 --- a/include/eepp/ui/tools/uifontpickerdialog.hpp +++ b/include/eepp/ui/tools/uifontpickerdialog.hpp @@ -33,11 +33,20 @@ struct UIFontSelection { bool strikeThrough{ false }; bool antialiasing{ true }; Color color{ Color::White }; + + bool operator==( const UIFontSelection& other ) const { + return font == other.font && size == other.size && underline == other.underline && + strikeThrough == other.strikeThrough && antialiasing == other.antialiasing && + color == other.color; + } + + bool operator!=( const UIFontSelection& other ) const { return !( *this == other ); } }; class EE_API UIFontPickerDialog : public UIWindow { public: using FontPickedCb = std::function; + using FontSelectionChangedCb = std::function; enum Flags { MonospaceOnly = 1 << 0, @@ -73,6 +82,8 @@ class EE_API UIFontPickerDialog : public UIWindow { void setOnFontPicked( FontPickedCb cb ); + void setOnFontSelectionChanged( FontSelectionChangedCb cb ); + UIPushButton* getButtonOK() const; UIPushButton* getButtonCancel() const; @@ -100,10 +111,13 @@ class EE_API UIFontPickerDialog : public UIWindow { KeyBindings::Shortcut mCloseShortcut{ KEY_UNKNOWN }; UIFontSelection mSelection; FontPickedCb mFontPickedCb; + FontSelectionChangedCb mFontSelectionChangedCb; std::vector mFonts; std::vector mFamilies; std::vector mStyles; std::vector mSizes; + UnorderedSet mLoadedFontKeys; + UnorderedMap mFontTags; std::shared_ptr mFamilyModel; std::shared_ptr mStyleModel; std::shared_ptr mSizeModel; @@ -125,13 +139,14 @@ class EE_API UIFontPickerDialog : public UIWindow { UICheckBox* mAntialiasing{ nullptr }; UICheckBox* mUnderline{ nullptr }; UICheckBox* mStrikeThrough{ nullptr }; - UIPushButton* mColorButton{ nullptr }; + UIWidget* mColorButton{ nullptr }; UIPushButton* mButtonOK{ nullptr }; UIPushButton* mButtonCancel{ nullptr }; UIPushButton* mButtonApply{ nullptr }; UIPushButton* mButtonBrowse{ nullptr }; bool mUpdating{ false }; bool mLoadingFonts{ false }; + bool mSelectionColorExplicit{ false }; Uint64 mLoadFontsTaskId{ 0 }; struct LoadFontsRequest { @@ -156,13 +171,21 @@ class EE_API UIFontPickerDialog : public UIWindow { void sortFonts(); + void mergeFontManagerFonts( std::vector& fonts ); + + void updateFontTags(); + void setFontsLoading( bool loading ); + void updateDefaultSelectionColor(); + void updateFamilies(); void updateStyles(); - void updateSelectionFromLists(); + void updateSelectionFromLists( bool notify = true ); + + void emitSelectionChanged(); void updatePreview(); @@ -170,6 +193,8 @@ class EE_API UIFontPickerDialog : public UIWindow { void selectFamily( const std::string& family ); + void selectRegularStyle(); + void selectStyle( const FontDesc& desc ); void selectSize( Uint32 size ); diff --git a/src/eepp/graphics/fonttruetype.cpp b/src/eepp/graphics/fonttruetype.cpp index c8ed8d345..b4b37b92c 100644 --- a/src/eepp/graphics/fonttruetype.cpp +++ b/src/eepp/graphics/fonttruetype.cpp @@ -545,6 +545,20 @@ const FontTrueType::Info& FontTrueType::getInfo() const { return mInfo; } +bool FontTrueType::getFontDesc( FontDesc& desc ) const { + if ( !loaded() || mInfo.filename.empty() || mInfo.fontpath.empty() ) + return false; + + desc = FontDesc{}; + desc.family = mInfo.family.empty() ? getName() : mInfo.family; + desc.path = mInfo.fontpath + mInfo.filename; + desc.faceIndex = getFaceIndex(); + desc.weight = isBold() || isBoldItalic() ? FontWeight::Bold : FontWeight::Normal; + desc.italic = isItalic() || isBoldItalic(); + desc.monospace = isIdentifiedAsMonospace(); + return !desc.family.empty() && !desc.path.empty(); +} + void FontTrueType::updateFontInternalId() { auto fontInternalId = fontsInternalIds.find( mInfo.family ); if ( fontsInternalIds.end() == fontInternalId ) { diff --git a/src/eepp/ui/tools/uifontpickerdialog.cpp b/src/eepp/ui/tools/uifontpickerdialog.cpp index 7fc3794d6..64d594144 100644 --- a/src/eepp/ui/tools/uifontpickerdialog.cpp +++ b/src/eepp/ui/tools/uifontpickerdialog.cpp @@ -23,6 +23,7 @@ #include #include #include +#include using namespace EE::UI::Abstract; using namespace EE::UI::Models; @@ -60,6 +61,9 @@ static const char* FONT_PICKER_STYLE = R"css( } #font_picker .color_button { min-width: 48dp; + border-color: var(--button-border); + border-radius: var(--button-radius); + border-width: var(--border-width); } #font_picker .options_panel { background-color: var(--list-back); @@ -108,7 +112,7 @@ static const char* FONT_PICKER_LAYOUT = R"xml( - + @@ -151,23 +155,57 @@ static Uint64 loadFontsTaskTag( const UIFontPickerDialog* dialog ) { return reinterpret_cast( dialog ); } -static std::string styleLabel( const FontDesc& desc ) { - std::string label; - if ( desc.weight == FontWeight::Normal ) { - label = desc.italic ? "Italic" : "Regular"; - } else { - label = Text::fontWeightToString( desc.weight ); - std::replace( label.begin(), label.end(), '-', ' ' ); - if ( !label.empty() ) { - label[0] = static_cast( std::toupper( label[0] ) ); - for ( size_t i = 1; i < label.size(); i++ ) { - if ( label[i - 1] == ' ' ) - label[i] = static_cast( std::toupper( label[i] ) ); - } - } - if ( desc.italic ) - label += " Italic"; +static std::string titleCaseStyleName( std::string label ) { + std::replace( label.begin(), label.end(), '-', ' ' ); + for ( size_t i = 0; i < label.size(); i++ ) { + if ( i == 0 || label[i - 1] == ' ' ) + label[i] = static_cast( std::toupper( label[i] ) ); } + return label; +} + +static std::string stretchLabel( FontStretch stretch ) { + switch ( stretch ) { + case FontStretch::UltraCondensed: + return "Ultra Condensed"; + case FontStretch::ExtraCondensed: + return "Extra Condensed"; + case FontStretch::Condensed: + return "Condensed"; + case FontStretch::SemiCondensed: + return "Semi Condensed"; + case FontStretch::Normal: + return ""; + case FontStretch::SemiExpanded: + return "Semi Expanded"; + case FontStretch::Expanded: + return "Expanded"; + case FontStretch::ExtraExpanded: + return "Extra Expanded"; + case FontStretch::UltraExpanded: + return "Ultra Expanded"; + } + return ""; +} + +static std::string styleLabel( const FontDesc& desc ) { + std::string label( stretchLabel( desc.stretch ) ); + + if ( desc.weight != FontWeight::Normal ) { + if ( !label.empty() ) + label += " "; + label += titleCaseStyleName( Text::fontWeightToString( desc.weight ) ); + } + + if ( desc.italic ) { + if ( !label.empty() ) + label += " "; + label += "Italic"; + } + + if ( label.empty() ) + label = "Regular"; + if ( desc.faceIndex != 0 ) label += " #" + String::toString( desc.faceIndex ); return label; @@ -209,6 +247,8 @@ UIFontPickerDialog::UIFontPickerDialog( Uint32 flags ) : UIWindow(), mFlags( fla loadWidgets(); if ( getUISceneNode()->getUIThemeManager()->getDefaultTheme() ) setTheme( getUISceneNode()->getUIThemeManager()->getDefaultTheme() ); + else + updateDefaultSelectionColor(); selectInitialRows(); loadFonts(); } @@ -259,6 +299,7 @@ void UIFontPickerDialog::setTheme( UITheme* theme ) { } onThemeLoaded(); + updateDefaultSelectionColor(); } void UIFontPickerDialog::loadWidgets() { @@ -397,16 +438,17 @@ void UIFontPickerDialog::loadFonts() { void UIFontPickerDialog::setFonts( std::vector fonts ) { FontDesc selectedFont = mSelection.font; + mergeFontManagerFonts( fonts ); for ( const auto& font : mFonts ) { - auto found = std::find_if( fonts.begin(), fonts.end(), [&]( const auto& desc ) { - return desc.path == font.path && desc.faceIndex == font.faceIndex; - } ); - if ( found == fonts.end() ) + if ( std::find_if( fonts.begin(), fonts.end(), [&]( const FontDesc& desc ) { + return desc.sameFile( font ); + } ) == fonts.end() ) fonts.push_back( font ); } mFonts = std::move( fonts ); sortFonts(); + updateFontTags(); setFontsLoading( false ); updateFamilies(); if ( !selectedFont.path.empty() || !selectedFont.family.empty() ) @@ -429,6 +471,42 @@ void UIFontPickerDialog::sortFonts() { } ); } +void UIFontPickerDialog::mergeFontManagerFonts( std::vector& fonts ) { + FontManager::instance()->each( [&]( const auto& res ) { + if ( res.second == nullptr || res.second->getType() != FontType::TTF ) + return; + + FontDesc desc; + if ( !static_cast( res.second )->getFontDesc( desc ) ) + return; + + mLoadedFontKeys.insert( desc.getFileKey() ); + if ( std::find_if( fonts.begin(), fonts.end(), [&]( const FontDesc& font ) { + return font.sameFile( desc ); + } ) == fonts.end() ) + fonts.push_back( desc ); + } ); +} + +void UIFontPickerDialog::updateFontTags() { + mFontTags.clear(); + + std::unordered_map styleCounts; + for ( const auto& font : mFonts ) + styleCounts[font.getStyleKey()]++; + + for ( const auto& font : mFonts ) { + const auto styleCountIt = styleCounts.find( font.getStyleKey() ); + if ( styleCountIt == styleCounts.end() || styleCountIt->second < 2 ) + continue; + if ( mLoadedFontKeys.find( font.getFileKey() ) == mLoadedFontKeys.end() ) + continue; + + const std::string fileName( FileSystem::fileNameFromPath( font.path ) ); + mFontTags[font.getFileKey()] = fileName.empty() ? font.path : fileName; + } +} + void UIFontPickerDialog::setFontsLoading( bool loading ) { mLoadingFonts = loading; if ( mFontContent ) @@ -445,6 +523,11 @@ void UIFontPickerDialog::setFontsLoading( bool loading ) { mButtonApply->setEnabled( !loading && ( mFlags & ShowApplyButton ) != 0 ); } +void UIFontPickerDialog::updateDefaultSelectionColor() { + if ( !mSelectionColorExplicit && mPreviewText ) + mSelection.color = mPreviewText->getFontColor(); +} + bool UIFontPickerDialog::wantsMonospaceOnly() const { return mMonospaceOnly && mMonospaceOnly->isChecked(); } @@ -488,8 +571,13 @@ void UIFontPickerDialog::updateStyles() { if ( row >= 0 && row < static_cast( mFamilies.size() ) ) { const std::string& family = mFamilies[row]; for ( const auto& font : mFonts ) { - if ( font.family == family && ( !wantsMonospaceOnly() || font.monospace ) ) - mStyles.push_back( { styleLabel( font ), font } ); + if ( font.family == family && ( !wantsMonospaceOnly() || font.monospace ) ) { + std::string label( styleLabel( font ) ); + auto tagIt = mFontTags.find( font.getFileKey() ); + if ( tagIt != mFontTags.end() ) + label += " [" + tagIt->second + "]"; + mStyles.push_back( { label, font } ); + } } } } @@ -500,13 +588,18 @@ void UIFontPickerDialog::updateStyles() { mUpdating = false; if ( !mStyles.empty() ) { - selectStyle( mSelection.font ); + const std::string selectedFamily( mStyles.front().desc.family ); + if ( selectedFamily == mSelection.font.family ) + selectStyle( mSelection.font ); + else + selectRegularStyle(); if ( mStyleList->getSelection().isEmpty() ) mStyleList->setSelection( mStyleModel->index( 0 ) ); } } -void UIFontPickerDialog::updateSelectionFromLists() { +void UIFontPickerDialog::updateSelectionFromLists( bool notify ) { + UIFontSelection previousSelection = mSelection; if ( !mStyleList->getSelection().isEmpty() ) { const Int64 row = mStyleList->getSelection().first().row(); if ( row >= 0 && row < static_cast( mStyles.size() ) ) @@ -520,6 +613,15 @@ void UIFontPickerDialog::updateSelectionFromLists() { mSelection.antialiasing = mAntialiasing->isChecked(); mSelection.underline = mUnderline->isChecked(); mSelection.strikeThrough = mStrikeThrough->isChecked(); + + if ( notify && previousSelection != mSelection ) + emitSelectionChanged(); +} + +void UIFontPickerDialog::emitSelectionChanged() { + if ( mFontSelectionChangedCb ) + mFontSelectionChangedCb( mSelection ); + sendCommonEvent( Event::OnValueChange ); } void UIFontPickerDialog::updatePreview() { @@ -574,13 +676,25 @@ void UIFontPickerDialog::selectFamily( const std::string& family ) { } } +void UIFontPickerDialog::selectRegularStyle() { + if ( !mStyleModel ) + return; + + for ( size_t i = 0; i < mStyles.size(); i++ ) { + const auto& style = mStyles[i].desc; + if ( style.weight == FontWeight::Normal && !style.italic ) { + mStyleList->setSelection( mStyleModel->index( i ) ); + return; + } + } +} + void UIFontPickerDialog::selectStyle( const FontDesc& desc ) { if ( desc.family.empty() || !mStyleModel ) return; for ( size_t i = 0; i < mStyles.size(); i++ ) { const auto& style = mStyles[i].desc; - if ( style.path == desc.path && style.faceIndex == desc.faceIndex && - style.weight == desc.weight && style.italic == desc.italic ) { + if ( style.sameFile( desc ) && style.sameStyle( desc ) ) { mStyleList->setSelection( mStyleModel->index( i ) ); return; } @@ -612,9 +726,11 @@ void UIFontPickerDialog::pickColor() { mColorPicker = UIColorPicker::NewWindow( [this]( Color color ) { mSelection.color = color; + mSelectionColorExplicit = true; updatePreview(); } ); mColorPicker->setColor( mSelection.color ); + mColorPicker->getUIWindow()->center(); mColorPickerCloseCb = mColorPicker->getUIWindow()->on( Event::OnWindowClose, [this]( const Event* ) { mColorPicker = nullptr; @@ -654,7 +770,7 @@ bool UIFontPickerDialog::addExternalFont( const std::string& path, Uint32 faceIn return false; auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& desc ) { - return desc.path == path && desc.faceIndex == faceIndex; + return desc.sameFile( path, faceIndex ); } ); if ( found != mFonts.end() ) { @@ -673,16 +789,16 @@ bool UIFontPickerDialog::addExternalFont( const std::string& path, Uint32 faceIn } FontDesc desc; - desc.family = font->getInfo().family.empty() ? fontName : font->getInfo().family; - desc.path = path; - desc.faceIndex = font->getFaceIndex(); - desc.weight = font->isBold() || font->isBoldItalic() ? FontWeight::Bold : FontWeight::Normal; - desc.italic = font->isItalic() || font->isBoldItalic(); - desc.monospace = font->isIdentifiedAsMonospace(); + if ( !font->getFontDesc( desc ) ) { + eeSAFE_DELETE( font ); + return false; + } eeSAFE_DELETE( font ); + mLoadedFontKeys.insert( desc.getFileKey() ); mFonts.push_back( desc ); sortFonts(); + updateFontTags(); if ( !mSearchInput->getText().empty() ) mSearchInput->setText( "" ); @@ -706,11 +822,11 @@ void UIFontPickerDialog::clearBrowseDialog() { } void UIFontPickerDialog::setSelectedFont( const FontDesc& desc ) { + UIFontSelection previousSelection = mSelection; FontDesc selection = desc; if ( !desc.path.empty() ) { - auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& font ) { - return font.path == desc.path && font.faceIndex == desc.faceIndex; - } ); + auto found = std::find_if( mFonts.begin(), mFonts.end(), + [&]( const auto& font ) { return font.sameFile( desc ); } ); if ( found == mFonts.end() && addExternalFont( desc.path, desc.faceIndex ) ) return; if ( found != mFonts.end() ) @@ -723,12 +839,15 @@ void UIFontPickerDialog::setSelectedFont( const FontDesc& desc ) { selectFamily( selection.family ); updateStyles(); selectStyle( selection ); + updateSelectionFromLists( false ); + if ( previousSelection != mSelection ) + emitSelectionChanged(); updatePreview(); } void UIFontPickerDialog::setSelectedFont( const std::string& path, Uint32 faceIndex ) { auto found = std::find_if( mFonts.begin(), mFonts.end(), [&]( const auto& desc ) { - return desc.path == path && desc.faceIndex == faceIndex; + return desc.sameFile( path, faceIndex ); } ); if ( found != mFonts.end() ) { setSelectedFont( *found ); @@ -744,6 +863,7 @@ const UIFontSelection& UIFontPickerDialog::getSelection() const { void UIFontPickerDialog::setSelection( const UIFontSelection& selection ) { mSelection = selection; + mSelectionColorExplicit = true; if ( mAntialiasing ) mAntialiasing->setChecked( selection.antialiasing ); if ( mUnderline ) @@ -758,6 +878,10 @@ void UIFontPickerDialog::setOnFontPicked( FontPickedCb cb ) { mFontPickedCb = std::move( cb ); } +void UIFontPickerDialog::setOnFontSelectionChanged( FontSelectionChangedCb cb ) { + mFontSelectionChangedCb = std::move( cb ); +} + UIPushButton* UIFontPickerDialog::getButtonOK() const { return mButtonOK; } diff --git a/src/tests/unit_tests/uifontpickerdialog_tests.cpp b/src/tests/unit_tests/uifontpickerdialog_tests.cpp index 79e717a74..006c7ff87 100644 --- a/src/tests/unit_tests/uifontpickerdialog_tests.cpp +++ b/src/tests/unit_tests/uifontpickerdialog_tests.cpp @@ -1,6 +1,10 @@ #include "utest.hpp" +#include +#include +#include #include #include +#include #include #include #include @@ -9,8 +13,33 @@ using namespace EE; using namespace EE::UI; +using namespace EE::UI::Abstract; using namespace EE::UI::Tools; +static std::string expectedStretchLabel( FontStretch stretch ) { + switch ( stretch ) { + case FontStretch::UltraCondensed: + return "Ultra Condensed"; + case FontStretch::ExtraCondensed: + return "Extra Condensed"; + case FontStretch::Condensed: + return "Condensed"; + case FontStretch::SemiCondensed: + return "Semi Condensed"; + case FontStretch::Normal: + return ""; + case FontStretch::SemiExpanded: + return "Semi Expanded"; + case FontStretch::Expanded: + return "Expanded"; + case FontStretch::ExtraExpanded: + return "Extra Expanded"; + case FontStretch::UltraExpanded: + return "Ultra Expanded"; + } + return ""; +} + template static void pumpUntil( UISceneNode* sceneNode, Predicate predicate ) { for ( size_t i = 0; i < 1000 && !predicate(); i++ ) { sceneNode->update( Time::Zero ); @@ -67,6 +96,92 @@ UTEST( UIFontPickerDialog, AsyncLoadPreservesExternalFontPreselection ) { EXPECT_FALSE( dialog->getStyleList()->getSelection().isEmpty() ); } +UTEST( UIFontPickerDialog, DefaultColorComesFromTheme ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + + EXPECT_TRUE( dialog->getSelection().color != Color::White ); + EXPECT_TRUE( dialog->getSelection().color.a != 0 ); +} + +UTEST( UIFontPickerDialog, IncludesDiskFontsLoadedInFontManager ) { + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + const std::string fontPath = Sys::getProcessPath() + "assets/fonts/NotoSansKR-Regular.ttf"; + ASSERT_TRUE( FileSystem::fileExists( fontPath ) ); + + FontTrueType* font = FontTrueType::New( "UIFontPickerDialogManagedNotoSansKR", fontPath ); + ASSERT_TRUE( font != nullptr ); + ASSERT_TRUE( font->loaded() ); + ASSERT_FALSE( font->getInfo().family.empty() ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + const ModelIndex familyIndex = dialog->getFamilyList()->findRowWithText( + font->getInfo().family, true, UIAbstractView::FindRowWithTextMatchKind::Equals ); + ASSERT_TRUE( familyIndex.isValid() ); + dialog->getFamilyList()->setSelection( familyIndex ); + + bool foundManagedFont = false; + Model* styleModel = dialog->getStyleList()->getModel(); + ASSERT_TRUE( styleModel != nullptr ); + for ( size_t row = 0; row < styleModel->rowCount(); row++ ) { + dialog->getStyleList()->setSelection( styleModel->index( row ) ); + if ( dialog->getSelection().font.path == fontPath && + dialog->getSelection().font.faceIndex == font->getFaceIndex() ) { + foundManagedFont = true; + break; + } + } + + EXPECT_TRUE( foundManagedFont ); +} + +UTEST( UIFontPickerDialog, StyleLabelsIncludeFontStretch ) { + std::vector fonts = SystemFontResolver::instance()->enumerate(); + auto fontIt = std::find_if( fonts.begin(), fonts.end(), []( const FontDesc& font ) { + return font.stretch != FontStretch::Normal && !font.family.empty() && !font.path.empty(); + } ); + if ( fontIt == fonts.end() ) + UTEST_SKIP( "no non-normal font stretch available" ); + + UIApplication app( + WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, + WindowBackend::Default, 32 ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + + UIFontPickerDialog* dialog = UIFontPickerDialog::New(); + const ModelIndex familyIndex = dialog->getFamilyList()->findRowWithText( + fontIt->family, true, UIAbstractView::FindRowWithTextMatchKind::Equals ); + ASSERT_TRUE( familyIndex.isValid() ); + dialog->getFamilyList()->setSelection( familyIndex ); + + const std::string stretchText( expectedStretchLabel( fontIt->stretch ) ); + ASSERT_FALSE( stretchText.empty() ); + + bool foundStyle = false; + Model* styleModel = dialog->getStyleList()->getModel(); + ASSERT_TRUE( styleModel != nullptr ); + for ( size_t row = 0; row < styleModel->rowCount(); row++ ) { + ModelIndex index = styleModel->index( row ); + dialog->getStyleList()->setSelection( index ); + if ( dialog->getSelection().font.sameFile( *fontIt ) && + dialog->getSelection().font.sameStyle( *fontIt ) ) { + foundStyle = true; + EXPECT_TRUE( index.data().toString().find( stretchText ) != std::string::npos ); + break; + } + } + + EXPECT_TRUE( foundStyle ); +} + UTEST( UIFontPickerDialog, ApplyButtonEmitsOnApply ) { UIApplication app( WindowSettings( 320, 240, "eepp - UIFontPickerDialog Test", WindowStyle::Default, diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 31ac7a91c..dee51bd74 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -3,6 +3,7 @@ #include "customwidgets.hpp" #include "datetimecontroller.hpp" #include "featureshealth.hpp" +#include "fontpickercontroller.hpp" #include "keybindingshelper.hpp" #include "pathhelper.hpp" #include "settingsactions.hpp" @@ -442,89 +443,8 @@ void App::openFolderDialog() { void App::openFontDialog( std::string& fontPath, bool loadingMonoFont, bool terminalFont, std::function onFinish ) { - std::string absoluteFontPath( fontPath ); - if ( FileSystem::isRelativePath( absoluteFontPath ) ) - absoluteFontPath = mResPath + fontPath; - UIFileDialog* dialog = UIFileDialog::New( - UIFileDialog::DefaultFlags | - ( mConfig.ui.nativeFileDialogs ? UIFileDialog::UseNativeFileDialog : 0 ), - "*.ttf; *.otf; *.woff; *.woff2; *.otb; *.bdf; *.ttc", - FileSystem::fileRemoveFileName( absoluteFontPath ) ); - if ( dialog->getMultiView() ) { - ModelIndex index = dialog->getMultiView()->getListView()->findRowWithText( - FileSystem::fileNameFromPath( fontPath ), true, - UIAbstractView::FindRowWithTextMatchKind::Equals ); - if ( index.isValid() ) - dialog->runOnMainThread( - [dialog, index]() { dialog->getMultiView()->setSelection( index ); } ); - } - dialog->setWindowFlags( UI_WIN_DEFAULT_FLAGS | UI_WIN_MAXIMIZE_BUTTON | UI_WIN_MODAL ); - dialog->setTitle( i18n( "select_font_file", "Select Font File" ) ); - dialog->setCloseShortcut( KEY_ESCAPE ); - dialog->setSingleClickNavigation( mConfig.editor.singleClickNavigation ); - dialog->on( Event::OnWindowClose, [this]( const Event* ) { - if ( App::instance() && mSplitter && mSplitter->getCurWidget() && - !SceneManager::instance()->isShuttingDown() ) { - mSplitter->getCurWidget()->setFocus(); - } - } ); - dialog->on( Event::OpenFile, [this, &fontPath, loadingMonoFont, terminalFont, - onFinish]( const Event* event ) { - auto newPath = event->getNode()->asType()->getFullPath(); - if ( String::startsWith( newPath, mResPath ) ) - newPath = newPath.substr( mResPath.size() ); - if ( fontPath != newPath ) { - if ( !loadingMonoFont ) { - fontPath = newPath; - if ( onFinish ) - onFinish(); - return; - } - auto fontName = - FileSystem::fileRemoveExtension( FileSystem::fileNameFromPath( newPath ) ); - FontTrueType* fontMono = loadFont( fontName, newPath ); - if ( fontMono ) { - auto loadMonoFont = [this, &fontPath, newPath, - terminalFont]( FontTrueType* fontMono ) { - fontPath = newPath; - if ( terminalFont ) - mTerminalFont = fontMono; - else - mFontMono = fontMono; - fontMono->setEnableDynamicMonospace( true ); - fontMono->setBoldAdvanceSameAsRegular( true ); - FontFamily::loadFromRegular( fontMono ); - if ( mSplitter ) { - if ( terminalFont ) { - mSplitter->forEachWidgetType( - UI_TYPE_TERMINAL, [fontMono]( UIWidget* term ) { - term->asType()->setFont( fontMono ); - } ); - } else { - mSplitter->forEachEditor( [fontMono]( UICodeEditor* editor ) { - editor->setFont( fontMono ); - } ); - - if ( auto buildOutputEditor = - mUISceneNode->find( "build_output_output" ) ) - buildOutputEditor->setFont( fontMono ); - - if ( auto appOutputEditor = - mUISceneNode->find( "app_output_output" ) ) - appOutputEditor->setFont( fontMono ); - - if ( mConfig.ui.editorFontInInputFields ) - updateInputFonts(); - } - } - }; - - loadMonoFont( fontMono ); - } - } - } ); - dialog->center(); - dialog->show(); + mFontPickerController->openFontDialog( fontPath, loadingMonoFont, terminalFont, + std::move( onFinish ) ); } void App::updateInputFonts() { @@ -1109,6 +1029,7 @@ App::App( const size_t& jobs, const std::vector& args ) : mThreadPool( ThreadPool::createShared( jobs > 0 ? jobs : eemax( 4, Sys::getCPUCount() ) ) ), mDateTimeController( std::make_unique( this ) ), + mFontPickerController( std::make_unique( this ) ), mSettingsActions( std::make_unique( this ) ) {} static void fsRemoveAll( const std::string& fpath ) { diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 587fb6c1e..a6f20ec70 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -32,6 +32,7 @@ class AutoCompletePlugin; class LinterPlugin; class FormatterPlugin; class DateTimeController; +class FontPickerController; class SettingsMenu; class UITreeViewFS; @@ -655,6 +656,8 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { size_t getMenuIconSize() const { return mMenuIconSize; } protected: + friend class FontPickerController; + std::vector mArgs; EE::Window::Window* mWindow{ nullptr }; UISceneNode* mUISceneNode{ nullptr }; @@ -750,6 +753,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { std::unique_ptr mPluginManager; std::unique_ptr mSettings; std::unique_ptr mDateTimeController; + std::unique_ptr mFontPickerController; std::string mFileToOpen; UITheme* mTheme{ nullptr }; UIStatusBar* mStatusBar{ nullptr }; diff --git a/src/tools/ecode/fontpickercontroller.cpp b/src/tools/ecode/fontpickercontroller.cpp new file mode 100644 index 000000000..81a5a8ed8 --- /dev/null +++ b/src/tools/ecode/fontpickercontroller.cpp @@ -0,0 +1,161 @@ +#include "fontpickercontroller.hpp" +#include "ecode.hpp" +#include +#include +#include +#include +#include +#include + +namespace ecode { + +struct MonospaceFontPreview { + std::string originalPath; + std::string previewPath; + FontTrueType* originalFont{ nullptr }; + bool confirmed{ false }; + UnorderedMap loadedFonts; +}; + +void FontPickerController::openFontDialog( std::string& fontPath, bool loadingMonoFont, + bool terminalFont, std::function onFinish ) { + std::string absoluteFontPath( fontPath ); + if ( FileSystem::isRelativePath( absoluteFontPath ) ) + absoluteFontPath = mApp->resPath() + fontPath; + + const auto normalizedFontPath = [this]( std::string path ) { + if ( String::startsWith( path, mApp->resPath() ) ) + path = path.substr( mApp->resPath().size() ); + return path; + }; + + const auto applyMonospaceFont = [this, terminalFont]( FontTrueType* fontMono ) { + if ( !fontMono ) + return; + + if ( terminalFont ) + mApp->mTerminalFont = fontMono; + else + mApp->mFontMono = fontMono; + + fontMono->setEnableDynamicMonospace( true ); + fontMono->setBoldAdvanceSameAsRegular( true ); + FontFamily::loadFromRegular( fontMono ); + + if ( !mApp->getSplitter() ) + return; + + if ( terminalFont ) { + mApp->getSplitter()->forEachWidgetType( UI_TYPE_TERMINAL, [fontMono]( UIWidget* term ) { + term->asType()->setFont( fontMono ); + } ); + } else { + mApp->getSplitter()->forEachEditor( + [fontMono]( UICodeEditor* editor ) { editor->setFont( fontMono ); } ); + + if ( auto buildOutputEditor = + mApp->uiSceneNode()->find( "build_output_output" ) ) + buildOutputEditor->setFont( fontMono ); + + if ( auto appOutputEditor = + mApp->uiSceneNode()->find( "app_output_output" ) ) + appOutputEditor->setFont( fontMono ); + + if ( mApp->getConfig().ui.editorFontInInputFields ) + mApp->updateInputFonts(); + } + }; + + std::shared_ptr preview; + if ( loadingMonoFont ) { + preview = std::make_shared(); + preview->originalPath = normalizedFontPath( fontPath ); + preview->previewPath = preview->originalPath; + preview->originalFont = static_cast( terminalFont ? mApp->getTerminalFont() + : mApp->getFontMono() ); + } + + const auto loadPreviewFont = [this, preview]( const std::string& newPath ) -> FontTrueType* { + if ( !preview ) + return nullptr; + + auto found = preview->loadedFonts.find( newPath ); + if ( found != preview->loadedFonts.end() ) + return found->second; + + auto fontName = FileSystem::fileRemoveExtension( FileSystem::fileNameFromPath( newPath ) ); + FontTrueType* fontMono = mApp->loadFont( fontName, newPath ); + if ( fontMono ) + preview->loadedFonts[newPath] = fontMono; + return fontMono; + }; + + const Uint32 flags = UIFontPickerDialog::DefaultFlags | + ( loadingMonoFont ? UIFontPickerDialog::MonospaceOnly : 0 ); + UIFontPickerDialog* dialog = UIFontPickerDialog::New( flags ); + dialog->setTitle( mApp->i18n( "select_font", "Select Font" ) ); + dialog->setCloseShortcut( KEY_ESCAPE ); + dialog->on( Event::OnWindowClose, [this, preview, applyMonospaceFont]( const Event* ) { + if ( preview && !preview->confirmed ) + applyMonospaceFont( preview->originalFont ); + + if ( App::instance() && mApp->getSplitter() && mApp->getSplitter()->getCurWidget() && + !SceneManager::instance()->isShuttingDown() ) { + mApp->getSplitter()->getCurWidget()->setFocus(); + } + } ); + if ( loadingMonoFont ) { + dialog->setOnFontSelectionChanged( + [preview, normalizedFontPath, loadPreviewFont, + applyMonospaceFont]( const UIFontSelection& selection ) { + auto newPath = normalizedFontPath( selection.font.path ); + if ( !preview || newPath.empty() || preview->previewPath == newPath ) + return; + + if ( newPath == preview->originalPath ) { + applyMonospaceFont( preview->originalFont ); + preview->previewPath = newPath; + return; + } + + FontTrueType* fontMono = loadPreviewFont( newPath ); + if ( fontMono ) { + applyMonospaceFont( fontMono ); + preview->previewPath = newPath; + } + } ); + } + dialog->setOnFontPicked( [&fontPath, loadingMonoFont, onFinish, preview, normalizedFontPath, + loadPreviewFont, + applyMonospaceFont]( const UIFontSelection& selection ) { + auto newPath = normalizedFontPath( selection.font.path ); + if ( newPath.empty() ) + return; + if ( fontPath != newPath ) { + if ( !loadingMonoFont ) { + fontPath = newPath; + if ( onFinish ) + onFinish(); + return; + } + + FontTrueType* fontMono = preview && newPath == preview->originalPath + ? preview->originalFont + : loadPreviewFont( newPath ); + if ( fontMono ) { + fontPath = newPath; + if ( preview ) + preview->confirmed = true; + applyMonospaceFont( fontMono ); + } + } else if ( preview ) { + preview->confirmed = true; + applyMonospaceFont( preview->originalFont ); + } + } ); + dialog->setSelectedFont( absoluteFontPath ); + dialog->center(); + dialog->show(); +} + +} // namespace ecode diff --git a/src/tools/ecode/fontpickercontroller.hpp b/src/tools/ecode/fontpickercontroller.hpp new file mode 100644 index 000000000..e8533cb1d --- /dev/null +++ b/src/tools/ecode/fontpickercontroller.hpp @@ -0,0 +1,24 @@ +#ifndef ECODE_FONTPICKERCONTROLLER_HPP +#define ECODE_FONTPICKERCONTROLLER_HPP + +#include +#include + +namespace ecode { + +class App; + +class FontPickerController { + public: + explicit FontPickerController( App* app ) : mApp( app ) {} + + void openFontDialog( std::string& fontPath, bool loadingMonoFont, bool terminalFont = false, + std::function onFinish = {} ); + + private: + App* mApp{ nullptr }; +}; + +} // namespace ecode + +#endif From 055bcfe549a42b17e036e117700b80e4a1f13f3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 30 Jun 2026 11:01:20 -0300 Subject: [PATCH 8/9] Add some logs to the failing compare images in macOS to understand what's going on. I don't want to keep forcing random window sizes to make them pass... --- src/tests/unit_tests/compareimages.hpp | 62 +++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/tests/unit_tests/compareimages.hpp b/src/tests/unit_tests/compareimages.hpp index cc85dca83..bf8baa0c3 100644 --- a/src/tests/unit_tests/compareimages.hpp +++ b/src/tests/unit_tests/compareimages.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include using namespace EE; @@ -31,17 +33,75 @@ static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Wi Image expectedImage( expectedImagePath ); ASSERT_TRUE( expectedImage.getPixelsPtr() != nullptr ); + const bool imageSizeMismatch = expectedImage.getWidth() != actualImage.getWidth() || + expectedImage.getHeight() != actualImage.getHeight(); EXPECT_EQ_MSG( expectedImage.getWidth(), actualImage.getWidth(), "Images width not equal" ); EXPECT_EQ_MSG( expectedImage.getHeight(), actualImage.getHeight(), "Images height not equal" ); Image::DiffResult result = actualImage.diff( expectedImage ); EXPECT_LE( result.numDifferentPixels, allowedNumDifferentPixels ); - if ( result.numDifferentPixels > allowedNumDifferentPixels ) { + if ( imageSizeMismatch || result.numDifferentPixels > allowedNumDifferentPixels ) { auto saveExt( Image::saveTypeToExtension( saveType ) ); std::string withTextShaper = Text::TextShaperEnabled ? ( Text::TextShaperOptimizations ? "_text_shape_no_opt" : "_text_shape" ) : ""; + Sizei winSize = win->getSize(); + Sizei screenCoordSize = win->getSizeInScreenCoordinates(); + Sizei lastWindowedSize = win->getLastWindowedSize(); + Sizei lastWindowedScreenCoordSize = win->getLastWindowedSizeInScreenCoordinates(); + Sizei desktop = win->getDesktopResolution(); + Vector2i pos = win->getPosition(); + Rect borders = win->getBorderSize(); + const WindowInfo* winInfo = win->getWindowInfo(); + std::cerr << "Window diagnostics for failed image '" << imageName << "':" << std::endl; + std::cerr << " OS: " << Sys::getOSName( true ) << " / " << Sys::getOSArchitecture() + << std::endl; + std::cerr << " Expected image: " << expectedImage.getWidth() << "x" + << expectedImage.getHeight() << " from " << expectedImagePath << std::endl; + std::cerr << " Actual image: " << actualImage.getWidth() << "x" << actualImage.getHeight() + << std::endl; + std::cerr << " WindowConfig: " << winInfo->WindowConfig.Width << "x" + << winInfo->WindowConfig.Height << " title='" << winInfo->WindowConfig.Title + << "' style=0x" << std::hex << winInfo->WindowConfig.Style << std::dec + << " disableHiDPI=" << ( winInfo->WindowConfig.DisableHiDPI ? "true" : "false" ) + << " pixelDensity=" << winInfo->WindowConfig.PixelDensity << std::endl; + std::cerr << " Window size: " << winSize.getWidth() << "x" << winSize.getHeight() + << " screenCoordinates=" << screenCoordSize.getWidth() << "x" + << screenCoordSize.getHeight() << " scale=" << win->getScale() << std::endl; + std::cerr << " Last windowed size: " << lastWindowedSize.getWidth() << "x" + << lastWindowedSize.getHeight() + << " screenCoordinates=" << lastWindowedScreenCoordSize.getWidth() << "x" + << lastWindowedScreenCoordSize.getHeight() << std::endl; + std::cerr << " Desktop: " << desktop.getWidth() << "x" << desktop.getHeight() + << " position=" << pos.x << "," << pos.y + << " displayIndex=" << win->getCurrentDisplayIndex() << std::endl; + std::cerr << " Borders top,left,bottom,right: " << borders.Top << "," << borders.Left + << "," << borders.Bottom << "," << borders.Right << std::endl; + DisplayManager* displayManager = + Engine::existsSingleton() ? Engine::instance()->getDisplayManager() : nullptr; + if ( displayManager ) { + const int displayCount = displayManager->getDisplayCount(); + std::cerr << " Displays: " << displayCount << std::endl; + for ( int i = 0; i < displayCount; i++ ) { + Display* display = displayManager->getDisplayIndex( i ); + if ( !display ) + continue; + Rect bounds = display->getBounds(); + Rect usableBounds = display->getUsableBounds(); + DisplayMode currentMode = display->getCurrentMode(); + Sizeu displaySize = display->getSize(); + std::cerr << " [" << i << "] name='" << display->getName() + << "' bounds=" << bounds.Left << "," << bounds.Top << " " << bounds.Right + << "x" << bounds.Bottom << " usable=" << usableBounds.Left << "," + << usableBounds.Top << " " << usableBounds.Right << "x" + << usableBounds.Bottom << " currentMode=" << currentMode.Width << "x" + << currentMode.Height << "@" << currentMode.RefreshRate + << " size=" << displaySize.getWidth() << "x" << displaySize.getHeight() + << " dpi=" << display->getDPI() + << " refreshRate=" << display->getRefreshRate() << std::endl; + } + } std::cerr << "Test FAILED: " << result.numDifferentPixels << " pixels differ." << std::endl; std::cerr << "Maximum perceptual difference (Delta E): " << result.maxDeltaE << std::endl; if ( !FileSystem::fileExists( "output" ) ) From f6da5ef76ed681db4df724ffcb6296baaedefbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 30 Jun 2026 11:35:33 -0300 Subject: [PATCH 9/9] Fix warning on macOS due to posix_spawn_file_actions_addchdir_np usage (we must use posix_spawn_file_actions_addchdir). Try to run tests with borderless windows to see if macOS windows are created with the right size. --- .agent/rules/build-project.md | 6 ++-- src/eepp/system/sys.cpp | 19 +++++++----- src/tests/unit_tests/compareimages.hpp | 2 ++ src/tests/unit_tests/fontrendering_tests.cpp | 32 ++++++++++---------- src/tests/unit_tests/richtext_tests.cpp | 2 +- src/tests/unit_tests/uihtml_tests.cpp | 18 +++++------ src/thirdparty/subprocess/subprocess.h | 5 +++ 7 files changed, 47 insertions(+), 37 deletions(-) diff --git a/.agent/rules/build-project.md b/.agent/rules/build-project.md index e5d15561d..816f980f3 100644 --- a/.agent/rules/build-project.md +++ b/.agent/rules/build-project.md @@ -2,8 +2,8 @@ All build commands must be executed from the **root project directory**. Follow these steps to build the project: -## Step 1: Update Makefiles (Conditional) -If you have **added, renamed, or deleted** any source files, you must regenerate the makefiles before compiling. +## Step 1: Regenerate Project Files +Always regenerate the project files before compiling or running tests after making changes. Do this even for edits to existing files, because the checked-in makefiles can be stale and may reference removed files or miss recently added targets. * **Tool:** Use `premake4` if installed; otherwise, fallback to `premake5` (the parameters are identical). * **Linker Flag (`--with-mold-linker`):** This flag is conditional. If the `mold` linker is installed on the system, you **must** include it to speed up linking. If `mold` is not installed, omit the flag. @@ -14,8 +14,6 @@ If you have **added, renamed, or deleted** any source files, you must regenerate **Command (if `mold` is NOT installed):** `premake4 --disable-static-build --with-debug-symbols --address-sanitizer gmake` -*(If no files were added/removed, you may skip Step 1).* - ## Step 1a: Format Changed Files After editing any C or C++ source file (`.c`, `.cpp`, `.h`, `.hpp`), you **must** run `clang-format` on all modified files to ensure consistent formatting with the project's style (defined in `.clang-format` at the repository root). diff --git a/src/eepp/system/sys.cpp b/src/eepp/system/sys.cpp index 6f7f96500..59ae9cfae 100644 --- a/src/eepp/system/sys.cpp +++ b/src/eepp/system/sys.cpp @@ -1293,7 +1293,12 @@ int Sys::execute( const std::string& cmd, const std::string& workingDir ) { if ( posix_spawn_file_actions_init( &actions ) != 0 ) return -1; // Failed to initialize +#if defined( __APPLE__ ) && defined( __MAC_OS_X_VERSION_MIN_REQUIRED__ ) && \ + __MAC_OS_X_VERSION_MIN_REQUIRED__ >= 110000 + if ( posix_spawn_file_actions_addchdir( &actions, workingDir.c_str() ) != 0 ) { +#else if ( posix_spawn_file_actions_addchdir_np( &actions, workingDir.c_str() ) != 0 ) { +#endif posix_spawn_file_actions_destroy( &actions ); return -1; // Failed to add chdir action } @@ -2250,13 +2255,13 @@ static bool _isOSUsingDarkColorScheme() { return false; // Default to light #elif EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - // Executes JavaScript: window.matchMedia('(prefers-color-scheme: dark)').matches - return EM_ASM_INT({ - if (typeof window !== 'undefined' && window.matchMedia) { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 1 : 0; - } - return 0; - }) != 0; + // Executes JavaScript: window.matchMedia('(prefers-color-scheme: dark)').matches + return EM_ASM_INT( { + if ( typeof window != = 'undefined' && window.matchMedia ) { + return window.matchMedia( '(prefers-color-scheme: dark)' ).matches ? 1 : 0; + } + return 0; + } ) != 0; #else return true; // Any other OS default to dark #endif diff --git a/src/tests/unit_tests/compareimages.hpp b/src/tests/unit_tests/compareimages.hpp index bf8baa0c3..3c6907387 100644 --- a/src/tests/unit_tests/compareimages.hpp +++ b/src/tests/unit_tests/compareimages.hpp @@ -14,6 +14,8 @@ using namespace EE::System; using namespace EE::Graphics; using namespace EE::Window; +static constexpr Uint32 VisualTestWindowStyle = WindowStyle::Borderless; + static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Window::Window* win, const std::string& imageName, const std::string& imagesFolder = "fontrendering", diff --git a/src/tests/unit_tests/fontrendering_tests.cpp b/src/tests/unit_tests/fontrendering_tests.cpp index c0c29a734..1ec000cef 100644 --- a/src/tests/unit_tests/fontrendering_tests.cpp +++ b/src/tests/unit_tests/fontrendering_tests.cpp @@ -69,8 +69,8 @@ UTEST( FontRendering, fontsTest ) { FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 650, "eepp - Fonts", WindowStyle::Default, WindowBackend::Default, 32, - {}, 1, false, true ) ); + WindowSettings( 1024, 650, "eepp - Fonts", VisualTestWindowStyle, WindowBackend::Default, + 32, {}, 1, false, true ) ); ASSERT_TRUE_MSG( win->isOpen(), "Failed to create Window" ); @@ -322,7 +322,7 @@ UTEST( FontRendering, loadFontFaceDataURI ) { UTEST( FontRendering, editorTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - CodeEditor", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - CodeEditor", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -354,7 +354,7 @@ UTEST( FontRendering, editorTest ) { UTEST( FontRendering, textEditTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -386,7 +386,7 @@ UTEST( FontRendering, textEditTest ) { UTEST( FontRendering, tabsTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - Tabs Test", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - Tabs Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -419,7 +419,7 @@ UTEST( FontRendering, tabsTest ) { UTEST( FontRendering, tabStopTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - Tab Stop Test", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - Tab Stop Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -453,7 +453,7 @@ UTEST( FontRendering, tabStopTest ) { UTEST( FontRendering, tabsTextEditTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit - Tabs Test", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit - Tabs Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -485,7 +485,7 @@ UTEST( FontRendering, tabsTextEditTest ) { UTEST( FontRendering, tabStopTextEditTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit - Tab Stop Test", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit - Tab Stop Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -517,7 +517,7 @@ UTEST( FontRendering, tabStopTextEditTest ) { UTEST( FontRendering, textViewTest ) { const auto runTest = [&]() { - UIApplication app( WindowSettings( 1024, 650, "eepp - TextView", WindowStyle::Default, + UIApplication app( WindowSettings( 1024, 650, "eepp - TextView", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); @@ -552,7 +552,7 @@ UTEST( FontRendering, textViewTest ) { UTEST( FontRendering, textEditBengaliTest ) { BoolScopedOp op( Text::TextShaperEnabled, true ); UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit Bengali", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit Bengali", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -572,7 +572,7 @@ UTEST( FontRendering, textEditBengaliTest ) { UTEST( FontRendering, textEditArabicTest ) { BoolScopedOp op( Text::TextShaperEnabled, true ); UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit Arabic", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit Arabic", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -592,7 +592,7 @@ UTEST( FontRendering, textEditArabicTest ) { UTEST( FontRendering, textEditHebrewTest ) { BoolScopedOp op( Text::TextShaperEnabled, true ); UIApplication app( - WindowSettings( 1024, 650, "eepp - TextEdit Hebrew", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextEdit Hebrew", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -918,7 +918,7 @@ UTEST( FontRendering, textSetFillColor ) { UTEST( FontRendering, UITextTest ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - UI Text Test", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - UI Text Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -1162,7 +1162,7 @@ UTEST( FontRendering, TextHardWrap ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - Text Hard Wrap", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - Text Hard Wrap", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -1205,7 +1205,7 @@ UTEST( FontRendering, TextHardWrap ) { UTEST( FontRendering, UITextViewWrappedSelection ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - TextView Wrapped Selection", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextView Wrapped Selection", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); @@ -1245,7 +1245,7 @@ UTEST( FontRendering, UITextViewWrappedSelection ) { UTEST( FontRendering, UITextViewWrappedSelection2 ) { const auto runTest = [&]() { UIApplication app( - WindowSettings( 1024, 650, "eepp - TextView Wrapped Selection", WindowStyle::Default, + WindowSettings( 1024, 650, "eepp - TextView Wrapped Selection", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f ) ); diff --git a/src/tests/unit_tests/richtext_tests.cpp b/src/tests/unit_tests/richtext_tests.cpp index 484abe2fa..e6959a4b6 100644 --- a/src/tests/unit_tests/richtext_tests.cpp +++ b/src/tests/unit_tests/richtext_tests.cpp @@ -831,7 +831,7 @@ UTEST( RichText, RichTextTest ) { const auto& runTest = [&createRichText, &utest_result]() { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 650, "RichText Example", WindowStyle::Default, + WindowSettings( 1024, 650, "RichText Example", VisualTestWindowStyle, WindowBackend::Default, 32, "", 1, EE_SCREEN_KEYBOARD_ENABLED, true ) ); if ( !win->isOpen() ) diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 634d7d712..fbc29f507 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -101,8 +101,8 @@ static String uiHtmlLineText( const RichText& richText, size_t lineIndex ) { UTEST( UIHTMLTable, complexLayout ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, - 32, {}, 1, false, true ), + WindowSettings( 1024, 653, "HTML Tables Test", VisualTestWindowStyle, + WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -162,7 +162,7 @@ UTEST( UIHTMLTable, complexLayout ) { UTEST( UIHTMLTable, complexLayout2 ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 650, "HTML Tables Test 2", WindowStyle::Default, + WindowSettings( 1024, 650, "HTML Tables Test 2", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -1241,7 +1241,7 @@ UTEST( UIHTML, InlineBlockVerticalAlignDoesNotInflateOwnTextLine ) { UTEST( UIHTMLTable, complexLayout3 ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 650, "HTML Tables Test 3", WindowStyle::Default, + WindowSettings( 1024, 650, "HTML Tables Test 3", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -2479,7 +2479,7 @@ UTEST( UIHTMLDetails, lobstersInlineBlockCachesWidth ) { UTEST( UIBorder, renderingVariations ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Border Rendering Test", WindowStyle::Default, + WindowSettings( 1024, 653, "Border Rendering Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -2513,7 +2513,7 @@ UTEST( UIBorder, renderingVariations ) { UTEST( UIBorder, renderingVariations2 ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Border Rendering Test 2", WindowStyle::Default, + WindowSettings( 1024, 653, "Border Rendering Test 2", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -3025,7 +3025,7 @@ UTEST( UIHTML, ContactFormLayout ) { UTEST( UIBackground, imageAtlasPositioning ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Background Atlas Test", WindowStyle::Default, + WindowSettings( 1024, 653, "Background Atlas Test", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); @@ -3140,7 +3140,7 @@ UTEST( UIBackground, inlineSpanColorRendersBehindBackgroundImage ) { UTEST( UIBackground, imageAtlasPositioningPixelDensity2 ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "Background Atlas Test PD2", WindowStyle::Default, + WindowSettings( 1024, 653, "Background Atlas Test PD2", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); EE::Graphics::PixelDensity::setPixelDensity( 2.0f ); @@ -3214,7 +3214,7 @@ UTEST( UIBackground, cssFileRelativeSpriteUrlAndNegativePosition ) { UTEST( UIBackground, InlineBlockImageSpans ) { auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 653, "inline-block image spans", WindowStyle::Default, + WindowSettings( 1024, 653, "inline-block image spans", VisualTestWindowStyle, WindowBackend::Default, 32, {}, 1, false, true ), ContextSettings( false, 0, 0, GLv_default, true, false ) ); FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); diff --git a/src/thirdparty/subprocess/subprocess.h b/src/thirdparty/subprocess/subprocess.h index 6b16e162f..74638258f 100644 --- a/src/thirdparty/subprocess/subprocess.h +++ b/src/thirdparty/subprocess/subprocess.h @@ -1027,7 +1027,12 @@ int subprocess_create_ex(const char *const commandLine[], int options, #if !TARGET_OS_IPHONE if (working_directory) { +#if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MIN_REQUIRED__) && \ + __MAC_OS_X_VERSION_MIN_REQUIRED__ >= 110000 + if (0 != posix_spawn_file_actions_addchdir(&actions, working_directory)) { +#else if (0 != posix_spawn_file_actions_addchdir_np(&actions, working_directory)) { +#endif posix_spawn_file_actions_destroy(&actions); return -1; }