Jump to content

GetParameter, when is it active?


Dewdman42

Recommended Posts

The following code gives me an error:

 

PluginParameters = [];
PluginParameters.push({
    name: "test",
    type: "menu",
    valueStrings: ["one", "two", "three"],
    defaultValue: 0
});
var info;
info = GetParameter("test");
Trace(info);

 

Error produced is:

GetParameter() called with an argument: (test) that does not equal any registered parameter name.

 

Anyone know why this is the case?  Seems as though GetParameter is not able to get any value from the PluginParameters[] array except for perhaps from inside one of the callback functions, maybe?  Or is there something else that needs to complete before PluginParameters is available to query for values or set values with SetParameter()?

  • Like 1
Link to comment
Share on other sites

I have decided that GetParameter and SetParameter are doing weird stuff under the covers and they basically only work if called from within one of the callback functions.  I did find a line in the manual that basically says something about GetParameter mainly being useful "from within HandleMIDI() and ProcessMIDI()", though it doesn't call it a requirement, its vague. 

 

 But I have had GetParameter return strange results when used from a singleton object , even after the script is running for a while, well past init stage and definitely when I try to call GetParameter() at the global level, not in a callback, it just reports the above error.

 

The approach I will employ from now on is basically something more or less like this:

 

PluginParameters.push({
  name: "test",
  type: "menu",
  defaultValue: 0,
  valueStrings: ["one","two","three"]
});
var parameter[0] = 0;   // save the default value to data array

function ParameterChanged(ctrl, val) {
   parameter[ctrl] = val;
}

 

Then in all my code I will use the parameter[] array to access the data, rather then GetParameter() function.  Unless I am doing a very very simple script, then GetParameter can work fine.  But it should not be called at the global level.  And I had problems also when I put it into a function of an object...it would return totally bogus data with no error reported.  There is something funny about the way that function is implemented.  I trust my own array more.

 

it could probably also be done like this:

 

PluginParameters.push({
   name: "test",
   type: "menu",
   defaultValue: 0,
   valueStrings: ["one","two","three"],
  data: 0               // set default value to the data member
});

function ParameterChanged(ctrl, val) {
    PluginParameters[ctrl].data = val;
}

Link to comment
Share on other sites

Unfortunately though that makes programmatic and pragmatic sense, the Scripter API contradicts that and does not work that way which is extremely frustrating. Runtime bindings and Scripter API method calls can be made before playback, but, though @unheardofski worded it differently, to the same ends, I've found that it is generally a necessity for the transport to be playing for many of the Scripter API methods to execute correctly. Investigate ProcessMIDI and tracing timing info... you may find some of the beat positions to go into negative numbers after the playhead has stopped which is pretty weird. 

 

