From 20f44697a0b49fe7b34cc5d7d769a0da153bb023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 16 Apr 2026 19:13:43 -0300 Subject: [PATCH] Add support to select multiple-files in file tree view (SpartanJ/ecode#869). --- src/eepp/ui/abstract/uiabstracttableview.cpp | 2 +- src/tools/ecode/ecode.cpp | 21 +- src/tools/ecode/ecode.hpp | 5 +- .../ecode/plugins/plugincontextprovider.hpp | 3 + src/tools/ecode/settingsmenu.cpp | 70 +++-- src/tools/ecode/settingsmenu.hpp | 2 +- src/tools/ecode/uitreeviewfs.cpp | 268 +++++++++++++++--- src/tools/ecode/uitreeviewfs.hpp | 24 +- 8 files changed, 313 insertions(+), 82 deletions(-) diff --git a/src/eepp/ui/abstract/uiabstracttableview.cpp b/src/eepp/ui/abstract/uiabstracttableview.cpp index b03f2c407..2b8e2eb29 100644 --- a/src/eepp/ui/abstract/uiabstracttableview.cpp +++ b/src/eepp/ui/abstract/uiabstracttableview.cpp @@ -539,7 +539,7 @@ UITableRow* UIAbstractTableView::createRow() { } } ); rowWidget->on( Event::MouseClick, [this]( const Event* event ) { - if ( !( event->asMouseEvent()->getFlags() & ( EE_BUTTON_LMASK | EE_BUTTON_RMASK ) ) || + if ( !( event->asMouseEvent()->getFlags() & ( EE_BUTTON_LMASK ) ) || !isRowSelection() ) return; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index c9c2355ba..bf99eba64 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1378,7 +1378,7 @@ UITextView* App::getDocInfo() const { return mDocInfo; } -UITreeView* App::getProjectTreeView() const { +UITreeViewFS* App::getProjectTreeView() const { return mProjectTreeView; } @@ -3778,7 +3778,7 @@ void App::initProjectViewEmptyCont() { void App::initProjectTreeViewUI() { initProjectViewEmptyCont(); - mProjectTreeView = mUISceneNode->find( "project_view" ); + mProjectTreeView = mUISceneNode->find( "project_view" ); mProjectTreeView->setColumnsHidden( { FileSystemModel::Icon, FileSystemModel::Size, FileSystemModel::Group, FileSystemModel::Inode, FileSystemModel::Owner, FileSystemModel::SymlinkTarget, @@ -3809,8 +3809,9 @@ void App::initProjectTreeViewUI() { } else { tab->getTabWidget()->setTabSelected( tab ); } - } else { // ModelEventType::OpenMenu - mSettings->createProjectTreeMenu( FileInfo( path ) ); + } else if ( !mProjectTreeView->getSelection().isEmpty() ) { + // ModelEventType::OpenMenu + mSettings->createProjectTreeMenu( mProjectTreeView->getSelectionsFileInfo() ); } } } @@ -3824,7 +3825,10 @@ void App::initProjectTreeViewUI() { if ( !mFileSystemModel ) return; const KeyEvent* keyEvent = static_cast( event ); - if ( keyEvent->getKeyCode() == KEY_F2 || keyEvent->getKeyCode() == KEY_DELETE ) { + if ( keyEvent->getKeyCode() == KEY_F2 || + ( ( keyEvent->getKeyCode() == KEY_DELETE || + keyEvent->getKeyCode() == KEY_BACKSPACE ) && + mProjectTreeView->getSelection().size() == 1 ) ) { ModelIndex modelIndex = mProjectTreeView->getSelection().first(); if ( !modelIndex.isValid() ) return; @@ -3833,10 +3837,15 @@ void App::initProjectTreeViewUI() { FileInfo fileInfo( vPath.toString() ); if ( keyEvent->getKeyCode() == KEY_F2 ) { renameFile( fileInfo ); - } else { + } else if ( keyEvent->getKeyCode() == KEY_DELETE || + keyEvent->getKeyCode() == KEY_BACKSPACE ) { mSettings->deleteFileDialog( fileInfo ); } } + } else if ( ( keyEvent->getKeyCode() == KEY_DELETE || + keyEvent->getKeyCode() == KEY_BACKSPACE ) && + !mProjectTreeView->getSelection().isEmpty() ) { + mProjectTreeView->asType()->deleteSelectedFiles(); } } ); mProjectTreeView->setDisableCellClipping( true ); diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 471216e6c..dca30b6c5 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -32,6 +32,7 @@ class AutoCompletePlugin; class LinterPlugin; class FormatterPlugin; class SettingsMenu; +class UITreeViewFS; class App : public UICodeEditorSplitter::Client, public PluginContextProvider { public: @@ -510,7 +511,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { UITextView* getDocInfo() const; - UITreeView* getProjectTreeView() const; + UITreeViewFS* getProjectTreeView() const; void loadCurrentDirectory(); @@ -693,7 +694,7 @@ class App : public UICodeEditorSplitter::Client, public PluginContextProvider { Float mDisplayDPI{ 96 }; std::shared_ptr mThreadPool; std::shared_ptr mDirTree; - UITreeView* mProjectTreeView{ nullptr }; + UITreeViewFS* mProjectTreeView{ nullptr }; UILinearLayout* mProjectViewEmptyCont{ nullptr }; std::shared_ptr mFileSystemModel; std::shared_ptr mFileSystemMatcher; diff --git a/src/tools/ecode/plugins/plugincontextprovider.hpp b/src/tools/ecode/plugins/plugincontextprovider.hpp index ec91c9fed..aa2809a3d 100644 --- a/src/tools/ecode/plugins/plugincontextprovider.hpp +++ b/src/tools/ecode/plugins/plugincontextprovider.hpp @@ -56,6 +56,7 @@ class NotificationCenter; class ProjectDirectoryTree; struct TerminalConfig; class UIMainLayout; +class UITreeViewFS; class PluginContextProvider { public: @@ -63,6 +64,8 @@ class PluginContextProvider { virtual UISplitter* getMainSplitter() const = 0; + virtual UITreeViewFS* getProjectTreeView() const = 0; + virtual void hideGlobalSearchBar() = 0; virtual void hideSearchBar() = 0; diff --git a/src/tools/ecode/settingsmenu.cpp b/src/tools/ecode/settingsmenu.cpp index 4e55057b4..3da33661e 100644 --- a/src/tools/ecode/settingsmenu.cpp +++ b/src/tools/ecode/settingsmenu.cpp @@ -1,4 +1,5 @@ #include "settingsmenu.hpp" +#include "uitreeviewfs.hpp" #include namespace fs = std::filesystem; @@ -2673,12 +2674,20 @@ static void fsRemoveAll( const std::string& fpath ) { #endif } -void SettingsMenu::createProjectTreeMenu( const FileInfo& file ) { +void SettingsMenu::createProjectTreeMenu( const std::vector& files ) { if ( mProjectTreeMenu && mProjectTreeMenu->isVisible() ) mProjectTreeMenu->close(); mProjectTreeMenu = UIPopUpMenu::New(); - if ( file.isDirectory() ) { + bool allFiles = true; + for ( const auto& file : files ) { + if ( file.isDirectory() ) { + allFiles = false; + break; + } + } + + if ( files.size() == 1 && files[0].isDirectory() ) { mProjectTreeMenu->add( i18n( "new_file_ellipsis", "New File..." ), findIcon( "file-add" ) ) ->setId( "new_file" ); mProjectTreeMenu @@ -2702,7 +2711,7 @@ void SettingsMenu::createProjectTreeMenu( const FileInfo& file ) { ->add( i18n( "find_in_folder_ellipsis", "Find in Folder..." ), findIcon( "file-search" ) ) ->setId( "find_in_folder" ); - } else { + } else if ( files.size() == 1 ) { mProjectTreeMenu->add( i18n( "open_file", "Open File" ), findIcon( "document-open" ) ) ->setId( "open_file" ); mProjectTreeMenu @@ -2713,29 +2722,44 @@ void SettingsMenu::createProjectTreeMenu( const FileInfo& file ) { ->add( i18n( "new_file_in_directory_ellipsis", "New File in directory..." ), findIcon( "file-add" ) ) ->setId( "new_file_in_place" ); + mProjectTreeMenu + ->add( i18n( "new_folder_in_directory_ellipsis", "New Folder in directory..." ), + findIcon( "folder-add" ) ) + ->setId( "new_folder_in_place" ); mProjectTreeMenu ->add( i18n( "duplicate_file_ellipsis", "Duplicate File..." ), findIcon( "file-copy" ) ) ->setId( "duplicate_file" ); + } else if ( allFiles && files.size() > 1 ) { + mProjectTreeMenu->add( i18n( "open_files", "Open Files" ), findIcon( "document-open" ) ) + ->setId( "open_files" ); } - mProjectTreeMenu->add( i18n( "rename", "Rename" ), findIcon( "edit" ), "F2" ) - ->setId( "rename" ); + + if ( files.size() == 1 ) { + mProjectTreeMenu->add( i18n( "rename", "Rename" ), findIcon( "edit" ), "F2" ) + ->setId( "rename" ); + } + mProjectTreeMenu ->add( i18n( "remove_ellipsis", "Remove..." ), findIcon( "delete-bin" ), "Delete" ) ->setId( "remove" ); - if ( file.isDirectory() || file.isExecutable() ) { - mProjectTreeMenu->addSeparator(); + if ( files.size() == 1 ) { + auto& file = files[0]; - if ( file.isDirectory() ) { - mProjectTreeMenu - ->add( i18n( "execute_dir_in_terminal", "Open directory in terminal" ), - findIcon( "filetype-bash" ) ) - ->setId( "execute_dir_in_terminal" ); - } else if ( file.isExecutable() ) { - mProjectTreeMenu - ->add( i18n( "execute_in_terminal", "Execute in terminal" ), - findIcon( "filetype-bash" ) ) - ->setId( "execute_in_terminal" ); + if ( file.isDirectory() || file.isExecutable() ) { + mProjectTreeMenu->addSeparator(); + + if ( file.isDirectory() ) { + mProjectTreeMenu + ->add( i18n( "execute_dir_in_terminal", "Open directory in terminal" ), + findIcon( "filetype-bash" ) ) + ->setId( "execute_dir_in_terminal" ); + } else if ( file.isExecutable() ) { + mProjectTreeMenu + ->add( i18n( "execute_in_terminal", "Execute in terminal" ), + findIcon( "filetype-bash" ) ) + ->setId( "execute_in_terminal" ); + } } } @@ -2762,20 +2786,24 @@ void SettingsMenu::createProjectTreeMenu( const FileInfo& file ) { ->setId( "configure-ignore-files" ); } - mProjectTreeMenu->on( Event::OnItemClicked, [this, file]( const Event* event ) { - if ( !event->getNode()->isType( UI_TYPE_MENUITEM ) ) + mProjectTreeMenu->on( Event::OnItemClicked, [this, files]( const Event* event ) { + if ( !event->getNode()->isType( UI_TYPE_MENUITEM ) || files.empty() ) return; UIMenuItem* item = event->getNode()->asType(); std::string id( item->getId() ); + auto file = files[0]; if ( "new_file" == id || "new_file_in_place" == id ) { mApp->newFile( file ); - } else if ( "new_folder" == id ) { + } else if ( "new_folder" == id || "new_folder_in_place" == id ) { mApp->newFolder( file ); } else if ( "open_file" == id ) { mApp->openFileFromPath( file.getFilepath() ); + } else if ( "open_files" == id ) { + for ( const auto& file : files ) + mApp->openFileFromPath( file.getFilepath() ); } else if ( "remove" == id ) { - deleteFileDialog( file ); + mApp->getProjectTreeView()->deleteSelectedFiles(); } else if ( "duplicate_file" == id ) { UIMessageBox* msgBox = mApp->newInputMsgBox( String::format( "%s \"%s\"", diff --git a/src/tools/ecode/settingsmenu.hpp b/src/tools/ecode/settingsmenu.hpp index ef11fba18..1a13158e9 100644 --- a/src/tools/ecode/settingsmenu.hpp +++ b/src/tools/ecode/settingsmenu.hpp @@ -64,7 +64,7 @@ class SettingsMenu { void createProjectTreeMenu(); - void createProjectTreeMenu( const FileInfo& file ); + void createProjectTreeMenu( const std::vector& files ); void updateColorSchemeMenu(); diff --git a/src/tools/ecode/uitreeviewfs.cpp b/src/tools/ecode/uitreeviewfs.cpp index 1cc1653b0..8eaf48c39 100644 --- a/src/tools/ecode/uitreeviewfs.cpp +++ b/src/tools/ecode/uitreeviewfs.cpp @@ -1,5 +1,6 @@ #include "uitreeviewfs.hpp" #include "customwidgets.hpp" +#include "ecode.hpp" #include "notificationcenter.hpp" #include @@ -7,6 +8,8 @@ #include #include +#include + namespace ecode { static const std::map getDefaultKeybindings() { @@ -14,6 +17,10 @@ static const std::map getDefaultKeybindings( { { KEY_C, KeyMod::getDefaultModifier() }, "copy" }, { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, { { KEY_V, KeyMod::getDefaultModifier() }, "paste" }, + { { KEY_RETURN }, "open-selected-files" }, + { { KEY_KP_ENTER }, "open-selected-files" }, + { { KEY_DELETE }, "delete-selected-files" }, + { { KEY_BACKSPACE }, "delete-selected-files" }, }; } @@ -81,7 +88,23 @@ class UITreeViewCellFS : public UITreeViewCell { sDragTV->setVisible( false )->setEnabled( false ); sDragTV->setText( getTextView()->getText() ); setClass( "dragged" ); - getTreeView()->setSourceDrag( getModel()->node( getCurIndex() ).fullPath() ); + + auto tvfs = getTreeView(); + const auto& selection = tvfs->getSelection(); + std::string dragPaths; + auto fsm = static_cast( getModel() ); + std::string rootPath = fsm->getRootPath(); + tvfs->getSourceDragMultiplePaths().clear(); + for ( int i = 0; i < selection.size(); ++i ) { + auto path = tvfs->getSelectionPathAtIndex( i ); + std::string relPath( path ); + FileSystem::filePathRemoveBasePath( rootPath, relPath ); + dragPaths += relPath; + if ( i < selection.size() - 1 ) + dragPaths += "\n"; + tvfs->getSourceDragMultiplePaths().emplace_back( path ); + } + sDragTV->setText( dragPaths ); return UITreeViewCell::onDragStart( position, flags ); } @@ -111,8 +134,7 @@ class UITreeViewCellFS : public UITreeViewCell { getTreeView()->execute( cmd ); return 1; } - - return 0; + return UITreeViewCell::onKeyDown( event ); } const std::string& getCurrentPath() const { @@ -129,30 +151,68 @@ UITreeViewFS::UITreeViewFS() : UITreeView(), mKeyBindings( getInput() ) { mCommands["copy"] = [this] { if ( getSelection().isEmpty() ) return; - mSrcCopy = getSelectionPath(); + auto& selection = getSelection(); + mSrcCopyMultiplePaths.clear(); mWasCut = false; - NotificationCenter::instance()->addNotification( - String::format( i18n( "copied_file", "Copied '%s'" ).toUtf8(), - FileSystem::fileNameFromPath( mSrcCopy ) ) ); + for ( int i = 0; i < selection.size(); ++i ) + mSrcCopyMultiplePaths.emplace_back( getSelectionPathAtIndex( i ) ); + if ( selection.size() > 1 ) { + NotificationCenter::instance()->addNotification( String::format( + i18n( "copied_files", "Copied %d items" ).toUtf8(), selection.size() ) ); + } else if ( selection.size() == 1 ) { + NotificationCenter::instance()->addNotification( + String::format( i18n( "copied_file", "Copied '%s'" ).toUtf8(), + FileSystem::fileNameFromPath( getSelectionPathAtIndex( 0 ) ) ) ); + } }; mCommands["cut"] = [this] { if ( getSelection().isEmpty() ) return; - mSrcCopy = getSelectionPath(); + auto& selection = getSelection(); + mSrcCopyMultiplePaths.clear(); mWasCut = true; - NotificationCenter::instance()->addNotification( String::format( - i18n( "cut_file", "Cut '%s'" ).toUtf8(), FileSystem::fileNameFromPath( mSrcCopy ) ) ); + for ( int i = 0; i < selection.size(); ++i ) + mSrcCopyMultiplePaths.emplace_back( getSelectionPathAtIndex( i ) ); + + if ( selection.size() > 1 ) { + NotificationCenter::instance()->addNotification( + String::format( i18n( "cut_files", "Cut %d items" ).toUtf8(), selection.size() ) ); + } else { + NotificationCenter::instance()->addNotification( + String::format( i18n( "cut_file", "Cut '%s'" ).toUtf8(), + FileSystem::fileNameFromPath( getSelectionPathAtIndex( 0 ) ) ) ); + } }; mCommands["paste"] = [this] { - if ( mWasCut ) - moveFile( mSrcCopy, getSelectionPath() ); - else - copyFile( mSrcCopy, getSelectionPath() ); + auto& selection = getSelection(); + std::string targetPath = selection.isEmpty() ? "" : getSelectionPath(); + if ( targetPath.empty() ) + return; + FileInfo targetInfo( targetPath ); + if ( !targetInfo.isDirectory() ) { + targetInfo = FileInfo( targetInfo.getDirectoryPath() ); + } + std::string dstPath( targetInfo.getFilepath() ); + if ( mWasCut ) { + if ( !mSrcCopyMultiplePaths.empty() ) + moveFiles( mSrcCopyMultiplePaths, dstPath ); + mSrcCopyMultiplePaths.clear(); + } else { + if ( !mSrcCopyMultiplePaths.empty() ) + copyFiles( mSrcCopyMultiplePaths, dstPath ); + mSrcCopyMultiplePaths.clear(); + } }; + mCommands["open-selected-files"] = [this] { openSelectedFiles(); }; + + mCommands["delete-selected-files"] = [this] { deleteSelectedFiles(); }; + mKeyBindings.addKeybinds( getDefaultKeybindings() ); + + setSelectionKind( SelectionKind::Multiple ); } UIWidget* UITreeViewFS::createCell( UIWidget* rowWidget, const ModelIndex& index ) { @@ -168,53 +228,84 @@ Uint32 UITreeViewFS::onMessage( const NodeMessage* msg ) { if ( dropMsg->getSender()->isType( expectedType ) && dropMsg->getDroppedNode()->isType( expectedType ) ) { auto dst = dropMsg->getSender()->asType()->getCurrentPath(); - moveFile( mSrcDrag, dst ); + moveFiles( mSrcDragMultiplePaths, dst ); return 1; } } return UITreeView::onMessage( msg ); } -void UITreeViewFS::moveFile( const std::string& src, const std::string& dst ) { - FileInfo srcInfo( src ); - if ( !srcInfo.exists() ) +void UITreeViewFS::moveFiles( const std::vector& paths, const std::string& dstDir ) { + if ( paths.empty() ) return; - FileInfo dstInfo( dst ); - if ( !dstInfo.exists() ) - return; - if ( srcInfo.getFilepath() != srcInfo.getRealPath() ) - srcInfo = FileInfo( srcInfo.getRealPath() ); - if ( !dstInfo.isDirectory() ) { - dstInfo = FileInfo( dstInfo.getDirectoryPath() ); - if ( dstInfo.getFilepath() != dstInfo.getRealPath() ) - dstInfo = FileInfo( dstInfo.getRealPath() ); - } - if ( srcInfo.getDirectoryPath() == dstInfo.getFilepath() ) - return; - - std::string srcPath( srcInfo.getFilepath() ); - std::string dstPath( dstInfo.getFilepath() ); - FileSystem::dirAddSlashAtEnd( dstPath ); - dstPath += srcInfo.getFileName(); - + std::string confirmMsg; + std::vector> validMoves; auto fsm = static_cast( getModel() ); - std::string partialSrc( srcPath ); - FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialSrc ); - std::string partialDst( dstPath ); - FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialDst ); - auto confirmMsg( String::format( - i18n( "confirm_move_file_or_dir", "Are you sure you want to move:\n%s\ninto:\n%s" ) - .toUtf8(), - partialSrc, partialDst ) ); + for ( const auto& srcPath : paths ) { + FileInfo srcInfo( srcPath ); + if ( !srcInfo.exists() ) + continue; + FileInfo dstInfo( dstDir ); + if ( !dstInfo.isDirectory() ) { + dstInfo = FileInfo( dstInfo.getDirectoryPath() ); + } + if ( srcInfo.getDirectoryPath() == dstInfo.getFilepath() ) + continue; + validMoves.emplace_back( srcPath, dstInfo.getFilepath() ); + } + + if ( validMoves.empty() ) + return; + + std::string partialDst( dstDir ); + FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialDst ); + if ( !FileSystem::isDirectory( partialDst ) ) + partialDst = FileSystem::fileRemoveFileName( partialDst ); + FileSystem::dirAddSlashAtEnd( partialDst ); + if ( partialDst.empty() ) // If it's empty it's the root path + partialDst = fsm->getRootPath(); + + if ( validMoves.size() == 1 ) { + std::string partialSrc( validMoves[0].first ); + FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialSrc ); + + confirmMsg = ( String::format( + i18n( "confirm_move_file_or_dir", "Are you sure you want to move:\n%s\ninto:\n%s" ) + .toUtf8(), + partialSrc, partialDst ) ); + } else { + std::string listFiles; + static constexpr auto MAX_LISTED_FILES = 10; + for ( size_t i = 0; i < validMoves.size() && i < MAX_LISTED_FILES; ++i ) { + std::string partialSrc( validMoves[i].first ); + FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialSrc ); + listFiles += partialSrc + "\n"; + } + if ( validMoves.size() > MAX_LISTED_FILES ) { + listFiles += + "... and " + String::format( "%d", validMoves.size() - MAX_LISTED_FILES ) + " more"; + } + + confirmMsg = String::format( + i18n( "confirm_move_multiple", "Are you sure you want to move:\n%s\ninto: %s" ) + .toUtf8(), + listFiles, partialDst ); + } UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, confirmMsg ); msgBox->setTitle( "ecode" ); msgBox->center(); msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); msgBox->showWhenReady(); - msgBox->on( Event::OnConfirm, [srcPath = std::move( srcPath ), dstPath = std::move( dstPath )]( - auto ) { FileSystem::fileMove( srcPath, dstPath ); } ); + msgBox->on( Event::OnConfirm, [validMoves]( const Event* ) { + for ( const auto& move : validMoves ) { + std::string dstPath( move.second ); + FileSystem::dirAddSlashAtEnd( dstPath ); + dstPath += FileInfo( move.first ).getFileName(); + FileSystem::fileMove( move.first, dstPath ); + } + } ); } void UITreeViewFS::copyFile( const std::string& src, const std::string& dst ) { @@ -241,12 +332,97 @@ void UITreeViewFS::copyFile( const std::string& src, const std::string& dst ) { FileSystem::fileCopy( srcPath, dstPath ); } +void UITreeViewFS::copyFiles( const std::vector& paths, const std::string& dstDir ) { + for ( const auto& srcPath : paths ) { + copyFile( srcPath, dstDir ); + } +} + std::string UITreeViewFS::getSelectionPath() const { return static_cast( getModel() ) ->node( getSelection().first() ) .fullPath(); } +std::string UITreeViewFS::getSelectionPathAtIndex( int index ) const { + auto& selection = getSelection(); + if ( index < 0 || static_cast( index ) >= static_cast( selection.size() ) ) + return ""; + auto indexVec = selection.indexes(); + return static_cast( getModel() ) + ->node( indexVec[static_cast( index )] ) + .fullPath(); +} + +std::vector UITreeViewFS::getSelectionsFileInfo() const { + std::vector ret; + auto indexVec = getSelection().indexes(); + auto model = static_cast( getModel() ); + for ( const auto& index : indexVec ) { + ret.emplace_back( FileInfo( model->node( index ).fullPath() ) ); + } + return ret; +} + +void UITreeViewFS::deleteSelectedFiles() { + auto& selection = getSelection(); + if ( selection.isEmpty() ) + return; + std::vector paths; + for ( int i = 0; i < selection.size(); ++i ) + paths.emplace_back( getSelectionPathAtIndex( i ) ); + deleteItems( paths ); +} + +void UITreeViewFS::deleteItems( const std::vector& paths ) { + if ( paths.empty() ) + return; + auto fsm = static_cast( getModel() ); + std::string confirmMsg; + if ( paths.size() == 1 ) { + std::string partialPath( paths[0] ); + FileSystem::filePathRemoveBasePath( fsm->getRootPath(), partialPath ); + confirmMsg = String::format( + i18n( "confirm_delete_file_or_dir", "Are you sure you want to delete:\n%s" ).toUtf8(), + partialPath ); + } else { + confirmMsg = String::format( + i18n( "confirm_delete_multiple", "Are you sure you want to delete %d items?" ).toUtf8(), + paths.size() ); + } + + UIMessageBox* msgBox = UIMessageBox::New( UIMessageBox::OK_CANCEL, confirmMsg ); + msgBox->setTitle( "ecode" ); + msgBox->center(); + msgBox->setCloseShortcut( { KEY_ESCAPE, 0 } ); + msgBox->showWhenReady(); + msgBox->on( Event::OnConfirm, [paths]( const Event* ) { + for ( const auto& path : paths ) { + FileInfo info( path ); + try { + if ( info.isDirectory() ) { + std::filesystem::remove_all( std::filesystem::path( path ) ); + } else { + FileSystem::fileRemove( path ); + } + } catch ( const std::filesystem::filesystem_error& ) { + } + } + } ); +} + +void UITreeViewFS::openSelectedFiles() { + auto selection = getSelection().indexes(); + std::vector paths; + paths.reserve( selection.size() ); + for ( size_t i = 0; i < selection.size(); ++i ) + paths.emplace_back( FileInfo( getSelectionPathAtIndex( i ) ) ); + for ( const auto& path : paths ) { + if ( !path.isDirectory() ) + App::instance()->openFileFromPath( path.getFilepath() ); + } +} + void UITreeViewFS::execute( const std::string& cmd ) { auto cmdIt = mCommands.find( cmd ); if ( cmdIt != mCommands.end() ) diff --git a/src/tools/ecode/uitreeviewfs.hpp b/src/tools/ecode/uitreeviewfs.hpp index 46b5b7fb7..5a9cc01ce 100644 --- a/src/tools/ecode/uitreeviewfs.hpp +++ b/src/tools/ecode/uitreeviewfs.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include using namespace EE::UI; @@ -19,24 +21,36 @@ class UITreeViewFS : public UITreeView { KeyBindings& getKeyBindings() { return mKeyBindings; } - void setSourceDrag( const std::string& src ) { mSrcDrag = src; } + std::vector& getSourceDragMultiplePaths() { return mSrcDragMultiplePaths; } - void setSourceCopy( const std::string& src ) { mSrcCopy = src; } + std::string getSelectionPathAtIndex( int index ) const; + + void deleteSelectedFiles(); + + void openSelectedFiles(); void execute( const std::string& cmd ); + std::vector getSelectionsFileInfo() const; + protected: KeyBindings mKeyBindings; UnorderedMap> mCommands; - std::string mSrcDrag; - std::string mSrcCopy; + std::vector mSrcDragMultiplePaths; + std::vector mSrcCopyMultiplePaths; bool mWasCut{ false }; + friend class UITreeViewCellFS; + std::string getSelectionPath() const; - void moveFile( const std::string& src, const std::string& dst ); + void moveFiles( const std::vector& paths, const std::string& dstDir ); void copyFile( const std::string& src, const std::string& dst ); + + void copyFiles( const std::vector& paths, const std::string& dstDir ); + + void deleteItems( const std::vector& paths ); }; } // namespace ecode