Jump to content
Developer Wiki and Function Reference Links ×

How do I populate a parameter's choices with a dynamic list...?


Recommended Posts

My PIO includes a pop-up to apply a background hatch.

Currently, the selections are hard-coded into the Parameter's "choices" (each hatch, by name, really basic).

It would be better to populate the info-palette Pop-Up button with a list of all hatches in the document?a dynamic list that shows available hatches (like, including new hatches as they are added).

Has anyone achieved this? can you describe the code, or provide a snippet?

My searches have been inconclusive, or maybe so far beyond my modest abilities I can't understand them... the most common result, Parametric Custom Shape Pane, is a little erudite for my interpretation. (if anyone could explain-it-like-I'm-5-years-old, I would be grateful!)

?

Link to comment

The first thing you need to know if that if you want this, your pio need to be event-enabled. The thing with event-enabled is that it's a lot of more work and can be harder if you aren't a regular programmer.

So the first thing you need to ask yourself is if you want this. Does the extra work/dificulty justify the result for this pio?

After you decided this, we can help you further. You can also see the Vectorlab site and developer.vectorworks.net site.

If it is just using a hatch from the resources of the document you want, you can do this in other ways without having your pio event-enabled. I assume you have this pull-down for the names of the hatches you want to use. This is good and you can put in the names for the standard commonly used hatches you use.

Aside from this pull-down menu, I assume you want to be able to use other hatches from the drawing. Am I right that this is the source of your question? Instead of adding these to your pull-down menu, you could also create a string parameter in your pio. The user can then just type in the name of the resource and your pio can then use that hatch. We use this method on our simple pio's where we need to use symbols in. We can then use any symbol by using it's name.

If you want it to be more flexible, you can create a dialog where you can select a hatch from the document. Create a checkbox that triggers this dialog and let the chosen hatch name be filled in in the string parameter I described above.

So you have more than one way of doing this, depending on how much you will use this feature/pio and how much time/energy you can put in it.

Link to comment

Hello DWorks! Thank you for your assessment!

I have implemented both of the "simple" ideas; entering a custom string is fine but problematic, and the hard-coded pop-up choices is very limiting, and a modal dialog box would be intrusive and defeat the intended ease of the tool. (As a certified Lazy Designer I am attempting to make my PIO do as much of the work for me as possible.)

The most flexible and desirable solution would be populate the parameter's choices with a list of available hatches. (I can't imagine I'm the only person who has ever wanted a slick dynamic hatch popup...? whoever provides insight/code will be a hero, a hero I tell you!)

I am not opposed to the work?I am simply overwhelmed by the obtuseness of the related texts. For example, despite numerous references to "PIO must be event-enabled," I have not actually found a single clear explanation of how to do that; mostly, I just encounter other poor bastards like myself.

Edited by AEChadwick
Link to comment

This will do what you're needing, assuming the pop-up parameter number is "xxxx:"

HatchResourceListID := BuildResourceList(105 {Hatches},0 {current document},'',HatchNumResources);

vsoWidgetPopupClear(xxxx);

If HatchNumResources > 0 then

Begin

For HatchCount := 1 to HatchNumResourcesDo

Begin

HatchResourceString := GetNameFromResourceList(HatchResourceListID,HatchCount);

vsoWidgetPopupAdd(xxxx,HatchResourceString, HatchResourceString );

End;

End;

Link to comment

You are quite welcome!

Since learning this from one of our wise colleagues, I've used this basic idea for grabbing and listing things like Symbols and Textures. Another on the long list of "simple but powerful" things we can do w. VS.

A few things you'll discover:

- As others have said, your PIO will need to be event-enabled.

- This snippet of code will need to go in the "41" event (kObjOnWidgetPrep).

- You'll need to include "vsoSetEventResult (-8);" at the end of the event block.

Don't hesitate to post your "omg" messages. You'll find the folks here to be infititely generous with help and insights.

Link to comment

Here is a quick attempt to draw a box and fill it with a dynamically selected hatch. It does not work!

