Jump to content

Adding a Help Button to CodeLab plugins


MJW

Recommended Posts

I admit, I care. I didn't know that's what the help button was meant to do. I guess I haven't seen that many windows with the question mark button. I did notice the question mark cursor, which setting the event arg's Cancel undoes.

I still may use it, assuming there isn't some hidden problem. (Which there might well be with something that seems so wacky.)

(What's up with the comment editor? At least for me, there seems to be no line wrapping.)

Edited by MJW
Link to comment
Share on other sites

Even the most users do not known, so there are no special expectations ;-)

You still may have to add a hint somewhere that there is a clickable button in the caption.

It's a mess that this button is not available if the form has a min or maxbox.

 

Next step would be to add an additional help button to the bottom line of the dialog.

But I guess Rick wouldn't be happy with this ;-)

midoras signature.gif

Link to comment
Share on other sites

MJW, I think your solution is brilliant!

I added a couple of screenshots to your post.

I guess Rick wouldn't be happy with this ;-)

Yeah, I'd like Rick's blessing before using this technique myself as he did post the following in this forum's rules:

Plugins must not modify the Paint.NET user interface.

There are non-Reflection methods for doing this. Plugins are not allowed to modify Paint.NET's UI in any way such as by adding menu commands, etc.

Link to comment
Share on other sites

It's a bad idea to rely on a Form's Name equalling anything unless you own that Form. Who knows, in a future version of Paint.NET I may need to use that for something.

 

In fact, Form.ActiveForm isn't even guaranteed to give you what you want -- it could be null if the active "form" is actually a non-WinForms window (like an Open/Save dialog, or a MessageBox). So you can't really rely on it for the BeginInvoke() trampoline either. And, Application.OpenForms is going to have race conditions ... I was thinking you could use Application.OpenForms.First().BeginInvoke() but that just feels like another slimey hack.

 

(These might actually work fine in practice for the scenario y'all are trying to solve right now, but it's not the right general solution and I'd like to avoid one of these techniques being propagated to other coding projects and stuff)

 

WinForms doesn't seem to have a proper way to send a callback (e.g. Invoke) to the "main" thread unless you already have a reference to a UI control from that thread. That's lame, but let's deal with it:

 

I'll give official blessing to use something called PdnSynchronizationContext (at the moment I can't recall if it's in Base.dll or Core.dll), which is what Paint.NET uses for doing exactly what we need here. It's derived from .NET's SynchronizationContext, but you can't just use SynchronizationContext.Current because that's a per-thread value. Instead, use PdnSynchronizationContext.Instance, which returns the singleton PdnSynchronizationContext that's parked on the main thread. (If you're already on the main thread, then SynchronizationContext.Current == PdnSynchronizationContext.Instance, but no need to worry about that).

using PaintDotNet.Threading;
 
void Render(...)
{
    ...
    PdnSynchronizationContext.Instance.Send(new delegate
    {
        // congratulations, you're now on the UI thread. go ahead and show your message box!
    });
    ...
}

:Warning: Note that SynchronizationContext uses the lingo of Send and Post instead of Invoke and BeginInvoke. They are equivalent, and quite often are used for wrapping the other. Send() and Invoke() are synchronous and will not return until your callback is finished, whereas Post() / BeginInvoke() are asynchronous. SynchronizationContext is a lower level component and is just reusing the lingo from Win32's SendMessage and PostMessage.

 

Send() and Post() also accept a second parameter called "state" of type Object. You don't need to care about this probably, but you can use it to pass along information to your callback. It's not really necessary in most cases because of the way .NET does closures. Notice how I used the anonymous delegate syntax without a parameter list? That's the syntactic form that says "well, the delegate type that's needed here has parameters but I don't care about them." The Send() method in use above is also just an extension method that I added so I would have to constantly put 'null' for the state parameter.

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

Later on, I'll try to figure out to do this within Rick's rules (or someone else is welcome to do so). But purely for don't-try-this-at-home entertainment:
 

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

bool hasHelpMenu = false;
Form form = null;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (!hasHelpMenu) // Help menu displayed?
    {
        hasHelpMenu = true;
        form = Form.ActiveForm;
        if (form.Name == "EffectConfigDialog")
        {
            form.Invoke(new Action(delegate() { AddHelpMenu(); } ));
        }
    }
    
    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;
        }
    }
}

