//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 21 Jul 08 Brian Frank Creation
//
using gfx
using fwt
**
** FileResource models a `sys::File` as a Flux resource.
**
class FileResource : Resource
{
**
** Make a resource for the specified file.
**
new make(Uri uri, File file)
{
this.uri = uri
this.file = file
this.name = file.name
this.icon = fileToIcon(file)
}
**
** Make from file using file's uri - the file must be normalized.
**
internal new makeFile(File file) : this.make(file.uri, file) {}
**
** The target file.
**
const File file
**
** The absolute file uri
**
override Uri uri
**
** Return the file name.
**
override Str name
**
** The icon is based on mime type.
**
override Image icon
**
** If we haven't loaded the children yet, then return
** true for directories and false for normal files.
**
override Bool hasChildren()
{
if (kids != null) return !kids.isEmpty
return file.isDir
}
**
** Get the navigation children of the resource. Return an
** empty list or null to indicate no children. Default
** returns null.
**
override FileResource[]? children()
{
if (kids != null) return kids
files := sortFiles(file.list)
kids = FileResource[,]
files.each |File f|
{
if (f.name.startsWith(".DS_Store")) return
try
kids.add(Resource.resolve(f.normalize.uri))
catch (Err e)
e.trace
}
return kids
}
private FileResource[]? kids
**
** View types are based on mime type. Register a file view
** using the facet "fluxViewMimeType" with a Str value for the
** MIME type such as "image/png". You can also register with
** just the media type, for example use "image" to register a
** view on any image file.
**
override Type[] views()
{
mime := file.mimeType ?: MimeType.fromStr("text/plain")
// first try exact mime type matching
acc := Type[,]
acc.addAll(Flux.qnamesToTypes(Env.cur.index("flux.view.mime.${mime.mediaType}/${mime.subType}")))
// then match by just media type
acc.addAll(Flux.qnamesToTypes(Env.cur.index("flux.view.mime.${mime.mediaType}")))
// filter out abstract
acc = acc.exclude |Type t->Bool| { return t.isAbstract }
return acc
}
**
** Add command specific Files.
**
override Menu? popup(Frame? frame, Event? event)
{
pod := FileResource#.pod
menu := super.popup(frame, event)
if (file.isDir)
{
menu.addCommand(Command.makeLocale(pod, "openIn") { openIn(file) })
menu.addCommand(Command.makeLocale(pod, CommandId.findInFiles, |->| { findInFiles(frame, file) }) { accelerator = null })
menu.addSep
menu.addCommand(Command.makeLocale(pod, "newDir") { newDir(frame,file) })
}
else menu.addSep
menu.addCommand(Command.makeLocale(pod, "duplicate") { duplicate(frame,file) })
menu.addCommand(Command.makeLocale(pod, "rename") { rename(frame,file) })
return menu
}
**
** Invoke the find-in-files command on the specified directory
**
internal Void findInFiles(Frame? frame, File dir)
{
FindHistory.load.pushDir(dir.uri)
frame?.command(CommandId.findInFiles)?.invoke(null)
}
**
** Open the given directory using the OS specific directory
** browser (i.e. Windows Explorer or Mac Finder)
**
internal Void openIn(File dir)
{
if (!dir.isDir) throw ArgErr("Not a directory: $dir")
if (Desktop.isWindows) Process(["explorer", dir.osPath]).run
else if (Desktop.isMac) Process(["open", dir.osPath]).run
else echo("Not yet implemented")
}
**
** Create a new diretory under the current directory.
**
internal Void newDir(Frame frame, File dir)
{
if (!dir.isDir) throw ArgErr("Not a directory: $dir")
newDir := promptFileName(frame, Flux.locale("newDir.name"), dir, "")
if (newDir == null) return
uri := dir.uri + "$newDir/".toUri
File(uri).create
}
**
** Duplicate the given file.
**
internal Void duplicate(Frame frame, File src)
{
name := promptFileName(frame, Flux.locale("duplicate.name"), src.parent, src.name)
if (name == null) return
target := src.parent + (src.isDir ? "$name/".toUri : name.toUri)
src.copyTo(target)
}
**
** Rename the given file.
**
internal Void rename(Frame frame, File src)
{
name := promptFileName(frame, Flux.locale("rename.name"), src.parent, src.name)
if (name == null) return
src.rename(name)
}
**
** Prompt the user for a new valid filename, returns the new
** filename, or null if the dialog was canceled.
**
private Str? promptFileName(Frame frame, Str label, File dir, Str oldName)
{
Str? newName := oldName
while (true)
{
newName = Dialog.openPromptStr(frame, label, newName)
if (newName == null) return null
try
{
if (!Uri.isName(newName))
{
Dialog.openErr(frame, "Invalid name: $newName")
continue
}
try
{
// TODO - need to clean up sys::File to make this easier;
// if file exists as a dir, this throws an exception b/c
// the uri is missing a trailing slash
if ((dir+newName.toUri).exists) throw Err()
}
catch (Err err)
{
Dialog.openErr(frame, "File already exists: $newName")
continue
}
return newName
}
catch (Err err) { Dialog.openErr(frame, "Error", err) }
}
return null
}
**
** Given a file size in bytes return a suitable string
** representation for display. If size is null return "".
**
static Str sizeToStr(Int? size)
{
size == null ? "" : size.toLocale("B")
}
private static const Int kb := 1024
private static const Int mb := 1024*1024
private static const Int gb := 1024*1024*1024
**
** Sort files in-place for display. Directories are always
** sorted before normal files using locale name comparison.
**
static File[] sortFiles(File[] files)
{
return files.sort |File a, File b->Int|
{
if (a.isDir != b.isDir) return a.isDir ? -1 : 1
return a.name.localeCompare(b.name)
}
}
**
** Get the icon for the specified file based on its mime type.
**
static Image fileToIcon(File f)
{
if (f.isDir) return Flux.icon(`/x16/folder.png`)
mimeType := f.mimeType
if (mimeType == null) return Flux.icon(`/x16/file.png`)
// look for explicit match based off ext
try { return Flux.icon("/x16/file${f.ext.capitalize}.png".toUri) }
catch {}
if (mimeType.mediaType == "text")
{
switch (mimeType.subType)
{
//case "html": return Flux.icon(`/x16/fileHtml.png`)
default: return Flux.icon(`/x16/file.png`)
}
}
switch (mimeType.mediaType)
{
//case "audio": return Flux.icon(`/x16/audio-x-generic.png`)
case "image": return Flux.icon(`/x16/fileImage.png`)
//case "video": return Flux.icon(`/x16/video-x-generic.png`)
default: return Flux.icon(`/x16/file.png`)
}
}
}