mirror of
https://github.com/beefytech/Beef.git
synced 2025-06-08 11:38:21 +02:00
Copy/cut/paste support for workspace files and folders
This commit is contained in:
parent
0907bd1f52
commit
05c2e1587c
4 changed files with 529 additions and 207 deletions
|
@ -792,9 +792,23 @@ namespace Beefy
|
||||||
BFApp_SetCursor((int32)cursor);
|
BFApp_SetCursor((int32)cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void* GetClipboardData(String format, out int32 size)
|
public virtual void* GetClipboardData(String format, out int32 size, int waitTime = 500)
|
||||||
{
|
{
|
||||||
return BFApp_GetClipboardData(format, out size);
|
Stopwatch sw = null;
|
||||||
|
repeat
|
||||||
|
{
|
||||||
|
if (sw != null)
|
||||||
|
Thread.Sleep(1);
|
||||||
|
void* result = BFApp_GetClipboardData(format, out size);
|
||||||
|
if (size != -1)
|
||||||
|
return result;
|
||||||
|
if (waitTime == 0)
|
||||||
|
return null;
|
||||||
|
if (sw == null)
|
||||||
|
sw = scope:: .()..Start();
|
||||||
|
}
|
||||||
|
while (waitTime < sw.ElapsedMilliseconds);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void ReleaseClipboardData(void* ptr)
|
public virtual void ReleaseClipboardData(void* ptr)
|
||||||
|
@ -802,9 +816,9 @@ namespace Beefy
|
||||||
BFApp_ReleaseClipboardData(ptr);
|
BFApp_ReleaseClipboardData(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool GetClipboardText(String outStr)
|
public virtual bool GetClipboardText(String outStr, int waitTime = 500)
|
||||||
{
|
{
|
||||||
return GetClipboardTextData("text", outStr);
|
return GetClipboardTextData("text", outStr, waitTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool GetClipboardText(String outStr, String extra)
|
public virtual bool GetClipboardText(String outStr, String extra)
|
||||||
|
@ -813,10 +827,10 @@ namespace Beefy
|
||||||
return GetClipboardTextData("text", outStr);
|
return GetClipboardTextData("text", outStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetClipboardTextData(String format, String outStr)
|
public bool GetClipboardTextData(String format, String outStr, int waitTime = 500)
|
||||||
{
|
{
|
||||||
int32 aSize;
|
int32 aSize;
|
||||||
void* clipboardData = GetClipboardData(format, out aSize);
|
void* clipboardData = GetClipboardData(format, out aSize, waitTime);
|
||||||
if (clipboardData == null)
|
if (clipboardData == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -304,6 +304,7 @@ namespace IDE
|
||||||
public Color mWorkspaceFailedText = 0xFFE04040;
|
public Color mWorkspaceFailedText = 0xFFE04040;
|
||||||
public Color mWorkspaceManualIncludeText = 0xFFE0E0FF;
|
public Color mWorkspaceManualIncludeText = 0xFFE0E0FF;
|
||||||
public Color mWorkspaceIgnoredText = 0xFF909090;
|
public Color mWorkspaceIgnoredText = 0xFF909090;
|
||||||
|
public Color mWorkspaceCutText = 0xFFC0B0B0;
|
||||||
|
|
||||||
public Color mCode = 0xFFFFFFFF;
|
public Color mCode = 0xFFFFFFFF;
|
||||||
public Color mKeyword = 0xFFE1AE9A;
|
public Color mKeyword = 0xFFE1AE9A;
|
||||||
|
|
|
@ -61,6 +61,17 @@ namespace IDE.ui
|
||||||
else if (projectItem.mIncludeKind == .Ignore)
|
else if (projectItem.mIncludeKind == .Ignore)
|
||||||
color = Color.Mult(color, gApp.mSettings.mUISettings.mColors.mWorkspaceIgnoredText);
|
color = Color.Mult(color, gApp.mSettings.mUISettings.mColors.mWorkspaceIgnoredText);
|
||||||
|
|
||||||
|
if (let projectFileItem = projectItem as ProjectFileItem)
|
||||||
|
{
|
||||||
|
if (projectPanel.mClipboardCutQueued != null)
|
||||||
|
{
|
||||||
|
var path = projectFileItem.mProject.GetProjectFullPath(projectFileItem.mPath, .. scope .());
|
||||||
|
IDEUtils.MakeComparableFilePath(path);
|
||||||
|
if (projectPanel.mClipboardCutQueued.Contains(path))
|
||||||
|
color = Color.Mult(color, gApp.mSettings.mUISettings.mColors.mWorkspaceCutText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (let projectSource = projectItem as ProjectSource)
|
if (let projectSource = projectItem as ProjectSource)
|
||||||
{
|
{
|
||||||
if (projectSource.mLoadFailed)
|
if (projectSource.mLoadFailed)
|
||||||
|
@ -146,6 +157,7 @@ namespace IDE.ui
|
||||||
public bool mShowIgnored = true;
|
public bool mShowIgnored = true;
|
||||||
public bool mSortDirty;
|
public bool mSortDirty;
|
||||||
public bool mWantsRehup;
|
public bool mWantsRehup;
|
||||||
|
public HashSet<String> mClipboardCutQueued ~ DeleteContainerAndItems!(_);
|
||||||
|
|
||||||
public this()
|
public this()
|
||||||
{
|
{
|
||||||
|
@ -2050,6 +2062,307 @@ namespace IDE.ui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CopyToClipboard()
|
||||||
|
{
|
||||||
|
String clipData = scope .();
|
||||||
|
mListView.GetRoot().WithSelectedItems(scope (selectedItem) =>
|
||||||
|
{
|
||||||
|
if (mListViewToProjectMap.GetValue(selectedItem) case .Ok(var sourceProjectItem))
|
||||||
|
{
|
||||||
|
String path = scope .();
|
||||||
|
if (var projectFileItem = sourceProjectItem as ProjectFileItem)
|
||||||
|
{
|
||||||
|
sourceProjectItem.mProject.GetProjectFullPath(projectFileItem.mPath, path);
|
||||||
|
path.Replace('\\', '/');
|
||||||
|
|
||||||
|
if (!clipData.IsEmpty)
|
||||||
|
clipData.Append("\n");
|
||||||
|
clipData.Append("file:///");
|
||||||
|
IDEUtils.URLEncode(path, clipData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!clipData.IsEmpty)
|
||||||
|
gApp.SetClipboardData("code/file-list", clipData.Ptr, (.)clipData.Length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CutToClipboard()
|
||||||
|
{
|
||||||
|
DeleteContainerAndItems!(mClipboardCutQueued);
|
||||||
|
mClipboardCutQueued = null;
|
||||||
|
|
||||||
|
CopyToClipboard();
|
||||||
|
mClipboardCutQueued = new .();
|
||||||
|
ValidateCutClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ValidateCutClipboard()
|
||||||
|
{
|
||||||
|
if (mClipboardCutQueued == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
void* data = gApp.GetClipboardData("code/file-list", var size, 0);
|
||||||
|
if (size == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClearAndDeleteItems!(mClipboardCutQueued);
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
StringView sv = .((.)data, size);
|
||||||
|
for (var line in sv.Split('\n'))
|
||||||
|
{
|
||||||
|
var uri = IDEUtils.URLDecode(line, .. scope .());
|
||||||
|
if (uri.StartsWith("file:///"))
|
||||||
|
{
|
||||||
|
var srcPath = scope String()..Append(uri.Substring("file:///".Length));
|
||||||
|
IDEUtils.MakeComparableFilePath(srcPath);
|
||||||
|
if (mClipboardCutQueued.TryAddAlt(srcPath, var entryPtr))
|
||||||
|
*entryPtr = new String(srcPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mClipboardCutQueued.IsEmpty)
|
||||||
|
DeleteAndNullify!(mClipboardCutQueued);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PasteFromClipboard()
|
||||||
|
{
|
||||||
|
ValidateCutClipboard();
|
||||||
|
|
||||||
|
var projectItem = GetSelectedProjectItem();
|
||||||
|
var projectFolder = projectItem as ProjectFolder;
|
||||||
|
if (projectFolder == null)
|
||||||
|
projectFolder = projectItem.mParentFolder;
|
||||||
|
if (projectFolder == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var folderPath = projectFolder.GetFullImportPath(.. scope .());
|
||||||
|
|
||||||
|
void* data = gApp.GetClipboardData("code/file-list", var size);
|
||||||
|
if (data == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isCut = mClipboardCutQueued != null;
|
||||||
|
Dictionary<String, SourceViewPanel> sourceViewPanelMap = null;
|
||||||
|
List<(SourceViewPanel sourceViewPanel, String fromPath, String toPath)> moveList = null;
|
||||||
|
|
||||||
|
HashSet<String> foundDirs = scope .();
|
||||||
|
|
||||||
|
if (isCut)
|
||||||
|
{
|
||||||
|
sourceViewPanelMap = scope:: .();
|
||||||
|
gApp.WithSourceViewPanels(scope (sourceViewPanel) =>
|
||||||
|
{
|
||||||
|
if (sourceViewPanel.mFilePath === null)
|
||||||
|
return;
|
||||||
|
if (sourceViewPanel.mProjectSource == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = scope String()..Append(sourceViewPanel.mFilePath);
|
||||||
|
IDEUtils.MakeComparableFilePath(path);
|
||||||
|
if (sourceViewPanelMap.TryAdd(path, var keyPtr, var valuePtr))
|
||||||
|
{
|
||||||
|
*keyPtr = new String(path);
|
||||||
|
*valuePtr = sourceViewPanel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
moveList = scope:: .();
|
||||||
|
}
|
||||||
|
|
||||||
|
defer
|
||||||
|
{
|
||||||
|
DeleteContainerAndItems!(mClipboardCutQueued);
|
||||||
|
mClipboardCutQueued = null;
|
||||||
|
|
||||||
|
if (sourceViewPanelMap != null)
|
||||||
|
{
|
||||||
|
for (var key in sourceViewPanelMap.Keys)
|
||||||
|
delete key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveList != null)
|
||||||
|
{
|
||||||
|
for (var val in moveList)
|
||||||
|
{
|
||||||
|
delete val.fromPath;
|
||||||
|
delete val.toPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearAndDeleteItems!(foundDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QueueDirectoryMove(StringView fromDir, StringView toDir)
|
||||||
|
{
|
||||||
|
var searchStr = scope String();
|
||||||
|
searchStr.Append(fromDir);
|
||||||
|
searchStr.Append("/*");
|
||||||
|
for (var dirEntry in Directory.Enumerate(searchStr, .Directories | .Files))
|
||||||
|
{
|
||||||
|
var fromChildPath = dirEntry.GetFilePath(.. scope .());
|
||||||
|
|
||||||
|
String toChildPath = scope String()..Append(toDir);
|
||||||
|
toChildPath.Append(Path.DirectorySeparatorChar);
|
||||||
|
dirEntry.GetFileName(toChildPath);
|
||||||
|
|
||||||
|
if (dirEntry.IsDirectory)
|
||||||
|
{
|
||||||
|
QueueDirectoryMove(fromChildPath, toChildPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmpPath = IDEUtils.MakeComparableFilePath(.. scope String()..Append(fromChildPath));
|
||||||
|
if (sourceViewPanelMap.TryGet(cmpPath, var matchKey, var sourceViewPanel))
|
||||||
|
{
|
||||||
|
moveList.Add((sourceViewPanel, new String(fromChildPath), new String(toChildPath)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> CopyDirectory(StringView fromDir, StringView toDir)
|
||||||
|
{
|
||||||
|
if (Directory.CreateDirectory(toDir) case .Err)
|
||||||
|
{
|
||||||
|
gApp.Fail(scope $"Failed to create directory '{toDir}'");
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchStr = scope String();
|
||||||
|
searchStr.Append(fromDir);
|
||||||
|
searchStr.Append("/*");
|
||||||
|
for (var dirEntry in Directory.Enumerate(searchStr, .Directories | .Files))
|
||||||
|
{
|
||||||
|
var fromChildPath = dirEntry.GetFilePath(.. scope .());
|
||||||
|
|
||||||
|
String toChildPath = scope String()..Append(toDir);
|
||||||
|
toChildPath.Append("/");
|
||||||
|
dirEntry.GetFileName(toChildPath);
|
||||||
|
|
||||||
|
if (dirEntry.IsDirectory)
|
||||||
|
{
|
||||||
|
CopyDirectory(fromChildPath, toChildPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Copy(fromChildPath, toChildPath) case .Err)
|
||||||
|
{
|
||||||
|
gApp.Fail(scope $"Failed to copy '{fromChildPath}' to '{toChildPath}'");
|
||||||
|
return .Err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringView sv = .((.)data, size);
|
||||||
|
SrcLoop: for (var line in sv.Split('\n'))
|
||||||
|
{
|
||||||
|
var uri = IDEUtils.URLDecode(line, .. scope .());
|
||||||
|
if (uri.StartsWith("file:///"))
|
||||||
|
{
|
||||||
|
var srcPath = scope String()..Append(uri.Substring("file:///".Length));
|
||||||
|
|
||||||
|
for (int i < 100)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(srcPath, .. scope .());
|
||||||
|
var destPath = scope String();
|
||||||
|
destPath.Append(folderPath);
|
||||||
|
destPath.Append("/");
|
||||||
|
destPath.Append(fileName);
|
||||||
|
if ((i > 0) && (!fileName.Contains(" - Copy")))
|
||||||
|
destPath.Append(" - Copy");
|
||||||
|
if (i > 1)
|
||||||
|
destPath.AppendF($" ({i})");
|
||||||
|
Path.GetExtension(srcPath, destPath);
|
||||||
|
|
||||||
|
IDEUtils.FixFilePath(srcPath);
|
||||||
|
IDEUtils.FixFilePath(destPath);
|
||||||
|
|
||||||
|
if ((isCut) && (Path.Equals(srcPath, destPath)))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (File.Exists(destPath))
|
||||||
|
continue;
|
||||||
|
if (Directory.Exists(destPath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (Directory.Exists(srcPath))
|
||||||
|
{
|
||||||
|
if (foundDirs.TryAdd(srcPath, var entryPtr))
|
||||||
|
*entryPtr = new String(srcPath);
|
||||||
|
|
||||||
|
if (isCut)
|
||||||
|
{
|
||||||
|
QueueDirectoryMove(srcPath, destPath);
|
||||||
|
|
||||||
|
if (Directory.Move(srcPath, destPath) case .Err)
|
||||||
|
{
|
||||||
|
gApp.Fail(scope $"Failed to move '{srcPath}' to '{destPath}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var val in moveList)
|
||||||
|
{
|
||||||
|
gApp.FileRenamed(val.sourceViewPanel.mProjectSource, val.fromPath, val.toPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (CopyDirectory(srcPath, destPath) case .Err)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var checkPath = scope String()..Append(srcPath);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
String checkDir = scope .();
|
||||||
|
if (Path.GetDirectoryPath(checkPath, checkDir) case .Err)
|
||||||
|
break;
|
||||||
|
if (foundDirs.Contains(checkDir))
|
||||||
|
{
|
||||||
|
// Already handled
|
||||||
|
continue SrcLoop;
|
||||||
|
}
|
||||||
|
checkPath.Set(checkDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCut)
|
||||||
|
{
|
||||||
|
if (File.Move(srcPath, destPath) case .Err)
|
||||||
|
{
|
||||||
|
gApp.Fail(scope $"Failed to move '{srcPath}' to '{destPath}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmpPath = IDEUtils.MakeComparableFilePath(.. scope String()..Append(srcPath));
|
||||||
|
if (sourceViewPanelMap.TryGet(cmpPath, var matchKey, var sourceViewPanel))
|
||||||
|
{
|
||||||
|
gApp.FileRenamed(sourceViewPanel.mProjectSource, srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (File.Copy(srcPath, destPath) case .Err)
|
||||||
|
{
|
||||||
|
gApp.Fail(scope $"Failed to copy '{srcPath}' to '{destPath}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void KeyDown(KeyCode keyCode, bool isRepeat)
|
public override void KeyDown(KeyCode keyCode, bool isRepeat)
|
||||||
{
|
{
|
||||||
mListView.KeyDown(keyCode, isRepeat);
|
mListView.KeyDown(keyCode, isRepeat);
|
||||||
|
@ -2067,28 +2380,11 @@ namespace IDE.ui
|
||||||
switch (keyCode)
|
switch (keyCode)
|
||||||
{
|
{
|
||||||
case (.)'C':
|
case (.)'C':
|
||||||
|
CopyToClipboard();
|
||||||
String clipData = scope .();
|
case (.)'X':
|
||||||
mListView.GetRoot().WithSelectedItems(scope (selectedItem) =>
|
CutToClipboard();
|
||||||
{
|
|
||||||
if (mListViewToProjectMap.GetValue(selectedItem) case .Ok(var sourceProjectItem))
|
|
||||||
{
|
|
||||||
if (var projectSource = sourceProjectItem as ProjectSource)
|
|
||||||
{
|
|
||||||
var path = scope String();
|
|
||||||
sourceProjectItem.mProject.GetProjectFullPath(projectSource.mPath, path);
|
|
||||||
path.Replace('\\', '/');
|
|
||||||
|
|
||||||
if (!clipData.IsEmpty)
|
|
||||||
clipData.Append("\n");
|
|
||||||
clipData.Append("file:///");
|
|
||||||
IDEUtils.URLEncode(path, clipData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!clipData.IsEmpty)
|
|
||||||
gApp.SetClipboardData("code/file-list", clipData.Ptr, (.)clipData.Length, true);
|
|
||||||
case (.)'V':
|
case (.)'V':
|
||||||
|
PasteFromClipboard();
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2943,12 +3239,6 @@ namespace IDE.ui
|
||||||
{
|
{
|
||||||
Regenerate(false);
|
Regenerate(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
item = gApp.AddMenuItem(menu, "Duplicate", "Duplicate Item");
|
|
||||||
item.mOnMenuItemSelected.Add(new (item) =>
|
|
||||||
{
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if (let projectFolder = projectItem as ProjectFolder)
|
else if (let projectFolder = projectItem as ProjectFolder)
|
||||||
{
|
{
|
||||||
|
@ -3037,6 +3327,14 @@ namespace IDE.ui
|
||||||
|
|
||||||
if (!isFailedLoad)
|
if (!isFailedLoad)
|
||||||
{
|
{
|
||||||
|
item = menu.AddItem("Copy|Ctrl+C");
|
||||||
|
item.mOnMenuItemSelected.Add(new (item) => CopyToClipboard());
|
||||||
|
item = menu.AddItem("Cut|Ctrl+X");
|
||||||
|
item.mOnMenuItemSelected.Add(new (item) => CopyToClipboard());
|
||||||
|
item = menu.AddItem("Paste|Ctrl+V");
|
||||||
|
item.mOnMenuItemSelected.Add(new (item) => CopyToClipboard());
|
||||||
|
menu.AddItem();
|
||||||
|
|
||||||
item = menu.AddItem("New Folder");
|
item = menu.AddItem("New Folder");
|
||||||
item.mOnMenuItemSelected.Add(new (item) =>
|
item.mOnMenuItemSelected.Add(new (item) =>
|
||||||
{
|
{
|
||||||
|
@ -3209,6 +3507,8 @@ namespace IDE.ui
|
||||||
mImportInstalledDeferred = false;
|
mImportInstalledDeferred = false;
|
||||||
ImportInstalledProject();
|
ImportInstalledProject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateCutClipboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Resize(float x, float y, float width, float height)
|
public override void Resize(float x, float y, float width, float height)
|
||||||
|
@ -3225,3 +3525,5 @@ namespace IDE.ui
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////
|
Loading…
Add table
Add a link
Reference in a new issue