using afIoc
using afReflux
using gfx
using fwt
** (Service) -
** The main service API for Explorer operations.
mixin Explorer {
abstract File rename(File file)
abstract Void delete(File file)
abstract Void cut(File file)
abstract Void copy(File file)
abstract Void paste(File destDir)
** Returns a unique file that (currently) does not exist on the file system.
abstract File uniqueFile(File destFile)
** Opens a dialogue for the file name before creating an empty file.
** File name defaults to 'NewFile.txt'.
**
** Returns 'null' if dialogue was cancelled.
abstract File? newFile(File containingFolder, Str? defFileName := null)
** Opens a dialogue for the folder name before creating an empty folder.
** Folder name defaults to 'NewFolder'.
abstract Void newFolder(File containingFolder, Str? defFolderName := null)
abstract Void openFileInSystem(File file)
abstract Void compressToZip(File toCompress, File dst)
abstract Void compressToGz(File toCompress)
abstract Image fileToIcon(File f)
abstract Image urlToIcon(Uri url)
abstract ExplorerPrefs preferences()
** Returns a cached version of 'File.osRoots' that is updated every other second *at most*.
abstract File[] osRoots()
@NoDoc // is there a way around having this method?
abstract Bool pasteEnabled()
}
internal class ExplorerImpl : Explorer {
@Inject private ExplorerEvents events
@Inject private Scope scope
@Inject private RefluxIcons icons
@Inject private Images images
@Inject private Preferences prefs
@Inject private Reflux reflux
@Inject private Errors errors
@Inject private Dialogues dialogues
@Inject private |->GlobalCommands| globalCommands
private Duration osRootsLastUpdated
private File[] osRootsCached
static const Uri fileIconsRoot := `fan://afExplorer/res/icons-file/`
static const Int bufferSize := 16 * 1024
internal File? copiedFile
internal File? cutFile
new make(|This| in) {
in(this)
osRootsCached = File.osRoots.map { it.normalize }
osRootsLastUpdated = Duration.now
}
override File rename(File file) {
newName := openRenameDialog(file)
if (newName != null && newName != file.name) {
// can't rename a file to the same (case insensitive) name
// so, rename it twice
if (newName.equalsIgnoreCase(file.name)) {
tmpName := newName + Int.random(0..9999).toStr
file = file.rename(tmpName)
}
newFile := file.rename(newName)
if (file.parent != null)
reflux.refresh(reflux.resolve(file.parent.uri.toStr))
events.onRename(file, newFile)
return newFile
}
return file
}
private Str? openRenameDialog(File file) {
field := Text { it.text = file.name; it.prefCols = 20 }
pane := GridPane {
numCols = 2
expandCol = 1
halignCells=Halign.fill
Label { text="Original name:" },
Text { it.text = file.name; it.prefCols = 20; it.editable = false; it.border = false },
Label { text="Rename to:" },
field,
}
field.onAction.add |Event e| { e.widget.window.close(dialogues.ok) }
r := dialogues.openMsgBox(Dialog#.pod, "question", pane, null, dialogues.okCancel) |Dialog diag| {
diag.title = file.isDir ? "Rename Folder" : "Rename File"
diag.image = images.get(`fan://afExplorer/res/icons/` + (file.isDir ? `folder-horizontal-x32.png` : `document-x32.png`), false)
diag.onOpen.add {
field.focus
}
}
if (r != dialogues.ok) return null
return field.text
}
override Void delete(File file) {
okay := dialogues.openQuestion("Delete ${file.name}?\n\n${file.osPath}", null, dialogues.yesNo)
if (okay == dialogues.yes) {
file.delete
if (file.parent != null)
reflux.refresh(reflux.resolve(file.parent.uri.toStr))
}
}
override Void cut(File file) {
cutFile = file
copiedFile = null
globalCommands()["afReflux.cmdPaste"].update
}
override Void copy(File file) {
cutFile = null
copiedFile = file
globalCommands()["afReflux.cmdPaste"].update
}
override Void paste(File destDir) {
// TODO dialog for copy overwrite options
if (cutFile != null) {
// if we're moving ourself, to ourself - just return!
if (cutFile.parent == destDir)
return
if (destDir.isDir == false)
throw IOErr("Can not move to inside a file!")
if (destDir.toStr.contains(cutFile.toStr))
throw IOErr("Can not move dir to inside itself!")
cutFile.moveInto(destDir)
cutFile = null
}
if (copiedFile != null) {
if (copiedFile.isDir == false) {
if (destDir.isDir == false) {
destFile := uniqueFile(destDir)
copiedFile.copyTo(destFile)
}
if (destDir.isDir == true) {
destFile := uniqueFile(destDir + copiedFile.name.toUri)
copiedFile.copyTo(destFile)
}
}
if (copiedFile.isDir == true) {
if (destDir.isDir == false)
throw IOErr("Can not copy a dir into a file")
if (destDir.isDir == true) {
destFile := uniqueFile(destDir.plus(copiedFile.name.toUri, false))
if (destFile.toStr.contains(copiedFile.toStr))
throw IOErr("Can not copy dir to inside itself!")
copiedFile.copyTo(destFile)
}
}
// once copied, allow multiple pastes by NOT setting it to null
// copiedFile = null
}
reflux.refresh(reflux.resolve(destDir.uri.toStr))
}
override File uniqueFile(File file) {
destFile := file
destName := destFile.name.toUri
fileIndex := 0
while (destFile.exists) {
fileIndex++
if (destFile.ext == null)
destName = `${file.name}($fileIndex)`
else
destName = `${file.basename} ($fileIndex).${file.ext}`
destFile = destFile.parent + destName
if (file.isDir)
destFile = destFile.uri.plusSlash.toFile
}
return destFile
}
override File? newFile(File containingFolder, Str? defFileName := null) {
fileName := dialogues.openPromptStr("New File", defFileName ?: "NewFile.txt")
if (fileName != null) {
newFile := containingFolder.createFile(fileName)
reflux.refresh(reflux.resolve(containingFolder.uri.toStr))
return newFile
}
return null
}
override Void newFolder(File containingFolder, Str? defFolderName := null) {
dirName := dialogues.openPromptStr("New Folder", defFolderName ?: "NewFolder")
if (dirName != null) {
containingFolder.createDir(dirName)
reflux.refresh(reflux.resolve(containingFolder.uri.toStr))
}
}
override Void openFileInSystem(File file) {
Desktop.launchProgram(file.uri)
}
override Void compressToGz(File toCompress) {
if (toCompress.isDir)
throw ArgErr("Cannot gzip directories: $toCompress")
scope := scope
pd := (ProgressDialogue) scope.build(ProgressDialogue#)
pd.title = "Compress to .gz"
pd.image = Image(`fan://afExplorer/res/images/zip.x48.png`)
pd.open(reflux.window) |ProgressWorker worker| {
locale := (LocaleFormat) scope.serviceById(LocaleFormat#.qname)
explorer := (Explorer) scope.serviceById(Explorer#.qname)
worker.update(0, 0, "Zipping ${toCompress.normalize.osPath} (${locale.fileSize(toCompress.size)})")
dst := toCompress.uri.plusName(toCompress.name + ".gz").toFile
dest := explorer.uniqueFile(dst)
bTotal := toCompress.size
bRead := 0
zipIn := toCompress.in(bufferSize)
zipOut := Zip.gzipOutStream(dest.out(false, bufferSize))
try {
buf := Buf(bufferSize)
piping := true
while (piping) {
i := zipIn.readBuf(buf.seek(0), bufferSize)
if (i == null) {
piping = false
continue
}
bRead += i
zipOut.writeBuf(buf.seek(0), i)
worker.update(bRead, bTotal)
}
} finally {
zipIn.close
zipOut.close
}
worker.update(100, 100, "Written ${dest.normalize.osPath} (${locale.fileSize(dest.size)})")
worker.update(100, 100, "Done.")
Desktop.callAsync |->| {
reflux := (Reflux) scope.serviceById(Reflux#.qname)
reflux.refresh
}
}
}
override Void compressToZip(File toCompress, File dst) {
if (dst.isDir)
throw ArgErr("Cannot write to $dst")
scope := scope
pd := (ProgressDialogue) scope.build(ProgressDialogue#)
pd.title = "Compress to .zip"
pd.image = Image(`fan://afExplorer/res/images/zip.x48.png`)
pd.open(reflux.window) |ProgressWorker worker| {
locale := (LocaleFormat) scope.serviceById(LocaleFormat#.qname)
explorer := (Explorer) scope.serviceById(Explorer#.qname)
worker.update(0, 0, "Zipping ${toCompress.normalize.osPath}")
worker.update(0, 0, "Inspecting source files...")
noOfFiles := 0
noOfBytes := 0
toCompress.walk |src| {
if (!src.isDir) {
noOfFiles++
noOfBytes += src.size
}
}
worker.update(0, 0, "Found $noOfFiles files with a sum total of ${locale.fileSize(noOfBytes)}")
dest := explorer.uniqueFile(dst)
zip := Zip.write(dest.out)
buf := Buf(bufferSize)
// don't include the name of the containing folder in zip paths
parentUri := toCompress.isDir ? toCompress.uri : toCompress.parent.uri
try {
// fileList := Str[,]
bytesWritten := 0
worker.update(bytesWritten, noOfBytes, "Compressing files...")
toCompress.walk |src| {
if (src.isDir) return
path := src.uri.relTo(parentUri)
// don't append path to detail path, cause Java Heap Space probs with big dirs ~ 24,000 files
// worker.update(bytesWritten, noOfBytes, "Compressing ${path}")
worker.update(bytesWritten, noOfBytes)
out := zip.writeNext(path)
try {
// this is the easy way to compress the file - but we do it the hard way
// so we can show progress when zipping large files
// src.in(bufferSize).pipe(out)
in := src.in
piping := true
while (piping) {
bytesRead := readFile(src, in, buf, worker)
if (bytesRead == null)
piping = false
else {
out.writeBuf(buf.flip)
bytesWritten += bytesRead
worker.update(bytesWritten, noOfBytes)
}
}
} finally
out.close
}
} finally
zip.close
worker.update(100, 100, "Written ${dest.normalize.osPath} (${locale.fileSize(dest.size)})")
worker.update(100, 100, "Done.")
Desktop.callAsync |->| {
reflux := (Reflux) scope.serviceById(Reflux#.qname)
reflux.refresh
}
}
}
private static Int? readFile(File src, InStream in, Buf buf, ProgressWorker worker) {
try {
return in.readBuf(buf.clear, bufferSize)
} catch (IOErr ioe) {
worker.warn("Problems reading: ${src.osPath}\n ${ioe.msg}\n")
return null
}
}
static const Str[] imageExts := "bmp jpg jpeg gif png".split
override Image fileToIcon(File f) {
hidden := preferences.isHidden(f)
if (f.isDir) {
// can't cache osRoots 'cos it changes with flash drives et al
name := osRoots.contains(f) ? "icoFolderRoot" : "icoFolder"
return hidden ? icons.getFaded(name) : icons.get(name)
}
// if the image is small enough ~5k, return a thumbnail as the icon
// .svg files and the like cause ugly stack traces as FWT logs the Err before returning null... Grrr!!
if (imageExts.contains(f.ext ?: "") && f.size < (5 * 1024)) {
if (images.contains(f.uri))
return hidden ? images.getFaded(f.uri) : images.get(f.uri)
icon := (Image?) images.load(f.uri, false)
if (icon != null) {
if (icon.size == Size(16, 16)) {
images[f.uri] = icon
return icon
}
// note we have to return a 16x16 image else SWT scales it for us
if (icon.size.w <= 16 && icon.size.h <= 16) {
newIcon := Image(Size(16, 16)) |Graphics g| {
g.drawImage(icon, (16 - icon.size.w) / 2, (16 - icon.size.h) / 2)
}
images[f.uri] = newIcon
return newIcon
}
if (icon.size.w >= icon.size.h) {
newH := icon.size.h * 16 / icon.size.w
if (newH > 0) { // really wide images don't scale well!
newIcon := icon.resize(Size(16, newH))
if (newH < 16) {
newIcon = Image(Size(16, 16)) |Graphics g| {
g.drawImage(newIcon, 0, (16 - newH) / 2)
}
}
images[f.uri] = newIcon
return newIcon
}
}
if (icon.size.w <= icon.size.h) {
newW := icon.size.w * 16 / icon.size.h
if (newW > 0) { // really tall images don't scale well!
newIcon := icon.resize(Size(newW, 16))
if (newW < 16) {
newIcon = Image(Size(16, 16)) |Graphics g| {
g.drawImage(newIcon, (16 - newW) / 2, 0)
}
}
images[f.uri] = newIcon
return newIcon
}
}
}
}
// look for explicit match based off ext
if (f.ext != null) {
icon := fileIcon("file${f.ext.capitalize}.png", hidden)
if (icon != null) return icon
}
mimeType := f.mimeType?.noParams
if (mimeType != null) {
mime := mimeType.mediaType.fromDisplayName.capitalize + mimeType.subType.fromDisplayName.capitalize
icon := fileIcon("file${mime}.png", hidden)
if (icon != null) return icon
mime = mimeType.mediaType.fromDisplayName.capitalize
icon = fileIcon("file${mime}.png", hidden)
if (icon != null) return icon
}
action := preferences.fileActions.find { it.matchesExt(f.ext) }
if (action != null) {
launcher := preferences.fileLaunchers.find { it.id == action.launcherId }
if (launcher != null)
return icons.fromUri(launcher.iconUri, false)
}
return fileIcon("file.png", hidden)
}
override Image urlToIcon(Uri url) {
// look for explicit match based off ext
if (url.ext != null) {
icon := fileIcon("file${url.ext.capitalize}.png", false)
if (icon != null) return icon
}
mimeType := url.mimeType?.noParams
if (mimeType != null) {
mime := mimeType.mediaType.fromDisplayName.capitalize + mimeType.subType.fromDisplayName.capitalize
icon := fileIcon("file${mime}.png", false)
if (icon != null) return icon
mime = mimeType.mediaType.fromDisplayName.capitalize
icon = fileIcon("file${mime}.png", false)
if (icon != null) return icon
}
return fileIcon("fileTextHtml.png", false)
}
override once ExplorerPrefs preferences() {
prefs.loadPrefs(ExplorerPrefs#, "afExplorer.fog")
}
override File[] osRoots() {
if (Duration.now - osRootsLastUpdated > 2sec) {
this.osRootsCached = File.osRoots.map { it.normalize }
this.osRootsLastUpdated = Duration.now
}
return osRootsCached
}
private Image? fileIcon(Str fileName, Bool hidden) {
uri := fileIconsRoot.plusName(fileName)
return hidden ? images.getFaded(uri, false) : images.get(uri, false)
}
override Bool pasteEnabled() {
copiedFile != null || cutFile != null
}
}