Trainer Notes

  • May 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Trainer Notes as PDF for free.

More details

  • Words: 3,988
  • Pages: 27
PML2

Training Notes

What’s New in PML2 Objects and Object methods Form names are now preceded with !! i.e. 

setup form _fred

followed by :   setup form !!fred results in an error because !!fred was already defined in the previous assignment Forms are now loaded dynamically return error noalert allows an error to be returned without any messages

What to avoid in PML2 Commands with dollars in    $m­ $m+    $W25 Numbered variables     $v1­120 The var command     var !x name     var !x read Synonyms Global variables

What to avoid in forms and menus Dots in gadget names selector gadgets Userdata Pixmap views

Alert gadgets Radiogroups Simple Textin gadgets Numbers at the beginning of gadget names Waiton to show forms Do list/pane/sel

What’s not changed in PML2 In theory, all old PML should work in PDMS11.1. Very little of the old appware has been  converted because this would be a waste of resources. Cadcentre policy on converting  appware to use new facilities is to only upgrade appware when modifications are being  made in that area. The only exception to this, is where new facilities in core code supersede or improve  functionality and it is necessary to write new code to use the extra functionality.

New Environment Variables PMLLIB

/DIRECTORYNAME

This is a directory which is searched when the system looks for functions and forms 

PMLTRACE

ON/OFF

When set to ON, PMLTRACE will display a trace of all macros called in the unix  command shell. This can also be switched on or off on the command line, so it is  probably better to set the variable to off and use commands to control trace as before. The command in PDMS is ­ PML TRACE ON/OFF PMLCOMPILE ON/OFF Environment variable PMLDEBUG ON will write a trace file

Available variable types

2

$v1 !a

old style not recommended Named variables both global and local !!global !local

NOTE:  !a is evaluated as text automatically by using $!A (early evaluation) Named variables are now typed variables as one of the following STRING REAL

‘Text in quotes or vertical bars up to a Max of 254? char’ Any number

BOOLEAN ARRAY

TRUE,FALSE These are not text i.e. ‘TRUE’,’FALSE’ Any type including arrays of arrays of anything

OBJECTS

A collection of different variables under a user defined type

Variables can be predefined as a particular type or revert to type when they are set There are two ways of setting variables Var !a (25) 

!a is a string

!a = 25

!a is a real

!a = ‘xyz’

!a is a string

!a[1] = 75

!a[1] is a real element of array !a  Note: other elements in the same array can be of any type

!a = TRUE

!a is BOOLEAN

Beware Pre version 11 the var command was used to set all variables and it produced a  string. Now we don’t use var apart from collections. Variables may also be created before they are used using Methods !!Answer = real() !textline = String() !test = boolean() !squares = array() All of the above have the value ‘unset’ 

3

There are various ways of testing whether a variable exists or is set: If(undefined(!x))then If(defined(!x))then If(unset(!x))then If(!x.unset())then If(set(!x))then If(!x.set())then To make a variable undefined i.e. empty it. !x.delete() var !x delete 

Arrays arrays may be sparse i.e. you can set in index order  Negative subscripts are not permitted A subscript of zero can be used but is not advised do loop from and to variables can now include expressions and even functions

Array methods !arraysize = !arraything.size()  

Replaces var !arraysisze (arraysize(!arraything))

Sets the variable arraysize to the number of elements in the array !testarray.clear()

Deletes all elements from !testarray

!test.removefrom(5, 20)

Removes 20 elements from element number 5

!new = !test.removefrom(5,20)

Removes 20 elements as above and creates a new  array with the 20 elements inside.

!myarray.append(!newdata)

acts like var !array append ‘fhjffj’

!myarray.[27].delete()

Deletes element 27 of the array the array still exists 4

!myarray.delete()

Deletes the array entirely

!line = ‘   this is a line of          text   with    some funny spaces ‘ !line = !line.trim(‘lrm’)  trims the spaces from the variable called !line !line = ‘   this is a line of          text   with    some funny spaces ‘ !lines = !line.split(!line)  Splits the string !line into an array of strings called !lines. Note: we could not use the same variable name because it is a different type !a = !lines.size()  !a = !lines.width()

gives the number of elements in the array gives the number of characters in the longest word of the array

No result methods. !lines.sort()  !lines.invert()

