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, " ", "-" );