Jump to content

Adding a Help Button to CodeLab plugins


MJW

Recommended Posts

The problem of lots of obscure plugins has bothered me too; both as a plugin writer and a plugin user. I don't know of any easy way to add instructions or a help menu to CodeLab plugins. I've never tried displaying text on the PDN image, itself, though I can look into that. The CodeLab plugins have support links, which could, I assume, be linked to the PDN forum thread on which they're explained, but I worry future forum changes might result in dead links -- and I hate dead links. The file option is a good idea, both because it's a useful option, and because it reminds the user what the plugin does. I haven't read a file into a plugin, either, so I'll have to look into that.

 

EDIT: I'm not sure of the logistics of a File option in a CodeLab plugin. Obviously it would have to ask for the file name when that option is selected, but what would launch the file selection menu? If it were launched as soon as the plugin was run, it would seem like it might be awkward to go back to the clipboard option. If there's currently a CodeLab plugin that does something like that, maybe I can take a look at it and use it as a guide.

 

ANOTHER EDIT: Because of mufti-threading, doing single actions, especially on demand, such opening a file, isn't really very well suited to CodeLab plugins. It's possible to do one-time initialization by using locks and flags, but those are actions that occur once automatically, not things done upon request.

Edited by MJW
Link to comment
Share on other sites

CodeLab maps to IndirectUI and creates PropertyBasedEffect plugins. This limits the possible features.

 

IndirectUi controls allow a description at the bottom of each control. But I guess CodeLab does not allow to enter one in the moment.

This may be a possible extension.

midoras signature.gif

Link to comment
Share on other sites

I don't know of any easy way to add instructions or a help menu to CodeLab plugins.

 

How about like this:

 

#region UICode
byte Amount1 = 0; // [255] Help
#endregion
 
int PreviousHelpButton = -1;
 
void Render(Surface dst, Surface src, Rectangle rect)
{
    if (PreviousHelpButton== -1)
    {
        PreviousHelpButton = Amount1;
    }
    if (PreviousHelpButton != Amount1)
    {
        PreviousHelpButton = Amount1;
        System.Windows.MessageBox.Show("This is the help text");
    }
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            // TODO: Add pixel processing code here
            // Access RGBA values this way, for example:
            // CurrentPixel.R = (byte)PrimaryColor.R;
            // CurrentPixel.G = (byte)PrimaryColor.G;
            // CurrentPixel.B = (byte)PrimaryColor.B;
            // CurrentPixel.A = (byte)PrimaryColor.A;
            dst[x,y] = CurrentPixel;
        }
    }
}
NOTE: Don't use this code. There is published improved code here.
  • Upvote 2
Link to comment
Share on other sites

Works for me! - useful example code - thanks.

(not sure about the .bat files on the desktop though) :/ :roll: :D

... just me being new to Pdn4.

 

MJW - I started something similar to 'MismatchEraser' a while back but didn't publish on the forum. I occasionally find it very useful if I've accidentally merged down an object onto a texture before saving the object. - If you have the texture and the texture + object  you can then isolate the object. Good work!

 

Red ochre Plugin pack.............. Diabolical Drawings ................Real Paintings

 

PdnForumSig2.jpg

Link to comment
Share on other sites

Works for me! - useful example code - thanks.

You're welcome. It is part of a tutorial of CodeLab tricks that I never finished.

not sure about the .bat files on the desktop though

Those are very handy. I'll tell you why.

1) You no longer need to run Paint.NET as an administrator in order to build dll files in CodeLab. This allows you to drag-and-drop files onto Paint.NET in order to open them--something you can't do in Admin mode.

2) It is actually a quick way to install your plugin dll file. The batch file will obtain admin rights if it doesn't have them before attempting to install the dll file.

Link to comment
Share on other sites

.bat files will be fine. I was confused for a second when it said it had successfully built but didn't show up in the Submenu.

I think (not certain) I still need to run Pdn under admin rights so that VS 2013 can copy the .dll into the effects folder.
I'm running both as admin and it works - which is convenient.

When/if you get time another codelab tutorial would be very appreciated... but that would be a discussion for the dev central or codelab threads.
(sorry for going off topic MJW). ;)

 

Red ochre Plugin pack.............. Diabolical Drawings ................Real Paintings

 

PdnForumSig2.jpg

Link to comment
Share on other sites

Wow, that's really nifty, BoltBait! I don't remember anything about those button controls, though perhaps I've just forgotten. I'll definitely add Help options to plugins, and there are probably other uses for those button controls.
 
Does anyone know off hand an easy way to kill the Message Box if the effect is cancelled? I noticed the cancellation hangs until the Message Box is closed, which is sometimes confusing because the Message Box can be hidden behind other windows. It's not a huge problem, but it'd be nice if the Help menu could be forced to close.