sorts the array !lines and returns the array in its sorted form inverts the array order first to last etc. These methods work only  on the original array. Attempts to set another array results in an  error.

i.e. !new = !lines.sort() Gives an error  The old method. !ind = !lines.sortedindices() Produces a sorted index without changing the original  array. This can be used to sort parallel arrays in the same way as  before. !lines = !lines.relindex(!ind)

Re­orders an array according to the order of array !ind If applied to the original array, it would have the same  effect as a simple sort, its use is to sort parallel arrays.

The Current Element variable

5

There is one global variable which is always set by the system to be the element you are  positioned at in the database ­ this is called !!CE. !!CE is an object of type DBREF. Its  attributes are described as members of the object.   e.g. !tem = !!ce 1 q var !tem

$* creates a variable which is a copy of !!ce $* Queries the attributes of the DBREF pointed to by !item 

 =8196/8      NAME   ­> <STRING>  ‘DA/2000’      TYPE    ­> <STRING>  ‘SITE’      LOCK   ­>   FALSE      OWNER   ­>   =8196/0      DESC   ­> <STRING>  ‘MAIN REACTOR AREA’      FUNC   ­> <STRING>  ‘’      PURP   ­> <STRING>  ‘’      NUMB   ­>   0      AREA   ­>   0       POS   ­>   E 0mm N 0mm U 0mm WRT /*      ORI   ­>   Y is N AND Z is U WRT /*      MODU   ­> <STRING>  ‘’      ORRF   ­>   Unset      STMF   ­>   Unset      :MATERIAL    ­> <STRING>  ‘Carbon Steel’ q var !item  /DA/2000

$* gives $* without all of the other stuff

!item

$* as a command, does nothing

$!item 

$* navigates to /DA/2000

Querying attributes q var !item.pos

$* gives

6

 E  0mm  N  0mm  U  0mm WRT /*        ORIGIN  =8196/0        UP  0        EAST  0        NORTH  0 q var !item.e  0

$*query the east position

q var !item.pos.origin  q var !item.pos.origin.lock 

$* gives all of the attributes of =8196/0  $* gives the lock of =8196/0

q var !item.pos.wrt(=8196/32) 

$* uses the method WRT(DBREF) to extract a  $*position relative to some other database  $*reference

METHODS/FUNCTIONS Different data types in pml have associated built in ‘methods’ which may be used to  convert or read data in a particular way. Some methods are similar to the built in  functions pre version 11.1 as in the example below: !NCHARS = !MYSTRING.LENGTH()

$* THIS IS A METHOD

!NCHARS = LENGTH(!MYSTRING)

$* THIS IS A FUNCTION CALL

The pre version 11.1 syntax for the function call would have been as follows: VAR !NCHARS LENGTH(|$!MYSTRING|)

$* THIS IS A 10.5 FUNCTION CALL

The difference between the 10.5 example and the 11.1 example is that version 11 now  returns a typed variable (REAL) where version 10.5 would have returned a string. Methods are built in for various data types and are listed in appendix A of the  Customisation guide.

7

Block evaluation of arrays Block evaluation is a method of stepping through each element of an array and  performing some action. In effect, it is like a single command do loop. To perform block evaluation, there are two stages: First you need to define the expression which will operate on the array elements  Second, you need to apply the expression to an array The expression is stored in a variable and is defined as follows: !EXPRESSIONVAR = OBJECT BLOCK (‘!!ARRAYNAME[!EVALINDEX] * 2’) the !!arrayname[!evalindex] bit is effectively the name of the variable being operated  on by the expression. In the example above, the result would be to multiply all of the  numbers in the input array by 2. The variable !evalindex is a special variable used in  block evaluation. Any other variable name will not work!  To apply the expression is as follows: !!newarray = !!arrayname.evaluate(!expressionvar) This creates an array called !!newarray containing the numbers of the original array  multiplied by 2. The original array can also be modified using this method: !!arrayname = !!arrayname.evaluate(!expressionvar) Here are some simple examples  !text = 'this is a line of text' !myarray = !text.split() This creates an array called !!myarray with six elements !myarray[1] = ‘this’ !myarray[2] = ‘is’ !myarray[3] = ‘a’ !myarray[4] = ‘line’

8

!myarray[5] = ‘of’ !myarray[6] = ‘text’ Now we can define some blocks !extractchar1 = object block ('subs(!myarray[!evalindex],1,1)') !new = !myarray.evaluate(!extractchar1) This extracts the first character of each word into and array called !new giving the following: !new[1] = ‘t’ !new[2] = ‘i’ !new[3] = ‘a’ !new[4] = ‘l’ !new[5] = ‘o’ !new[6] = ‘t’ !extractchar1 = object block ('upcase(subs(!myarray[!evalindex],1,1))') !new = ! myarray.evaluate(!!extractchar1) This does the same as before, but also changes the letters to upper case !new[1] = ‘T’ !new[2] = ‘I’ !new[3] = ‘A’  !new[4] = ‘L’ !new[5] = ‘O’ !new[6] = ‘T’ !extractchar1 = object block ('upcase(subs(!myarray[!evalindex],1,1)) & |fred|') !new = !myarray.evaluate(!extractchar1) This does as before, but uses the & character to append the string fred to the result !new[1] = ‘Tfred’ !new[2] = ‘Ifred’ !new[3] = ‘Afred’ !new[4] = ‘Lfred’

9

!new[5] = ‘Ofred’ !new[6] = ‘Tfred’ !extractchar1 = object block ('!!testfunc(!myarray[!evalindex])') !myarray.evaluate(!extractchar1) This example passes the evaluation into a function called !!testfunc, resulting in the  function being run on each element of the array. More on functions later! 

10

Objects define object COMPONENT     member .detail is string     member .material is string     member .bore is real     member .position is position endobject To use this object !x  = object COMPONENT() !x.detail = !!ce.dtxr !x.material = !!ce.mtxx !x.bore = !!ce.pbor1 !.pos = !ce.pos

External Functions The new environment variable PMLLIB points to an area where both forms and  functions(macros) can be dynamically loaded according to name. PML functions are  macros which may or may not return a value. They must be named with a suffix of  “.pmlfrm” , “pmlobj” & “pmlfnc” and they are called as follows: Officially: call !!functionname() Unofficially: !!functionname() e.g a function definition define function !!dir_to_next(!comp is dbref)     $!comp          ­­ go to component passed     dir to next     next      if (!!ce.type eq ‘ELBO’)then 11

     back      dir to next    endif    next forw return

To run this function on the current element, type: !!dir_to_next or  call !!dir_to_next This would result in the system searching for a file called “dir_to_next.pmlfnc” and if  found, running it as a macro. To get all of the text of the selected elements in a list would require a simple function as  follows: ­­ Function to get all selected text items in a list gadget ­­ Pass the name of the list gadget is the input parameter define function !!textlist(!input is gadget)is array !selected = !input.val !expression = object block(‘!input.rtext[!selected[!evalindex]]’) !x = !selected.evaluate(!expression) return !x endfunction to run this !!text = !!textlist(!!myform.members)

Alert Views Alert views are slightly different to pre version 11. They are treated as functions and  called in the same way

12

!!Alert.Error(‘This is an error message’) !!Alert.warning(‘This is a warning message’) !!Alert.Message(‘This is a message’) !answer = !!Alert.confirm(‘Do you want to savework’) This returns ‘YES’ or ‘NO’ !answer = !!alert.question(‘ok to delete world’) This returns ‘YES’  ‘NO’ or ‘CANCEL’

Forms and Menus setup form !!myform     Paragraph .message text ‘Press goodbye button to close’     button .goodbye ok exit !!myform.show() by convention, all form definition macros have the extension .frm to load the form type $m/myform.frm In the above example the button has no display text. In fact the display text is taken from  the name and made upper case. The method !!myform.show() is the equivalent of show _myform  Querying the form q var !!myform would give something like the following:

13

 MYFORM       GOODBYE         MESSAGE   Press goodbye button to close       AUTOCALL  ­> <STRING> ‘ ‘       CALLBACK  ­> <STRING> ‘ ‘       USERDATA        FORMTITLE  ­> <STRING> ‘ ‘    ICONTITLE  ­> <STRING> ‘ ‘    FORMREVISION  ­> <STRING> ‘ ‘    KEYBOARDFOCUS  ­>  Unset The important form properties are : AUTOCALL What is autocall? CALLBACK  This is in fact the initialisation call It can be set as follows !!MYFORM.CALLBACK = ‘$$P ABOUT TO SHOW MYFORM’ or as part of the original macro INIT  ‘$$P ABOUT TO SHOW MYFORM’

USERDATA Userdata seems to work as before  i.e.  VAR USER _MYFORM ADD |FRED|  Adds the text FRED to the userdata area of the form NOTE: userdata is sort of superseded by new variables called form members which will  be discussed later

14

FORMTITLE FORMTITLE is a text string which is shown across the top border of the form it can be set in two ways as before: as part of the form definition TITLE | My Demonstration Form| Or as a change to an existing form !!MYFORM.FORMTITLE = | My Demonstration Form| ICONTITLE This is the text displayed on the forms icon. It is set in the same way as above but the  keyword ICONTITLE replaces TITLE and FORMTITLE in the syntax FORMREVISION this is an optional revision text which again is set in a similar way KEYBOARDFOCUS Keyboardfocus is meant to set the focus point to one of the form gadgets so that the  user is positioned in the right place when a form is shown. The syntax is as follows: !!myform.keyboardfocus = !!myform.textpane !!myform.show If the form is visible on the screen, the focus will not change Currently, Focus only applies to text input gadgets, textpanes and alpha displays

15

General Form features Only one OK button allowed per form Pressing OK on a parent form actions the OK gadgets of all child forms (youngest first) Pressing Cancel on a parent form effectively presses cancel on all child forms as well. In both cases the parent and child forms are removed from the screen This “Form Family”  effect does not apply to RESET and APPLY buttons Each form has a show/hide method associated with it. these are slightly enhanced on  what went before. !!MYFORM.SHOW()  shows the form in its default state !!MYFORM.SHOW(‘AT’,0.5,0.5)

positions the form origin half way across the  screen and half way down

!!MYFORM.SHOW(‘CEN’,0.5,0.5)

positions the form centre point half way across the  screen and half way down

If a form is shown by some other form, it joins the form family of the parent. Exceptions  to this are: The form is shown in an explicit position The form is shown by an OK or CANCEL gadget The parent form is a FREE form To show a form as a FREE form use !!MYFORM.SHOW(‘FREE’) Forms can still be setup as blocking or resizable using: SETUP FORM !!MYFORM BLOCKING SETUP FORM !!MYFORM RESIZABLE

16

to hide a form use: !!MYFORM.HIDE()

Gadgets Gadgets are stored in a similar way to forms. The command: Q VAR !!MYFORM.MESSAGE results in the following:  Press goodbye button to close VAL   ­> <STRING> ‘Press goodbye button to close’ ACTIVE   ­>  TRUE CALLBACK   ­> <STRING> ‘’ BACKGROUND   ­>   255 All of these values can be changed by a function or macro although on this gadget,  callback seems to be a little pointless. To set the variables is as follows: !!MYFORM.MESSAGE.VAL = ‘This is a different piece of text’ sets the text part ­ using the old syntax is as follows: var !! MYFORM.MESSAGE ‘This is a different piece of text’ !!MYFORM.MESSAGE.ACTIVE = FALSE Makes the gadget inactive (greys it out) !!MYFORM.MESSAGE.BACKGROUND = 22 17

Sets the text background colour to colour number 22 (RED). The text changes colour to  be a suitable contrast if necessary

18

Members of forms Instead of assigning userdata to forms, it is now possible to set a series of dedicated  form variables for use by the appware writer. These can be any variable type including  arrays. To define form member variables is as follows: SETUP FORM !!MYFORM  .  . MEMBER .MYNUMBER IS REAL MEMBER .MYTEXT IS STRING MEMBER .MYARRAY IS ARRAY MEMBER .MYOBJECT IS POSITION  .  . EXIT  These variables can be used and set in exactly the same way as any other global  variables.  !!MYFORM.MYARRAY.APPEND(‘THIS IS SOME TEXT’) !!MYFORM.MYARRAY.APPEND(53) !!MYFORM.MYARRAY.APPEND(TRUE)

!!MYFORM.MYOBJECT = !!CE.POS To query what the form contains: Q VAR !!MYFORM.MEMBERS() 

Paragraph Gadgets examples PARAGRAPH .MESSAGE TEXT ‘This is some text’ PARAGRAPH .MESSAGE  AT X2 Y2 TEXT ‘Optional starting text’ WIDTH 16 LINES 2 PARAGRAPH .MESSAGE TEXT AT X2 Y2 BACKGROUND 22 TEXT  ‘This is some text’ 19

PARAGRAPH .MESSAGE TEXT PIXMAP /FILENAME PARAGRAPH .MESSAGE TEXT PIXMAP /FILENAME WIDTH 256 ASPECT 2

Button Gadgets Button gadgets are defined as follows: BUTTON .BUTTON1 ‘SHOW DATA’ FORM !!DATAFORM This is a simple button which shows a form when pressed BUTTON .OK  ‘OK’  AT X2 Y3 CALBACK ‘!!MYOKCALLBACK()’  OK This is a button which runs a function when pressed. It is also an OK button which  means the form will automatically be removed from the screen when the button is  pressed. There are 5 types of button which have different effects on how the button behaves: OK CANCEL

As described above Removes the form from the screen and resets the form variables to the  state when the form was shown or the status after the last APPLY.  APPLY This activates the callback, but the form is not hidden RESET This works like CANCEL but the form is not hidden HELP Invokes form specific help

BUTTON .BUTTON2 AT Y2 X4 PIXMAP /FILENAME FORM !!DATAFORM This is a button which contains a single pixmap view. In fact, a button may have up to  three pixmaps associated with it. These represent three possibilities: The button is in it’s normal state The button is pressed 20

The button is inactive(greyed out) The syntax for defining these is as follows: BUTTON .BUTTONNAME PIXMAP UNSELECTED /PIX1 WIDTH 26 HEIGHT 26  SELECTED /PIX2 INACTIVE /PIX3 CALLBACK ‘CALLBACK TEXT’

Frame Gadgets

Frame gadgets are new gadgets which produce an enclosing frame around other  gadgets. They are used to group gadgets together and are described by a title text in the  top left corner.

They are defined as follows: frame .framename ‘Title’ at x2 y4 width 10 height      gadget1      gadget2 exit Gadgets within a frame are positioned  relative to the frame origin. If you wanted to put in a four line paragraph gadget, specifying the text during definition  has the effect of resizing the frame to suit the text. To get a paragraph gadget to fit into  the frame, the text must be specified afterwards.

Toggle Gadgets Toggle gadgets are now very simple. They have two states true or false and are treated  as a simple boolean variable. TOGGLE .INSULATION ‘INSULATION’ TOGGLE .GRID PIXMAP /FILENAME WIDTH 64 HEIGHT 64 CALLBACK ‘  ‘ to set toggle gadgets 21

!!MYFORM.INSULATION.VAL = TRUE to use IF(!!MYFORM.INSULATION.VAL)THEN .. ..

Rgroup Gadgets Rgroup gadgets are a new type of radio group. It is defined as follows: RGROUP .STYLE ‘STYLE’  AT X2 Y3 CALLBACK ‘ ‘ VERTICAL    ADD TAG ‘Bold Text’ SELECT  ‘BOLD’    ADD TAG ‘Italic Text’ SELECT ‘ITALIC’    ADD TAG ‘Underline’ SELECT ‘UNDERLINE’ EXIT When the group appears, item one is selected. The text in the RGROUP line is the title of the rgroup To select any other item it is done by number !!MYFORM.STYLE.VAL = 2 This selects item 2 in the group Two other methods are available to operate on rgroup gadgets !STYLETEXT = !!MYFORM.STYLE.SELECTION() This gets the text of the gadget ­ in this case ‘BOLD’ !!MYFORM.STYLE.SELECTI(‘UNDERLINE’) This sets the toggle by selecting the toggle with the select text equal to ‘UNDERLINE’ Querying: 22

Q VAR !!MYFORM.STYLE  BOLD      VAL   ­>  1      ACTIVE   ­>  TRUE      CALLBACK    ­> <STRING>  ‘ ‘ $P $!!MYFORM.STYLE  BOLD

Option Gadgets Option Gadgets are compact lists which look like buttons. they have two sets of text: Display text which what appears on the gadget and replacement text which is stored in  the background.  OPTION .COLOPT ‘Text Colour’ AT X6 Y6 CALL ‘!!function()’ Setting the options is simply an array assignment using a method. !!MYFORM.COLOPT.DTEX = !COLARRAY  !!MYFORM.COLOPT.DTEX = !NUMARRAY  Both arrays must be STRINGS Options can also use pixmap views whose file names are inserted into the display text of  the option. All of the pixmap views must be the same size and the width and height must  be part of the declaration. OPTION .COLOPT ‘Text Colour’ CALL ‘ ‘ PIXMAP WIDTH 64 HEIGHT 64

List Gadgets

23

List Gadgets are either single or multiple choice gadgets which again use display and  replacement texts like option gadgets. The list area is defined as an area on a form but  the text in a list can be very large.

LIST .MEMBERS AT X5 Y6 MULTIPLE WIDTH 20 LENGTH 6 LIST .MEMBERS AT X5 Y6 SINGLE WIDTH 20 LENGTH 6 Assigning values to a list is a matter of setting the dtext and rtext fields as in options !!MYFORM.MEMBERS.DTEXT = !VARNAME !!MYFORM.MEMBERS.RTEXT = !OTHERVARNAME Note: Setting the DTEXT only causes the RTEXT to have the same value.

List Gadget Manipulation There are a number of ways of extracting and manipulating the data from a list: To get the list positions of any currently selected elements !SELECTED =  !!MYFORM.MEMBERS.VAL For a multiple form, !SELECTED would be an array of real. For a single choice list it  would be a single real. to get all of the text of the selected elements in a list would require a simple command  as follows: !NEWARRAY = !!MYFORM.MEMBERS.SELECTION

To get all of the text of a list: !NEWARRAY = !!MYFORM.MEMBERS.RTEXT copies all of the replacement text into !newarray

24

!NEWARRAY = !!MYFORM.MEMBERS.DTEXT does the same for the display text to clear all selections  !!MYFORM.MEMBERS.CLEARSELECTION() to select items in the list

!!MYFORM.MEMBERS.SELECT(‘DTEXT’,’/FRED’) Note: There are no facilities to match just part of the string. to select all items in a list  !!MYFORM.MEMBERS.SELECT(‘DTEXT’,!!MYFORM.MEMBERS.DTEXT) to clear selections !!MYFORM.CLEARSELECT() to clear the list entirely !!MYFORM.CLEAR()

Text Input Gadgets Text input gadgets allow text to be entered into a variable. The width of the text is  described by the width command the maximum text size is determined by the scroll  command to create a text gadget TEXT .MYTEXT AT X2 Y 2 ‘NAME’ CALLBACK |  | WIDTH 10 SCROLL 20 IS STRING The types of variable now available are:   IS STRING 25

IS REAL IS BOOLEAN IS word ? to maintain compatibility with old PML ISOUNIT NAME NUMERIC INTEGER STRING ISOBORE To set an initial value  !!myform.mytext.val = |insert text here| T0 set focus !!myform.mytext.focus() NOTE: it is only possible to set the focus to text gadgets To enable the entry of passwords into a gadget, it is possible to set NOECHO on a text input gadget so that the input is not shown to the screen. It is also possible to set the format of a gadget to be in a format which can be defined by  a FORMAT object.  TEXT .MYTEXT AT X2 Y 2 ‘NAME’ WIDTH 10 FORMAT IS !!FORMATBORE Where !!FORMATBORE is a FORMAT object These are defined as follows: !!FORMATBORE = FORMAT() !!FORMATBORE.UNITS = ‘FINCH’ !!FORMATBORE.FRACTION = TRUE !!FORMATBORE.DENOMINATOR = 16

26

Textpanes Textpanes are like editable notepads the occupy an area on the screen and can have  text associated with them. TEXTPANES HAVE NO CALLBACK they are defined as follows: TEXTPANE .TEXTP AT X3 Y4 WIDT 20 HEIGHT 5  You can add text by typing it in or it can be inserted from an array as follows: !!MYFORM.TEXTP = !ARRAYNAME  There are methods for making the text editable or not  !!MYFORM.TEXTP.SETEDITABLE(TRUE) !!MYFORM.TEXTP.SETEDITABLE(FALSE)

27

Related Documents

Trainer Notes
May 2020 6
Trainer Presentation
June 2020 9
Trainer Packet
November 2019 22
Glp Trainer
December 2019 12
Trainer Battles.docx
June 2020 8
Trainer Basics
June 2020 6