private void AddHelpMenu()
{
    if (form.Menu != null)
        return;
      
    MenuItem menuItemHelp = new MenuItem("&Help");
    MainMenu mainMenu = new MainMenu();
    mainMenu.MenuItems.Add(menuItemHelp);

    menuItemHelp.Click += new System.EventHandler(ShowHelpMessage);
    form.Menu = mainMenu;
    form.Height += 20;
    form.PerformLayout();   
}

public void ShowHelpMessage(Object sender, EventArgs e)
{
     System.Windows.Forms.MessageBox.Show("This is the help text");
}


 
HelpMenu.png

post-53337-0-37853900-1434579970_thumb.p

Edited by MJW
Link to comment
Share on other sites

I don't really see what's wrong with using the parent's menu name. The idea is aimed exclusively at CodeLab plugins, and only when the dialog is being displayed. The name test is intended to avoid problems with inadvertently changing another form. I'd rather have the code not run when I'd want it to then run when I don't want it to.

Edited by MJW
Link to comment
Share on other sites

MJW, sorry, but I don't like the look of adding a menu to the IndirectUI.

Let's get some code working based on your idea of adding the ? button with Rick's code. I'd do it myself, but I'm on my phone. :)

BTW, Rick, I thought delegate required () after it.

Link to comment
Share on other sites

The following uses PdnSynchronizationContext.
:Warning: Note that it works in Paint.NET 4.0 only.
 

#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
        PaintDotNet.Threading.PdnSynchronizationContext.Instance.Send(new System.Threading.SendOrPostCallback(delegate(object state)
        {
            // This line runs on the UI thread and not on the Render thread
            System.Windows.Forms.MessageBox.Show("This is the help text");
        }), null);
    }
    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;
        }
    }
}

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

Thanks, null54, that will certainly help. Looks like the change from Invoke to PdnSynchronizationContext is pretty direct (as Rick suggested it was). Is the PdnSynchronizationContext not available prior to 4.0, or will it just not work correctly?

Link to comment
Share on other sites

Is the PdnSynchronizationContext not available prior to 4.0, or will it just not work correctly?

 

It is located in PaintDotNet.Base.dll in 4.0, it is not available in 3.5.11.

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

Here is my Help Button version;

// Author: MJW
// Name: Test PDN Help Button
// Title: Test PDN Help Button
// Desc: Add a PDN-approved help button.
#region UICode
byte Amount1 = 0; // [255] Randomize
#endregion

bool hasHelpButton = false;

void Render(Surface dst, Surface src, Rectangle rect)
{
    if (!hasHelpButton) // Help button displayed?
    {
        hasHelpButton = true;
        PaintDotNet.Threading.PdnSynchronizationContext.Instance.Send(new System.Threading.SendOrPostCallback(AddHelpButton), null);
    }
    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;
        }
    }
}

void AddHelpButton(object state)
{
    // Get the parent form.
    FormCollection forms = System.Windows.Forms.Application.OpenForms;
    Form form = forms[forms.Count - 1];
    
    if (!form.HelpButton)
    {
          form.HelpButton = true;
          form.HelpButtonClicked += new System.ComponentModel.CancelEventHandler(HelpButtonClicked);
    }
}

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


There may be a better way to get the parent's form. I don't even know if the method I used always works. Rick didn't seem to like Form.ActiveForm, so I tried to find an alternative.

Link to comment
Share on other sites

Like what, particularly? If you mean the method by which I add the Help Button, I know of no other way, though I'd be happy to learn. If you mean don't add the Help Button, I'll, of course, abide by your decision, even though I won't like it and I don't see the reason for it.

 

Other than replacing Invoke with PdnSynchronizationContext, it's essentially the same code you commented on previously, without particular disapproval. I can understand not liking the menu-bar version, which was mostly done as a lark, anyway, but the Help Button version seems fairly benign, and is a lot prettier than borrowing the reseed button.

Link to comment
Share on other sites

Like what, particularly? If you mean the method by which I add the Help Button, I know of no other way, though I'd be happy to learn. If you mean don't add the Help Button, I'll, of course, abide by your decision, even though I won't like it and I don't see the reason for it.

 

