From 7e6de3f9b219fb7cd9749efeb83f842eb2433cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sun, 7 Sep 2025 19:48:29 -0300 Subject: [PATCH] Added keyboard shortcut operations in the file system tree view, to complete SpartanJ/ecode#71. --- include/eepp/system/filesystem.hpp | 13 ++- src/eepp/system/filesystem.cpp | 66 ++++++----- src/tools/ecode/ecode.cpp | 1 + src/tools/ecode/ecode.hpp | 1 - src/tools/ecode/notificationcenter.cpp | 7 ++ src/tools/ecode/notificationcenter.hpp | 2 + .../ecode/plugins/aiassistant/chatui.cpp | 17 +-- src/tools/ecode/uitreeviewfs.cpp | 105 ++++++++++++++---- src/tools/ecode/uitreeviewfs.hpp | 15 +++ src/tools/ecode/uiwelcomescreen.cpp | 1 + 10 files changed, 160 insertions(+), 68 deletions(-) diff --git a/include/eepp/system/filesystem.hpp b/include/eepp/system/filesystem.hpp index 9d832fa1f..647aeab85 100644 --- a/include/eepp/system/filesystem.hpp +++ b/include/eepp/system/filesystem.hpp @@ -39,9 +39,9 @@ class EE_API FileSystem { */ static bool fileGet( const std::string& path, std::string& data ); - /** Copy a file to location. - * @param src Source File Path - * @param dst Destination File Path + /** Copy a file or a directory (recursively) to location. + * @param src Source file / directory Path + * @param dst Destination file path * @return If success. */ static bool fileCopy( const std::string& src, const std::string& dst ); @@ -79,6 +79,12 @@ class EE_API FileSystem { /** Deletes a file from the file system. */ static bool fileRemove( const std::string& filepath ); + /** Moves a file or folder to the destination path + * @param fromPath The path of the file or folder to move + * @param toPath The folder / directory destination path + */ + static bool fileMove( const std::string& fromPath, const std::string& toPath ); + /** Hides the file in the file system */ static bool fileHide( const std::string& filepath ); @@ -172,6 +178,7 @@ class EE_API FileSystem { /** Opens a file path with a path and mode encoded in UTF-8 */ static FILE* fopenUtf8( const std::string& path, const std::string& mode ); + }; }} // namespace EE::System diff --git a/src/eepp/system/filesystem.cpp b/src/eepp/system/filesystem.cpp index 1da8d4a45..8b0bab3a9 100644 --- a/src/eepp/system/filesystem.cpp +++ b/src/eepp/system/filesystem.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #if EE_PLATFORM == EE_PLATFORM_WIN #ifndef WIN32_LEAN_AND_MEAN @@ -93,38 +94,32 @@ bool FileSystem::fileGet( const std::string& path, std::string& data ) { } bool FileSystem::fileCopy( const std::string& src, const std::string& dst ) { - if ( fileExists( src ) ) { - Int64 chunksize = EE_1MB; - Int64 size = fileSize( src ); - Int64 size_left = (Int32)size; - Int64 allocate = ( size < chunksize ) ? size : chunksize; - Int64 copysize = 0; + try { +#if EE_PLATFORM == EE_PLATFORM_WIN + std::filesystem::path source = String( src ).toWideString(); + std::filesystem::path destination = String( dst ).toWideString(); +#else + std::filesystem::path source = src; + std::filesystem::path destination = dst; +#endif - TScopedBuffer data( allocate ); - char* buff = data.get(); + if ( !std::filesystem::exists( source ) ) + return false; - IOStreamFile in( src, "rb" ); - IOStreamFile out( dst, "wb" ); - - if ( in.isOpen() && out.isOpen() && size > 0 ) { - do { - if ( size_left - chunksize < 0 ) { - copysize = size_left; - } else { - copysize = chunksize; - } - - in.read( &buff[0], copysize ); - out.write( &buff[0], copysize ); - - size_left -= copysize; - } while ( size_left > 0 ); - - return true; + if ( std::filesystem::is_directory( source ) ) { + std::filesystem::copy( source, destination, + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::overwrite_existing ); + } else { + std::filesystem::copy( source, destination, + std::filesystem::copy_options::overwrite_existing ); } + return true; + } catch ( const std::filesystem::filesystem_error& ) { + return false; + } catch ( const std::exception& ) { + return false; } - - return false; } std::string FileSystem::fileExtension( const std::string& filepath, const bool& lowerExt ) { @@ -710,4 +705,19 @@ FILE* FileSystem::fopenUtf8( const std::string& path, const std::string& mode ) return fopenUtf8( path.c_str(), mode.c_str() ); } +bool FileSystem::fileMove( const std::string& fpath, const std::string& newFilePath ) { + try { +#if EE_PLATFORM == EE_PLATFORM_WIN + std::filesystem::rename( String( fpath ).toWideString(), + String( newFilePath ).toWideString() ); +#else + std::filesystem::rename( fpath, newFilePath ); +#endif + } catch ( const std::filesystem::filesystem_error& ) { + return false; + } + + return true; +} + }} // namespace EE::System diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 5c0463e0f..4f7cd7b16 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -1,4 +1,5 @@ #include "ecode.hpp" +#include "customwidgets.hpp" #include "featureshealth.hpp" #include "keybindingshelper.hpp" #include "pathhelper.hpp" diff --git a/src/tools/ecode/ecode.hpp b/src/tools/ecode/ecode.hpp index 8f90ea6b7..95af10a22 100644 --- a/src/tools/ecode/ecode.hpp +++ b/src/tools/ecode/ecode.hpp @@ -2,7 +2,6 @@ #define ECODE_HPP #include "appconfig.hpp" -#include "customwidgets.hpp" #include "docsearchcontroller.hpp" #include "featureshealth.hpp" #include "filesystemlistener.hpp" diff --git a/src/tools/ecode/notificationcenter.cpp b/src/tools/ecode/notificationcenter.cpp index 6ef6669ae..6734a9e5c 100644 --- a/src/tools/ecode/notificationcenter.cpp +++ b/src/tools/ecode/notificationcenter.cpp @@ -8,8 +8,15 @@ using namespace EE::Scene; namespace ecode { +NotificationCenter* sInstance = nullptr; + +NotificationCenter* NotificationCenter::instance() { + return sInstance; +} + NotificationCenter::NotificationCenter( UILayout* layout, PluginManager* pluginManager ) : mLayout( layout ), mPluginManager( pluginManager ) { + sInstance = this; mPluginManager->subscribeMessages( "notificationcenter", [this]( const PluginMessage& msg ) -> PluginRequestHandle { if ( !msg.isBroadcast() ) diff --git a/src/tools/ecode/notificationcenter.hpp b/src/tools/ecode/notificationcenter.hpp index f425bd430..b272c4975 100644 --- a/src/tools/ecode/notificationcenter.hpp +++ b/src/tools/ecode/notificationcenter.hpp @@ -7,6 +7,8 @@ namespace ecode { class NotificationCenter { public: + static NotificationCenter* instance(); + NotificationCenter( UILayout* layout, PluginManager* pluginManager ); void addNotification( const String& text, const Time& delay = Seconds( 2.5 ), diff --git a/src/tools/ecode/plugins/aiassistant/chatui.cpp b/src/tools/ecode/plugins/aiassistant/chatui.cpp index 9e2d463ab..7c6325e99 100644 --- a/src/tools/ecode/plugins/aiassistant/chatui.cpp +++ b/src/tools/ecode/plugins/aiassistant/chatui.cpp @@ -1306,26 +1306,11 @@ void LLMChatUI::updateTabTitle() { tab->setText( title ); } -static bool fsRenameFile( const std::string& fpath, const std::string& newFilePath ) { - try { -#if EE_PLATFORM == EE_PLATFORM_WIN - std::filesystem::rename( String( fpath ).toWideString(), - String( newFilePath ).toWideString() ); -#else - std::filesystem::rename( fpath, newFilePath ); -#endif - } catch ( const std::filesystem::filesystem_error& ) { - return false; - } - - return true; -} - void LLMChatUI::renameChat( const std::string& newName, bool invertLockedState ) { auto oldPath = getNewFilePath( mUUID.toString(), mSummary, mChatLocked ); auto newPath = getNewFilePath( mUUID.toString(), newName, invertLockedState ? !mChatLocked : mChatLocked ); - if ( fsRenameFile( oldPath, newPath ) ) { + if ( FileSystem::fileMove( oldPath, newPath ) ) { mSummary = newName; if ( invertLockedState ) mChatLocked = !mChatLocked; diff --git a/src/tools/ecode/uitreeviewfs.cpp b/src/tools/ecode/uitreeviewfs.cpp index c75daf700..15733e5a2 100644 --- a/src/tools/ecode/uitreeviewfs.cpp +++ b/src/tools/ecode/uitreeviewfs.cpp @@ -1,15 +1,22 @@ #include "uitreeviewfs.hpp" #include "customwidgets.hpp" +#include "notificationcenter.hpp" #include #include #include #include -#include - namespace ecode { +static const std::map getDefaultKeybindings() { + return { + { { KEY_C, KeyMod::getDefaultModifier() }, "copy" }, + { { KEY_X, KeyMod::getDefaultModifier() }, "cut" }, + { { KEY_V, KeyMod::getDefaultModifier() }, "paste" }, + }; +} + class UITreeViewCellFS : public UITreeViewCell { public: static UITextView* sDragTV; @@ -30,7 +37,9 @@ class UITreeViewCellFS : public UITreeViewCell { return widget->isType( static_cast( CustomWidgets::UI_TYPE_TREEVIEWCELLFS ) ); } - UITreeViewFS* getTreeView() const { return getParent()->getParent()->asType(); } + inline UITreeViewFS* getTreeView() const { + return getParent()->getParent()->asType(); + } const FileSystemModel* getModel() const { auto model = getParent()->getParent()->asType()->getModel(); @@ -95,6 +104,14 @@ class UITreeViewCellFS : public UITreeViewCell { getEventDispatcher()->setNodeDragging( nullptr ); return 1; } + + std::string cmd = getTreeView()->getKeyBindings().getCommandFromKeyBind( + { event.getKeyCode(), event.getMod() } ); + if ( !cmd.empty() ) { + getTreeView()->execute( cmd ); + return 1; + } + return 0; } @@ -108,7 +125,33 @@ class UITreeViewCellFS : public UITreeViewCell { UITextView* UITreeViewCellFS::sDragTV = nullptr; -UITreeViewFS::UITreeViewFS() : UITreeView() {} +UITreeViewFS::UITreeViewFS() : UITreeView(), mKeyBindings( getInput() ) { + mCommands["copy"] = [this] { + if ( getSelection().isEmpty() ) + return; + mSrcCopy = getSelectionPath(); + mWasCut = false; + NotificationCenter::instance()->addNotification( + String::format( i18n( "copied_file", "Copied '%s'" ).toUtf8(), + FileSystem::fileNameFromPath( mSrcCopy ) ) ); + }; + + mCommands["cut"] = [this] { + if ( getSelection().isEmpty() ) + return; + mSrcCopy = getSelectionPath(); + mWasCut = true; + NotificationCenter::instance()->addNotification( String::format( + i18n( "cut_file", "Cut '%s'" ).toUtf8(), FileSystem::fileNameFromPath( mSrcCopy ) ) ); + }; + + mCommands["paste"] = [this] { + if ( mWasCut ) + moveFile( mSrcCopy, getSelectionPath() ); + }; + + mKeyBindings.addKeybinds( getDefaultKeybindings() ); +} UIWidget* UITreeViewFS::createCell( UIWidget* rowWidget, const ModelIndex& index ) { auto* widget = UITreeViewCellFS::New(); @@ -130,21 +173,6 @@ Uint32 UITreeViewFS::onMessage( const NodeMessage* msg ) { return UITreeView::onMessage( msg ); } -static bool fsRenameFile( const std::string& fpath, const std::string& newFilePath ) { - try { -#if EE_PLATFORM == EE_PLATFORM_WIN - std::filesystem::rename( String( fpath ).toWideString(), - String( newFilePath ).toWideString() ); -#else - std::filesystem::rename( fpath, newFilePath ); -#endif - } catch ( const std::filesystem::filesystem_error& ) { - return false; - } - - return true; -} - void UITreeViewFS::moveFile( const std::string& src, const std::string& dst ) { FileInfo srcInfo( src ); if ( !srcInfo.exists() ) @@ -179,11 +207,48 @@ void UITreeViewFS::moveFile( const std::string& src, const std::string& dst ) { partialSrc, 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 ) { fsRenameFile( srcPath, dstPath ); } ); + auto ) { FileSystem::fileMove( srcPath, dstPath ); } ); +} + +void UITreeViewFS::copyFile( const std::string& src, const std::string& dst ) { + FileInfo srcInfo( src ); + if ( !srcInfo.exists() ) + 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(); + FileSystem::fileCopy( srcPath, dstPath ); +} + +std::string UITreeViewFS::getSelectionPath() const { + return static_cast( getModel() ) + ->node( getSelection().first() ) + .fullPath(); +} + +void UITreeViewFS::execute( const std::string& cmd ) { + auto cmdIt = mCommands.find( cmd ); + if ( cmdIt != mCommands.end() ) + return cmdIt->second(); } } // namespace ecode diff --git a/src/tools/ecode/uitreeviewfs.hpp b/src/tools/ecode/uitreeviewfs.hpp index f2990e0c5..46b5b7fb7 100644 --- a/src/tools/ecode/uitreeviewfs.hpp +++ b/src/tools/ecode/uitreeviewfs.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include using namespace EE::UI; @@ -16,12 +17,26 @@ class UITreeViewFS : public UITreeView { Uint32 onMessage( const NodeMessage* msg ) override; + KeyBindings& getKeyBindings() { return mKeyBindings; } + void setSourceDrag( const std::string& src ) { mSrcDrag = src; } + void setSourceCopy( const std::string& src ) { mSrcCopy = src; } + + void execute( const std::string& cmd ); + protected: + KeyBindings mKeyBindings; + UnorderedMap> mCommands; std::string mSrcDrag; + std::string mSrcCopy; + bool mWasCut{ false }; + + std::string getSelectionPath() const; void moveFile( const std::string& src, const std::string& dst ); + + void copyFile( const std::string& src, const std::string& dst ); }; } // namespace ecode diff --git a/src/tools/ecode/uiwelcomescreen.cpp b/src/tools/ecode/uiwelcomescreen.cpp index cc68e42e8..e367e7dbe 100644 --- a/src/tools/ecode/uiwelcomescreen.cpp +++ b/src/tools/ecode/uiwelcomescreen.cpp @@ -1,4 +1,5 @@ #include "uiwelcomescreen.hpp" +#include "customwidgets.hpp" #include "ecode.hpp" #include #include