Link to comment
Share on other sites

(sorry for going off topic MJW)

 

As if I ever felt compelled to stick to the topic. I'm the one who posted a completely different plugin to this thread. The .bat files do sort of clutter up the desktop, but they make installing the plugins easier than the old method.

Link to comment
Share on other sites

Please be aware that this button is also used to increment the seed for the random number generator. So, if you're using random numbers, this button trick will affect the number series generated.

Now, in my example, I used a message box. That's not the only thing you can do in there. You could popup a modal window that is forced on top of all other windows. That would take care of the cancel problem.

Code for that is an exercise left up to the reader.

:P

Link to comment
Share on other sites

I knew that a button was used to update the random seed, but I've always assumed it was restricted to that use.

 

I'm quite confused about how the Message Box works in this situation. A Message Box is a modal window, but it runs independently -- essentially modelessly -- from the main plugin dialog because it's started in a rendering thread. If I remember correctly, each time a new rendering pass is started, a single thread is first completed before the rest to the threads are started. I assume this first thread runs the Message Box and blocks till it completes, but the main dialog continues to run. I tried some things I thought might work, but they didn't, and afterward I could see why. It's sort of tricky. I think a flag that allowed an interactive rendering pass to be distinguished from a non-interactive pass would solve the problem, but alas...

 

I don't know what kinds of forms can be built within the CodeLab structure. I also admit I've never built a form on the fly, programmatically. I've let VS or CodeLab do that for me.

Edited by MJW
Link to comment
Share on other sites

Be very careful placing MessageBox inside the render loop.  If it's badly misplaced (and not well trapped) you could be looking at it popping up once for every pixel. Ouch.

Link to comment
Share on other sites

Be very careful placing MessageBox inside the render loop.  If it's badly misplaced (and not well trapped) you could be looking at it popping up once for every pixel. Ouch.

Been there. Done that. :D

BTW, I checked in the CodeLab code and noticed that if you have multiple "randomize" buttons, only the last one will actually update the random number generator.

For example, look at this code:

 

#region UICode
byte Amount1 = 0; // [255] Help
byte Amount2 = 0; // [255] Randomize
#endregion

int PreviousHelpButton = -1;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (PreviousHelpButton== -1)
    {
        PreviousHelpButton = Amount1;
    }
    if (PreviousHelpButton != Amount1)
    {
        PreviousHelpButton = Amount1;
        System.Windows.MessageBox.Show("This is the help text");
    }
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            CurrentPixel.R = (byte)RandomNumber.Next(255);
            CurrentPixel.G = (byte)RandomNumber.Next(255);
            CurrentPixel.B = (byte)RandomNumber.Next(255);
            CurrentPixel.A = (byte)255;
            dst[x,y] = CurrentPixel;
        }
    }
}
NOTE: Don't use this code. There is published improved code here.

I'm quite confused about how the Message Box works in this situation. A Message Box is a modal window, but it runs independently -- essentially modelessly -- from the main plugin dialog because it's started in a rendering thread.

Yes, that is exactly the problem. One way to solve the problem is to supply the handle to the UI window to the MessageBox.Show function. That will make the Message Box modal to the specified window. Let's see if we can come up with some code that does that!

 

I also admit I've never built a form on the fly, programmatically. I've let VS or CodeLab do that for me.

I did post a tutorial for that in another thread, here: http://forums.getpaint.net/index.php?/topic/31543-controlling-the-progress-bar/?p=423664

Link to comment
Share on other sites

Improving on BoltBait's code above, the following adds the ability to show a MessageBox (or any other dialog) as a modal window.

#region UICode
byte Amount1 = 0; // [255] Help
byte Amount2 = 0; // [255] Randomize
#endregion

private sealed class ActiveFormHandle : IWin32Window
{
    private IntPtr handle;

    public IntPtr Handle
    {
        get { return handle; }
    }
    
    private delegate IntPtr GetHandleDelegate();
    internal ActiveFormHandle()
    {
        Form active = Form.ActiveForm;
        // As Render is called by background threads use Invoke to marshal the call to the thread that owns the Form.
        this.handle = (IntPtr)active.Invoke(new GetHandleDelegate(delegate() { return active.Handle; }));
    }
}

int PreviousHelpButton = -1;
ActiveFormHandle activeForm = null;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (PreviousHelpButton== -1)
    {
        PreviousHelpButton = Amount1;
    }
    if (PreviousHelpButton != Amount1)
    {
        PreviousHelpButton = Amount1;
        if (activeForm == null)
        {
            activeForm = new ActiveFormHandle();
        }
        System.Windows.Forms.MessageBox.Show(activeForm, "This is the help text");
    }
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            CurrentPixel.R = (byte)RandomNumber.Next(255);
            CurrentPixel.G = (byte)RandomNumber.Next(255);
            CurrentPixel.B = (byte)RandomNumber.Next(255);
            CurrentPixel.A = (byte)255;
            dst[x,y] = CurrentPixel;
        }
    }
}
  • Upvote 2

