sourceafExplorer::ImageViewer.fan

using afIoc
using afReflux
using gfx
using fwt

** (View) - A simple image viewer for file resources. 
class ImageViewer : View {

    @Inject private Images              images
    @Inject private RefluxIcons         icons
    @Inject private Scope               scope
    @Inject private Reflux              reflux
    @Inject private GlobalCommands      globalCommands
            private ImageViewWidget?    imageWidget

    @NoDoc  // Boring!
    protected new make(|This| in) : super(in) { }
    
    @NoDoc
    override Void onActivate() {
        enableCmds
    }

    private Void enableCmds() {
        if (resource != null) {
            globalCommands["afReflux.cmdSaveAs"].addInvoker("afExplorer.imageViewer", |Event? e| { this->onSaveAs() } )
            globalCommands["afReflux.cmdSaveAs"].addEnabler("afExplorer.imageViewer", |  ->Bool| { true } )
        }       
    }
    
    @NoDoc
    override Void onDeactivate() {
        globalCommands["afReflux.cmdSaveAs"].removeEnabler("afExplorer.imageViewer")
        globalCommands["afReflux.cmdSaveAs"].removeInvoker("afExplorer.imageViewer")
    }

    ** Displays the given 'FileResource' as an image.
    override Void load(Resource resource) {
        super.load(resource)
        enableCmds
        fileResource := (FileResource) resource
        image := images.load(resource.uri) 

        imageWidget = ImageViewWidget(image ?: icons["icoImageNotFound"])
        toolBar := ToolBar {
            it.addCommand(scope.build(ImageFullSizeCommand#, [this]))
            it.addCommand(scope.build(ImageFitToWindowCommand#, [this]))
        }

        content = EdgePane {
            top = EdgePane {
                it.top = InsetPane(2) {
                    EdgePane {
                        if (image != null) {
                            left = GridPane {
                                numCols = 2
                                Label { text="Size"; font=Desktop.sysFont.toBold },
                                Label { text="${image.size.w}px x ${image.size.h}px"},
                            }
                        } else
                            left = Label { text="Image not found: ${fileResource.file.osPath}"}
                        right =  toolBar
                    },
                }
                it.bottom = BorderPane {
                    it.border = Border("1, 0, 1, 0 $Desktop.sysNormShadow, #000, $Desktop.sysHighlightShadow")
                }
            }
            center = ScrollPane { it.content = imageWidget; it.border = false }
        }

        // dunno why super.load(resource) doesn't do this!?
        this.icon = resource.icon
        this.name = resource.name
    }

    ** Expands or shrinks the image to fit the view.
    Void fitToWindow() {
        imageWidget?.doFitToWindow
    }

    ** Displays the image at 100%
    Void showFullSize() {
        imageWidget?.doFullSize
    }
    
    ** Callback for when the 'afReflux.cmdSaveAs' 'GlobalCommand' is activated.
    ** Default implementation is to perform the *save as*.
    @NoDoc
    virtual Void onSaveAs() {   
        fileResource := (FileResource) resource
        file := (File?) FileDialog {
            it.mode         = FileDialogMode.saveFile
            it.dir          = fileResource.file.parent
            it.name         = fileResource.file.name
            it.filterExts   = ["*.${fileResource.file.ext}", "*.*"]
        }.open(reflux.window)

        if (file != null) {
            fileResource.file.copyTo(file)

            fileRes := scope.build(FileResource#, [file])
            reflux.loadResource(fileRes)
            
            isDirty = false // mark as not dirty so confirmClose() doesn't give a dialog
            reflux.closeView(this, true)

            // refresh any views on the containing directory
            dirRes := scope.build(FolderResource#, [file.parent])
            reflux.refresh(dirRes)
        }
    }

    private Button toolBarCommand(Type cmdType, Obj[] args) {
        command := (Command) scope.build(cmdType, args)
        button  := Button.makeCommand(command)
        if (command.icon != null)
            button.text = ""
        return button
    }
}

internal class ImageViewWidget : Canvas {
    Image   image
    Float   zoom    := 1f
    Int     border  := 8
    Size    iSize
    
    new make(Image image) {
        this.image = image
        this.iSize = image.size
    }

    override Void onPaint(Graphics g) {
        // centre the image if it's smaller than the view
        w  := (parent.size.w - (border*2))
        h  := (parent.size.h - (border*2))
        dw := (zoom * image.size.w.toFloat).toInt
        dh := (zoom * image.size.h.toFloat).toInt
        x  := 0.max((w - dw) / 2)
        y  := 0.max((h - dh) / 2)
        g.brush = Color.white
        g.fillRect(0, 0, size.w, size.h)
        g.copyImage(image, Rect(0, 0, image.size.w, image.size.h), Rect(x + border, y + border, dw, dh))
    }

    override Size prefSize(Hints hints := Hints.defVal) { iSize }

    Void doFitToWindow() {
        w := (parent.size.w - (border*2)).toFloat / image.size.w.toFloat
        h := (parent.size.h - (border*2)).toFloat / image.size.h.toFloat
        zoom = w.min(h)
        iSize = parent.size
        parent->onLayout
        repaint     
    }

    Void doFullSize() {
        zoom = 1f
        iSize = image.size
        parent->onLayout
        repaint
    }
}

internal class ImageFitToWindowCommand : RefluxCommand {
            private ImageViewer imageView
    @Inject private RefluxIcons icons

    new make(ImageViewer imageView, |This|in) : super.make(in) {
        this.imageView = imageView
        this.name = "Fit to Window"
        this.icon = icons["cmdImageFitToWindow"]
    }

    override Void doInvoke(Event? event) {
        imageView.fitToWindow
    }
}

internal class ImageFullSizeCommand : RefluxCommand {
            private ImageViewer imageView
    @Inject private RefluxIcons icons

    new make(ImageViewer imageView, |This|in) : super.make(in) {
        this.imageView = imageView
        this.name = "Zoom to 100%"
        this.icon = icons["cmdImageFullSize"]
    }

    override Void doInvoke(Event? event) {
        imageView.showFullSize
    }
}