But the reason is simple. We are back to what we said at the start of this thread. CodeLab maps to PropertyBased effects. And if you bypass this API then there is always the risk that something is not working in the future.

midoras signature.gif

Link to comment
Share on other sites

Do not add UI to the effect dialog like that.

 

 

Like what, particularly?

MJW, I think it all comes down to the rules of this forum where Rick states:

 

Plugins must not modify the Paint.NET user interface.

There are non-Reflection methods for doing this. Plugins are not allowed to modify Paint.NET's UI in any way such as by adding menu commands, etc.

As for this...

 

the Help Button version seems fairly benign, and is a lot prettier than borrowing the reseed button.

I agree, 100%

Rick, throw us a bone here!

Let us use this:

 

// Name: Help Button Demo [?]
#region UICode
byte Amount1 = 0; // [255] Randomize
#endregion

string HelpText = "This is the help text.\r\n\r\nMore Help here.";
//string HelpText = "http://www.BoltBait.com/pdn";

void Render(Surface dst, Surface src, Rectangle rect)
{
    Form IndirectUIForm = Form.ActiveForm;
    if (IndirectUIForm != null)
    {
        if (IndirectUIForm.Name == "EffectConfigDialog")
        {
            if (!IndirectUIForm.HelpButton)
            {
                IndirectUIForm.Invoke(new Action(delegate()
                {
                    IndirectUIForm.HelpButton = true;
                    IndirectUIForm.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;
    if (HelpText.ToLower().StartsWith("http://") || HelpText.ToLower().StartsWith("https://"))
    {
        System.Diagnostics.Process.Start(HelpText);
    }
    else
    {
        System.Windows.Forms.MessageBox.Show(HelpText,"Help",MessageBoxButtons.OK,MessageBoxIcon.Information);
    }
}
or give us a better way to do the same thing.

MoreHelp.png

__________

post-44727-0-23113400-1434656323_thumb.p

Link to comment
Share on other sites

Plugins must not modify the Paint.NET user interface.

There are non-Reflection methods for doing this. Plugins are not allowed to modify Paint.NET's UI in any way such as by adding menu commands, etc.

 

I don't see this as modifying Paint.NET's UI. It modifies the plugin's UI. I realize it's a fine line when dealing with something as integrated into PDN as the IndirectUI, but there's an important distinction: changes to the effects IndirectUI dialog only affect that effect; once it's completed, nothing in PDN is changed. The PDN interface is the same, and no other plugins or other effects are changed. I'm aware that "if you bypass this API then there is always the risk that something is not working in the future," but that's true of many things. Lots of plugins didn't work when 4.0 came out. I don't see the Help Button change as something that would likely cause major headaches in the future, but if it does, plugins that use it will have to be modified. I added the test for the HelpButton property already set with the idea that someday it might be used by the IndirectUI dialog, and I didn't want to interfere with that use.

 

Obviously the best solution would be to have some help-menu functionality incorporated into the PropertyBased effects, but I hope until that happens, Rick takes pity on us and provides a solution which is acceptable to him.

 

EDIT: One thing I could do to reduce the chance of future incompatibility, is to add the form name check back into my into my PdnSynchronizationContext version. Then if Rick changes the IndirectUI interface to something that will make the help-button trick not work, he could change the dialog name, and the help-button change would have no affect. I could be wrong, but I assume the choice of the dialog's name is pretty arbitrary. If for some reason he changed the dialog name for some other reason, all that would happen is that plugins that used the help-button trick would no longer provide a help-menu option. (I like the form name check anyway, since though I assume the last-run dialog is the one at the end of the application's form list, I don't know it for a fact. I'd intended to look into that, so that I didn't inadvertently modify some other form. The version I posted last night was intended partly as a "proof of concept," and  partly to see if someone knows a better way to get the dialog's form.)

Link to comment
Share on other sites

I don't see this as modifying Paint.NET's UI. It modifies the plugin's UI. I realize it's a fine line when dealing with something as integrated into PDN as the IndirectUI, but there's an important distinction: changes to the effects IndirectUI dialog only affect that effect; once it's completed, nothing in PDN is changed. The PDN interface is the same, and no other plugins or other effects are changed.

I agree with this.

I was just thinking... Code to add the ? button doesn't need to go in the Render function at all. In fact, if I modified CodeLab, I could have it build all the necessary code in other places and the user's CodeLab script could look like this:

 

// Name: Help Button Demo [?]
// Help: This is the help text.\r\n\r\nMore Help here.
#region UICode
byte Amount1 = 0; // [255] Randomize
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    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;
        }
    }
}
It would be quite simple, really.

The help comment could take either of two forms:

// Help: 
which would open a web browser when ? is clicked. Or,

// Help: This is the help text.\r\n\r\nMore Help here.
which would show the dialog box.

EDIT: I just tested it and it works great. I could have CodeLab ready in an hour.

All we need now is Rick's blessing.

Link to comment
Share on other sites

I certainly like the idea of incorporating the Help function into CodeLab, but the one thing I wish is that the method would allow for longer help text. What I'd prefer is to put the sort of control-by-control description I put with, say, the HSV Eraser plugin, preferably with some degree of text formatting to allow bolding. I know the trend is to make Help open informational websites in a browser, but it isn't a trend I care for.

Link to comment
Share on other sites

What I'm proposing could handle simple help text that could be displayed in a message box* and if you have more complex needs you supply a URL where you have ultimate flexibility. URL's could point to the message board or your own web site.

*My demo shows how to make blank lines in a messagebox. You can also use "\t" for a tab character and Alt-255 for a forced space to control word wrap.

Link to comment
Share on other sites

Or supply the Help file as a PDF and have the URL point to a local version which is downloaded with the plugin.

Link to comment
Share on other sites

Adding menu items to a Form that you don't own is bad. You may feel reasonably entitled to it since in a way it is "your Form" because it is your plugin, and I can certainly understand that.

 

I'd be OK with using HelpButton and HelpButtonClicked if there was a reliable way to ensure that the Form you're getting is "yours." Inspecting the Name, or even the type (e.g. "if (Form is EffectConfigDialog)"), is both a fragile solution and one that that ties my hands on future architecture choices.

 

It's "fragile" because relying on the Name is circumstantial and weak (I mean "weak" in sort of a mathematical sense there). It's "weak" because any Form can set its Name to that and it'll get caught by your code. Checking based on the type is also circumstantial because it assumes 1) your Effect has a Form, and 2) that it's the only Effect with a Form.

 

I'm happy to add some kind of HelpButton access to IndirectUI in the next Paint.NET update. But, it won't work on previous versions of Paint.NET. And BoltBait will have to update CodeLab for it (AFAIK).

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

BTW, Rick, I thought delegate required () after it.

 

delegate doesn't require () in some contexts and is a clean way of ignoring parameters. Technically you can do this with Linq ...

IEnumerable<int> someIntList = ...
IEnumerable<int> anIntListOfAllSevensThatIsTheSameLengthAsSomeIntList = someIntList.Select(delegate { return 7; });

Although it's more often used in situations like you see with SynchronizationContext.Post/Send. In that case you have a "context" or "state" parameter and it's not always used. Using delegate-without-() is a nice way to document to readers and future maintainers of your code that the parameter isn't used. In my opinion it's better than doing something like "delegate(object ignoredState) { ... }".

 

I don't believe it would work if there's ambiguity as a result. For instance, if the compiler has to choose between a method that takes an Action<T> vs. Action<T1, T2>, then that would be ambiguous., e.g.,

public static void GimmeYourAction<T>(Action<T> action) { ... }
 
public static void GimmeYourAction<T1, T2>(Action<T1, T2> action) { ... }
 
...
GimmeYourAction(new delegate { return; }); // clearly ambiguous!

The Paint.NET Blog: https://blog.getpaint.net/

Donations are always appreciated! https://www.getpaint.net/donate.html

forumSig_bmwE60.jpg

Link to comment
Share on other sites

The modal Help window is far superior to the pseudo-modeless window that prevents the dialog from closing until it's closed, but better yet would be a modeless Help window that closed when the main dialog was closed. That way, the user could consult the Help menu while using the effect.  Best of all, would be probably be allowing the plugin to choose between modal or modeless.

Edited by MJW
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...