Wednesday, November 11, 2009

Picking files with the mouse using Get-DroppedFile

Sometimes (often?) it is just easier picking your files with the mouse. As long as the files are in one folder that is not that hard, but if you have files scattered all around that is tougher. Also right-clicking and copy-as-path is annoying.

To make this easier, I have created a small drop box function. A small transparent window will be shows and when you drop files on it, those files will be send to the output pipeline where you can do the rest of your processing.

I made the forms part using Visual C#. Relativily trivial. And Add-Type enabled me to embed it into my script. The hard part was making it async so that files would appear ín the output pipeline as soon as they were dropped. I had to resort to good old VB5-style DoEvents (just revealed my age, I guess). If you can come up with a non-polling solution, please let me know.

What it can be used for -

  • Testing scripts with different files
  • Move photos to a folder, converting them as they are moved
  • Renaming files
  • Compressing files
  • continue the list yourself

All tasks where you – the human – can make the decision about what to do with a file are relevant.

With PowerShell v2 being available on all platform, do I have to say, that this is a V2-only script?

Please, read the comments in the script for further information.

Get-DroppedFile.ps1


<#
.Synopsis
Create a drop box window and output the files dropped to the pipeline
.Description
Create a drop box window. When files are dropped, they are send to the output pipeline right away.
Stop Get-DroppedFile by closing the window.
.Inputs
None
.Outputs
File names (-asText), IO.DirectoryInfo or IO.FileInfo objects
.Example
Get-DroppedFile | Copy -destination e:\ -passthru | foreach { $x.attributes=$x.Attributes.ToString()+",readonly" }
Copy dropped files to e:\ and set the readonly bit
#>

param(
   [string]
   # The caption of the drop box
   $Caption,
   [switch]
   # Return file names (full path) as text
   $AsText,
   [switch]
   # Recurse directories, I.E. the folder itself is not returned, only its children
   $Recurse,
   [switch]
   # (Internal switch used to detect -sta invocation)
   $_InternalReinvoked)

# Create the script code - direct execution or SingleThreadedApartment is determined later
$script={

$loaded=$false
try {
    $null=[system.type] "Get_DroppedFile.Form1"
    $loaded=$true
}
catch {}

# The form code. Created in Visual C# 2008 Express and slightly adopted
if (!$loaded) {
    add-type -TypeDefinition @'

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace Get_DroppedFile
{
public partial class Form1 : Form
{
Color defaultColor;
public Form1()
{
InitializeComponent();
defaultColor = this.BackColor;

}

public void avoidWarning()
{
}

private void Form1_DragDrop(object sender, DragEventArgs e)
{
// Back to default color
this.BackColor = defaultColor;
}

private void Form1_DragOver(object sender, DragEventArgs e)
{
// Start dragdrop
e.Effect = DragDropEffects.Copy;
}

private void Form1_DragEnter(object sender, DragEventArgs e)
{
// visual feedback in drag
this.BackColor = Color.FromArgb(defaultColor.ToArgb() - 0x101010);
}

private void Form1_DragLeave(object sender, EventArgs e)
{
// restore color
this.BackColor = defaultColor;
}
}
}

namespace Get_DroppedFile
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// Form1
//
this.AllowDrop = true;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.ActiveCaption;
this.ClientSize = new System.Drawing.Size(116, 49);
this.Cursor = System.Windows.Forms.Cursors.Default;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "Form1";
this.Opacity = 0.75;
this.ShowIcon = false;
this.Text = "Drop Box";
this.TopMost = true;
//this.Load += new System.EventHandler(this.Form1_Load);
this.DragLeave += new System.EventHandler(this.Form1_DragLeave);
this.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
this.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
this.DragOver += new System.Windows.Forms.DragEventHandler(this.Form1_DragOver);
this.ResumeLayout(false);

}

#endregion


}
}

'@
 -verbose -ReferencedAssemblies system.drawing,system.windows.forms
}

# Create our form object
$form=New-object get_droppedfile.form1

# Get rid of any events leftover
get-event get-droppedfile -erroraction silentlycontinue | remove-event

# Add handlers
# The handlers transfer the action/file to the main loop using event
$form.add_dragdrop( { $null=new-event -sourceidentifier get-droppedfile -messagedata $args[1].Data.GetData("FileDrop", $true)  })
$form.add_formclosed( { $null=new-event -sourceidentifier get-droppedfile -messagedata "[close]" })

#Register-ObjectEvent -InputObject $form -EventName dragdrop #-SourceIdentifier blah#
#Register-ObjectEvent -InputObject $form -EventName formclosed #-SourceIdentifier blah

# Custom caption
if ($caption) {$form.text=$caption}

# This is the tricky part. Dropping is quite simple, but feeding the output pipeline with
# the dropped files (so you do not have to wait until the drop box is closed) is not simple.
# I came up with this solution:
# - Do not use ShowDialog as is modal and will suspend PowerShell processing
# - use Show and doevents in a loop
# - this method consumes some CPU, but the Start-Sleep keeps it to a few per cent
# - The Event actions generates events and they are read here in the main loop
# and converted to file names which are sent to the pipeline

# Show the drop box
$form.show()

do {
  $e=get-event get-droppedfile -erroraction silentlycontinue # suppress no such events
$exit=$e.messagedata -eq "[close]" # Test close message
if ($e.messagedata -and !$exit) {$e.MessageData} # Send file to pipeline
if ($e) {$e | remove-event} # Remove event from queue
if ($exit) {break}
start-sleep -m 100 # Wait a little
[System.Windows.Forms.Application]::doevents() # React to form events so the windows can be moved etc.
} while($true)

# Shutdown
$form.close()
} # end of script assignment


# Generate command for -sta recursive call. Done here where $myinvocation has the right value
$command = "&'" + $myinvocation.mycommand.definition + "' -caption '$caption' -_InternalReinvoked"

# Execute the next in a scriptblock so output can be piped
&{

# Forms must run in SingleThreadedApartment style
# Re-invoke PowerShell if necessary
    if ($host.runspace.ApartmentState -ne "sta") {


     write-verbose "Invoking PowerShell with -sta"
        $bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
        $encodedCommand = [Convert]::ToBase64String($bytes)
        powershell.exe -sta -noprofile -encodedCommand $encodedCommand
     #powershell -sta -noprofile $script
    }
    else {
     &$script
    }

} | where {$_} | foreach {
    # Handle the different return options
if ($_InternalReinvoked.ispresent) {
        # -sta call, always return strings
$_
}
elseif ($recurse.ispresent -and (test-path -pathtype container $_)) {
        # Recurse folder tree, return text or objects
Get-ChildItem $_ -recurse -force | foreach {
if ($astext.ispresent) {
$_.fullname
}
else {
$_
}
}
}
elseif ($astext.ispresent) {
$_
}
else {
Get-Item $_
}
}


Have fun

1 comment:

Klaus Graefensteiner said...

This is a really neat idea. I like the fact that you can just add your C# code inline with the PowerShell script with Add-Type. A very handy productivity utility that I am going to add to my profile.

Klaus