PdnSig.png

Plugin Pack | PSFilterPdn | Content Aware Fill | G'MICPaint Shop Pro Filetype | RAW Filetype | WebP Filetype

The small increase in performance you get coding in C++ over C# is hardly enough to offset the headache of coding in the C++ language. ~BoltBait

 

Link to comment
Share on other sites

I appreciate your effort null54 but IMHO such code should be published in the DeveloperZone.

Plus a small explanation that a delegate is needed because the effect dialog is running in a different thread than the Render code.

midoras signature.gif

Link to comment
Share on other sites

I appreciate your effort null54 but IMHO such code should be published in the DeveloperZone.

 

I asked null54 to post his code in this thread after he solved the modal problem and sent it to me privately.

 

My original code was written as part of a CodeLab tutorial that I never finished.  Perhaps, with his additional code, I'll finally finish the next installment of my CodeLab tutorial series.

Link to comment
Share on other sites

I asked null54 to post his code in this thread after he solved the modal problem and sent it to me privately.

 

My original code was written as part of a CodeLab tutorial that I never finished.  Perhaps, with his additional code, I'll finally finish the next installment of my CodeLab tutorial series.

 

Great. Still I would say that this is an issue regarding PropertyBased effects and not an issue special to CodeLab.

midoras signature.gif

Link to comment
Share on other sites

Very impressive, null54! I didn't even know that could be done. I agree with midora that this whole subject deserves a separate thread, though I have no particular opinion on what area it should be under.

Edited by MJW
Link to comment
Share on other sites

Link to comment
Share on other sites

You could popup a modal window that is forced on top of all other windows. That would take care of the cancel problem.

Code for that is an exercise left up to the reader.

:P

Rick Brewster offered up this solution to the modal problem (Comments added by me):

#region UICode
byte Amount1 = 0; // [255] Help
byte Amount2 = 0; // [255] Randomize
#endregion

int PreviousHelpButton = -1;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (PreviousHelpButton== -1) // Initial run?
    {
        PreviousHelpButton = Amount1; // Don't show help
    }
    if (PreviousHelpButton != Amount1) // Help button pressed?
    {
        PreviousHelpButton = Amount1; // Reset help button
        Form.ActiveForm.Invoke(new Action(delegate()
        {
            // This line runs on the UI thread and not on the Render thread
            System.Windows.Forms.MessageBox.Show("This is the help text");
        }));
    }
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            CurrentPixel.R = (byte)RandomNumber.Next(255);
            CurrentPixel.G = (byte)RandomNumber.Next(255);
            CurrentPixel.B = (byte)RandomNumber.Next(255);
            CurrentPixel.A = (byte)255;
            dst[x,y] = CurrentPixel;
        }
    }
}
As you can see the code is MUCH simpler and works perfectly.

The idea here is as an alternative to telling the MessageBox who it's parent is, force the MessageBox.Show code to run on the UI thread instead of the Render thread. This will cause the MessageBox to be modal to the UI thus preventing you from pressing the Cancel button at all.

HelpButtonA.png

HelpButtonB.png

______________

post-44727-0-84189900-1434558527_thumb.p

post-44727-0-34960500-1434558534_thumb.p

Link to comment
Share on other sites

EDIT: I'm not sure of the logistics of a File option in a CodeLab plugin. Obviously it would have to ask for the file name when that option is selected, but what would launch the file selection menu? If it were launched as soon as the plugin was run, it would seem like it might be awkward to go back to the clipboard option. If there's currently a CodeLab plugin that does something like that, maybe I can take a look at it and use it as a guide.

Here is the source code to Effects > Fill > From File...

 

// Title: BoltBait's Fill From File v1.2
// Name: From File DEMO
// Submenu: Fill
// URL: http://boltbait.com/pdn/
// Author: BoltBait
#region UICode
string Amount1 = ""; // [0,255] Image file path
byte Amount2 = 0; // [255] Browse
Pair<double, double> Amount3 = Pair.Create(0.0, 0.0); // Placement
double Amount4 = 1; // [0.01,10] Zoom
#endregion

