mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 03:00:23 +03:00
Add cross-directory filesystem move support
- enable efsw cross-directory move reporting in ecode - transfer FileSystemModel nodes between materialized parents - preserve selections, descendant paths, and lazy destination branches - update open document paths when parent directories move - fix persistent indexes across insert, delete, and move operations - clear stale tree-view metadata before model nodes are deleted - safely handle getRealPath() failures for stale paths - add model operation and filesystem move regression tests - update efsw and SOIL2 submodules
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
#include <limits.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef EE_COMPILER_MSVC
|
||||
#ifndef S_ISDIR
|
||||
#define S_ISDIR( f ) ( ( f ) & _S_IFDIR )
|
||||
@@ -42,6 +46,43 @@
|
||||
|
||||
namespace EE { namespace System {
|
||||
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
static void getWindowsFileIdentity( const std::string& filePath, Uint64& device, Uint64& inode,
|
||||
Uint64& linkCount ) {
|
||||
wchar_t stackPath[512];
|
||||
const int pathLength = static_cast<int>( filePath.size() );
|
||||
const int wideLength =
|
||||
MultiByteToWideChar( CP_UTF8, 0, filePath.data(), pathLength, nullptr, 0 );
|
||||
if ( wideLength <= 0 )
|
||||
return;
|
||||
|
||||
std::wstring heapPath;
|
||||
wchar_t* widePath = stackPath;
|
||||
if ( static_cast<size_t>( wideLength + 1 ) > sizeof( stackPath ) / sizeof( *stackPath ) ) {
|
||||
heapPath.resize( wideLength + 1 );
|
||||
widePath = heapPath.data();
|
||||
}
|
||||
if ( MultiByteToWideChar( CP_UTF8, 0, filePath.data(), pathLength, widePath, wideLength ) !=
|
||||
wideLength )
|
||||
return;
|
||||
widePath[wideLength] = L'\0';
|
||||
|
||||
HANDLE handle = CreateFileW( widePath, FILE_READ_ATTRIBUTES,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
||||
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr );
|
||||
if ( handle == INVALID_HANDLE_VALUE )
|
||||
return;
|
||||
|
||||
BY_HANDLE_FILE_INFORMATION info;
|
||||
if ( GetFileInformationByHandle( handle, &info ) ) {
|
||||
device = info.dwVolumeSerialNumber;
|
||||
inode = static_cast<Uint64>( info.nFileIndexHigh ) << 32 | info.nFileIndexLow;
|
||||
linkCount = info.nNumberOfLinks;
|
||||
}
|
||||
CloseHandle( handle );
|
||||
}
|
||||
#endif
|
||||
|
||||
bool FileInfo::exists( const std::string& filePath ) {
|
||||
FileInfo fi( filePath );
|
||||
return fi.exists();
|
||||
@@ -53,11 +94,7 @@ bool FileInfo::isLink( const std::string& filePath ) {
|
||||
}
|
||||
|
||||
bool FileInfo::inodeSupported() {
|
||||
#if EE_PLATFORM != EE_PLATFORM_WIN
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
FileInfo::FileInfo() :
|
||||
@@ -97,7 +134,9 @@ FileInfo::FileInfo( const FileInfo& other ) :
|
||||
mOwnerId( other.mOwnerId ),
|
||||
mGroupId( other.mGroupId ),
|
||||
mPermissions( other.mPermissions ),
|
||||
mInode( other.mInode ) {}
|
||||
mInode( other.mInode ),
|
||||
mDevice( other.mDevice ),
|
||||
mLinkCount( other.mLinkCount ) {}
|
||||
|
||||
void FileInfo::getInfo() {
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
@@ -129,6 +168,11 @@ void FileInfo::getInfo() {
|
||||
mGroupId = st.st_gid;
|
||||
mPermissions = st.st_mode;
|
||||
mInode = st.st_ino;
|
||||
mDevice = st.st_dev;
|
||||
mLinkCount = st.st_nlink;
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
getWindowsFileIdentity( std::string( fp ), mDevice, mInode, mLinkCount );
|
||||
#endif
|
||||
}
|
||||
|
||||
if ( isDirectory() )
|
||||
@@ -153,6 +197,11 @@ void FileInfo::getRealInfo() {
|
||||
mGroupId = st.st_gid;
|
||||
mPermissions = st.st_mode;
|
||||
mInode = st.st_ino;
|
||||
mDevice = st.st_dev;
|
||||
mLinkCount = st.st_nlink;
|
||||
#if EE_PLATFORM == EE_PLATFORM_WIN
|
||||
getWindowsFileIdentity( mFilepath, mDevice, mInode, mLinkCount );
|
||||
#endif
|
||||
}
|
||||
|
||||
if ( isDirectory() )
|
||||
@@ -197,6 +246,14 @@ const Uint64& FileInfo::getInode() const {
|
||||
return mInode;
|
||||
}
|
||||
|
||||
const Uint64& FileInfo::getDevice() const {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
const Uint64& FileInfo::getLinkCount() const {
|
||||
return mLinkCount;
|
||||
}
|
||||
|
||||
bool FileInfo::isUninitialized() const {
|
||||
return mModificationTime == 0;
|
||||
}
|
||||
@@ -208,7 +265,8 @@ std::string FileInfo::getExtension( const bool& lowerExt ) const {
|
||||
bool FileInfo::operator==( const FileInfo& Other ) const {
|
||||
return ( mModificationTime == Other.mModificationTime && mSize == Other.mSize &&
|
||||
mOwnerId == Other.mOwnerId && mGroupId == Other.mGroupId &&
|
||||
mPermissions == Other.mPermissions && mInode == Other.mInode );
|
||||
mPermissions == Other.mPermissions && mDevice == Other.mDevice &&
|
||||
mInode == Other.mInode );
|
||||
}
|
||||
|
||||
bool FileInfo::isDirectory() const {
|
||||
@@ -286,6 +344,8 @@ FileInfo& FileInfo::operator=( const FileInfo& other ) {
|
||||
this->mOwnerId = other.mOwnerId;
|
||||
this->mPermissions = other.mPermissions;
|
||||
this->mInode = other.mInode;
|
||||
this->mDevice = other.mDevice;
|
||||
this->mLinkCount = other.mLinkCount;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -299,7 +359,7 @@ bool FileInfo::isExecutable() const {
|
||||
}
|
||||
|
||||
bool FileInfo::sameInode( const FileInfo& other ) const {
|
||||
return inodeSupported() && mInode == other.mInode;
|
||||
return inodeSupported() && mInode != 0 && mDevice == other.mDevice && mInode == other.mInode;
|
||||
}
|
||||
|
||||
bool FileInfo::operator!=( const FileInfo& other ) const {
|
||||
|
||||
@@ -383,13 +383,17 @@ std::string FileSystem::getRealPath( const std::string& path ) {
|
||||
std::string realPath;
|
||||
#ifdef EE_PLATFORM_POSIX
|
||||
char dir[PATH_MAX];
|
||||
realpath( path.c_str(), &dir[0] );
|
||||
realPath = std::string( dir );
|
||||
if ( realpath( path.c_str(), &dir[0] ) )
|
||||
realPath = std::string( dir );
|
||||
else
|
||||
realPath = path;
|
||||
#elif EE_PLATFORM == EE_PLATFORM_WIN
|
||||
wchar_t dir[_MAX_PATH + 1];
|
||||
GetFullPathNameW( String::fromUtf8( path ).toWideString().c_str(), _MAX_PATH, &dir[0],
|
||||
nullptr );
|
||||
realPath = String( dir ).toUtf8();
|
||||
if ( GetFullPathNameW( String::fromUtf8( path ).toWideString().c_str(), _MAX_PATH, &dir[0],
|
||||
nullptr ) )
|
||||
realPath = String( dir ).toUtf8();
|
||||
else
|
||||
realPath = path;
|
||||
#else
|
||||
#warning FileSystem::getRealPath() not implemented on this platform.
|
||||
#endif
|
||||
|
||||
@@ -641,11 +641,14 @@ void FileSystemModel::setPreviouslySelectedIndex( const ModelIndex& previouslySe
|
||||
mPreviouslySelectedIndex = previouslySelectedIndex;
|
||||
}
|
||||
|
||||
size_t FileSystemModel::getFileIndex( Node* parent, const FileInfo& file ) {
|
||||
size_t FileSystemModel::getFileIndex( Node* parent, const FileInfo& file,
|
||||
const Node* excludedNode ) {
|
||||
std::vector<FileInfo> files;
|
||||
files.reserve( parent->mChildren.size() + 1 );
|
||||
|
||||
for ( Node* nodeFile : parent->mChildren ) {
|
||||
if ( nodeFile == excludedNode )
|
||||
continue;
|
||||
files.emplace_back( nodeFile->info() );
|
||||
|
||||
if ( nodeFile->info().getFileName() == file.getFileName() )
|
||||
@@ -777,6 +780,13 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
|
||||
} );
|
||||
|
||||
if ( beginDeleteRows( index.parent(), index.row(), index.row() ) ) {
|
||||
auto notifyDescendantsDeleted = [&]( auto&& notify, const Node* node ) -> void {
|
||||
for ( const Node* childNode : node->mChildren ) {
|
||||
notifyIndexDeleted( childNode );
|
||||
notify( notify, childNode );
|
||||
}
|
||||
};
|
||||
notifyDescendantsDeleted( notifyDescendantsDeleted, child );
|
||||
{
|
||||
Lock l( mResourceLock );
|
||||
eeDelete( parent->mChildren[index.row()] );
|
||||
@@ -813,11 +823,14 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
|
||||
}
|
||||
case FileSystemEventType::Moved: {
|
||||
FileInfo file( event.directory + event.filename, false );
|
||||
const std::string oldFilePath = FileSystem::isRelativePath( event.oldFilename )
|
||||
? event.directory + event.oldFilename
|
||||
: event.oldFilename;
|
||||
|
||||
if ( !file.exists() )
|
||||
return false;
|
||||
|
||||
auto* node = getNodeFromPath( event.directory + event.oldFilename, false, false );
|
||||
auto* node = getNodeFromPath( oldFilePath, false, false );
|
||||
if ( !node ) {
|
||||
return handleFileEventLocked(
|
||||
{ FileSystemEventType::Add, event.directory, event.filename } );
|
||||
@@ -826,89 +839,81 @@ bool FileSystemModel::handleFileEventLocked( const FileEvent& event ) {
|
||||
ModelIndex index = node->index( *this, 0 );
|
||||
if ( !index.isValid() )
|
||||
return false;
|
||||
ModelIndex sourceParentIndex = index.parent();
|
||||
|
||||
Node* parent = node->mParent;
|
||||
if ( !parent )
|
||||
Node* sourceParent = node->mParent;
|
||||
if ( !sourceParent )
|
||||
return false;
|
||||
|
||||
if ( ( getMode() == Mode::DirectoriesOnly && !file.isDirectory() ) )
|
||||
return false;
|
||||
|
||||
if ( !node->info().isHidden() && getDisplayConfig().ignoreHidden && file.isHidden() ) {
|
||||
return handleFileEventLocked(
|
||||
{ FileSystemEventType::Delete, event.directory, event.oldFilename } );
|
||||
return handleFileEventLocked( { FileSystemEventType::Delete, "", oldFilePath } );
|
||||
}
|
||||
|
||||
const auto& displayCfg = getDisplayConfig();
|
||||
|
||||
if ( displayCfg.fileIsVisibleFn && !displayCfg.fileIsVisibleFn( file.getFilepath() ) ) {
|
||||
return handleFileEventLocked(
|
||||
{ FileSystemEventType::Delete, event.directory, event.oldFilename } );
|
||||
return handleFileEventLocked( { FileSystemEventType::Delete, "", oldFilePath } );
|
||||
}
|
||||
|
||||
Node* childNode = parent->mChildren[index.row()];
|
||||
{
|
||||
Lock l( mResourceLock );
|
||||
childNode->rename( file );
|
||||
parent->mChildren.erase( parent->mChildren.begin() + index.row() );
|
||||
}
|
||||
|
||||
size_t pos = getFileIndex( node->getParent(), file );
|
||||
Node* targetParent = getNodeFromPath(
|
||||
file.isDirectory() ? FileSystem::removeLastFolderFromPath( file.getDirectoryPath() )
|
||||
: file.getDirectoryPath(),
|
||||
true, false );
|
||||
// Keep unopened branches lazy. The node only needs to disappear from its old,
|
||||
// materialized parent; a later traversal of the destination will discover it.
|
||||
if ( !targetParent )
|
||||
return handleFileEventLocked( { FileSystemEventType::Delete, "", oldFilePath } );
|
||||
|
||||
// Don't add the file if already exists (if moved an old file to another old
|
||||
// file)
|
||||
if ( pos == INDEX_ALREADY_EXISTS ) {
|
||||
eeDelete( childNode );
|
||||
return false;
|
||||
}
|
||||
Node* targetChild = targetParent->findChildName( file.getFileName(), *this );
|
||||
if ( targetChild && targetChild != node )
|
||||
return handleFileEventLocked( { FileSystemEventType::Delete, "", oldFilePath } );
|
||||
|
||||
std::map<UIAbstractView*, std::vector<ModelIndex>> keptSelections;
|
||||
std::map<UIAbstractView*, std::vector<std::string>> prevSelections;
|
||||
std::map<UIAbstractView*, std::vector<ModelIndex>> prevSelectionsModelIndex;
|
||||
UnorderedMap<UIAbstractView*, std::vector<ModelIndex>> selections;
|
||||
|
||||
forEachView( [&]( UIAbstractView* view ) {
|
||||
view->getSelection().forEachIndex( [&]( const ModelIndex& selectedIndex ) {
|
||||
Node* curNode = static_cast<Node*>( selectedIndex.internalData() );
|
||||
if ( curNode->mParent == parent ) {
|
||||
prevSelectionsModelIndex[view].emplace_back( selectedIndex );
|
||||
prevSelections[view].emplace_back(
|
||||
( curNode->getName() == event.oldFilename ) ? event.filename
|
||||
: curNode->getName() );
|
||||
} else {
|
||||
keptSelections[view].emplace_back( selectedIndex );
|
||||
}
|
||||
selections[view].emplace_back( selectedIndex );
|
||||
} );
|
||||
} );
|
||||
|
||||
beginMoveRows( index.parent(), index.row(), index.row(), index.parent(), pos );
|
||||
Node* childNode = sourceParent->mChildren[index.row()];
|
||||
size_t pos = getFileIndex( targetParent, file, childNode );
|
||||
eeASSERT( pos != INDEX_ALREADY_EXISTS );
|
||||
beginMoveRows( sourceParentIndex, index.row(), index.row(),
|
||||
targetParent->index( *this, 0 ), pos );
|
||||
|
||||
{
|
||||
Lock l( mResourceLock );
|
||||
if ( pos >= parent->mChildren.size() ) {
|
||||
parent->mChildren.emplace_back( childNode );
|
||||
sourceParent->mChildren.erase( sourceParent->mChildren.begin() + index.row() );
|
||||
childNode->rename( file );
|
||||
childNode->mParent = targetParent;
|
||||
}
|
||||
|
||||
{
|
||||
Lock l( mResourceLock );
|
||||
if ( pos >= targetParent->mChildren.size() ) {
|
||||
targetParent->mChildren.emplace_back( childNode );
|
||||
} else {
|
||||
parent->mChildren.insert( parent->mChildren.begin() + pos, childNode );
|
||||
targetParent->mChildren.insert( targetParent->mChildren.begin() + pos,
|
||||
childNode );
|
||||
}
|
||||
}
|
||||
|
||||
endMoveRows();
|
||||
|
||||
forEachView( [&]( UIAbstractView* view ) {
|
||||
std::vector<std::string> names = prevSelections[view];
|
||||
std::vector<ModelIndex> newIndexes = keptSelections[view];
|
||||
int i = 0;
|
||||
for ( const auto& name : names ) {
|
||||
Int64 row = -1;
|
||||
{
|
||||
Lock l( mResourceLock );
|
||||
row = parent->findChildRowFromName( name, *this );
|
||||
}
|
||||
if ( row >= 0 ) {
|
||||
newIndexes.emplace_back(
|
||||
this->index( row, prevSelectionsModelIndex[view][i].column(),
|
||||
prevSelectionsModelIndex[view][i].parent() ) );
|
||||
}
|
||||
++i;
|
||||
std::vector<ModelIndex> newIndexes;
|
||||
newIndexes.reserve( selections[view].size() );
|
||||
for ( const ModelIndex& selectedIndex : selections[view] ) {
|
||||
Node* selectedNode = static_cast<Node*>( selectedIndex.internalData() );
|
||||
ModelIndex newIndex = selectedNode->index( *this, selectedIndex.column() );
|
||||
if ( newIndex.isValid() )
|
||||
newIndexes.emplace_back( std::move( newIndex ) );
|
||||
}
|
||||
view->getSelection().set( newIndexes, false );
|
||||
} );
|
||||
|
||||
+99
-137
@@ -80,8 +80,10 @@ void Model::beginMoveRows( ModelIndex const& sourceParent, int first, int last,
|
||||
eeASSERT( first >= 0 );
|
||||
eeASSERT( first <= last );
|
||||
eeASSERT( targetIndex >= 0 );
|
||||
mOperationStack.push( { OperationType::Move, Direction::Row, sourceParent, first, last,
|
||||
targetParent, targetIndex } );
|
||||
Operation operation{ OperationType::Move, Direction::Row, sourceParent, first, last,
|
||||
targetParent, targetIndex };
|
||||
saveMovedIndices( operation );
|
||||
mOperationStack.push( std::move( operation ) );
|
||||
}
|
||||
|
||||
void Model::beginMoveColumns( ModelIndex const& sourceParent, int first, int last,
|
||||
@@ -89,12 +91,55 @@ void Model::beginMoveColumns( ModelIndex const& sourceParent, int first, int las
|
||||
eeASSERT( first >= 0 );
|
||||
eeASSERT( first <= last );
|
||||
eeASSERT( targetIndex >= 0 );
|
||||
mOperationStack.push( { OperationType::Move, Direction::Column, sourceParent, first, last,
|
||||
targetParent, targetIndex } );
|
||||
Operation operation{ OperationType::Move, Direction::Column, sourceParent, first, last,
|
||||
targetParent, targetIndex };
|
||||
saveMovedIndices( operation );
|
||||
mOperationStack.push( std::move( operation ) );
|
||||
}
|
||||
|
||||
void Model::saveMovedIndices( Operation& operation ) {
|
||||
const bool isRow = operation.direction == Direction::Row;
|
||||
const bool moveWithin = operation.sourceParent == operation.targetParent;
|
||||
const bool movingDown = operation.target > operation.first;
|
||||
const int count = operation.last - operation.first + 1;
|
||||
const int workAreaStart = std::min( operation.first, operation.target );
|
||||
const int workAreaEnd = std::max( operation.last + 1, operation.target + count );
|
||||
|
||||
operation.persistentMoves.reserve( mPersistentHandles.size() );
|
||||
for ( const auto& entry : mPersistentHandles ) {
|
||||
const ModelIndex& index = entry.first;
|
||||
const int dimension = isRow ? index.row() : index.column();
|
||||
const ModelIndex parent = index.parent();
|
||||
|
||||
if ( parent == operation.sourceParent && dimension >= operation.first &&
|
||||
dimension <= operation.last ) {
|
||||
operation.persistentMoves.push_back(
|
||||
{ index, operation.targetParent, operation.target + dimension - operation.first } );
|
||||
} else if ( moveWithin && parent == operation.sourceParent ) {
|
||||
if ( movingDown && dimension > operation.last && dimension < workAreaEnd ) {
|
||||
operation.persistentMoves.push_back(
|
||||
{ index, operation.sourceParent,
|
||||
workAreaStart + dimension - operation.last - 1 } );
|
||||
} else if ( !movingDown && dimension >= workAreaStart && dimension < operation.first ) {
|
||||
operation.persistentMoves.push_back(
|
||||
{ index, operation.sourceParent, dimension + count } );
|
||||
}
|
||||
} else if ( !moveWithin && parent == operation.sourceParent &&
|
||||
dimension > operation.last ) {
|
||||
operation.persistentMoves.push_back(
|
||||
{ index, operation.sourceParent, dimension - count } );
|
||||
} else if ( !moveWithin && parent == operation.targetParent &&
|
||||
dimension >= operation.target ) {
|
||||
operation.persistentMoves.push_back(
|
||||
{ index, operation.targetParent, dimension + count } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Model::beginDeleteRows( ModelIndex const& parent, int first, int last ) {
|
||||
if ( first >= 0 && first <= last && (size_t)last < rowCount( parent ) ) {
|
||||
for ( int row = first; row <= last; ++row )
|
||||
notifyIndexDeleted( index( row, 0, parent ).internalData() );
|
||||
saveDeletedIndices<true>( parent, first, last );
|
||||
mOperationStack.push( { OperationType::Delete, Direction::Row, parent, first, last } );
|
||||
return true;
|
||||
@@ -102,6 +147,11 @@ bool Model::beginDeleteRows( ModelIndex const& parent, int first, int last ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Model::notifyIndexDeleted( const void* internalData ) const {
|
||||
forEachView(
|
||||
[internalData]( UIAbstractView* view ) { view->onModelIndexDeleted( internalData ); } );
|
||||
}
|
||||
|
||||
bool Model::beginDeleteColumns( ModelIndex const& parent, int first, int last ) {
|
||||
if ( first >= 0 && first <= last && (size_t)last < columnCount( parent ) ) {
|
||||
saveDeletedIndices<false>( parent, first, last );
|
||||
@@ -232,75 +282,70 @@ void Model::endDeleteColumns() {
|
||||
|
||||
void Model::handleInsert( Operation const& operation ) {
|
||||
bool isRow = operation.direction == Direction::Row;
|
||||
std::vector<const ModelIndex*> toShift;
|
||||
std::vector<std::pair<ModelIndex, ModelIndex>> indexChanges;
|
||||
int offset = operation.last - operation.first + 1;
|
||||
|
||||
for ( auto& entry : mPersistentHandles ) {
|
||||
if ( entry.first.parent() == operation.sourceParent ) {
|
||||
if ( isRow && entry.first.row() >= operation.first ) {
|
||||
toShift.emplace_back( &entry.first );
|
||||
} else if ( !isRow && entry.first.column() >= operation.first ) {
|
||||
toShift.emplace_back( &entry.first );
|
||||
}
|
||||
bool shifts = isRow ? entry.first.row() >= operation.first
|
||||
: entry.first.column() >= operation.first;
|
||||
if ( !shifts )
|
||||
continue;
|
||||
int newRow = isRow ? entry.first.row() + offset : entry.first.row();
|
||||
int newColumn = isRow ? entry.first.column() : entry.first.column() + offset;
|
||||
indexChanges.emplace_back( entry.first,
|
||||
createIndex( newRow, newColumn, entry.first.internalData(),
|
||||
entry.first.internalId() ) );
|
||||
}
|
||||
}
|
||||
|
||||
int offset = operation.last - operation.first + 1;
|
||||
|
||||
for ( auto currentIndex : toShift ) {
|
||||
int newRow = isRow ? currentIndex->row() + offset : currentIndex->row();
|
||||
int newColumn = isRow ? currentIndex->column() : currentIndex->column() + offset;
|
||||
auto newIndex = createIndex( newRow, newColumn, currentIndex->internalData() );
|
||||
|
||||
auto it = mPersistentHandles.find( *currentIndex );
|
||||
auto handle = std::move( it->second );
|
||||
|
||||
handle->mIndex = newIndex;
|
||||
|
||||
mPersistentHandles.erase( it );
|
||||
mPersistentHandles[std::move( newIndex )] = std::move( handle );
|
||||
}
|
||||
applyPersistentIndexChanges( indexChanges );
|
||||
}
|
||||
|
||||
void Model::handleDelete( Operation const& operation ) {
|
||||
bool isRow = operation.direction == Direction::Row;
|
||||
std::vector<ModelIndex> deletedIndices = mDeletedIndicesStack.top();
|
||||
mDeletedIndicesStack.pop();
|
||||
std::vector<const ModelIndex*> toShift;
|
||||
std::vector<std::pair<ModelIndex, ModelIndex>> indexChanges;
|
||||
|
||||
// Get rid of all persistent handles which have been marked for death
|
||||
for ( auto& deletedIndex : deletedIndices ) {
|
||||
mPersistentHandles.erase( deletedIndex );
|
||||
}
|
||||
|
||||
int offset = operation.last - operation.first + 1;
|
||||
for ( auto& entry : mPersistentHandles ) {
|
||||
if ( entry.first.parent() == operation.sourceParent ) {
|
||||
if ( isRow ) {
|
||||
if ( entry.first.row() > operation.last ) {
|
||||
toShift.emplace_back( &entry.first );
|
||||
}
|
||||
} else {
|
||||
if ( entry.first.column() > operation.last ) {
|
||||
toShift.emplace_back( &entry.first );
|
||||
}
|
||||
}
|
||||
bool shifts =
|
||||
isRow ? entry.first.row() > operation.last : entry.first.column() > operation.last;
|
||||
if ( !shifts )
|
||||
continue;
|
||||
int newRow = isRow ? entry.first.row() - offset : entry.first.row();
|
||||
int newColumn = isRow ? entry.first.column() : entry.first.column() - offset;
|
||||
indexChanges.emplace_back( entry.first,
|
||||
createIndex( newRow, newColumn, entry.first.internalData(),
|
||||
entry.first.internalId() ) );
|
||||
}
|
||||
}
|
||||
|
||||
int offset = operation.last - operation.first + 1;
|
||||
|
||||
for ( auto currentIndex : toShift ) {
|
||||
int newRow = isRow ? currentIndex->row() - offset : currentIndex->row();
|
||||
int newColumn = isRow ? currentIndex->column() : currentIndex->column() - offset;
|
||||
auto newIndex = createIndex( newRow, newColumn, currentIndex->internalData() );
|
||||
|
||||
auto it = mPersistentHandles.find( *currentIndex );
|
||||
auto handle = std::move( it->second );
|
||||
|
||||
handle->mIndex = newIndex;
|
||||
applyPersistentIndexChanges( indexChanges );
|
||||
}
|
||||
|
||||
void Model::applyPersistentIndexChanges(
|
||||
const std::vector<std::pair<ModelIndex, ModelIndex>>& indexChanges ) {
|
||||
std::vector<std::pair<ModelIndex, std::shared_ptr<PersistentHandle>>> changedHandles;
|
||||
changedHandles.reserve( indexChanges.size() );
|
||||
for ( const auto& change : indexChanges ) {
|
||||
auto it = mPersistentHandles.find( change.first );
|
||||
if ( it == mPersistentHandles.end() )
|
||||
continue;
|
||||
it->second->mIndex = change.second;
|
||||
changedHandles.emplace_back( change.second, std::move( it->second ) );
|
||||
mPersistentHandles.erase( it );
|
||||
mPersistentHandles[std::move( newIndex )] = std::move( handle );
|
||||
}
|
||||
for ( auto& changedHandle : changedHandles )
|
||||
mPersistentHandles.emplace( std::move( changedHandle.first ),
|
||||
std::move( changedHandle.second ) );
|
||||
}
|
||||
|
||||
Variant Model::stylizeModel( const ModelIndex& index, const void* data ) const {
|
||||
@@ -336,101 +381,18 @@ void Model::unsubscribeModelStyler( Uint32 id ) {
|
||||
void Model::handleMove( Operation const& operation ) {
|
||||
bool isRow = operation.direction == Direction::Row;
|
||||
bool moveWithin = operation.sourceParent == operation.targetParent;
|
||||
bool movingDown = operation.target > operation.first;
|
||||
|
||||
if ( moveWithin && operation.first == operation.target )
|
||||
return;
|
||||
|
||||
if ( isRow ) {
|
||||
eeASSERT( operation.target <= (int)rowCount( operation.targetParent ) );
|
||||
eeASSERT( operation.last < (int)rowCount( operation.sourceParent ) );
|
||||
} else {
|
||||
eeASSERT( operation.target <= (int)columnCount( operation.targetParent ) );
|
||||
eeASSERT( operation.last < (int)columnCount( operation.sourceParent ) );
|
||||
}
|
||||
|
||||
// NOTE: to_shift_down is used as a generic "to shift" when move_within is true.
|
||||
std::vector<const ModelIndex*> toMove; // Items to be moved between the source and target
|
||||
std::vector<const ModelIndex*> toShiftDown; // Items to be shifted down after a move-to
|
||||
std::vector<const ModelIndex*> toShiftUp; // Items to be shifted up after a move-from
|
||||
|
||||
int count = operation.last - operation.first + 1;
|
||||
// [start, end)
|
||||
int workAreaStart = std::min( operation.first, operation.target );
|
||||
int work_area_end = std::max( operation.last + 1, operation.target + count );
|
||||
|
||||
for ( auto& entry : mPersistentHandles ) {
|
||||
int dimension = isRow ? entry.first.row() : entry.first.column();
|
||||
|
||||
if ( moveWithin ) {
|
||||
if ( entry.first.parent() == operation.sourceParent ) {
|
||||
if ( dimension >= operation.first && dimension <= operation.last ) {
|
||||
toMove.emplace_back( &entry.first );
|
||||
} else if ( movingDown && dimension > operation.last &&
|
||||
dimension < work_area_end ) {
|
||||
toShiftDown.emplace_back( &entry.first );
|
||||
} else if ( !movingDown && dimension >= workAreaStart &&
|
||||
dimension < operation.first ) {
|
||||
toShiftDown.emplace_back( &entry.first );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ( entry.first.parent() == operation.sourceParent ) {
|
||||
if ( dimension >= operation.first && dimension <= operation.last ) {
|
||||
toMove.emplace_back( &entry.first );
|
||||
} else if ( dimension > operation.last ) {
|
||||
toShiftUp.emplace_back( &entry.first );
|
||||
}
|
||||
} else if ( entry.first.parent() == operation.targetParent ) {
|
||||
if ( dimension >= operation.target ) {
|
||||
toShiftDown.emplace_back( &entry.first );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto replaceHandle = [&]( ModelIndex const& currentIndex, int newDimension, bool relative ) {
|
||||
int newRow = isRow ? ( relative ? currentIndex.row() + newDimension : newDimension )
|
||||
: currentIndex.row();
|
||||
int newColumn = !isRow ? ( relative ? currentIndex.column() + newDimension : newDimension )
|
||||
: currentIndex.column();
|
||||
auto newIndex = index( newRow, newColumn, operation.targetParent );
|
||||
|
||||
auto it = mPersistentHandles.find( currentIndex );
|
||||
auto handle = std::move( it->second );
|
||||
|
||||
handle->mIndex = newIndex;
|
||||
|
||||
mPersistentHandles.erase( it );
|
||||
mPersistentHandles[std::move( newIndex )] = std::move( handle );
|
||||
};
|
||||
|
||||
for ( auto currentIndex : toMove ) {
|
||||
int dimension = isRow ? currentIndex->row() : currentIndex->column();
|
||||
int targetOffset = dimension - operation.first;
|
||||
int newDimension = operation.target + targetOffset;
|
||||
|
||||
replaceHandle( *currentIndex, newDimension, false );
|
||||
}
|
||||
|
||||
if ( moveWithin ) {
|
||||
for ( auto currentIndex : toShiftDown ) {
|
||||
int dimension = isRow ? currentIndex->row() : currentIndex->column();
|
||||
int targetOffset =
|
||||
movingDown ? dimension - ( operation.last + 1 ) : dimension - workAreaStart + count;
|
||||
int newDimension = workAreaStart + targetOffset;
|
||||
|
||||
replaceHandle( *currentIndex, newDimension, false );
|
||||
}
|
||||
} else {
|
||||
for ( auto currentIndex : toShiftDown ) {
|
||||
replaceHandle( *currentIndex, count, true );
|
||||
}
|
||||
|
||||
for ( auto currentIndex : toShiftUp ) {
|
||||
replaceHandle( *currentIndex, count, true );
|
||||
}
|
||||
std::vector<std::pair<ModelIndex, ModelIndex>> indexChanges;
|
||||
indexChanges.reserve( operation.persistentMoves.size() );
|
||||
for ( const auto& move : operation.persistentMoves ) {
|
||||
int newRow = isRow ? move.targetDimension : move.index.row();
|
||||
int newColumn = isRow ? move.index.column() : move.targetDimension;
|
||||
indexChanges.emplace_back( move.index, index( newRow, newColumn, move.targetParent ) );
|
||||
}
|
||||
applyPersistentIndexChanges( indexChanges );
|
||||
}
|
||||
|
||||
}}} // namespace EE::UI::Models
|
||||
|
||||
@@ -45,6 +45,10 @@ UITreeView::MetadataForIndex& UITreeView::getIndexMetadata( const ModelIndex& in
|
||||
return mViewMetadata[index.internalData()];
|
||||
}
|
||||
|
||||
void UITreeView::onModelIndexDeleted( const void* internalData ) {
|
||||
mViewMetadata.erase( const_cast<void*>( internalData ) );
|
||||
}
|
||||
|
||||
UITreeView::IterationDecision UITreeView::traverseIndex( TraverseTreeVars& v,
|
||||
const ModelIndex& index ) const {
|
||||
if ( index.isValid() ) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "utest.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <eepp/system/fileinfo.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
|
||||
namespace {
|
||||
|
||||
class FileInfoIdentity : public FileInfo {
|
||||
public:
|
||||
void setIdentity( Uint64 device, Uint64 inode ) {
|
||||
mDevice = device;
|
||||
mInode = inode;
|
||||
}
|
||||
};
|
||||
|
||||
struct TempDirectory {
|
||||
TempDirectory() {
|
||||
path = std::filesystem::temp_directory_path() /
|
||||
( "eepp-fileinfo-" +
|
||||
std::to_string( std::chrono::steady_clock::now().time_since_epoch().count() ) );
|
||||
std::filesystem::create_directories( path );
|
||||
}
|
||||
|
||||
~TempDirectory() { std::filesystem::remove_all( path ); }
|
||||
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
UTEST( FileInfo, sameInodeUsesDeviceAndRejectsInvalidIdentity ) {
|
||||
FileInfoIdentity first;
|
||||
FileInfoIdentity same;
|
||||
FileInfoIdentity otherDevice;
|
||||
FileInfoIdentity invalid;
|
||||
|
||||
first.setIdentity( 1, 42 );
|
||||
same.setIdentity( 1, 42 );
|
||||
otherDevice.setIdentity( 2, 42 );
|
||||
invalid.setIdentity( 1, 0 );
|
||||
|
||||
EXPECT_TRUE( FileInfo::inodeSupported() );
|
||||
EXPECT_TRUE( first.sameInode( same ) );
|
||||
EXPECT_FALSE( first.sameInode( otherDevice ) );
|
||||
EXPECT_FALSE( invalid.sameInode( invalid ) );
|
||||
}
|
||||
|
||||
UTEST( FileInfo, reportsHardLinkIdentityAndLinkCount ) {
|
||||
TempDirectory temp;
|
||||
const std::filesystem::path original = temp.path / "original.txt";
|
||||
const std::filesystem::path hardLink = temp.path / "hard-link.txt";
|
||||
|
||||
ASSERT_TRUE( FileSystem::fileWrite( original.string(), "contents" ) );
|
||||
std::error_code error;
|
||||
std::filesystem::create_hard_link( original, hardLink, error );
|
||||
if ( error ) {
|
||||
const std::string message = "hard links are unavailable: " + error.message();
|
||||
UTEST_SKIP( message.c_str() );
|
||||
}
|
||||
|
||||
FileInfo originalInfo( original.string() );
|
||||
FileInfo hardLinkInfo( hardLink.string() );
|
||||
|
||||
EXPECT_TRUE( originalInfo.sameInode( hardLinkInfo ) );
|
||||
EXPECT_EQ( originalInfo.getDevice(), hardLinkInfo.getDevice() );
|
||||
EXPECT_EQ( originalInfo.getInode(), hardLinkInfo.getInode() );
|
||||
EXPECT_TRUE( originalInfo.getLinkCount() >= 2 );
|
||||
EXPECT_TRUE( hardLinkInfo.getLinkCount() >= 2 );
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "utest.hpp"
|
||||
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/ui/models/filesystemmodel.hpp>
|
||||
#include <eepp/ui/models/persistentmodelindex.hpp>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace EE::UI::Models;
|
||||
|
||||
namespace {
|
||||
|
||||
class MoveTestModel : public Model {
|
||||
public:
|
||||
struct Node {
|
||||
std::string name;
|
||||
Node* parent{ nullptr };
|
||||
std::vector<std::unique_ptr<Node>> children;
|
||||
};
|
||||
|
||||
MoveTestModel() {
|
||||
a = append( root, "a" );
|
||||
b = append( root, "b" );
|
||||
a0 = append( *a, "a0" );
|
||||
a1 = append( *a, "a1" );
|
||||
a2 = append( *a, "a2" );
|
||||
b0 = append( *b, "b0" );
|
||||
b1 = append( *b, "b1" );
|
||||
}
|
||||
|
||||
size_t rowCount( const ModelIndex& parent = {} ) const {
|
||||
return node( parent ).children.size();
|
||||
}
|
||||
|
||||
size_t columnCount( const ModelIndex& = {} ) const { return 1; }
|
||||
|
||||
Variant data( const ModelIndex& index, ModelRole = ModelRole::Display ) const {
|
||||
return static_cast<Node*>( index.internalData() )->name.c_str();
|
||||
}
|
||||
|
||||
ModelIndex parentIndex( const ModelIndex& index ) const {
|
||||
Node* parent = static_cast<Node*>( index.internalData() )->parent;
|
||||
if ( !parent || parent == &root )
|
||||
return {};
|
||||
return indexFor( parent );
|
||||
}
|
||||
|
||||
ModelIndex index( int row, int column = 0, const ModelIndex& parent = {} ) const {
|
||||
const auto& children = node( parent ).children;
|
||||
if ( row < 0 || static_cast<size_t>( row ) >= children.size() )
|
||||
return {};
|
||||
return createIndex( row, column, children[row].get() );
|
||||
}
|
||||
|
||||
ModelIndex indexFor( Node* node ) const {
|
||||
for ( size_t row = 0; row < node->parent->children.size(); ++row )
|
||||
if ( node->parent->children[row].get() == node )
|
||||
return createIndex( row, 0, node );
|
||||
return {};
|
||||
}
|
||||
|
||||
void moveA1ToB() {
|
||||
beginMoveRows( indexFor( a ), 1, 1, indexFor( b ), 1 );
|
||||
a1->parent = b;
|
||||
auto moved = std::move( a->children[1] );
|
||||
a->children.erase( a->children.begin() + 1 );
|
||||
b->children.insert( b->children.begin() + 1, std::move( moved ) );
|
||||
endMoveRows();
|
||||
}
|
||||
|
||||
Node root{ "root" };
|
||||
Node* a{ nullptr };
|
||||
Node* b{ nullptr };
|
||||
Node* a0{ nullptr };
|
||||
Node* a1{ nullptr };
|
||||
Node* a2{ nullptr };
|
||||
Node* b0{ nullptr };
|
||||
Node* b1{ nullptr };
|
||||
|
||||
private:
|
||||
static Node* append( Node& parent, std::string name ) {
|
||||
parent.children.emplace_back(
|
||||
std::make_unique<Node>( Node{ std::move( name ), &parent } ) );
|
||||
return parent.children.back().get();
|
||||
}
|
||||
|
||||
const Node& node( const ModelIndex& index ) const {
|
||||
return index.isValid() ? *static_cast<Node*>( index.internalData() ) : root;
|
||||
}
|
||||
};
|
||||
|
||||
class PersistentRowsModel : public Model {
|
||||
public:
|
||||
PersistentRowsModel( bool useInternalIds ) : mUseInternalIds( useInternalIds ) {
|
||||
mRows = { 10, 20, 30, 40 };
|
||||
}
|
||||
|
||||
size_t rowCount( const ModelIndex& = {} ) const { return mRows.size(); }
|
||||
|
||||
size_t columnCount( const ModelIndex& = {} ) const { return 1; }
|
||||
|
||||
Variant data( const ModelIndex& index, ModelRole = ModelRole::Display ) const {
|
||||
return mRows[index.row()];
|
||||
}
|
||||
|
||||
ModelIndex index( int row, int column = 0, const ModelIndex& = {} ) const {
|
||||
if ( row < 0 || static_cast<size_t>( row ) >= mRows.size() )
|
||||
return {};
|
||||
return createIndex( row, column, nullptr, mUseInternalIds ? mRows[row] : 0 );
|
||||
}
|
||||
|
||||
void insertAt( int row, int value ) {
|
||||
beginInsertRows( {}, row, row );
|
||||
mRows.insert( mRows.begin() + row, value );
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void deleteAt( int row ) {
|
||||
if ( beginDeleteRows( {}, row, row ) ) {
|
||||
mRows.erase( mRows.begin() + row );
|
||||
endDeleteRows();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<int> mRows;
|
||||
bool mUseInternalIds;
|
||||
};
|
||||
|
||||
struct TempTree {
|
||||
TempTree() {
|
||||
static unsigned long long id = 0;
|
||||
path = std::filesystem::temp_directory_path() /
|
||||
( "eepp-filesystem-model-move-" + std::to_string( ++id ) );
|
||||
std::filesystem::remove_all( path );
|
||||
std::filesystem::create_directories( path );
|
||||
}
|
||||
|
||||
~TempTree() { std::filesystem::remove_all( path ); }
|
||||
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
UTEST( ModelMove, crossParentPersistentIndexesFollowNodesAndShiftSiblings ) {
|
||||
MoveTestModel model;
|
||||
PersistentModelIndex moved( model.indexFor( model.a1 ) );
|
||||
PersistentModelIndex sourceSibling( model.indexFor( model.a2 ) );
|
||||
PersistentModelIndex targetSibling( model.indexFor( model.b1 ) );
|
||||
|
||||
model.moveA1ToB();
|
||||
|
||||
ASSERT_EQ( moved.row(), 1 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( moved ).internalData(), model.a1 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( moved.parent() ).internalData(), model.b );
|
||||
ASSERT_EQ( sourceSibling.row(), 1 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( sourceSibling ).internalData(), model.a2 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( sourceSibling.parent() ).internalData(), model.a );
|
||||
ASSERT_EQ( targetSibling.row(), 2 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( targetSibling ).internalData(), model.b1 );
|
||||
ASSERT_EQ( static_cast<ModelIndex>( targetSibling.parent() ).internalData(), model.b );
|
||||
}
|
||||
|
||||
UTEST( ModelInsert, persistentIndexesShiftWithoutIdentityCollisions ) {
|
||||
PersistentRowsModel model( false );
|
||||
PersistentModelIndex first( model.index( 0 ) );
|
||||
PersistentModelIndex second( model.index( 1 ) );
|
||||
PersistentModelIndex third( model.index( 2 ) );
|
||||
|
||||
model.insertAt( 0, 5 );
|
||||
|
||||
ASSERT_EQ( first.row(), 1 );
|
||||
ASSERT_EQ( second.row(), 2 );
|
||||
ASSERT_EQ( third.row(), 3 );
|
||||
ASSERT_EQ( first.data().asInt(), 10 );
|
||||
ASSERT_EQ( second.data().asInt(), 20 );
|
||||
ASSERT_EQ( third.data().asInt(), 30 );
|
||||
}
|
||||
|
||||
UTEST( ModelInsert, persistentIndexesPreserveInternalIds ) {
|
||||
PersistentRowsModel model( true );
|
||||
PersistentModelIndex tracked( model.index( 2 ) );
|
||||
|
||||
model.insertAt( 1, 15 );
|
||||
|
||||
ModelIndex index = tracked;
|
||||
ASSERT_EQ( tracked.row(), 3 );
|
||||
ASSERT_EQ( index.internalId(), 30 );
|
||||
}
|
||||
|
||||
UTEST( ModelDelete, persistentIndexesExpireOrShiftWithoutIdentityCollisions ) {
|
||||
PersistentRowsModel model( false );
|
||||
PersistentModelIndex deleted( model.index( 1 ) );
|
||||
PersistentModelIndex third( model.index( 2 ) );
|
||||
PersistentModelIndex fourth( model.index( 3 ) );
|
||||
|
||||
model.deleteAt( 1 );
|
||||
|
||||
ASSERT_FALSE( deleted.isValid() );
|
||||
ASSERT_EQ( third.row(), 1 );
|
||||
ASSERT_EQ( fourth.row(), 2 );
|
||||
ASSERT_EQ( third.data().asInt(), 30 );
|
||||
ASSERT_EQ( fourth.data().asInt(), 40 );
|
||||
}
|
||||
|
||||
UTEST( ModelDelete, persistentIndexesPreserveInternalIds ) {
|
||||
PersistentRowsModel model( true );
|
||||
PersistentModelIndex tracked( model.index( 3 ) );
|
||||
|
||||
model.deleteAt( 1 );
|
||||
|
||||
ModelIndex index = tracked;
|
||||
ASSERT_EQ( tracked.row(), 2 );
|
||||
ASSERT_EQ( index.internalId(), 40 );
|
||||
}
|
||||
|
||||
UTEST( FileSystemModelMove, preservesNodeAndDescendantPathsAcrossDirectories ) {
|
||||
TempTree tree;
|
||||
auto source = tree.path / "source";
|
||||
auto target = tree.path / "target";
|
||||
std::filesystem::create_directories( source / "folder" );
|
||||
std::filesystem::create_directories( target );
|
||||
std::FILE* file = std::fopen( ( source / "folder" / "file.txt" ).string().c_str(), "wb" );
|
||||
ASSERT_TRUE( file != nullptr );
|
||||
std::fclose( file );
|
||||
|
||||
auto model = FileSystemModel::New( tree.path.string() );
|
||||
auto* folderNode = model->getNodeFromPath( ( source / "folder" ).string(), true );
|
||||
auto* fileNode = model->getNodeFromPath( ( source / "folder" / "file.txt" ).string() );
|
||||
ASSERT_TRUE( folderNode != nullptr );
|
||||
ASSERT_TRUE( fileNode != nullptr );
|
||||
|
||||
std::filesystem::rename( source / "folder", target / "renamed" );
|
||||
ASSERT_EQ( model->getNodeFromPath( ( source / "folder" ).string(), false, false ), folderNode );
|
||||
ASSERT_TRUE( model->handleFileEvent( { FileSystemEventType::Moved,
|
||||
target.string() + EE::System::FileSystem::getOSSlash(),
|
||||
"renamed", ( source / "folder" ).string() } ) );
|
||||
|
||||
std::string expectedFolderPath( ( target / "renamed" ).string() );
|
||||
EE::System::FileSystem::dirAddSlashAtEnd( expectedFolderPath );
|
||||
ASSERT_STDSTREQ( folderNode->fullPath(), expectedFolderPath );
|
||||
ASSERT_EQ( model->getNodeFromPath( ( target / "renamed" ).string(), true, false ), folderNode );
|
||||
ASSERT_EQ( model->getNodeFromPath( ( target / "renamed" / "file.txt" ).string(), false, false ),
|
||||
fileNode );
|
||||
ASSERT_STDSTREQ( fileNode->fullPath(), ( target / "renamed" / "file.txt" ).string() );
|
||||
}
|
||||
|
||||
UTEST( FileSystemModelMove, keepsUnopenedDestinationBranchLazy ) {
|
||||
TempTree tree;
|
||||
auto source = tree.path / "source";
|
||||
auto destination = tree.path / "outer" / "destination";
|
||||
std::filesystem::create_directories( source );
|
||||
std::filesystem::create_directories( destination );
|
||||
std::FILE* file = std::fopen( ( source / "file.txt" ).string().c_str(), "wb" );
|
||||
ASSERT_TRUE( file != nullptr );
|
||||
std::fclose( file );
|
||||
|
||||
auto model = FileSystemModel::New( tree.path.string() );
|
||||
ASSERT_TRUE( model->getNodeFromPath( ( source / "file.txt" ).string() ) != nullptr );
|
||||
ASSERT_TRUE( model->getNodeFromPath( destination.string(), true, false ) == nullptr );
|
||||
|
||||
std::filesystem::rename( source / "file.txt", destination / "file.txt" );
|
||||
ASSERT_TRUE( model->handleFileEvent(
|
||||
{ FileSystemEventType::Moved, destination.string() + EE::System::FileSystem::getOSSlash(),
|
||||
"file.txt", ( source / "file.txt" ).string() } ) );
|
||||
|
||||
ASSERT_TRUE( model->getNodeFromPath( ( source / "file.txt" ).string(), false, false ) ==
|
||||
nullptr );
|
||||
ASSERT_TRUE( model->getNodeFromPath( destination.string(), true, false ) == nullptr );
|
||||
ASSERT_TRUE( model->getNodeFromPath( ( destination / "file.txt" ).string(), false, true ) !=
|
||||
nullptr );
|
||||
}
|
||||
Vendored
+1
-1
Submodule src/thirdparty/SOIL2 updated: af13ed898a...dc92c9ec6d
Vendored
+1
-1
Submodule src/thirdparty/efsw updated: 3535c3a382...cb189cd3ce
@@ -3572,7 +3572,8 @@ void App::loadDirTree( const std::string& path ) {
|
||||
mFolderWatches.insert( { dirTree.getPath(), 0 } );
|
||||
}
|
||||
mFolderWatches[dirTree.getPath()] =
|
||||
mFileWatcher->addWatch( dirTree.getPath(), mFileSystemListener, true );
|
||||
mFileWatcher->addWatch( dirTree.getPath(), mFileSystemListener, true,
|
||||
{ { efsw::Options::ReportCrossDirectoryMoves, 1 } } );
|
||||
}
|
||||
mFileSystemListener->setDirTree( mDirTree );
|
||||
},
|
||||
|
||||
@@ -62,14 +62,12 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
if ( file.isLink() )
|
||||
file = FileInfo( file.linksTo() );
|
||||
|
||||
if ( isFileOpen( oldFile ) )
|
||||
notifyMove( oldFile, file );
|
||||
notifyMove( oldFile, file );
|
||||
|
||||
if ( oldFile.isLink() ) {
|
||||
oldFile = FileInfo( oldFile.linksTo() );
|
||||
|
||||
if ( isFileOpen( oldFile ) )
|
||||
notifyMove( oldFile, file );
|
||||
notifyMove( oldFile, file );
|
||||
}
|
||||
} else if ( action == efsw::Actions::Delete ) {
|
||||
notifyDelete( file );
|
||||
@@ -83,8 +81,7 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
|
||||
Lock l( mCbsMutex );
|
||||
if ( !mCbs.empty() ) {
|
||||
auto cbs = mCbs;
|
||||
for ( const auto& cb : cbs )
|
||||
for ( const auto& cb : mCbs )
|
||||
cb.second( event, file );
|
||||
}
|
||||
|
||||
@@ -98,9 +95,8 @@ void FileSystemListener::handleFileAction( efsw::WatchID, const std::string& dir
|
||||
|
||||
Lock l( mCbsMutex );
|
||||
if ( !mCbs.empty() ) {
|
||||
auto cbs = mCbs;
|
||||
FileEvent event( (FileSystemEventType)action, dir, filename, oldFilename );
|
||||
for ( const auto& cb : cbs )
|
||||
for ( const auto& cb : mCbs )
|
||||
cb.second( event, file );
|
||||
}
|
||||
}
|
||||
@@ -162,9 +158,21 @@ void FileSystemListener::notifyChange( const FileInfo& file ) {
|
||||
}
|
||||
|
||||
void FileSystemListener::notifyMove( const FileInfo& oldFile, const FileInfo& newFile ) {
|
||||
std::string oldPath( oldFile.getFilepath() );
|
||||
std::string newPath( newFile.getFilepath() );
|
||||
const bool directoryMoved = newFile.isDirectory();
|
||||
if ( directoryMoved ) {
|
||||
FileSystem::dirAddSlashAtEnd( oldPath );
|
||||
FileSystem::dirAddSlashAtEnd( newPath );
|
||||
}
|
||||
|
||||
mSplitter->forEachDoc( [&]( TextDocument& doc ) {
|
||||
if ( oldFile.getFilepath() == doc.getFileInfo().getFilepath() )
|
||||
doc.notifyDocumentMoved( newFile.getFilepath() );
|
||||
const std::string& documentPath = doc.getFileInfo().getFilepath();
|
||||
if ( oldPath == documentPath ) {
|
||||
doc.notifyDocumentMoved( newPath );
|
||||
} else if ( directoryMoved && String::startsWith( documentPath, oldPath ) ) {
|
||||
doc.notifyDocumentMoved( newPath + documentPath.substr( oldPath.size() ) );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user