If you want the GUI to update with parameter values changed, you can either use UpdatePluginParameters, which will cause a refresh of ParameterChanged for all parameters (why??), or by invoking SetParameter with the correct parameter index or parameter name during playback, but without the expected ParameterChanged hook to be called until yet another parameters value has been changed in the GUI. Its not right, but it seems to be a constraint :( I suppose thats why I take a 'hack' approach rather than a systems approach to Scripter (but that can work actually - you just have to factor for the constraints, and obtain the headspace to accept them). Personally, I think they are using some strange or contradictory MVC/MVVM/MVP pattern, or a number  of  anti-patterns, but as Logic is a walled garden, its impossible to determine accurately.

 

If I get the time, I'll post the state change sequence of events as I understand them in a few days.

Link to comment
Share on other sites

I suppose I was trying to answer a few of your many questions at the same time in the most general way as many people read this forum, and I was feeling a little uncomfortable about it because I don't have confidence that this is the best place to do that on. 'Insider information' is not really what this is about my friend  I really am trying to help you out by highlighting a lot of the counter intuitive factors in the Scripter API (which is why I was suggesting a hook up between you, me and unheardofski on a GitHub gist where we could separate out the concerns: script making and scripter constraints, programming styles etc)

About the strange results you were getting: GetParameter will look up a parameter by name if the transport has started playing, or has played at least once. That is assumably due to Scripters internal implementation and it is... hmmmmm. You are trying to maintain synchronised state of the parameter via a proxy data value added to the vanilla parameter literal. That will work, but it may be difficult to maintain if you want to scale your application. I suppose it depends on what one is trying to build, or explore.I was also going to inform you about Scripter state changes and the order in which the sequence of events takes place because you asked the question, but I feel a little bit of hostility here which I do not understand, or am misinterpreting.

Link to comment
Share on other sites

. Investigate ProcessMIDI and tracing timing info... you may find some of the beat positions to go into negative numbers after the playhead has stopped which is pretty weird. 

if the playhead is stopped, then beat positions become meaningless, doesn't matter what they are.  I would suggest that code should check if the transport is playing before making any assumptions that the beatPos means anything.

 

If you want the GUI to update with parameter values changed, you can either use UpdatePluginParameters, which will cause a refresh of ParameterChanged for all parameters (why??), 

 

I am not sure that is the case, I thought that for a while, but after trying various different things, I have decided not.  But it is very easy to get endless ParameterChanged loops happening if you're not careful, agree with you there. 

 

 

or by invoking SetParameter with the correct parameter index or parameter name during playback, but without the expected ParameterChanged hook to be called until yet another parameters value has been changed in the GUI. Its not right, but it seems to be a constraint :(

 

Perhaps you can provide a code snippet that demonstrates this oddity you describe?  I haven't had that experience.  I can get SetParameter to work just fine without playback happening.  It works from within Idle(), for example.  It also works during HandleMIDI, which does not at all require playback.  My experience is that GetParameter and SetParameter need to be called from callbacks, but not necessarily during playback.  

 

If you think about it, Scripter is basically a function callback paradigm.  There are 5 known callbacks, that I know of:

  1. HandleMIDI
  2. ProcessMIDI
  3. Idle
  4. ParameterChanged
  5. Reset

It also looks for a few globally scoped objects which, similar to callback functions, LPX will reference whenever it feels like to decide behavior.  For example:

  1. NeedsTimingInfo
  2. PluginParameters[]
  3. etc

When you "run" a script, it will run whatever code you placed at the global scope, and then if you defined any of those callbacks, it will sit and wait for LPX to call them.  My experience is that anything you run at the global level, must be getting run before Scripter has fully initialized itself.  One of the things Scripter does is use the PluginParameters[] array to manage the UI and it stashes the actual corresponding data in some backend hidden structure.  Scripter manages that for us, we don't have direct access to it.  So at runtime, Scripter has to look at the PluginParameters[] array and initialize some backend structures for it.  When does it do that?  Apparently it does not do it until after the global scope has executed.  Which kind of makes sense since that's where a lot of people setup PluginParameters.    

 

So you can build up the PluginParameters array at the global scope level but if you try to call GetParameter, then Scripter isn't ready for that yet.  When will it be ready?  You say after playback starts...  ok maybe, but I've found it will be perfectly ready without playback ever happening, as long as its called from within a callback.  So LPX doesn't call any callbacks until its fully initialized and ready.  

 

The Idle() function can be a decent place to try to do that sort of thing.  bottom line, don't try to use SetParameter or GetParameter outside of a callback function.

Link to comment
Share on other sites

Now that being said, I also ran into a problem with calling GetParameter, indirectly inside a callback function.   I had some code that was located in a globally scoped object.  That method was getting called from within a callback function, and sometimes it was returning bogus data, like an extremely large negative number.  I do not know why that was happening, hey perhaps that was related to playback as you say, not sure.  I should try to recreate it again and figure it out.  I got around the problem entirely by using the method I mentioned above..  I set global variables with ParameterChanged and always reference those from my code.  So far, working perfectly, playback or not.
Link to comment
Share on other sites

Here is a quick example of how I am handling data now.  This method enables me to avoid calling GetParameter pretty much ever.  I do have to call SetParameter for updating the value of a control in the UI.  This method also shows UI controls being hidden/shown and having menus updated dynamically, etc..   

 

Load it up in scripter and try messing with the two UI controls to see what happens.  Watch the trace messages in the console.

 

Note the following:

 

  1. GetParameter is never called, the data is piggybacked into the PluginParameters array
  2. Playback is not happening
  3. Idle() is used to update the GUI, which means in a real midi application, these updates would not be happening while midi is being processed.
  4. Note two variables used to keep track of whether to do things.  UpdatePluginParameters is not called unless the dirty flag is set and the dirty flag is only set if the first menu item actually changes.
  5. Note that indeed, calling UpdatePluginParameters causes the ParameterChanged callback to be called for all UI controls on the page.  This is managed by the dirty flag to avoid a loop.

this is just one quick example to show the relationship between PluginParameters, Idle() and ParameterChanged.

 

PluginParameters = [];

var dirty = false;

const letters = [ "A", "B", "C" ];
const numbers = [ "1", "2", "3" ];


PluginParameters.push({
    name: "Select",
    type: "menu",
    valueStrings: ["Letters", "Numbers", "Hide"],
    defaultValue: 0,
    data: 0
});


PluginParameters.push({
    name: "List",
    type: "menu",
    valueStrings: letters,
    defaultValue: 0,
    data: 0
});

PluginParameters.push({
    name: "Count",
    type: "lin",
    minValue: 0,
    maxValue: 1000,
    numberOfSteps: 1000,
    defaultValue: 0,
    data: 0
});

// use to set dirty state and update data
function ParameterChanged(ctrl, val) {

    Trace("ParameterChanged:  CTRL:"+ctrl+" Value:"+val);

    // if the select menu was changed, then set dirty flag
    if (ctrl==0) {
    
        if (val != PluginParameters[ctrl].data) {
        
            // update the counter control
           PluginParameters[2].data++;
            SetParameter(2, PluginParameters[2].data);
            Trace("Setting counter="+PluginParameters[2].data);
            
            dirty = true;
            Trace("Dirty flag set, UI update pending");
        } 
    }
    
    PluginParameters[ctrl].data = val;
}

// Use Idle function to update the actual UI
function Idle() {

   if (dirty) {
 
      Trace("Updating dirty UI");

      // this changes the UI
      if ( PluginParameters[0].data == 2 ) {
           PluginParameters[1].hidden = true;
      }
       
      else if ( PluginParameters[0].data == 0) {
           PluginParameters[1].valueStrings = letters;
           PluginParameters[1].hidden = false;
      }

      else {
           PluginParameters[1].valueStrings = numbers;
           PluginParameters[1].hidden = false;
      }
      
      Trace("Calling UpdatePluginParameters()");
      UpdatePluginParameters();
      dirty = false;
   }
}

Link to comment
Share on other sites

Its great to see you using Idle for its intended system downtime use. Its one of the Scripter API's forgotten hooks. Just try to keep an eye out for a problem with ParameterChanged when used in conjunction with UpdatePluginParameters:

 

PluginParameters = [
{
	name: "LabelA"
,	type: "checkbox"
,	defaultValue: 0
}
,	{
	name: "LabelB"
,	type: "checkbox"
,	defaultValue: 0
}
]

let hiding = false

ParameterChanged = (index, value) => {
Trace(`${PluginParameters[index].name} updated`)
}

Idle = () => {
if (hiding)
    return

hiding = true
PluginParameters[0].hidden = true
UpdatePluginParameters()
}

The mock up above should show that UpdatePluginParameters can give notifications for all parameters out of context when used in conjunction with hidden which could make state management a little more annoying. At least on my system.... do you run into the same issue? I would also say that use UpdatePluginParameters cautiously - the refresh rate of the GUI may be slower than you may imagine. I'm writing the manual for Scriptable and putting the finishing touches to it at the moment so rather than comment here, I want to keep focus on the project for the next few weeks. Keep on going!

Link to comment
Share on other sites

I have lots of cases where I need the GUI to dynamically change which controls are hidden or shown dynamically, or change the values of a menu. It is what it is. The example I gave above does show that updatepluginparameters is causing parameterchanged to be called on all ui controls, as mentioned above, and you see that in the Tracing. But this is managed by the pattern I'm using which exits immediately if the actual value hasn't been changed. It's a bit annoying I agree, but it's also manageable.
Link to comment
Share on other sites

Another thing about the example I gave which should be pointed out, the pattern defers calling updatepluginparameters until during idle() and does as many changes to the ui as it can without calling updatepluginparameters so that it only has to be called once for a group of ui changes all together. yes it still ends up calling parameterchanged redundantly but the pattern I'm using is keeping it minimal and ui response is very smooth since it's not out of control
Link to comment
Share on other sites

  • 10 months later...
Unfortunately though that makes programmatic and pragmatic sense, the Scripter API contradicts that and does not work that way which is extremely frustrating. Runtime bindings and Scripter API method calls can be made before playback, but, though @unheardofski worded it differently, to the same ends, I've found that it is generally a necessity for the transport to be playing for many of the Scripter API methods to execute correctly. Investigate ProcessMIDI and tracing timing info... you may find some of the beat positions to go into negative numbers after the playhead has stopped which is pretty weird. 

 

If you want the GUI to update with parameter values changed, you can either use UpdatePluginParameters, which will cause a refresh of ParameterChanged for all parameters (why??), or by invoking SetParameter with the correct parameter index or parameter name during playback, but without the expected ParameterChanged hook to be called until yet another parameters value has been changed in the GUI. Its not right, but it seems to be a constraint :( I suppose thats why I take a 'hack' approach rather than a systems approach to Scripter (but that can work actually - you just have to factor for the constraints, and obtain the headspace to accept them). Personally, I think they are using some strange or contradictory MVC/MVVM/MVP pattern, or a number  of  anti-patterns, but as Logic is a walled garden, its impossible to determine accurately.

 

Is there a way to force UpdatePluginParameters() not to refreesh ParameterChanged?

If I get the time, I'll post the state change sequence of events as I understand them in a few days.

Link to comment
Share on other sites

what is your point or question T-Ride?

 

Hmmm, now that I am thinking about it I am not 100% sure what my question was, but my problem is that when I call UpdatePluginParameters from ParameterChanged, then an infinite loop starts which I don't know how to overcome

Link to comment
Share on other sites

Study my example above carefully. You have to keep state. So if you have a change you first set to pending state and then inside parameter changed you only do work if that state needs it and clear the state etc. There are slightly different approaches but the key is to set a state variable to keep track of when work needs to be done. You can send me some code pm to look at if you want
Link to comment
Share on other sites

  • 2 months later...
The following code gives me an error:

 

PluginParameters = [];
PluginParameters.push({
    name: "test",
    type: "menu",
    valueStrings: ["one", "two", "three"],
    defaultValue: 0
});
var info;
info = GetParameter("test");
Trace(info);

 

Error produced is:

GetParameter() called with an argument: (test) that does not equal any registered parameter name.

Let's step back for a minute on this.

So you've added a menu item on PluginParameters, no problem with that but the next line, less than a nano second at computer speed, you want to test to see if anyone used the mouse to make a selection on your "test" menu item?

 

You're code is like the following code.

var y = 20;
var x = 0;

if(x > 0)
{
	y = 0;
}

This code produces no syntax error but why would anyone ever write code like this when you know that y = 0; will never get reached.

 

When someone asks the question you're asking it's because they don't understand the setup process of a Logic Scripter.

The best way to understand how Logic sets up a Scripter object is to create code to see how this works.

 

Here's sample code that I used to understand the setup process of a Logic Scripter.

// You only get a callback of HandleMIDI once
// it hits items in the EventList.
function HandleMIDI(event)
{
event.send();
DisplayTest("HandleMIDI");
}
// This gets called for every item in a PluginParameters
// and when a user makes a selection on a PluginParameters item.
function ParameterChanged(param, value)
{
DisplayTest("ParameterChanged(" + param + ")(" + value + ")");
}
// This gets called everytime you press Play.
function Reset()
{
DisplayTest("Reset");
}
// I would avoid even creating a ProcessMIDI() callback
// because this is very CPU intensive.
// Think about this in your design, if you even need to use this.
//function ProcessMIDI()
//{
//	DisplayTest("ProcessMIDI");
//}
function CreateGUI()
{
PluginParameters.push({
   name: "test",
   type: "menu",
   valueStrings: ["one", "two", "three"],
   defaultValue: 0
});

// Uncomment this then "Run Script". See the output.
//	PluginParameters.push({
//    name: "test2",
//    type: "menu",
//    valueStrings: ["four", "five", "six"],
//    defaultValue: 0
//	});


// Uncomment this then "Run Script". See the output.
//	PluginParameters.push({
//    name: "test3",
//    type: "menu",
//    valueStrings: ["seven", "eight", "nine"],
//    defaultValue: 0
//	});

}
function DisplayTest(fromWhere)
{
var info;
info = GetParameter("test");
Trace("[" + fromWhere + "] " + info);
}

PluginParameters = [];
CreateGUI();
// Once you're here that means you'll be exiting Global setup.
//
// So at the exit of the Global setup, that's when PluginParameters
// is available via GetParemeter() but only thru one of the callback
// functions.
//
// Based on your script, you'll need to decide where to place GetParameter()
// in one of the callback functions.

Link to comment
Share on other sites

Yea I guess I don't understand what point or question you're trying to make. I do not think you have understood my post from a year ago, and I do not really understand the point you're making now.

 

The point I made a year ago is that when you setup PluginParameters array, the values contained therein are stored internally by Scripter in some kind of structure and you use the GetParameter function to obtain values from that structure, which should, theoretically, return the values as they appear in the GUI.

 

However, what I found a year ago is that calling GetParameter outside of the callback functions can result in an error. My thought is that Scripter does not have that structure fully initialized until such time that callbacks have begun, so if you have some code in your script that executes at the global level, it gets called when the script is first loaded. You can put all kinds of code at that level, which can initialize variables and so forth or actually can do quite a lot of different things. Typically that is how and when to initialize PluginParameters array, for example. However, calling GetParameter() calls do not function during that phase, the internal structure is not fully ready for it. Once that phase is over and the callbacks are happening, then calling GetParameter() works as intended.

 

For that reason I have offered an approach that I often use, which is that when I initialize PluginParameters array. I also add an additional "data" attribute to each element of the PluginParameters array, which holds the actual data value. That way during the startup phase I can just reference that value directly and avoid calling GetParameters. By using the strategy I employed, that data attribute is always kept up to date with the current value and it can be reference directly without having to ever use GetParameters() at all, and can be used during global startup phase.

 

Hope that clarifies it for you..

Link to comment
Share on other sites

Let's step back for a minute on this.

So you've added a menu item on PluginParameters, no problem with that but the next line, less than a nano second at computer speed, you want to test to see if anyone used the mouse to make a selection on your "test" menu item?

 

You are missing the point. The GetParameter() call does not tell you, ever, whether someone has changed the UI element. It it supposed to tell you what the current value is, regardless of whether it has been changed recently or not.

 

A lot of people use the UI as the holding place of that current value, and the only place to "get" it is to call GetParameter() to give you the current value of that UI element. Unfortunately Calling GetParameter() will not work properly until some point in time after which callbacks have begun getting called. If you don't need to access that value prior to callbacks getting called, then its a moot point. But if you do, then you need to keep it in a separate variable and keep the variable and UI in sync. There are undoubtedly other ways to work around this limitation, but I have presented the way that I have made my standard approach for handling data values in the GUI. This is like a data/view design pattern. I use the PluginParameters array itself to hold the data and keep it in sync with changes by the user through appropriate callbacks.

 

Hope that makes sense to you.

Link to comment
Share on other sites

  • 1 year later...

This is an old post and it's been a long time since I've played with Scripter. But 10.5 and Sampler have gotten me interested again in possibilities. This is a great thread. What Dewdman42 is craving is a legitimate data "model" that we have proper access to in trying to conform Scripter to a more MVC pattern. As it stands, Scripter's managing of data and UI bindings reminds me more of a framework like React . . . but not really. I just mean that, as he explains, data values feel like they are almost being "stored" in the UI. I like his suggestion of 'mirroring' the PluginParameters array with a custom, global array of parameter data that we can access traditionally when we want with typical syntax. I also tested the second approach of simply adding custom properties to the original parameter objects and mirroring data there. That's nice too.

 

As I started exploring scripter for real last night, I was amazed that I couldn't just do

PluginParameters[3].value

for example to get what I wanted. Where is the formal API documentation for this with its organized lists of objects and methods? Aside from the LP Effects Manual, scripter section and the tutorial examples, I can't find anything significant.

 

GetParameter() with its string argument that has to match something in the PluginParameters array feels so hackish. And for that matter, creating plugin parameters and specifying their type with a string ("lin", "target" etc.) is so contrary to just instantiating objects: new LinSlider(), new Target(). It's like I want to abstract this whole thing and write an interface on top of Scripter that works more like what we are used to. Just sayin.

Link to comment
Share on other sites

Keep in mind also the following.. The Parameters array serves two actual purposes. One is to configure the GUI, but the other is to store the values of the GUI. This is all fine, but that is also how you save settings in your DAW project. All plugins generally will save the "settings" of the plugin with the DAW project, so that when you reload the project, each plugin loads with the settings you were using last time you used the DAW project. Scripter works the same way. So the values of the the GUI...are saved with the project. That is important in a lot of cases.

 

So basically you have to access the data from there in the script. But GetParameter is a very computationally expensive operation which is why i did the work around mentioned above, which operates much much more efficiently to grab that value from the loaded settings.

 

You always have the option to store values in a different place other then as a so called "Parameter", but then the data will not be saved with the project. Which may or may not be totally fine..just depends on what it is.

Link to comment
Share on other sites

A few more comments...

 

As I started exploring scripter for real last night, I was amazed that I couldn't just do

PluginParameters[3].value

for example to get what I wanted. Where is the formal API documentation for this with its organized lists of objects and methods? Aside from the LP Effects Manual, scripter section and the tutorial examples, I can't find anything significant.

 

The actual data is not normally stored in the PluginParameters[] array. Scripter stores it internally in some way. The PluginParameters array is normally only used to configure the GUI itself...not the data.

 

The workaround I linked above, basically is a design pattern to stash the data into the PluginParameters array AS A COPY of the true data that Scripter is keeping internally. This is only as a convenience to use in code in a faster way. Ultimately, Scripter will save with the DAW project the value that is it keeping internally and we have no other way to access that data then by calling GetParameter.

 

GetParameter() with its string argument that has to match something in the PluginParameters array feels so hackish.

 

You can also reference the Parameter by id number which is the manner I prefer because I assume it will be faster javascript for one thing. Sometimes its a hassle to keep track of which UI element is which id # though, especially during development as you are adding or removing things from the UI.

 

And for that matter, creating plugin parameters and specifying their type with a string ("lin", "target" etc.) is so contrary to just instantiating objects: new LinSlider(), new Target(). It's like I want to abstract this whole thing and write an interface on top of Scripter that works more like what we are used to. Just sayin.

 

Yea a proper OOP model would have been cool, I don't disagree, but this is javascript after all. I have spent quite a lot of time experimenting with Scripter and trying various OOP design patterns and it always turns out to be way more trouble then its worth. For one thing, we can't include files to re-use classes between projects easily. I have found that from a practical perspective, its just easier to use javascript's flexibility and break some OOP rules...get it done in less code. The target audience for this API is also never going to be able to understand some of the OOP principles of abstraction, so in some ways...from an OOP perspective you might consider it "hackish" but from a simple usability perspective, its very straight forward as is...and OOP knowledge is not neccessary. There are a lot of things that could make Scripter a lot more useful with better OOP built in classes and support. But in reality, its not there..and anyway, with all the UI limitations I have more or less come to realize that most projects of any substantial scope should just be done in C++ instead of Scripter.

Link to comment
Share on other sites

Yeah. I like the workaround. Just add a .data property to each object in the array. For that matter, I might just access the value directly. without the getter function. This should work too:

 

var PluginParameters = [{name:"slider", type: "lin", data: 4}]; //example
var mySlider = PluginParameters[0]; 

function ParameterChanged(id, val) {
   PluginParameters[id].data = val;
}

//mySlider.data gives current value whenever I want it
//at least this works in regular js where mySlider simply references/points to PluginParameters[0] 

 

When you refer to a real project in C++ . . . (or Swift for that matter) I guess we can write actual Midi Plugins too. I didn't realize that they are just part of the the Audio Unit spec.

Link to comment
Share on other sites

I would still use the getter function because it does the job of initializing the data member on first use. That is important when opening a daw project and you want to get the saved parameter value rather then the hard coded value from the script.

 

Check out JUCE for an easy intro to c++ midi plugins

Link to comment
Share on other sites

JUCE looks very cool, thanks.

 

On the one hand, I've always been a fan of using a native code base. But my day job is teaching programming in a high school. After 4 years of mobile development separately for both iOS development and Android, I'm now teaching a class using Flutter. I still smile every time I complete a build for both platforms.

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.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  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...