I am confident an {Addict} can explain it to me {Greenhorn}. (I swear this is just my programming experiment, you're not doing my homework assignment for me.)

??

***several dummy mistakes in this, please ignore it, update below***

{Script includes PARAMETER... Type: POP-UP, Name: 9999}

PROCEDURE BoxHatcher;

CONST

theResource = 105; {Hatches}

thePlace = 0; {Current Document}

VAR

HatchResourceListID : LONGINT;

HatchNumResources : LONGINT;

HatchNumResourcesDo : INTEGER;

HatchCount : INTEGER;

HatchResourceString : STRING;

x, y, w, h, LineLength, BoxWidth : REAL;

wLeft, wBottom, wRight, wTop : REAL;

result : BOOLEAN;

objname : STRING;

oh,rh,wh : HANDLE;

theSelectedHatch : STRING;

itsBeenHatched : BOOLEAN;

PROCEDURE MakeItClassy(theClass : STRING);

BEGIN

SetClass(LNewObj, theClass);

SetFillColorByClass(LNewObj);

SetPenColorByClass(LNewObj);

SetFPatByClass(LNewObj);

SetLWByClass(LNewObj);

SetLSByClass(LNewObj);

SetOpacityByClass(LNewObj);

END;

BEGIN

HatchResourceListID := BuildResourceList(theResource,thePlace,'',HatchNumResources);

vsoWidgetPopupClear(9999);

If HatchNumResources > 0 then

Begin

For HatchCount := 1 to HatchNumResourcesDo

Begin {<---- this line gets called out with [ Error: Expected DO ] }

HatchResourceString := GetNameFromResourceList(HatchResourceListID,HatchCount);

vsoWidgetPopupAdd(9999,HatchResourceString,HatchResourceString);

End;

End;

{Build Shape}

result:= GetCustomObjectInfo(objname,oh,rh,wh);

IF result THEN BEGIN

w := PLINELENGTH;

h := PBOXWIDTH;

wLeft:=x; wBottom:=y-(h/2); wRight:=x+w; wTop:=y+(h/2);

Rect(wLeft,wBottom,wRight,wTop);

MakeItClassy('SET-(5) thick (thick)'); {?forgive my class names}

theSelectedHatch := P9999; {I just know this is NOT how to transfer that info?}

itsBeenHatched := SetVectorFill(LNewObj, theSelectedHatch);

END;

{Done building shape.}

END;

RUN(BoxHatcher);

Edited by AEChadwick
Link to comment

alright?I need to go get lunch (who spends the 4th of July scripting, anyway?!)

Current non-functioning code below. (add a head-on-wall when I figured out "oh, right, Parameter Number...")

Submitted for review:

***revised again, update below***

{includes PARAMETER... Type: POP-UP, Name: theHatchPopup, Number: 3}

{Event Enable: Go Properties > Execution Options > check "Event Based," "Reset on Move," "Reset on Rotate"}

PROCEDURE BuildHatchPopup;

CONST

theResource = 105; {Hatches}

thePlace = 0; {Current Document}

VAR

HatchResourceListID : LONGINT;

HatchNumResources : LONGINT;

HatchNumResourcesDo : INTEGER;

HatchCount : INTEGER;

HatchResourceString : STRING;

x, y, w, h, LineLength, BoxWidth : REAL;

wLeft, wBottom, wRight, wTop : REAL;

result : BOOLEAN;

objname : STRING;

oh,rh,wh : HANDLE;

theSelectedHatch : STRING;

itsBeenHatched : BOOLEAN;

41: {kObjOnWidgetPrep}

BEGIN

HatchResourceListID := BuildResourceList(theResource,thePlace,'',HatchNumResources);

vsoWidgetPopupClear(3);

If HatchNumResources > 0 then

Begin

For HatchCount := 1 to HatchNumResources Do

Begin

HatchResourceString := GetNameFromResourceList(HatchResourceListID,HatchCount);

vsoWidgetPopupAdd(3,HatchResourceString,HatchResourceString);

End;

End;

vsoSetEventResult(-8); {kObjectEventHandled}

END;

PROCEDURE MakeItClassy(theClass : STRING);

BEGIN

SetClass(LNewObj, theClass);

SetFillColorByClass(LNewObj);

SetPenColorByClass(LNewObj);

SetFPatByClass(LNewObj);

SetLWByClass(LNewObj);

SetLSByClass(LNewObj);

SetOpacityByClass(LNewObj);

END;

BEGIN

{Build Shape}

result:= GetCustomObjectInfo(objname,oh,rh,wh);

IF result THEN BEGIN

w := PLINELENGTH;

h := PBOXWIDTH;

wLeft:=x; wBottom:=y-(h/2); wRight:=x+w; wTop:=y+(h/2);

Rect(wLeft,wBottom,wRight,wTop);

MakeItClassy('SET-(5) thick (thick)');

theSelectedHatch := PtheHatchPopup; {I just know this is wrong?}

itsBeenHatched := SetVectorFill(LNewObj, theSelectedHatch);

END;

{Done building shape.}

END;

RUN(BuildHatchPopup);

Edited by AEChadwick
Link to comment

Just to keep everyone up-to-date as I flail along. (This post live from behind Kitchen Stadium as we load in Series 11.)

This script compiles but the pop-up is greyed out, useless! Why doesn?t it fill up? Just stubborn is my guess. I?m going to keep trying to comprehend Events as the kitchen comes together. (Note, if some of the lines are inscrutable, I'll admit, it's because copy-pasta... I just keep trying snippets and bits.)

I appreciate all the help, and look forward to any more insights!

PS. shout out to Mr Joshua, you've offered excellent advice in several threads.

{Script includes PARAMETER... Type: POP-UP, Name: theHatchPopup, Number: 3}

{Event Enable: Go Properties > Execution Options > check "Event Based," "Reset on Move," "Reset on Rotate"}

PROCEDURE Plugin;

CONST

theResource = 105; {Hatches}

thePlace = 0; {Current Document}

displayString = ''; {Nothing}

VAR

theEvent, theMessage :LONGINT;

HatchResourceListID : LONGINT;

HatchNumResources : LONGINT;

HatchNumResourcesDo : INTEGER;

HatchCount : INTEGER;

HatchResourceString : STRING;

x, y, w, h, LineLength, BoxWidth : REAL;

wLeft, wBottom, wRight, wTop : REAL;

result : BOOLEAN;

objname : STRING;

oh,rh,wh : HANDLE;

theSelectedHatch : STRING;

itsBeenHatched : BOOLEAN;

PROCEDURE MakeItClassy(theClass : STRING);

BEGIN

SetClass(LNewObj, theClass);

SetFillColorByClass(LNewObj);

SetPenColorByClass(LNewObj);

SetFPatByClass(LNewObj);

SetLWByClass(LNewObj);

SetLSByClass(LNewObj);

SetOpacityByClass(LNewObj);

END;

BEGIN {the main body}

{Build Hatch Popup}

vsoGetEventInfo(theEvent, theMessage);

CASE theEvent OF

41: {kObjOnWidgetPrep}

BEGIN

HatchResourceListID := BuildResourceList(theResource,thePlace, displayString,HatchNumResources);

vsoWidgetPopupClear(3);

If HatchNumResources > 0 then

Begin

For HatchCount := 1 to HatchNumResources Do

Begin

HatchResourceString := GetNameFromResourceList(HatchResourceListID,HatchCount);

vsoWidgetPopupAdd(3,HatchResourceString,HatchResourceString);

End;

End;

vsoSetEventResult(-8); {kObjectEventHandled}

END;

END; {of Case}

{Build Shape}

result:= GetCustomObjectInfo(objname,oh,rh,wh);

IF result THEN BEGIN

w := PLINELENGTH;

h := PBOXWIDTH;

wLeft:=x; wBottom:=y-(h/2); wRight:=x+w; wTop:=y+(h/2);

Rect(wLeft,wBottom,wRight,wTop);

MakeItClassy('SET-(5) thick (thick)');

theSelectedHatch := PtheHatchPopup;

itsBeenHatched := SetVectorFill(LNewObj, theSelectedHatch);

END; {Build shape.}

END; {the main body}

RUN(Plugin);

Edited by AEChadwick
Link to comment

Investigating some of Andy's tips led me to this page: http://developer.vectorworks.net/index.php?title=VS:Parametric_Custom_Shape_Pane_Popup

Included at the bottom are some sample objects which should give you a good framework.

One thing that jumps out in the last code snippet is that the section drawing the object should actually be part of the event handler, under the reset event. (I haven't opened the example scripts, but I'm hoping they will clarify the PIO structure).

-Josh

Link to comment

AEChadwick, below is more food-for-thought for you. It is the general/basic structure several of my PIOs follow. Others may have different approaches, but this is what works for me:

procedure DemoPIO;

{--------------- Declare Constants and Variables ---------------}

Const {Constant Setting Section}

kResetEventID = 3;

kObjOnInitXProperties = 5;

kObjXPropHasUIOverride = 8;

kObjXHasCustomWidgetVisibilities = 12;

kObjOnWidgetPrep = 41;

var {Variable Setting Section}

gPIOName : string;

gPIOHandle, gPIORecordHandle,gWallHandle : handle;

gTheEvent,gMsgData : longint;

Result : Boolean;

{--------------- Procedure to Do Real Work ---------------}

Procedure RealWork;

Begin

End;

{--------------- Equivalent of Master Cue List ---------------}

Begin {main}

Result := GetCustomObjectInfo(gPIOName, gPIOHandle, gPIORecordHandle, gWallHandle);

vsoGetEventInfo(gTheEvent, gMsgData);

CASE gTheEvent OF

kResetEventID: {Event: 3}

BEGIN

RealWork;

END;

kObjOnInitXProperties: {Event: 5}

BEGIN

Result := SetObjPropVS (kObjXPropHasUIOverride,True);

Result := SetObjPropVS (kObjXHasCustomWidgetVisibilities,True);

Result := vsoInsertAllParams;

END;

kObjOnWidgetPrep: {Event: 41 - Section to set Parameter visibilities and enablings}

Begin

{lines to hide/show/enable/disable parameters and clear/populate pop-ups}

vsoSetEventResult (-8);

End;

END; {Event Case}

end;{main}

run(DemoPIO);

Link to comment

I use the same approach as Andrew and start with a basic structure and then build upon it.

The following is a working sample for a point plug-in that draws a 1" x 1" rect. The fill hatch can be selected from a popup parameter which is populated with the code provided by Andrew

PROCEDURE EventPlugin;
{DEBUG}
CONST
kResetObjectEventID = 3;
kInitObjPropEventID = 5;
kButtonPressEventID = 35;
kOnWidgetDefEventID = 41;
kObjAddStateEventID = 44;

kObjectEventHandled = -8;
kObjXPropHasUIOverride =  8;
kObjXHasCustomWidgtVis = 12;
kObjXPropAcceptsStates = 18;

kTYP_HATCHES = 66;

kFLD_OBJWDT  = 'Wdt';
kFLD_OBJHGT  = 'Hgt';
kFLD_HTCHPOP = 'Hatch Popup';

kPRM_OBJWDT  = 1;
kPRM_OBJHGT  = 2;
kPRM_HTCHPOP = 3;

VAR
gResult: BOOLEAN;
gObjName: STRING;
gObjHdl,gRecHdl,gWallHdl: HANDLE;
gVSEvent,gEvtMsg: LONGINT;

PROCEDURE InsertParams;
VAR
	result: BOOLEAN;
BEGIN
result:= vsoInsertAllParams;
END;

PROCEDURE InsertButtons;
BEGIN
END;

PROCEDURE ProcessBtnHit;
BEGIN
END;

PROCEDURE SetHatchPopup;
VAR
	resType,fldrIdx: INTEGER;
	subFldrName,hatchName: STRING;
	idx,hatchResourceListID,hatchNumResources: LONGINT;
BEGIN
resType:= kTYP_HATCHES;
fldrIdx:= 0;
subFldrName:= '';
hatchResourceListID := BuildResourceList(resType,fldrIdx,subFldrName,hatchNumResources);

vsoWidgetPopupClear(kPRM_HTCHPOP);
vsoWidgetPopupAdd(kPRM_HTCHPOP,'None','None');
IF hatchNumResources > 0 THEN
	BEGIN
	For idx:= 1 to hatchNumResources Do
		Begin
		hatchName:= GetNameFromResourceList(hatchResourceListID,idx);
		vsoWidgetPopupAdd(kPRM_HTCHPOP,hatchName,hatchName);
		End;
	END;
END;

PROCEDURE ResetPlugin;
VAR
	fPathIdx: LONGINT;
	hatchName: STRING;
	layScale,objWdt,objHgt: REAL;
BEGIN
PushAttrs;
layScale:= GetLScale(ActLayer);
objWdt:= layScale * PWDT;
objHgt:= layScale * PHGT;
hatchName:= PHATCH_POPUP;

Rect(-objWdt/2, objHgt/2, objWdt/2,-objHgt/2);
IF hatchName = 'None' THEN
	SetFPat(LNewObj,0)
ELSE
	BEGIN
	fPathIdx:= Name2Index(hatchName);
	SetFPat(LNewObj,-fPathIdx);
	END;

Locus(0,0);
PopAttrs;
END;

BEGIN
vsoGetEventInfo(gVSEvent,gEvtMsg);
CASE gVSEvent OF
kInitObjPropEventID: 
	BEGIN
	SetPrefInt(590,1);
	gResult:= SetObjPropVS(kObjXPropAcceptsStates,TRUE);

	IF SetObjPropVS(kObjXPropHasUIOverride,TRUE) & 
	   SetObjPropVS(kObjXHasCustomWidgtVis,TRUE) THEN
		InsertParams;

	InsertButtons;
	END;
kButtonPressEventID:
	BEGIN
	IF GetCustomObjectInfo(gObjName,gObjHdl,gRecHdl,gWallHdl) THEN
		BEGIN
		ProcessBtnHit;
		END;
	END;
kOnWidgetDefEventID:
	BEGIN
	SetHatchPopup;
	vsoSetEventResult(kObjectEventHandled);
	END;
kObjAddStateEventID:
	BEGIN
	IF GetCustomObjectInfo(gObjName,gObjHdl,gRecHdl,gWallHdl) THEN
		gEvtMsg:= vsoStateAddCurrent(gObjHdl,gEvtMsg);
	END;
kResetObjectEventID:
	BEGIN
	IF GetCustomObjectInfo(gObjName,gObjHdl,gRecHdl,gWallHdl) THEN
		BEGIN
		ResetPlugin;
		vsoStateClear(gObjHdl);
		END;
	END;
END;
END;
Run(EventPlugin);

{includes PARAMETER... Type: POP-UP, Name: theHatchPopup, Number: 3}

{Event Enable: Go Properties > Execution Options > check "Event Based," "Reset on Move," "Reset on Rotate"}

If nothing changes when you move or rotate the plug-in, you do not need to check "Reset on Move or Rotate". Check these only when the geometry or a parameter will change based on its location/rotation (i.e. a pile foundation height will depend on the surface elevation at the insertion point).

Link to comment

Holy smokes, you guys are amazing!

(Mr Barrera, Mr Dunning, Mr Benghiat, thank you all so much. I was just about to check in with a lame excuse about "got busy on set, still working on script," but you've beaten me to the punch.)

I am going to sit and read every line until I can really parse what's happening, it really is much more complex than I allowed (pardon my youthful naivet?).

I hope other folks find this as awesome and helpful as I do; thank you again for all your attention. Now: to work!

?

Link to comment

A pop up can only give you a list of the hatch names.

If you using an event enabled tool, you can add a button that triggers a dialog. A dialog lets you use a graphic interface to select the hatch.

Below is a drop in function I hacked together.

ImportSymbol(FolderID:INTEGER;SubFolder:STRING;FolderName:STRING):Handle;

This Function allows you to specify a File Directory to import symbols from. You specify the folderName inside the current document where the symbol gets filed.

To get symbols only in the active file use something like:

hTemp:=ImportSymbol(0,'','MyCoolFolder');

I used Vectorworks Dialog builder (DB5) to create the dialog part of the script. I think Dialog Builder has been updated recently. It is a good set of tools for dialog scripting. It is a starting point, from memory it has a hatch selection dialog.

If you look at Dialog Builder you will find what you need.

_____________________

Function ImportSymbol(FolderID:INTEGER;SubFolder:STRING;FolderName:STRING):Handle;

{

FolderID points the function to a file directory Folder. Look at developer.vectoworks.com 'VS:BuildResourceList'

13 is /Library

Subfolder is a folder in the file directory. This allows you to build your on folder structure for different tools

ie to get symbols out of a specific VW file you could have 'Electrical/Lights'

FolderName allows you to file the symbol once it gets imported.

the PIO needs variables:

__ResourceIndex

The Script needs a global VAR

gResourceIndex:INTEGER;

}

CONST

kStartStringAt = 3000;

kOK = 1;

kCancel = 2;

kImagePopup15 = 15;

kStaticText28 = 28;

kStaticText29 = 29;

kRight = 1;

kBottom = 2;

kLeft = 3;

kResize = 0;

kShift = 1;

VAR

dialog1 :INTEGER;

gImagePopup15Int :INTEGER;

gImagePopup15Str :STRING;

defConListID :LONGINT;

defConResType :INTEGER;

defConFoldID :INTEGER;

defConCount :LONGINT;

xmlID :LONGINT;

cnt :INTEGER;

int :INTEGER;

boo :BOOLEAN;

str :STRING;

ndx :INTEGER;

iImage:INTEGER;

FUNCTION ResourceIsOK :BOOLEAN;

BEGIN

IF SetVSResourceFile('IP Resources')

THEN ResourceIsOK := TRUE

ELSE Message('The "IP Resources" file was not found.');

END;

FUNCTION GetPlugInString(ndx :INTEGER) :STRING;

BEGIN

CASE ndx OF

{Static Text}

3001: GetPlugInString := 'OK';

3002: GetPlugInString := 'Cancel';

3003: GetPlugInString := 'Untitled Dialog';

3028: GetPlugInString := 'Image:';

3029: GetPlugInString := 'System Color:';

{Help Text}

4001: GetPlugInString := 'Accepts dialog data.';

4002: GetPlugInString := 'Cancels operation without changes.';

4015: GetPlugInString := 'An image popup control.';

4028: GetPlugInString := 'Static text control.';

4029: GetPlugInString := 'Static text control.';

END;

END;

FUNCTION GetStr(ndx :INTEGER) :STRING;

BEGIN

GetStr := GetPlugInString(ndx + kStartStringAt);

END;

PROCEDURE dialog1_Setup;

BEGIN

dialog1 := CreateLayout(GetStr( 3), True, GetStr(kOK), GetStr(kCancel));

CreateControl (dialog1, kImagePopup15, 10, '', 0);

CreateStaticText (dialog1, kStaticText28, GetStr(kStaticText28), 12);

CreateStaticText (dialog1, kStaticText29, GetStr(kStaticText29), 12);

SetFirstLayoutItem(dialog1, kStaticText29);

SetBelowItem (dialog1, kStaticText29, kStaticText28, 0, 0);

SetRightItem (dialog1, kStaticText28, kImagePopup15, 0, 0);

AlignItemEdge(dialog1, kImagePopup15, kLeft, 222, kShift);

FOR cnt := 1 TO 6 DO SetHelpString(cnt, GetStr(cnt + 1000));

END;

PROCEDURE dialog1_Handler(VAR item :LONGINT; data :LONGINT);

BEGIN

CASE item OF

SetupDialogC:

BEGIN

defConResType := 16; {This sets the type of resource to build a list of Symbols. Hatch Definiting is 66}

defConFoldID := FolderID;

defConListID := BuildResourceList(defConResType, defConFoldID, SubFolder, defConCount);

FOR cnt := 1 TO defConCount DO BEGIN

int := InsertImagePopupResource(dialog1, kImagePopup15, defConListID, cnt);

END;

IF p__ResourceIndex

END;

kOK:

BEGIN

iImage:=GetImagePopupSelectedItem(dialog1, kImagePopup15);

gResourceIndex:=iImage;

ImportSymbol:=ImportResourceToCurrentFile(defConListID, iImage);

IF GetObject(FolderName)=NIL Then

Begin;

NameObject(FolderName);

BeginFolder;

EndFolder;

END;

InsertSymbolInFolder(GetObject(FolderName),ImportSymbol)

END;

END;

END;

BEGIN

IF ResourceIsOK THEN dialog1_Setup;

IF RunLayoutDialog(dialog1, dialog1_Handler) = 1 then BEGIN

END;

END;

Link to comment

Hey Mr Assembly!

Your version looks great too, thank you for the input! I will explore your code, I love learning all of this and I love having options...

In my PIO, a easy-access (read: non-modal) list of simple hatch names is the desired outcome?the hatches are fairly uniform, providing only subtle visual difference; the more important factor is the name, and the handle provided by the hatch. (like, a connected database finds everything called "Wallpaper 1" or "Paint White" and provides square footage.)

?

Link to comment

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