protected Surface img
{
    get
    {
        if (_img != null)
        {
            return _img;
        }
        else
        {
            GetImageFromFile();
            return _img;
        }
    }
}
private Surface _img = null;
string PreviousPath = null;
int PreviousBrowseButton = -1;
string imgPath = null;
private void GetImageFromFile()
{
    if (imgPath == null)
    {
        imgPath = Amount1;
    }
    Bitmap aimg = null;
    try
    {
        aimg = (Bitmap)Image.FromFile(imgPath, false);
    }
    catch (Exception)
    {
    }
    if (aimg != null)
    {
        _img = Surface.CopyFromBitmap(aimg);
    }
    else
    {
        _img = null;
    }
}
void GetFileName()
{
    System.Windows.Forms.OpenFileDialog ofd = new System.Windows.Forms.OpenFileDialog();
    ofd.Title = "Open Image File";
    ofd.Filter = "Image Files(*.PNG;*.BMP;*.JPG;*.GIF)|*.PNG;*.BMP;*.JPG;*.GIF|All files (*.*)|*.*";
    ofd.DefaultExt = ".png";
    ofd.Multiselect = false;

    if (ofd.ShowDialog() == DialogResult.OK)
    {
        imgPath = ofd.FileName;
        _img = null;
        PreviousPath = Amount1;
    }
}
void Render(Surface dst, Surface src, Rectangle rect)
{
    if (IsCancelRequested) return;
    if (PreviousBrowseButton == -1)
    {
        PreviousBrowseButton = Amount2;
    }
    if (PreviousPath != Amount1)
    {
        _img = null;
        imgPath = null;
        PreviousPath = Amount1;
    }
    if (PreviousBrowseButton != Amount2)
    {
        PreviousBrowseButton = Amount2;
        System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(GetFileName));
        t.SetApartmentState(System.Threading.ApartmentState.STA);
        t.Start();
        t.Join();
    }

    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        for (int x = rect.Left; x < rect.Right; x++)
        {
            if (IsCancelRequested) return;  // we need to check this in the x loop to avoid lots of try/catches which are soooo slow
            if (img == null)
            {
                dst[x, y] = src[x, y];
            }
            else
            {
                float px = (float)(((Amount3.First + 1) / 2 * img.Height) + x) * (float)Amount4;
                float py = (float)(((Amount3.Second + 1) / 2 * img.Width) + y) * (float)Amount4;
                dst[x, y] = img.GetBilinearSampleWrapped(px, py);
            }
        }
    }
}
This is an example of how to use Window's built-in File Open dialog box.

This code suffers from the same "modal" problem as the original help button demo. It needs to be fixed before being used!

Link to comment
Share on other sites

I'm using this HelpButton trick in several basic effects for a while now (in most cases just to open a link in the browser).

 

Later I added it to FileTypes OnSave too but there is an additional issue I couldn't find a clean solution for. Maybe someone has an idea.

The problem is that the user may press the 'Defaults' button, which resets the button to its initial value.

 

This may be an issue too if Rick adds a global reset button to property based effects in the future.

midoras signature.gif

Link to comment
Share on other sites

For your possible amusement:

// Author: MJW
// Author: MJW
// Name: Test Help Button
// Title: Test Help Button
// Desc: Try to add a help button.
#region UICode
byte Amount1 = 0; // [255] Randomize
#endregion

bool hasHelpButton = false;
Form form = null;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (!hasHelpButton) // Help button displayed?
    {
        hasHelpButton = true;
        form = Form.ActiveForm;
        if (form.Name == "EffectConfigDialog")
        {
            form.Invoke(new Action(delegate()
            {
                if (!form.HelpButton)
                {
                    form.HelpButton = true;
                    form.HelpButtonClicked += new System.ComponentModel.CancelEventHandler(HelpButtonClicked);
                }
            }));
        }
    }
    ColorBgra CurrentPixel;
    for (int y = rect.Top; y < rect.Bottom; y++)
    {
        if (IsCancelRequested) return;
        for (int x = rect.Left; x < rect.Right; x++)
        {
            CurrentPixel = src[x,y];
            CurrentPixel.R = (byte)RandomNumber.Next(255);
            CurrentPixel.G = (byte)RandomNumber.Next(255);
            CurrentPixel.B = (byte)RandomNumber.Next(255);
            CurrentPixel.A = (byte)255;
            dst[x,y] = CurrentPixel;
        }
    }
}

public void HelpButtonClicked(Object sender, System.ComponentModel.CancelEventArgs e)
{
     e.Cancel = true;
     System.Windows.Forms.MessageBox.Show("This is the help text");
}


EDIT: Originally, I used an Invoke in the the event handler, but then I realized it would probably run under the parent form's thread, which testing confirmed.

EDIT: Added a just-in-case check to make sure HelpButton is currently false before setting it and adding event handler. (Thanks for the very nice screen shots, BoltBait!)

HelpButton1.png

HelpButton2.png

______________

post-44727-0-05796800-1434557127_thumb.p

Edited by MJW
I added some screenshots to your post.
  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...