Jump to content
Server Maintenance This Week. ×

Recommended Posts

Door 1 - Barcodes

Fact of the day
Did you know that you can generate over 80 different barcode types with MBS?

Welcome to the first door of our advent calendar. In this advent calendar I will introduce you to various components of the MBS FileMaker Plugin and show you how to use them. Today it's all about barcodes. Did you know that you can create over 80 different barcode types with the MBS FileMaker Plugin? How this works I will show you today in a project in which we create a WIFI QR code. If your guests get bored over the Christmas period, they can simply scan this QR code with their smartphone to gain access to your WiFi.

First of all, let's consider the question of how such a WiFi QR code is actually structured. The information you need for the QR code is the SSID, In other words the network name, the access password, the type of WiFi encryption and the visibility. For this test, let's imagine we have a WLAN with the following data:

SSID: MyWIFI 
Password: 123456789
Encryption WPA 2
Visibility: Yes

First of all, we have to specify in the QR code that it is a WIFI QR code, so we first enter the service tag WIFI. This way, many QR code scanners know what to do with the information. Now the information about our network follows. The individual information is provided with an associated key so that you know what information it is. The key and the corresponding value are separated from each other with a colon. The individual values are separated from each other with a semicolon. The QR code type is followed by the used encryption protocol. The key to the encryption protocol is T, followed by the information on the protocol. For the protocols, we have the choice between WPA, WEP and nopass. We use WPA in our example. This is followed by the key S for the SSID and the corresponding value. If the WLAN requires a password, this is specified with the P key. If there is no password, the value remains empty. Now we need to specify the visibility of the network. Here we can only choose between the values true and false. If it is hidden then our value is true, if it is visible then the value is false. The corresponding key is H. If the network is visible, we can also omit the value here.

For our example network, this would be the content of the QR code:
WIFI:T:WPA;S:MyWIFI;P:123456789;H:; 

In our example, we have a field for each of the entries and a container in which the image of the barcode should be displayed.

Advent23_D1_B1.png

Now we come to our script with which we create the Wifi code. We have two options for this. We can use the Barcode.Generate function and pass it the constructed string. We can also define further properties for the QR code in the other parameters, but more on that in a moment. Or we can use the Barcode.GenerateJSON function. We pass it a JSON with our settings. In this example, we choose the Barcode.Generate function. 
First, we assemble the string.

Set Variable [ $Text ; Value: "WIFI:T:" & DoorOne::Encryption & "; 
S:" & DoorOne::SSID & ";P:" & DoorOne::Password & ";H: "& DoorOne::Hidden &";" ] 

We want to create a QR Code with a high ECC level. We can select an ECC of 1-4. A high ECC level helps us to recognize a barcode better, even if parts of the code are missing, because the information in a QR code appears several times. The higher the ECC level, the more extensive the QR code is, but the higher the chances of it being recognized. For a QR code, we need to assign the value 4 to option1.

 Set Variable [ $r ; Value: MBS( "Barcode.SetOption"; 1; 4) ] 
 

Now we call the Barcode.Generate function. In the first parameter we specify what type of barcode it is, in our case a QR code. This is followed by the content of our QR code, in this case our string. Optionally, we can now specify the size of the QR code, if we do not want to define fixed values for height and width, we enter a 0 in each case. Now we can decide whether we want to rotate our QR code, which we do not want to do, so we also enter a 0 here. The next parameter is interesting for us again, because this is about the scaling. This should be at least 4 for a QR code that may later be printed. In our example, we are finished with the parameters, but you can also specify in your own projects whether, for example, the text under the barcode should be displayed for an EAN barcode and whether we want an encoding that differs from the standard UTF-8 encoding.

 Set Variable [ $QR_Ref ; Value: MBS("Barcode.Generate"; "QRCode"; $Text; 0; 0; 0; 4) ] 
 

However, this function does not yet return the image of our barcode, but a GMImage reference. We could then use GMImage references to edit the image further. However, we do not want to do this today and for this reason we now write the barcode to the container with GMImage.WriteToPNGContainer. We then have to release the resulting reference.

Set Field [ DoorOne::Barcode ; MBS("GMImage.WriteToPNGContainer"; $QR_Ref; "barcode.png") ] 
Set Variable [ $r ; Value: MBS("GMImage.FreeAll") ]
 
Advent23_D1_B2.png

Now you can happily share your WIFI with your friends and family. I wish you lots of fun.

Link to comment
Share on other sites

Door 2 - Vision

Fact of the day
The MBS FileMaker Plugin not only offers you the Apple Vision as a framework, using MapKit you can also integrate Apple maps into your application or take pictures with your iPhone via Continuty Camera and store them directly in FileMaker.

The second door provides something for Mac users. Apple provides a framework called Vision. With this framework you can recognize barcodes, recognize texts from images or classify images into categories with the help of machine learning, e.g. so that you can search for them in your image database. This framework makes MBS usable with the plugin for FileMaker.

Barcode recognition

Let's first look at the possibility of recognizing multiple barcodes on an image. You simply pass the image to the Vision.DetectBarcodefunction as a parameter and Vision then searches for the barcodes on the image and returns a JSON in which the barcodes are listed. If you only want to search for QR codes on the image, for example, you can restrict the barcode types in the search by specifying the desired types as parameters. You can find out which barcode types can be recognized with Vision on your computer using the Vision.SupportedSymbologies function.

Advent23_D2_B1.png
Result from Vision.DetectBarcode

Text recognition 

We now have two possible returns for text recognition. If we use the Vision.RecognizeText function, we get plain text as a return, which we can then output in a field, for example. The other return option is a JSON. This JSON not only contains the plain text, but also the position of the individual lines. This has the advantage that you can, for example, make a page out of the image with DynaPDF and place the text behind it so that the text can be marked.

In the example, we have created both options as buttons. Otherwise, the parameters that we have to pass to the two text recognition functions are identical. First we pass the image from which the text should be recognized. This is sufficient for now, the other parameters are optional. Here we can specify the type of recognition to be performed. We can choose between Fast and Accurate. Accurate uses neurological networks for word recognition. Fast recognizes the individual characters. Fast also works well if the desired language is not available. Next, we can specify the language of the text to be recognized. The Vision.SupportedRecognitionLanguages function tells you which languages can be recognized. Then you can specify a list of words that may occur in the text but are not in the dictionary so that they can be recognized more easily. The last parameter PageLimit is interesting for PDF documents, because with Vision you can use text recognition not only in images, but also in PDF documents. With the last parameter, we decide how many pages the text recognition should run over. If we do not specify anything, only the text recognition for the first page is performed automatically.

Here you can see the code from our example for the JSON function. We did not need the last two parameters in our example:

Set Variable [ $JSON ; Value: MBS("Vision.RecognizeTextJSON"; DoorTwo::Input_Image;"Accurate"; "en_US") ] 
Set Field [ DoorTwo::Output ; MBS("JSON.Colorize"; MBS("JSON.Format"; $JSON)) ] 
Advent23_D2_B2.png
Result from Vision.RecognizeText
Advent23_D2_B3.png
Result from Vision.RecognizeTextJSON

Classification of images 

Advent23_D2_B4.png

Vision also offers the option of automatically classifying images, which can help you, for example, if you have a photo library and you want to search for images from a specific category. You can then enter and use this classification in a field. Vision.ClassifyImage provides us with a result for this.

Set Variable [ $Content ; Value: 
   MBS( "Vision.ClassifyImage";DoorTwo::Input_Image; 0 ) ] 

Here again, we first pass the image. As an optional parameter, we can now specify whether we only want to get back the most suitable category (the default) or whether we want to receive a JSON with all suitable categories. The JSON also contains the probability with which the image fits into this category.

Now you can get more out of your images. We hope you enjoy using these functions.

 

Link to comment
Share on other sites

Door 3 - Binary File

Fact of the day
You may also be interested in the Text component. Because you can also create a text file with this component.

Behind door 3 is a component that is mostly unseen, but is used in many applications by customers and has become essential. It is the BinaryFile component. You can use it to read binary data from a file and also write it to a file. 

But what is it actually good for? Although the texts are saved as binary data, you can also create normal text files with this component and read them out again later. For example, you can create a log file yourself in which you can write information. We would like to do this now by logging in the file when a certain script has been called and what result it has. The aim is to have as less code as possible in the script that we want to log so that the script doesn't become confusing. For this reason, we use the Perform Script script step and call a script that writes the desired text to the log file. We pass the text that we want to write to the log file as a parameter. In this way, for example, the results that were calculated in the script can also be written to the file.

In the script below, a line should be written to the file containing the timestamp, the script name of the calling script and the information that the script was started at this time. The script is then executed. In this script we stop for 2 seconds so that we can also see a change in the timestamp and set the result of the script in the variable $res. Let's call the same script as at the beginning again and pass the text in the parameter again. This again contains the timestamp and script name and this time still returns the result that we have in the calling script.

Perform Script [ Specified: From list ; "Write" ; Parameter: Get ( CurrentTimestamp ) & "-" & Get(ScriptName) & "-Start" ]

Pause/Resume Script [ Duration (seconds): 2 ] 
Set Variable [ $res ; Value: 1 ] 

Perform Script [ Specified: From list ; "Write" ; Parameter: Get ( CurrentTimestamp ) & "-" & Get(ScriptName)  & "-Finish-res:" & $res ]

The Write script which executes the log entries looks like this:

Set Variable [ $Param ; Value: Get(ScriptParameter) ] 
Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; MBS("Folders.UserDesktop"); "MBS_Binary_File.txt") ] 
If [ MBS("Files.FileExists"; $Path) ] 
	# Add Information
	Set Variable [ $ref ; Value: MBS( "BinaryFile.Append"; $Path ) ] 
	Set Variable [ $r ; Value: MBS( "BinaryFile.WriteText"; $ref; "¶" & $Param ; "UTF-8"  ) ] 
	Set Variable [ $r ; Value: MBS( "BinaryFile.Close"; $ref ) ] 
Else
	# Create one
	Set Variable [ $ref ; Value: MBS( "BinaryFile.Create"; $Path ) ] 
	Set Variable [ $r ; Value: MBS("BinaryFile.WriteText"; $ref; $Param;"UTF-8") ] 
	Set Variable [ $r ; Value: MBS( "BinaryFile.Close"; $ref ) ] 
	
End If

First we save the text from the passed parameter. Next, we assemble the path where the file is located. More about this in a later door ;-). The path now leads to a file with the name MBS_Binary_File.txt which is located on the desktop. Now we have to make our procedure dependent on whether the file already exists or still has to be created. We check this with the Files.FileExists function. 

If the file already exists, we use the "BinaryFile.Append" function to open the file. It returns a reference to this file. The cursor is at the last position of the document. We now call BinaryFile.WriteText and first pass the document reference and the text to be written to the file. We place a line break in front of the text so that each new entry is on a new line. We can also specify the encoding in the optional parameter. By default this is UTF-8 but can be changed to ANSI, ISO-8859-1, Latin1, Mac, Native, DOS, Hex, Base64 or Windows. When we are finished with the entry, we call the function "BinaryFile.Close", which closes the file again.

It is a little different if we do not yet have a file. In this case, it is first created and opened with the "BinaryFile.Create" function. We receive the reference number again and can then enter the text in the file as usual, with the difference that we do not need the line break here, because we write the first line in the document.

Advent23_D3_B1.png

For example, it is also possible for you to log errors. Here we have a script in which an MBS function is used incorrectly and thus throws an error, the reason being that the sybology ABC does not exist. This error is now entered in the text file.

...
Set Variable [ $r ; Value: MBS("Barcode.Generate"; "ABC"; "xyz") ] 
If [ MBS("IsError") ] 
	Perform Script [ Specified: From list ; "Write" ; Parameter: Get ( CurrentTimestamp ) & "-" & Get(ScriptName)  & "Error: " & $r ]
	Exit Script [ Text Result:] 
End If
...
Advent23_D3_B2.png

We can now not only write normal text to the document, but also a value in hexadecimal, for which we have the BinaryFile.WriteHex function. In the parameters, you can specify a character string coded in hexadecimal, which is then written to the document as text. For example, if we pass the character string 48656C6C6F20576F726C64 to this function, our output will look like this:

Advent23_D3_B3.png

ke to add a line break before the hexadecimal text. We would therefore like to enter a Chr(13) in the document. This can be realized with the function BinaryFile.WriteByte. We write a single byte into the file with this function. We pass the desired character to be written to this function. 

...
	Set Variable [ $r ; Value: MBS("BinaryFile.WriteByte"; $ref; 13) ] 
	Set Variable [ $r ; Value: MBS("BinaryFile.WriteHex"; $ref; $Param) ] 
...

In addition to writing text to a file, you can also write the content of a container to a file. To do this, use the BinaryFile.WriteContainer function. In the parameters, enter the reference to the file and the container of which the content is supposed to be written to the file.

Set Variable [ $Param ; Value: Get(ScriptParameter) ] 
Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; MBS("Folders.UserDesktop"); "MBS_Container.png") ] 
Set Variable [ $ref ; Value: MBS( "BinaryFile.Create"; $Path ) ] 
Set Variable [ $r ; Value: MBS("BinaryFile.WriteContainer"; $ref; DoorThree::Container) ] 
Set Variable [ $r ; Value: MBS( "BinaryFile.Close"; $ref ) ] 

Now, of course, we don't just want to write data to a file, we also want to be able to read it. We have the "BinaryFile.ReadText" function for this purpose. This reads out as many letters as we specify in the parameters, starting from our current position. As we do not want to change the file in this case, we open it with the "BinaryFile.Open" function.

Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; MBS("Folders.UserDesktop"); "MBS_Binary_File.txt") ] 
Set Variable [ $ref ; Value: MBS("BinaryFile.Open"; $Path) ] 
Set Variable [ $r ; Value: MBS("BinaryFile.Seek"; $ref; DoorThree::Start) ] 
Set Field [ DoorThree::Text ; MBS( "BinaryFile.ReadText"; $ref; DoorThree::Length; "UTF-8" ) ] 
Set Variable [ $r ; Value: MBS("BinaryFile.Close"; $ref) ] 

In our example, you have the option of defining the start position and the readout length in two fields. We can change the current position with the BinaryFile.Seek function. If you want to determine what the current position is, use the BinaryFile.Position function. If you want the length of the entire document, call BinaryFile.Length. 

But here, too, we not only have the option of reading out normal text, but we can also read out hexadecimal values, for example. To do this, we use the BinaryFile.ReadHex function instead of the BinaryFile.ReadText function.

Advent23_D3_B4.png

In the image you can see that the first two characters in this document are a 0 and a 3 which have been output in hexadecimal. 

I hope you enjoyed this excursion into the world of binary files and I'll see you again tomorrow.

Link to comment
Share on other sites

Door 4 - Speech

Fact of the day
A voice output can not only make reading easier, but also creates freedom for people with physical disabilities.

In this door, you have the opportunity to ensure that your solution never has to be at a loss for words again. Today we are teaching your application to speak, because MBS offers speech output for Windows, Mac and iOS with the Component Speech.

Advent23_D4_B1.png

If we want to output a text, we can use the Speech.Speak function. We first pass the text to be read out in the parameters. That's enough for now. Optionally, we can also choose from various speakers. You can find out which speakers are available with the Speech.AvailableVoices function. This function provides you with a list of all speakers. You can specify in this function whether you want the speaker names or the ID for the speaker to be displayed in this list.

If you want to use a specific speaker in the Speech.Speak function, you need to enter the ID. In the Speech.Speak function, you can also set whether or not your script should be continued while the speech output is running. Stopping a script also makes sense if you call the Speech.Speak function in the script several times so that the text that follows is also read out and is not lost because another text is being read out. You can also set the volume and playback rate in the function. The volume is between 0.0 (silent) and 1.0 (full). The playback speed ranges between 0.0 (silent) and 2.0 (double speed). The default value for both settings is 1.0.

Set Variable [ $r ; Value: MBS( "Speech.Speak"; DoorFour::Text ; DoorFour::Speaker ; 1 ; 1; .7 ) ]

Of course, there are also functions with which you can stop the speech output. With Speech.Stop, you can stop the current output at the next possible time. The text is then completely interrupted and cannot be continued again. So if you only want to pause the output in order to continue it later, you need the Speech.Pause function. To resume the output, you need the Speech.Resume function. You should pay attention to the Stop and Pause functions: If a script has a Wait instruction from the Speech.Speak function, then of course we remain in the script and it cannot be stopped or the speech output terminated prematurely. 

With just a few functions, you can integrate a very useful functionality into your solution. I wish you a lot of fun with it.

 

Link to comment
Share on other sites

Door 5 - WindowsOCR

Fact of the day
If you want to program a cross-platform solution or need text recognition for older Windows versions, you can use functions of the Tesseract component from the MBS FileMaker Plugin.

In Door 2 we already introduced you to a way of performing text recognition under Mac. But this possibility is not only available for Mac in the MBS FileMaker Plugin, but the MBS FileMaker Plugin also makes it possible to use Windows' own OCR functions. These WindowsOCR functions are available under Windows 10 and 11.

You can test whether you can use the functions under an operating system by running the WindowsOCR.Availablefunction. If you can use the functions, first create a new OCR engine with WindowsOCR.New. This function returns a reference number which you can use in other functions to address the OCR engine. In this function, you can also optionally specify which language is to be recognized by the engine. If you skip this parameter, the language, that you get back with the WindowsOCR.CurrentInputMethodLanguageTag function, is automatically used for language recognition. Which languages can be recognized depends on which languages are installed on your system. You can obtain a list of the languages that you can currently use in your system with WindowsOCR.AvailableRecognizerLanguages.

Show Custom Dialog [ "Supported languages" ; MBS( "WindowsOCR.AvailableRecognizerLanguages" ) ] 
You can now select whether you want to recognize the text from an image from a container or an image file. If the text is on an image from a container, use the WindowsOCR.Recognize function and enter the reference number and the container in which the image is located, in the parameters.
If [ MBS( "WindowsOCR.Available" ) ] 
	Set Variable [ $OCRen ; Value: MBS( "WindowsOCR.New" ; "en-US" ) ] 
	...
	Set Variable [ $r ; Value: MBS( "WindowsOCR.Recognize"; $OCRen; DoorFive::Container ) ] 
...

Alternatively, you can recognize the text of an image in a file. To do this, use the WindowsOCR.RecognizeFile function and enter the file path to the file instead of the container.

...
	Set Variable [ $r ; Value: MBS( "WindowsOCR.RecognizeFile"; $OCRen; DoorFive::Path ) ] 
	...

You can now query the result. Again, you have two options as to how the result can be presented to you. If you only want the plain text, use WindowsOCR.Text and get the text back, which you can then store in a field, for example. If you want information beyond the plain text, you can use the WindowsOCR.Result function. Here you receive a detailed JSON in which the individual lines and the individual words are specified with their position and size.

...
	Set Variable [ $Text ; Value: MBS( "WindowsOCR.Text"; $OCRen ) ] 
	...
	Set Variable [ $JSON ; Value: MBS( "WindowsOCR.Result"; $OCRen ) ] 
	...

Here you can see such a generated JSON from an image:

{
	"Text":	"WindowsOCR: Functions for OCR in Windows 10 or 11.",
	"TextAngle":	0,
	"LineCount":	1,
	"Lines":	[
		{
			"Text":	"WindowsOCR: Functions for OCR in Windows 10 or 11.",
			"WordCount":	9,
			"X":	19,
			"Y":	9,
			"Width":	2139,
			"Height":	61,
			"Words":	[
				{
					"Text":	"WindowsOCR:",
					"X":	19,
					"Y":	9,
					"Width":	538,
					"Height":	61
				}, 
				{
					"Text":	"Functions",
					"X":	604,
					"Y":	11,
					"Width":	364,
					"Height":	59
				}, 
				{
					"Text":	"for",
					"X":	1000,
					"Y":	9,
					"Width":	107,
					"Height":	61
				}, 
				{
					"Text":	"OCR",
					"X":	1137,
					"Y":	10,
					"Width":	167,
					"Height":	60
				}, 
				{
					"Text":	"in",
					"X":	1337,
					"Y":	11,
					"Width":	58,
					"Height":	58
				}, 
				{
					"Text":	"Windows",
					"X":	1432,
					"Y":	9,
					"Width":	343,
					"Height":	61
				}, 
				{
					"Text":	"10",
					"X":	1815,
					"Y":	10,
					"Width":	84,
					"Height":	60
				}, 
				{
					"Text":	"or",
					"X":	1935,
					"Y":	24,
					"Width":	78,
					"Height":	46
				}, 
				{
					"Text":	"11.",
					"X":	2049,
					"Y":	11,
					"Width":	109,
					"Height":	58
				}
			]
		}
	],
	"TextLines":	"WindowsOCR: Functions for OCR in Windows 10 or 11.\r"
}

The degree of text angle can also be found in the JSON. You can also determine this text angle separately with WindowsOCR.TextAngle. This allows you to straighten the text, for example.

...
	Set Variable [ $Angle ; Value: MBS( "WindowsOCR.TextAngle"; $OCRen ) ] 
...
Advent23_D5_B1.png

I hope you enjoyed this article. Have fun recognizing your texts.

Link to comment
Share on other sites

Door 6 - MongoDB

Fact of the day
MongoDB is the backend of Claris Studio, unfortunately you cannot use the advantages of MongoDB directly with Claris Studio, but MBS makes it possible.

Welcome to December 6th. Today is St. Nicholas Day and we also want to take part in filling the Christmas boot with our calendar. Today it contains MongoDB. The special thing about MongoDB is that it is not a relational database based on tables and relationships, but its data has a JSON-like structure. This allows you to perform queries that were previously not possible due to the restriction of relationships or table limits.

Advent23_D6_B1.png

In a MongoDB database we can have several collections. These collections can in turn contain several documents. The documents can be compared with data records, with the difference that different documents in a collection do not have to share the same structure. For example, you can have data in one and the same collection that describes the data for an employee and for a warehouse item. So you can easily find out who is in your company longer: the carpet in the warehouse or your trainee.

MongoDB will certainly be familiar to many of you, because Claris Studio is based on this backend. With the plugin, you can now use this flexibility directly. How to install a MongoDB server and how to use MongoDB with MBS is explained in this video. 

At the end of last year, the possibility to perform transactions in MongoDB databases was added with MBS. This means that you can make changes to the database, which can then all be committed at the same time. This has the advantage that if something goes wrong when changing the database or if the data change process is canceled, the changes can be discarded and we are back in the state in which the database was before the change and the data integrity remains guaranteed. If you are interested, simply take a look at our documentation on the topic.

See also

Link to comment
Share on other sites

Door 7 - Paths

Fact of the day
Did you know that you can easily copy the path of a file under Mac if you press the option key in the menu of the right mouse button at the same time? The menu will then show the entry as Copy...as pathname

Today it's all about how you can build paths in FileMaker with the help of the MBS FileMaker Plugin. We already got a little insight into this topic in the third door, where we created a path to a file on the desktop. The MBS FileMaker Plugin offers you some of these paths to special folders in your system. For example, you can use the Folders.UserDesktop function to determine the path to a user's desktop. Or you can determine the path to the temporary folder with Folders.UserTemporary. In this picture you can see the functions that are available for special folders.

Advent23_D7_B1.png

In most cases, this is not enough as a path specification and we want to address a very specific file in this folder. To do this, we can assemble the path with MBS. For example, we have the function Path.AddPathComponent, which appends a component to the path in accordance with the operating system. Let's assume we want to address a file on the desktop called xyz.pdf, then our script can look like this:

Set Variable [ $Dektop ; Value: MBS("Folders.UserDesktop") ] 
Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; $Dektop; "xyz.pdf") ] 

First we get the path of the desktop with Folders.UserDesktop and append the file name as the last component. 

Advent23_D7_B2.png

If we already have a file path and want to know the last component of this file path, then we use the function Path.LastPathComponent.

Set Variable [ $Last ; Value: MBS("Path.LastPathComponent"; $Path) ] 

It is also possible, for example, if you have a file path from a file and want to write another file in the same folder as this file, then you can also remove the last path component by using the function Path.RemoveLastPathComponent to remove the last component. The functions I have used so far do not always need to have a file name as the last component, but the last component can also be a folder if it is the path to a folder.

In the path component we have further functions that can help you when working with paths. 

If you need the file URL of a file, then you get the file URL back by specifying the path with the function Path.FilePathToFileURL. For the reverse path from a file URL to a path, you can use the Path.FileURLToFilePath function.

Advent23_D7_B3.png

However, we can not only determine the file URL for a file, but can also return the FileMaker path for a native path and the other way round. For each direction we use the function Path.NativePathToFileMakerPath and Path.FileMakerPathToNativePath. 

Advent23_D7_B4.png

We also have something special for Windows users, because in Windows you can have a long form for paths and a short one. So that you can convert from one type to the other within FileMaker as you wish, we have the two functions Path.LongPath and Path.ShortPath. 

I hope you enjoyed this door and we'll see each other tomorrow.

Link to comment
Share on other sites

Door 8 - Determine your own position (for Mac users)

Fact of the day
A man from Munich parked his car in a parking lot in 2015 and was unable to find it again. The car was reported stolen in the meantime. After around 6 months, he was informed by the parking garage company that his car was still in the parking lot. This man should have remembered his location ;-)

Today I would like to show you how you can determine your own position as a Mac or iOS SDK user. The CoreLocation component is available for this purpose. 

If you want to use this option in your apps, we need authorization. We can request various authorizations. The Always authorization requests permission to use location services whenever the app is running. The next weaker authorization is the when in use authorization. It Requests permission to use location services while the app is in the foreground. There is also the temporary full accuracy authorization. This access will expire automatically, but it won't expire while the user is still engaged with your app. In our example, we use the Always authorization. If the authorization fails, please check the settings in Security. You can check whether the authorization was successful using the CoreLocation.authorizationStatus function. It displays the current authorization status. The following responses are possible.

notDetermined The user has not yet made a choice regarding whether this app can use location services.
restricted This app is not authorized to use location services.
denied The user explicitly denied the use of location services for this app or location services are currently disabled in Settings.
authorizedAlways This app is authorized to start location services at any time.
authorizedWhenInUse This app is authorized to start most location services while running in the foreground.

In the example, I will show you how you can deal with a situation where the authorization does not correspond to the desired one.

Set Variable [ $r ; Value: MBS("CoreLocation.requestAlwaysAuthorization") ] 
Set Variable [ $status ; Value: MBS( "CoreLocation.authorizationStatus" ) ] 
If [ $status = "denied" ] 
	Show Custom Dialog [ "No authorization" ; "We do not have an authorization. Take a look to security settings." ] 
	Exit Script [ Text Result:    ] 
End If

If the authorization was successful, we can start querying the current position. The CoreLocation.startUpdatingLocation function is available for this purpose. You can then use the CoreLocation.hasLocation function to test whether a location has been found. Now we can query several pieces of information about the location. First of all, the time at which this position was determined. To do this, we use the CoreLocation.timestampfunction, which, as the name suggests, returns a timestamp. We can also determine the longitude and latitude. We can find out exactly how these are specified using the CoreLocation.horizontalAccuracy function. We get back the radius in meters in which the location is located. 

However, not only longitude and latitude can be determined, but also the height at which we are currently located. This information makes sense on a hike, for example. Use the CoreLocation.altitude function for this. Here, too, there is a function that returns the accuracy of this information (CoreLocation.verticalAccuracy).

 

...
Set Variable [ $r ; Value: MBS( "CoreLocation.startUpdatingLocation" ) ] 
If [ MBS("CoreLocation.hasLocation") ] 
	Set Field [ DoorEight::Time ; MBS("CoreLocation.timestamp") ] 
	Set Variable [ $Lat ; Value: MBS("CoreLocation.latitude") ] 
	Set Field [ DoorEight::Latitude ; $Lat ] 
	Set Variable [ $lon ; Value: MBS("CoreLocation.longitude") ] 
	Set Field [ DoorEight::Longitude ; $lon ] 
	Set Field [ DoorEight::Horizontal accuracy ; MBS("CoreLocation.horizontalAccuracy") ] 
	Set Field [ DoorEight::Altitude ; MBS("CoreLocation.altitude") ] 
	Set Field [ DoorEight::Vertical accuracy ; MBS("CoreLocation.verticalAccuracy") ] 
...

People who are gifted with miracles may be able to determine exactly where they are with the latitude and longitude information, but for most people it would be very useful if we additionally got an address. This is also possible with the CLGeocoder component from the plugin. The geocoder can provide the appropriate coordinates for an address, but also an address for coordinates. We use the CLGeocoder.ReverseGeocodeLocation for this. This function gives us a reference to the GeoCoder, from which we receive a JSON using the CLGeocoder.JSON function.

{
  "location" : {
    "verticalAccuracy" : -1,
    "longitude" : 7.2572950031855843,
    "horizontalAccuracy" : 100,
    "latitude" : 50.545874889063896,
    "timestamp" : "2023-12-03 16:30:29 +0000"
  },
  "region" : {
    "radius" : 70.756292662272614,
    "longitude" : 7.2574633999999998,
    "latitude" : 50.545909899999998,
    "identifier" : "<+50.54590990,+7.25746340> radius 70.76"
  },
  "cancelled" : 0,
  "timeZone" : {
    "abbreviation" : "CET",
    "secondsFromGMT" : 3600,
    "name" : "Europe\/Berlin"
  },
  "address" : "Lindenstraße 4\n53489 Sinzig\nGermany",
  "placemarks" : [
    {
      "ISOcountryCode" : "DE",
      "subThoroughfare" : "4",
      "areasOfInterest" : null,
      "subLocality" : "Sinzig",
      "administrativeArea" : "Rhineland-Palatinate",
      "country" : "Germany",
      "thoroughfare" : "Lindenstraße",
      "ocean" : null,
      "name" : "Lindenstraße 4",
      "postalCode" : "53489",
      "inlandWater" : null,
      "locality" : "Sinzig",
      "subAdministrativeArea" : "Ahrweiler"
    }
  ],
  "key" : "24002",
  "addressString" : null,
  "done" : 1,
  "error" : null,
  "started" : 1
}

We can then read out the address under the key "address". We then release the geocoder reference again by calling the CLGeocoder.Closefunction.

...
	If [ $Lat ≠ "" and $lon ≠ "" ] 
		Set Variable [ $Geo ; Value: MBS("CLGeocoder.ReverseGeocodeLocation"; $Lat; $lon; 1) ] 
		Set Variable [ $GeoJSON ; Value: MBS("CLGeocoder.JSON"; $Geo) ] 
		Set Variable [ $address ; Value: JSONGetElement ( $GeoJSON ; "address") ] 
		Set Variable [ $r ; Value: MBS("CLGeocoder.Close"; $Geo) ] 
		Set Field [ DoorEight::address ; $address ] 
	End If
...
Advent23_D8_B1.png

A location like this can change from time to time if the device we are querying moves. We can use the functions CoreLocation.SetUpdateLocationEvaluate and CoreLocation.SetUpdateLocationHandler to specify an expression or a script that is executed when a new location has been detected. For a moving device, there is also other interesting information, such as the speed or the direction that you can query. If you no longer require new location updates, stop this process with CoreLocation.stopUpdatingLocation. 

I hope this door helped you a little with your orientation. Maybe we'll read each other again tomorrow.

Link to comment
Share on other sites

Door 9 - IBAN

Fact of the day
Saint Lucia has the longest IBAN with 32 characters. This country is not a member of the SEPA area. Within the SEPA area, Malta has the longest IBAN with 31 characters.

Today I would like to introduce you to a component that currently only contains 7 functions, but can be very useful to you if required. We are talking about the IBAN component. The IBAN number is an international number that can be clearly assigned to a specific account. It is intended to contribute to the standardization of global payment transactions. Mainly the EU countries have already implemented this standard, but countries outside of Europe are also adopting this standard. 

Advent23_D9_B1.png

The structure of the IBAN depends on the country from which the IBAN originates. All IBAN numbers start with the country code, which consists of two characters. With the MBS FileMaker Plugin, you can call up a list of these country codes using the IBAN.Countries function.

Advent23_D9_B2.png

The country code is then followed by a check number, which is calculated from the remaining digits. This is to support the correctness of the IBAN. The MBS FileMaker Plugin can also calculate this check number separately. To do this, enter the IBAN in the parameters and enter 00 instead of the unknown check number, which consists of two digits

Set Variable [ $Sum ; Value: MBS( "IBAN.CalcCheckSum"; DoorNine::IBAN) ] 

Advent23_D9_B3.png

This is followed by a sequence of digits that can vary in length from country to country. With such a long number, it is easy to lose track. For this reason, it is practical that the plugin comes with a function that uses spaces to put the IBAN into a clearer form. Use the IBAN.Format function for this

 
Set Variable [ $IBAN ; Value: MBS( "IBAN.Format"; DoorNine::IBAN) ]

Advent23_D9_B5.pngIf we have the IBAN in a formatted form and want to remove the spaces or other characters that are not valid, the IBAN.Compact function is your tool of choice.

 
Set Variable [ $IBAN ; Value: MBS( "IBAN.Compact"; DoorNine::IBAN) ]

Advent23_D9_B4.png

If you would like to find out more about the structure of an IBAN from a specific country, the IBAN.RegEx function provides you with a regular expression that describes the structure by specifying the appropriate country code.

 
Set Variable [ $reg ; Value: MBS("IBAN.RegEx"; DoorNine::Country) ] 
Advent23_D9_B6.png

You can check whether an IBAN has this structure and whether the check digit matches the IBAN using the really practical IBAN.IsValid function. Here too, invalid characters are ignored when entering the IBAN.

 
Set Variable [ $Valid ; Value: MBS( "IBAN.IsValid"; DoorNine::IBAN) ] 
If [ $Valid ] 
	Show Custom Dialog [ "Is the IBAN valid?" ; DoorNine::IBAN & " is a valid IBAN" ] 
Else
	Show Custom Dialog [ "Is the IBAN valid?" ; DoorNine::IBAN & " is not a valid IBAN" ] 
End If

If you would like to save the IBAN in your database, it is recommended that you first use the IBAN.Compact function.

If you would like to experiment a little with the individual IBANs of the countries, you can use IBAN.Example to obtain an example IBAN for the appropriate country.

Set Variable [ $IBAN ; Value: MBS("IBAN.Example"; DoorNine::Country) ] 
If [ MBS("IBAN.IsValid"; $IBAN) ] 
	Set Field [ DoorNine::IBAN ; $IBAN ] 
Else
	Show Custom Dialog [ "Error" ; $IBAN ] 
End If
Advent23_D9_B7.png

I hope you enjoyed this excursion into the world of banking and that you can use some of it in your projects.

Link to comment
Share on other sites

Door 10 - Phidgets

Fact of the day
Did you know that Phidgets are used in schools to get students interested in computer science?

Do you already know the small input/output devices from Phidgets Inc. Phidgets are small additional devices that you can connect to your computer and with which you can then input or output data. For example you can connect a small motor, a temperature sensor, a humidity sensor, a gyroscope or a small LED display and exchange data with these devices. 

Advent23_D10_B3.jpegIn this example, I would like to introduce you to three different Phidgets and how we can read data from these Phidgets in FileMaker. Our three phidgets are a light sensor, a temperature sensor and a slider. In our example, the phidgets are all connected together to a hub. This hub has various outputs to which the individual Phidgets can be connected. This hub is then connected to the computer with a USB cable. All Phidgets also draw their power via this USB cable. It is also possible to purchase a wireless HUB. In this case, a separate power supply is required.

I would now like to show you how to retrieve the Phidget data via FileMaker. First of all, we need to load the Phidget libary. To be able to load the libary, we first have to download it. You can find the appropriate  download here. 

If you do not save the installation in a separate location, the standard paths to the library look like this:

Windows: C:\Program Files\Phidgets\Phidget22\phidget22.dll
Mac: /Library/Frameworks/Phidget22.framework
Linux: libphidget22.so 

We then pass this path to the Phidget.Load function, which loads the library for us. Now we can create an instance of the light sensor using the Phidget.Create function. Here we specify the appropriate type of phidget from the list of types. In our case LightSensor. 

Set Variable [ $$phidget ; Value: MBS( "Phidget.Create"; "LightSensor" ) ] 

Now we can set script triggers that refer to a script when a certain event comes from the Phidget. To do this, we call the function Phidget.SetScriptTrigger. First we specify the reference that we received from Phidget.Create. Then we specify the event for which the following script is to be called. In our case, we now want to handle the Attach and Detach events. There are events that exist for all Phidget like Attach and Detach, but the sensors can also have their own events, just as the light sensor has an IlluminanceChange event, but we will come to this in a moment. But first, let's define the scripts that should be called when a Phidget is connected. In this case, the Attach script should be called which is in the same file as the currently running script. If the Phidget is removed again, the Detached script is called. We will look at these scripts in a moment.

 Set Variable [ $r ; Value: MBS( "Phidget.SetScriptTrigger"; $$phidget; "Attach"; Get(FileName); "Attached_Light" ) ] 
 Set Variable [ $r ; Value: MBS( "Phidget.SetScriptTrigger"; $$phidget; "Detach_Light"; Get(FileName); "Detached" ) ] 
 

After we have set the script triggers we can now open the Phidget channel to receive data. When it is ready it will send us an attach event.

Set Variable [ $r ; Value: MBS( "Phidget.Open"; $$phidget ) ] 
 

Let's take a look at the attached script that is then called. When the Attach event fires, we get a JSON with information back. We first get this via Get(Scriptparameter) so that we can read it out. This is what such a JSON looks like:

{
	"ID":	"93005",
	"Tag":	"",
	"ScriptName":	"IlluminanceChange",
	"FileName":	"Phidget Light Sensor",
	"Trigger":	"IlluminanceChange",
	"illuminance":	595.57
}

We can now read the Phidget ID from this JSON. We need this to set and read the settings of the phidget.

Set Variable [ $parameter ; Value: Get(ScriptParameter) ] 
Set Variable [ $phidget ; Value: JSONGetElement ( $parameter ; "ID" ) ] 

We first set the setting with which the speed of data transmission is set. We set the data interval to 2000. Then we actually come to the most important point of the solution, because we have to specify the script that is called when the value we get from the sensor has changed. To do this, we use the function "Phidget.SetScriptTrigger" again, specify the Phidget ID to identify the Phidget, the IlluminanceChange event and specify the script "IlluminanceChange", which is in the same file, as the script to be called.

Set Variable [ $r ; Value: MBS( "Phidget.SetProperty"; $phidget; "DataInterval"; 2000 ) ] 
Set Variable [ $r ; Value: MBS( "Phidget.SetScriptTrigger"; $phidget; "IlluminanceChange"; Get(FileName); "Illuminance_Change" ) ] 

We'll see what the script does in a moment. The information we want now is the minimum and maximum value that our sensor can measure. To do this, we use the function "Phidget.GetProperty" to enter the ID in the parameters again and specify that we want the value MaxIlluminance to obtain the maximum value. For the minimum value for the light sensor, we specify MinIlluminance. To get the current value, we enter Illuminance here. Further values can also be determined using this function. The values to be determined depend on the sensor type. Please take a look at our documentation or the Phidgets documentation.

Set Field [ DoorTen::LightIntensityMax ; MBS("Phidget.GetProperty"; $phidget; "MaxIlluminance") ] 
Set Field [ DoorTen::LightIntensityMin ; MBS("Phidget.GetProperty"; $phidget; "MinIlluminance") ] 
Set Field [ DoorTen::Lightintensity ; MBS("Phidget.GetProperty"; $phidget; "Illuminance") ] 

We have already talked about the most important script: IlluminanceChange, which we call when the incoming value changes. This script also receives a JSON with data from our event that we can read out. With the illuminance key, we get the new brightness value that we can write back into the field.

Set Variable [ $json ; Value: Get(ScriptParameter) ] 
Set Field [ DoorTen::Lightintensity ; JSONGetElement ( $json ; "illuminance" ) ] 

Finally, if the Phidget is no longer to provide us with data, we must close the Phidget. You can see how this works in Script Close. The Phidget is closed with the Phidget.Close function and the memory space is released again with Phidget.Release.

Advent23_D10_B1.jpeg

This allows you to use your light sensor. It now works in a similar way with the other sensors, except that you have to specify different types, different events and different JSON keys.

 

The slider has another special characteristic. The slider is actually an adjustable resistor and we look at how much voltage passes through the resistor at a certain time interval. To do this, we need to know the channel on which the phidget is connected. We set this when we open the phidget in the open script.

...
Set Variable [ $$phidget ; Value: MBS( "Phidget.Create"; "VoltageInput" ) ] 
# 
# set where to find the voltage input, in our case on first port of a hub
Set Variable [ $r ; Value: MBS( "Phidget.SetProperty"; $$phidget; "Channel"; 0 ) ] 
Set Variable [ $r ; Value: MBS( "Phidget.SetProperty"; $$phidget; "HubPort"; 0 ) ] 
Set Variable [ $r ; Value: MBS( "Phidget.SetProperty"; $$phidget; "IsHubPortDevice"; 1 ) ] 
# 
Set Variable [ $r ; Value: MBS( "Phidget.SetScriptTrigger"; $$phidget; "Attach"; Get(FileName); "Attach_Slider" ) ] 
Set Variable [ $r ; Value: MBS( "Phidget.SetScriptTrigger"; $$phidget; "Detach"; Get(FileName); "Detached" ) ] 
...

In addition, we define a property in the attach script that specifies how often we fetch the value

Set Variable [ $r ; Value: MBS( "Phidget.SetProperty"; $phidget; "VoltageChangeTrigger"; ,1 ) ] 

Now you can realize great projects with your Phidgets. If you have realized a project with the Phidgets, please let us know what you have created. We are always happy to get feedback. Who knows, maybe Santa Claus will be driving his sleigh with Phidgets next year.

Advent23_D10_B2.png
Link to comment
Share on other sites

Door 11 - WindowsLocation

Fact of the day
Did you know that one of the reasons why the Greenwich meridian became established was because the area on the other side, i.e. the International Date Line, was sparsely settled? This had the advantage that you rarely had to add a whole day when you were traveling. This avoided confusion.

In Door 8 we have already seen how we can determine a location from a Mac or an iOS device. With the WindowsLocation component, we also have a way to determine the location for Windows.

First we have to initialize the location functions for Windows with the function WindowsLocation.Initialize. Once this has worked, we ask Windows for permission to query the location. To do this, we use the WindowsLocation.RequestPermissions function. Now we can get the following statuses with WindowsLocation.Status.

  • Not supported
  • Error
  • Access Denied
  • Initializing
  • Running
If we have Running, we can query the location with WindowsLocation.Location. We then receive a JSON with our data that can look like this, for example: 
{
    "Latitude":    50.4833,
    "Longitude":    7.4655,
    "Altitude":    0,
    "ErrorRadius":    6713,
    "AltitudeError":    0,
    "SensorID":    "{00000000-0000-0000-0000-000000000000}",
    "Timestamp":    "06.12.2023 12:38:40,532"
}

We can then read this information from the JSON using JSON functions. For example, here we see the script step with which we read the latitude:

...
Set Variable [ $JSON ; Value: MBS( "WindowsLocation.Location" ) ] 
# 
Set Variable [ $lat ; Value: JSONGetElement ( $JSON ; "Latitude") ] 
Set Field [ DoorEleven::Latitude ; $lat ] 
...
Advent23_D11_B1.png

I hope you enjoy identifying your location

Link to comment
Share on other sites

Door 12 - Files

Fact of the day
Did you know that the first hard disk in 1956 only had a storage capacity of 3.75 megabytes? Today, many of the pictures we take with a high-resolution digital camera are already larger than that.

Today we come to a topic that I already mentioned in a previous door: Today we come to the Files section. This is all about your files. In Door 3, we already got to know the Files.FileExists function, which can tell us whether a file is stored behind a certain path. However, this function is not only available for files but also for folders. With Files.DirectoryExists you can check whether a path specified in the parameters leads you to a folder. If a file is hidden behind the path, the result is 0 and if it is a folder, the result is 1. If we want to know whether a folder or a file is hidden behind this path, we use the Files.ItemExists function, which returns a 1 for a file and a folder.

  File Folder
Files.FileExists 1 0
Files.DirectoryExists 0 1
Files.ItemExists 1 1

Advent23_D12_B1.png

If you have a file, you can use the component to query a lot of information about this file. Firstly, we have the Files.FileInfo function, which provides us with various pieces of information. This is intended for bundled app/plugin/framework files on Mac and on Windows for EXE/DLL files. In addition to the path, we also specify the desired selector in the function. On Mac, this can be Version, ShortVersion, MinimumSystemVersion, InfoString, ExecutableName and Identifier for the bundle identifier. 

Under Windows, we have the selectors Description, Version, InternalName, CompanyName, LegalCopyright, OriginalFilename, ProductName, ProductVersion, and Copyright

We also have functions that query individual file information. For example: Files.FileName, Files.FileNameWithoutExtension, Files.AccessDate, Files.CreationDate, Files.ModificationDate, Files.FileKind, Files.FileSize, Files.IsHidden, Files.IsReadOnly, Files.IsExcludedFromBackup, Files.IsPackage, Files.GetFinderLabel or Icon.GetIcon. You can query all of this information using functions. For most functions, there is also a function with which you can change these properties.

 

You can also display a list of the files that are stored in a specific folder. This makes sense, for example, if you want to import all the files in a folder into the database. You can use Files.List for the list. If you would prefer to receive this list as JSON and get additional information about the files, you can use the Files.ListAsJSON function. If you use Files.List and Files.ListAsJSON, we always keep the list at the top level, which means we don't know what happens in the subfolders. For this we have the Files.ListRecursive function, which also returns the paths of the subfolders.

Advent23_D12_B2.png Advent23_D12_B3.png Advent23_D12_B4.png

But you can not only query but also work with the files. You can create a new folder with Files.CreateDirectory. You can also copy one or more files from one folder to another using the Files.CopyFile and Files.CopyFiles functions. Creating an alias is also no problem with Files.CreateAlias.

Another cool thing that many of our customers use is that you can open a file from which you have the path, using a script. To do this, use the Files.LaunchFile function for normal files. This opens a file or folder. Use Files.Lauch e.g. to open a database file with FileMaker or your runtime solution. In addition, you can not only copy a file, but also move the file. This is where Files.MoveFile comes into play. A special form of moving is offered by the Files.MoveToTrash function, which places the file in the trash without a dialog. If you want to delete the file directly without going through the trash, you can use the Files.Delete function. In this case, use Files.DeleteFolder for a folder. There is another function for deleting that is still relatively new: Files.DeleteLater. This function writes a file to a list that is to be deleted afterwards. The files on this list are deleted when File Maker is closed. This function makes sense because there are some functions that can only be used with a path, such as the Files.LaunchFile function. So if we want to open a file from a container, we write this file to the temporary folder and enter the file in the deletion list.

Set Variable [ $Temp ; Value: MBS("Folders.UserTemporary") ] 
Set Variable [ $Name ; Value: MBS( "Container.GetName"; DoorTwelve::Container ) ] 
Set Variable [ $Path ; Value: MBS("Path.AddPathComponent"; $Temp; $Name) ] 
Set Variable [ $r ; Value: MBS( "Container.WriteFile"; DoorTwelve::Container; $Path ) ] 
Set Variable [ $r ; Value: MBS("Files.DeleteLater"; $Path) ] 
Set Variable [ $r ; Value:  MBS("Files.LaunchFile"; $Path) ] 

If we want to read a file from the disk into a container, this also works with this component. We have various functions for the different data formats that enable the file to be read: Files.ReadFile, Files.ReadJPEG, Files.ReadPNG and Files.ReadPDF. 

Here you can see how a PNG is loaded into the container.

Set Variable [ $File ; Value: MBS( "Files.ReadPNG"; DoorTwelve::Path ) ] 
Set Field [ DoorTwelve::Container ; $File ]
 

I hope you enjoy using these functions, which I hope will also be helpful for you.

Link to comment
Share on other sites

Door 13 - LibXL

Fact of the day
XL Did you know that there is even an Excel World Championship in which 8 Excel experts compete against each other to win the prize money of $10,000?

Welcome to door 13 of our advent calendar. Today it's all about the LibXL component. Did you know that you can use the MBS FileMaker Plugin to read, create and modify Excel files without having Excel installed? 

This is made possible by the LibXL component. LibXL is an independent product that can be used in FileMaker with the plugin. This means that you need an additional LibXL license in addition to your plugin license to be able to use the functions properly. Today I will show you how you can read and modify data with LibXL and even create your own files.

Preparation 

But before we start our work, we first need to make the LibXL library available so that we can use the functions in the plugin. The library file you need can be found in the examples supplied with the plugin download. Exactly which file you need depends on your operating system. If you want to initialize the library on a Mac, then you need the library file with the extension dylib. If you use Windows, we have a file for the old Windows computers that work with 32 bit and a file with the extension dll for the 64 bit systems. If you want to use LibXL on a Linux server, we have included libxl.so with the Linux plugins. 

For initialization we have the function XL.Initialize. Here we enter the path to the library. If you already have a LibXL license, enter the name and license key here. If you have customers who use different operating systems, you can also copy the InitXL script from the examples, the libraries can then simply be placed in the same folder as the database file. But for actual deployment, it may be easier to put the libXL file into the same folder as the plugin. Then you can pass "" as path and the plugin automatically checks the plugin folder. At the end of the script, the Initialize function is called with the appropriate parameters. You can adjust the values of the parameters in the script previously.

  # If you like to get a LibXL license, please follow links on our pricing page:
# https://www.monkeybreadsoftware.de/filemaker/pricing.shtml

Set Variable [ $LicenseeName ; Value: "your name" ] 
Set Variable [ $LicenseKey ; Value: "your LibXL license key" ] 
Set Variable [ $r ; Value: MBS( "XL.Initialize"; $path; $LicenseeName; $LicenseKey) ] 
If [ $r  ≠ "OK" ] 
	Show Custom Dialog [ "Error" ; $r ] 
	Halt Script
End If

When we work in a script, it makes sense to check at the beginning of each script whether the LibXL is initialized. To do this, we use the XL.IsInitialized function which returns a 0 if the initialization has not yet been performed. The beginning can look like this:

If [ MBS("XL.IsInitialized") ≠ 1 ] 
	Perform Script [ Specified: From list ; "InitXL" ; Parameter:    ]
End If

Create file

Let's start our work. First we want to create a file in which we want to enter data. An Excel file is called a book in LibXL. The individual tables within such a file are on sheets. So we want to create a new workbook. To do this, we have the XL.NewBook function. It is used to create a working environment in the memory for which we receive a reference number back with the function, which we can then continue to work with. In an optional parameter we can also decide whether an Excel file should be created in the old xls format (parameter = 0) or the newer xlsx format (parameter = 1). Of course, our sheet must now be added to this workbook using XL.Book.AddSheet. In the parameters we specify the reference to which the book belongs and the name that the sheet should have. If you want to use an already created sheet as a template for the new one, you can optionally specify the sheet index of the template here. We then receive the sheet index of the sheet just created as a return.

Set Variable [ $Book ; Value: MBS("XL.NewBook"; 1) ] 
Set Variable [ $Sheet ; Value: MBS("XL.Book.AddSheet"; $Book; "Sheet 1") ]

We can now fill this with life. 

If you do not yet have a license, there is always a warning text in the first line of the document and you cannot write to this line. If you try to do this, an error will occur, which is why we want to start filling the second row in our example. We have many different functions for writing in a cell, which we use depending on what we want to write in the cell. If we want to write a simple text, for example, we use the XL.Sheet.CellWriteText function. Here we first enter the book reference and then the sheet index in the parameters, followed by the cell that is to be written to in the table. In our case the second row and the first column. Since we start counting at 0, we have a 1 as the value for the row and a 0 for the column. Now the text follows and we can optionally specify a format, but more on that later.

Set Variable [ $r ; Value: MBS("XL.Sheet.CellWriteText"; $Book; $Sheet; 1; 0; "Hello") ]

Of course, there is not only a function for text, but also for numbers, dates, a Boolean value or formulas. For formulas, we enter the formula we want to calculate as the value. We can even use cells with their coordinates as values or, for example, calculate totals in formulas over entire ranges. In this example, cells B2 and C2 are added together. We have also previously set the values 49 and 2.

Set Variable [ $r ; Value: MBS("XL.Sheet.CellWriteNumber"; $Book; $Sheet; 1; 1; 49) ] 
Set Variable [ $r ; Value: MBS("XL.Sheet.CellWriteNumber"; $Book; $Sheet; 1; 2; 2) ] 
Set Variable [ $r ; Value: MBS("XL.Sheet.CellWriteFormula"; $Book; $Sheet; 1; 3; "B2+C2") ] 

If we use the XL.Sheet.CellWriteDate function, we get a number in a cell that is formatted as text, for example. If we want this date to be displayed as a date, the cell must be of type Date

Set Variable [ $r ; Value: MBS( "XL.Sheet.CellWriteDate"; $Book; $Sheet;1; 4; Get(CurrentTimestamp)  ) ] 

For a Boolean value, we can use 1 for TRUE and 0 for FALSE.

Set Variable [ $r ; Value: MBS( "XL.Sheet.CellWriteBoolean"; $Book; $Sheet;1; 5; 1) ]

We have already talked about formats and that we can also pass a format in each of these functions. This formats the text of a document and thus defines its appearance. 

Here, for example, we first create a font, which gets the font via the XL.Font.SetName function. In addition, the font should be italic and have a size of 20pt. Now we create a format ( XL.Book.AddFormat ) and transfer the created font to this format ( XL.Format.SetFont ). In a format, you could now enter additional information about the border, for example. You can then specify the format as an additional parameter in the XL.Sheet.CellWriteText function, for example.

Set Variable [ $Font ; Value: MBS("XL.Book.AddFont"; $Book) ] 
Set Variable [ $r ; Value: MBS("XL.Font.SetName"; $Book; $Font; "Comic Sans MS") ] 
Set Variable [ $r ; Value: MBS("XL.Font.SetItalic"; $Book; $Font; 1) ] 
Set Variable [ $r ; Value: MBS("XL.Font.SetSize"; $Book; $Font; 20) ] 
Set Variable [ $CellFormat ; Value: MBS("XL.Book.AddFormat"; $Book) ] 
Set Variable [ $r ; Value: MBS( "XL.Format.SetFont"; $Book; $CellFormat; $Font ) ] 
Set Variable [ $r ; Value: MBS("XL.Sheet.CellWriteText"; $Book; $Sheet; 1; 0; "Hello"; $CellFormat) ] 

Another possibility to transfer formatting from the FileMaker database to the fields is the XL.Sheet.CellWriteStyledText function. This takes the formatting from your field in FileMaker

Set Variable [ $r ; Value: MBS( "XL.Sheet.CellWriteStyledText"; $Book; $Sheet; 1; 6; DoorThirteen::Text ) ] 
Advent23_D13_B1.png

If you now want to write this Excel document to a file, you can use the XL.Book.SaveToFile function to write the document from the working memory to a file. If you want to place the file in a container in your database, please use XL.Book.Save. 

Set Variable [ $r ; Value: MBS( "XL.Book.SaveToFile";$Book; "/Users/sj/Desktop/AdventXl.xlsx" ) ] 

So that the memory is again free and can continue to be used, call XL.Book.ReleaseAll to remove all Excel working environments. If you only want to remove a specific one, use the XL.Book.Release function and specify the reference in the parameters. 

But we can not only create files, we can also load existing files. To do this, we use the XL.LoadBook function, which gives us an Excel file that is located in a file at a specific path or in a container. As a return we then get the reference number with which we can continue working.

Set Variable [ $Book ; Value: MBS( "XL.LoadBook"; "/Users/sj/Desktop/AdventXl.xlsx") ]

Just as we have different functions for writing values, we also have different functions for reading cells. With the XL.Sheet.CellReadValue we have a function that reads the value from the cell and returns a value of the corresponding type. We also have the appropriate function for each individual type.

Set Variable [ $A2 ; Value: MBS("XL.Sheet.CellReadText"; $Book; 0; 1; 0) ] 
Set Variable [ $B2 ; Value: MBS("XL.Sheet.CellReadNumber"; $Book; 0; 1; 1) ] 
Set Variable [ $C2 ; Value: MBS("XL.Sheet.CellReadNumber"; $Book; 0; 1; 2) ] 
Set Variable [ $D2 ; Value: MBS("XL.Sheet.CellReadFormula"; $Book; 0; 1; 3) ] 
Advent23_D13_B2.png

The Componte XL has many more cool functions available, just take a look at the documentation or try out our examples. I wish you lots of fun with your tables.

Link to comment
Share on other sites

Door 14 - Progress Dialog

Fact of the day
And speaking of waiting, did you know that in Gary, Indiana, the law says you can't go to the movies or theater until four hours after eating garlic?

Do you know this? You have to go through many records and it takes and takes and takes time? Some users then become frustrated and start hammering on the keyboard or pressing all kinds of buttons because they don't know that there are already processes running in the background that simply need its time. 

With the MBS Progress dialog, you can display a dialog to your users so that there is no confusion or panic. To do this, you have the functions from the ProgressDialog component.

Before we can display the progress dialog, we first have to make a few settings for the dialog. The special thing about the dialog is that you can set the content in a script and then call it up at a later time in the different layouts in our solution. For this reason, we must first reset the dialog to its initial state to remove legacy content before we make new settings. To do this, we use the ProgressDialog.Reset function. Now we can give the dialog box a title. We use ProgressDialog.SetTitle for this. In the dialog you can also enter a text above the progress bar and one below. We use the ProgressDialog.SetTopText and ProgressDialog.SetBottomText functions for this. We can also select a font and size for these two texts. We set these values in the ProgressDialog.SetFont function. To make the dialog look nicer, we can also display an icon in the dialog to make it easier to understand. In our case, this is the monkey with the Christmas hat. We set this image, which is in a container, with the ProgressDialog.SetImage. If you use a PNG with transparent background, you don't get the white border around.

Set Variable [ $r ; Value: MBS("ProgressDialog.SetTitle"; DoorFourTeen::Title) ]
Set Variable [ $r ; Value: MBS("ProgressDialog.SetFont"; "Comic Sans MS"  ; 14  ) ]
Set Variable [ $r ; Value: MBS("ProgressDialog.SetTopText"; DoorFourTeen::Text) ]
Set Variable [ $r ; Value: MBS("ProgressDialog.SetBottomText"; "Wait...") ]
Set Variable [ $r ; Value: MBS("ProgressDialog.SetImage"; DoorFourTeen::Container) ]
Advent23_D14_B1.png

If we want to display the dialog, we use the ProgressDialog.Show function. This shows the dialog. The dialog is displayed as long as FileMaker is not closed or the ProgressDialog.Hide function is not called.

We see that the dialog still has a button, which is usually used as a cancel button. If we do not want this button to be displayed, we use the ProgressDialog.SetShowButton function and pass a 0 in the parameters. If we want to change the text of the button, we use the ProgressDialog.SetButtonCaption function to which we pass the text to be displayed on the button. If we leave the script as it is and display the dialog, nothing happens when we click on the button. Because we first have to define a script that is called when the button is pressed or specify an expression. If we want to specify a script, we use the Progressdialog.SetScript function. In our example, we use an expression that we specify in the Progressdialog.SetEvaluate function. We want to set the cancel flag in this function to 1. I'll explain what this does for us in more detail later. The flag is set with the ProgressDialog.SetCancel function *.

Set Variable [ $r ; Value: MBS("Progressdialog.SetEvaluate"; "MBS(\"ProgressDialog.SetCancel\"; 1)") ] 

Now our dialog should not only show the standard animation of the bar, but should also show the progress. In our case, we would like to display a bar that runs for approximately a number of seconds, but it could also be the passes with which data records are written to your database, or whatever else you need the bar for. To do this, we first set the progress bar to the value 0, i.e. at the very beginning of the bar. Then we pause for a second because the animation in the progress dialog should run to the end before it starts at 0. The value that we set in ProgressDialog.SetProgress again and again, in the following loop, can have values between 0 and 100. For this reason, we first want to calculate how much the value of the progress increases with each loop run. To do this, we calculate 100/number of loop passes. We then always add this calculated value to the current value in the loop and enter this as the new value to be set. The value for the loop passes originally comes from a field. Here we have to make sure that the field is of type number, otherwise the value is recognized as text and the number of loop passes is usually incorrect.

Within the loop, we also change the text below the progress bar. To do this, we again use the ProgressDialog.SetBottomTextfunction. To ensure that this change, as well as all other changes in the dialog, are displayed reliably, we call the ProgressDialog.Update function, which sets a flag so that the dialog is redrawn when the memory usage allows it. 

We exit the loop when we are either done with the runs or when the cancel flag is set. This means when someone has pressed the cancel button in the dialog. We query this flag with ProgressDialog.GetCancel.

After the loop has been exited, we then hide the dialog again with ProgressDialog.Hide

Set Variable [ $r ; Value: MBS("ProgressDialog.SetProgress"; 0) ] 
Pause/Resume Script [ Duration (seconds): 1 ] 
Set Variable [ $TotalLoops ; Value: DoorFourTeen::Time ] 
Set Variable [ $CurrentProgress ; Value: 0 ] 
Set Variable [ $ProgressValue ; Value: 100/$TotalLoops ] 
Set Variable [ $i ; Value: 0 ] 
Loop
	Set Variable [ $CurrentProgress ; Value: $CurrentProgress + $ProgressValue ] 
	Set Variable [ $r ; Value: MBS("ProgressDialog.SetProgress"; $CurrentProgress) ] 
	Set Variable [ $r ; Value: MBS("ProgressDialog.SetBottomText"; $i+1 & " of " & $TotalLoops) ] 
	Exit Loop If [ $i  ≥ $TotalLoops or MBS("ProgressDialog.GetCancel") ] 
	Pause/Resume Script [ Duration (seconds): 1 ] 
	Set Variable [ $r ; Value: MBS("ProgressDialog.Update") ] 
	Set Variable [ $i ; Value: $i+1 ] 
	# 
End Loop
Set Variable [ $r ; Value: MBS("ProgressDialog.Hide") ] 
Advent23_D14_B2.png

I hope you enjoyed this door and can't wait to see your progress bars in your solutions.

 

* The cancel flag is set automatically if you don't add a custom action to the button, but we still like to show you how to do the evaluate.

Link to comment
Share on other sites

Door 15 - DynaPDF

Fact of the day
The groundwork for today's PDF documents was laid back in 1991 with the Camelot project. The aim was to develop a file format that can capture data from all programs, this document can be shared and displayed in the same way on any end device so that it can also be printed

Today I would like to introduce you to an area that is very appreciated by our customers. We are talking about DynaPDF. With our DynaPDFfunctions you can work with your PDF documents. You can create PDF documents according to your wishes, write in these documents, sign them, create forms and much much more. In order to be able to work with DynaPDF without a watermark being permanently superimposed on your PDF pages, you need the correct additional DynaPDF license. Which license you need depends on what you want to do with DynaPDF. If you are only working create a PDF from scratch, for example, then all you need is a Starter license. If you want to load an existing PDF file, you need a Lite license. If you also want to optimize or render your PDF file, you need a Professional license. This table can help you decide which licenses you need.

Today in the example I would like to show you how to merge two PDF files and then add page numbers to them. As with LibXL, we first have to initialize DynaPDF and specify a library to work with. From the examples, you can use the InitDynaPDF script in your database and place the appropriate library file in the same folder as your database, or you can use the DynaPDF.Initialize function and enter the path to the appropriate library and your license key in the parameters. If you do not have a license key yet, because you want to test DynaPDF, then leave the license key blank. If you want to test whether your solution works with a specific license, then write the license name in the place of the license key. If you are using Windows, you must use the dll files. There are two dll files, one is for use on 32 bit systems and the other on 64 bit systems. On Mac you need the file with the extension .dylib. For use on a Linux server you need the dynapdf.linux.so file.

Advent23_D15_B1.png

You need to perform the initialization before you use the first DynaPDF function. If you are working with the initialization script from the examples, it is advisable to place the following lines before each script in which DynaPDF functions are used.

If [ MBS("DynaPDF.IsInitialized")  ≠  1 ] 
	Perform Script [ Specified: From list ; "InitDynaPDF" ; Parameter:    ]
End If
If [ MBS("DynaPDF.IsInitialized")  ≠  1 ] 
	Exit Script [ Text Result:    ] 
End If

First we have to create a fresh working environment into which we can import our two files one after the other. To do this, we use the DynaPDF.New function. This creates the working environment and returns a reference number with which we can continue working in the other functions.

Set Variable [ $pdf ; Value: MBS("DynaPDF.New") ]

Before we can import a file, we have to open this file. We can either import files from a PDF file on disk, in which case we use the DynaPDF.OpenPDFFromFile function, or we can import the file from a container, in that case we use the DynaPDF.OpenPDFFromContainer function to open the file. In the parameters, we first specify our working environment, followed by the file path or container. If your document to be opened here has a password, we can first enter the password type and then the password itself. Now we come to the import. If you only want to import one page, it is best to use the DynaPDF.ImportPDFPage function. If you want to import several pages, as in this example, use the DynaPDF.ImportPDFFile function. Here we specify the working environment in which we want to write our import and specify the page on which the first page of the imported document is to be placed. In Document 1, this is the first page. The function returns how many pages it has imported. We need this information for the start page of document two. We repeat the process with the second document.

# Combine 
Set Variable [ $pdf1 ; Value: MBS("DynaPDF.OpenPDFFromContainer"; $pdf; DoorFifteen::FirstDocument) ] 
Set Variable [ $pages ; Value: MBS("DynaPDF.ImportPDFFile"; $pdf; 1) ] 
Set Variable [ $pages ; Value: $pages + 1 ] 
Set Variable [ $pdf2 ; Value: MBS("DynaPDF.OpenPDFFromContainer"; $pdf; DoorFifteen::SecondDocument) ] 
Set Variable [ $pages ; Value: MBS("DynaPDF.ImportPDFFile"; $pdf; $pages) ] 

Now we have a document in the working environment that contains both PDF files and it would be nice if we could number these pages. To do this, we write a text with the page number in a specific place on each individual page. First we set the coordinate alignment so that we can orient ourselves better. Normally the coordinates run from bottom to top and from left to right. We now want to change this and set the coordinates to take on larger values from top to bottom. This orientation is more intuitive for most people.

Then we determine the number of pages that are in the working environment. To do this we use the DynaPDF.GetPageCount function. We also specify the page number we want to start with. Before we can write the text on the page, we want to determine the dimensions of the page with DynaPDF.GetPageWidth and DynaPDF.GetPageHeight. With DynaPDF.EditPage we make a page whose index we specify in the parameters editable. We then use the DynaPDF.SetFont function to set the font and font size of the text that we want to write with the DynaPDF.WriteFTextEx function. This function writes the text with formatting commands to the current open page. In the parameters we specify our reference again, followed by the position of the text. We determine the position of the top left corner of the text field in our case 50 pixels away from the left and bottom edge. Then comes the size of the text field. In our case, we take the page width minus the margins to the right and left of 50 pixels. The height is 30. Now we can use the text alignment to decide exactly where our page number should be. We want it to be on the right-hand side, so we choose right-aligned text. Last but not least follows our text. This is made up as follows: Page number of total number of pages.

After we write our text on the page, we can stop editing this page and run through the loop again until we have reached the last page.

Set Variable [ $r ; Value: MBS( "DynaPDF.SetPageCoords"; $PDF; "TopDown" ) ] 
Set Variable [ $PageCount ; Value: MBS( "DynaPDF.GetPageCount"; $pdf ) ] 
Set Variable [ $PageNumber ; Value: 1 ] 
Set Variable [ $pageWidth ; Value: MBS("DynaPDF.GetPageWidth"; $pdf) ] 
Set Variable [ $pageHeight ; Value: MBS("DynaPDF.GetPageHeight"; $pdf) ] 
Loop
	Set Variable [ $r ; Value: MBS("DynaPDF.EditPage"; $pdf; $PageNumber) ] 
	Set Variable [ $r ; Value: MBS( "DynaPDF.SetFont"; $pdf; "Helvetica"; 0; 20) ] 
	Set Variable [ $r ; Value: MBS( "DynaPDF.WriteFTextEx"; $pdf; 50; $pageHeight - 50; 
	 	$pageWidth-100; 30; "right"; GetAsText($PageNumber) & " of " & $PageCount) ] 
	Set Variable [ $r ; Value: MBS("DynaPDF.EndPage"; $pdf) ] 
	Set Variable [ $PageNumber ; Value: $PageNumber +1 ] 
	Exit Loop If [ $PageNumber > $PageCount ] 
End Loop

Finally, we save the PDF document in a separate container. To do this, we use the DynaPDF.Save function.

Set Field [ DoorFifteen::Combine ; MBS( "DynaPDF.Save"; $PDF ; "Combine.pdf") ] 

After we have finished our work we have to release the working memory. If we want to release all references at the same time we use the function "DynaPDF.ReleaseAll. If we only want to release a single reference, we can use the DynaPDF.Release function by specifying it in the parameters.

Advent23_D15_B2.png

I hope you liked the door this time as well and we'll see us again tomorrow for the next door.

Link to comment
Share on other sites

Door 16 - GraphicsMagick

Fact of the day
MBS already had an advent calendar for last year. So take a trip back in time to last year and enjoy the articles

Did you know that with FileMaker and the component GraphicsMagick you can change a lot in your images? 

Last year we had an advent calendar that only dealt with GraphicsMagick. You are welcome to have a look here as well. To be able to work with an image, the image must be loaded from a file or a container. The appropriate function then gives us a reference number with which we can continue working.

Set Variable [ $Image ; Value: 
	MBS("GMImage.NewFromContainer"; DoorSixTeen::Container) ]

First of all, you can easily query the height and width of an image in a container using the functions GMImage.GetWidth and GMImage.GetHeight.

Advent23_D16_B1.png

Advent23_D16_B2.png

If we want, we can rotate the image. To do this, we use the GMImage.Rotate function. If we enter a positive angle in the parameters, we rotate to the right. If we enter a negative angle, we rotate to the left.


Advent23_D16_B3.png

We can also mirror such an image vertically or horizontally. To do this, we use the functions GMImage.Flip for vertical flipping and GMImage.Flopfor horizontal mirroring.


Advent23_D16_B4.png

There are also several effects available for editing the image. Let's start with the blur effect. We can create this with GMImage.Blur. We can then set the intensity in the parameters.


Advent23_D16_B5.png

We can also use the Sharpen effect with the GMImage.Sharpen function when editing images. However, this effect is not the opposite function to Blur!


Advent23_D16_B6.png

If we want to use the effect of a charcoal drawing, GMImage.Charcoal is there to help us.


Advent23_D16_B7.png

If you want to try your hand at being a real Leonardo da Vinci, you can use the GMImage.OilPaint function to apply an oil painting effect to your photos.


Advent23_D16_B8.png

If you want everything to be really swirly on your images, use the GMImage.Swirl function. This creates a swirl in your image. 


Advent23_D16_B9.png

A little depth in the images can often do no harm either. You can add an embossing effect with the GMImage.Emboss function.


Advent23_D16_B10.png

Grayscale images in photography can be very aesthetic and give the image a completely different mood. Use the GMImage.SetType function and enter a 2 in the parameters to obtain a grayscale image.

# Load from container 
Set Variable [ $Image ; Value: MBS("GMImage.NewFromContainer"; DoorSixTeen::Original) ] 
# Grayscale
Set Variable [ $result ; Value: MBS("GMImage.SetType";$Image; 2) ] 
If [ MBS("IsError") = 0 ] 
	# Write to container
	Set Variable [ $result ; Value: MBS("GMImage.WriteToPNGContainer";$Image) ] 
	Set Field [ DoorSixTeen::Output ; $result ] 
End If
# Release image
Set Variable [ $r ; Value: MBS("GMImage.Free"; $Image) ] 

Advent23_D16_B11.png

Many people only see the negative in everyday life. For you, this should only be the case with images. You can use the GMImage.Negatefunction to view the negative of an image. If you use the function twice in a row, you will get back the original image.


Advent23_D16_B12.png

The GMImage.Solarize function offers us a similar effect. However, if we use the function twice in a row, the pure white areas remain black. If we combine the function with GMImage.Negate, our monkey gets a pink cap. The functions are therefore similar but not identical.

This and much more awaits you in the GraphicsMagick component. I hope you enjoy using it!

 

Link to comment
Share on other sites

Door 17 - Dialogs

Fact of the day
Did you know that you can also influence FileMaker's own dialogs a little? 
Take a look at the DialogModifications component in the plugin.

The dialog box for displaying information to the user is essential and can be found in almost every application. But sometimes you want more design options for the dialog box and this is where the plugin comes into play, because with this you can build your dialog box according to your wishes. I will show you how this works in this door.

Such a dialog is valid for the entire application, which means that you can make the settings for a dialog in one script and call the dialog in another script. However, this also means that we have to get rid of old settings at the beginning. To do this, we use the Dialog.Reset function, which resets all dialog settings to their original state.

Set Variable [ $r ; Value: MBS( "Dialog.Reset" ) ]

Now we can start making our settings. We can set the text to be displayed in the dialog. We use the function Dialog.SetMessage for this. If we also want to display an additional information text (a text in a smaller font), this can be specified with Dialog.SetInformativeText. Icons can be very practical so that you can see at first glance what the dialog might be about. You can use the Dialog.SetIcon function to bring this icon into your dialog from a container, for example. Would you like to display your text in the dialog right-aligned? No problem Dialog.SetTextAlignment makes it possible. All you have to do is select the appropriate alignment in the parameters.

Set Variable [ $r ; Value: MBS( "Dialog.SetMessage"; DoorSevenTeen::Text ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.SetInformativeText"; DoorSevenTeen::Infotext ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.SetIcon"; DoorSevenTeen::Icon ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.SetTextAlignment"; "right") ] 

Would you like to have several options for the buttons? That's no problem at all to have up to 10 more buttons in addition to the default button. First of all, we can change the title of the default button if we don't like the default OK. To do this, we use the function Dialog.SetDefaultButton and enter the new name in the parameters. If we now want more buttons, we can use the function Dialog.SetButton to set the titles of the buttons that then appear. In the parameters, we specify the index to say which button we want to add now, followed by the title.

…
Set Variable [ $r ; Value: MBS( "Dialog.SetDefaultButton"; "DefaultButton" ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.SetButton"; 1; "Santa Clause" ) ] 
…

But we can not only add buttons, for example, if you need information from your user, you can also use fields under Mac in the dialog. The Dialog.AddField function is available for this purpose. Here we have many optional options in addition to the label title of the field.

MBS( "Dialog.AddField"; Label { ; Text; Placeholder; Password; TextViewHeight } )  

After the title, we can place a text that has already been entered in the field. This is useful, for example, if the data has already been entered in the database and you want to check it again so that the user can make changes if necessary. 
Advent23_D17_B1.pngWith the Placeholder parameter, you can display a text in the field that tells you what should be entered in the field. The Password parameter expects a 0 or 1 as a value and determines whether only dots are displayed in the field instead of the plain text, as we know it from password entries. Last but not least, we can also influence the height of the text field. With a higher text field, we can specify a list, for example. If the value we specify in this parameter, is higher than 20, the field changes from a standard field to a text view. This text view behaves a little differently to a normal field, as we cannot create a placeholder text or use a password option. The values that are set in this way are simply ignored, but we can already enter text in this field. The following configuration creates this dialog

# Fields
Set Variable [ $r ; Value: MBS( "Dialog.AddField"; "Name" ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.AddField"; "FirstName" ; ""; " Write your name" ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.AddField"; "Do you like gingerbread" ; "Yes"; "Type 'Yes' or 'No'" ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.AddField"; "Your Secret Santa" ; ""; ""; 1 ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.AddField"; "Your special wish" ; ""; "What do you want for Christmas"; 0; 50 ) ] 

We can also determine the position of the dialog on the screen. To do this, we can specify the coordinates of the upper left corner of the dialog.

# Position 
Set Variable [ $r ; Value: MBS( "Dialog.SetTop"; 100 ) ] 
Set Variable [ $r ; Value: MBS( "Dialog.SetLeft"; 100 ) ] 
 

Under windows the dialog looks a little different. Here the dialog also has a title bar. We can set this title with Dialog.SetWindowTitle.

Advent23_D17_B2.png

If we now run the script, we don't see anything yet, because we first have to give the program the command that the dialog should be displayed. We do this with Dialog.Run.

# Show Dialog
Set Variable [ $r ; Value: MBS( "Dialog.Run" ) ] 

At this point, our script is stopped and we are shown the dialog. We can now enter our values. By clicking on one of the buttons, the dialog disappears. Now, of course, we want to know what the user has clicked or what is in the fields. We can find out which button was clicked with the Dialog.GetButtonPressed function. This provides us with the index of the button that was pressed. We can then use the Dialog.GetButton function to find out the title of the button. We have to be careful here. If we have previously omitted an index when creating the button, an mistake will occur here and we will get back the wrong title of the button. It is therefore important to ensure that sequential indexes are used when creating the button.

# Results
# Button
Set Variable [ $Button ; Value: MBS( "Dialog.GetButtonPressed" ) ] 
Set Variable [ $TitelOfButton ; Value: MBS("Dialog.GetButton"; $Button) ] 
Set Field [ DoorSevenTeen::Button Result ; $TitelOfButton ] 

For each field that we have in our dialog, we query the value individually. This is where the Dialog.GetFieldText function comes into play. We enter the index of the field we want to query and get the content back.

Set Variable [ $F1 ; Value: MBS("Dialog.GetFieldText"; 0) ] 
Set Variable [ $F2 ; Value: MBS("Dialog.GetFieldText"; 1) ] 
Set Variable [ $F3 ; Value: MBS("Dialog.GetFieldText"; 2) ] 
Set Variable [ $F4 ; Value: MBS("Dialog.GetFieldText"; 3) ] 
Set Variable [ $F5 ; Value: MBS("Dialog.GetFieldText"; 4) ] 
Set Field [ DoorSevenTeen::Fields Result ; "Name:" & $F1 & "¶¶First name:" & $F2 & "¶¶Gingerbread:" & $F3 & "¶¶Your Secret Santa:" & $F4 & "¶¶Wish:" & $F5 ] 

If we have now entered the information in our dialog, a return can look like this, for example:

Advent23_D17_B3.png

I hope you enjoyed this insight into the dialogs.

Link to comment
Share on other sites

Door 18 - Preview

Fact of the day
Did you know that you can also embed an XML file in PDF documents? This is also done with the ZUGFeRD format, for example. With the help of DynaPDF and the plugin, you can create and read such files yourself

Did you know that MBS offers a PDF preview for Windows and Mac, with which you can view the document and even copy content with the mouse? 

We have included this control in our plugins since version 13.3 of this year. You can use this function on Windows 10 and MacOS. You can easily test whether your system fulfills the requirements with the Preview.Available function.

If [ MBS( "Preview.Available" ) ] 

If this fits, you can create the preview control. We have two options for this. First, we can create a PDF control with a fixed size and a specific position. To do this, we use the Preview.Create function. We first enter the window reference in the parameters. If it is the window that is furthest forward, enter a 0 or find out the reference with Window.FindByTitle or Window.FindByIndex. Then comes the position, which we specify with x and y for the top left corner. Finally, enter the width and height of the control.

Set Variable [ $$Preview ; Value: MBS( "Preview.Create"; 0; 177; 64 ; 320; 420 ) ] 

The other option is to position the preview with a control. To do this, we use the Preview.CreateWithControl function. For example, a rectangle that has been positioned on the layout can be used as such a control. In the parameters, we again specify the window reference and also the name of the placeholder. In this case it is now Placeholder. If required, we can now specify an offset. In other words, values that move the preview from the placeholder object by a certain value, but we are don't use this here.

Set Variable [ $$Preview ; Value: MBS( "Preview.CreateWithControl"; 0; "Placeholder"  ) ] 

Before we can see the PDF, we must first load it. Again, we have two options. It can be from a container, in which case we use the "Preview.LoadContainer" function.

Set Variable [ $r ; Value: MBS( "Preview.LoadContainer"; $$Preview; DoorEightTeen::Container ) ]

The other option is to load the PDF file from a file that is stored in a path. Then we use the "Preview.LoadFile" function.

Set Variable [ $r ; Value: MBS( "Preview.LoadFile"; $$Preview; DoorEightTeen::File ) ]

The PDF file should now be displayed and we can view the pages and select and copy content using the mouse menu. 

Advent23_D18_B1.png

If you want to remove the file from the PDF view again, but the preview may be needed again. The function Preview.Unload unloads the current PDF document.

If you no longer need the preview, release the preview again. You can use Preview.Release to release a single preview by specifying the preview reference, or you can use Preview.ReleaseAll to release all currently existing previews at the same time. 

I hope the new preview can help you in your work with PDF documents.

Link to comment
Share on other sites

Door 19 - RegEx

Fact of the day
Regular expressions originally come from mathematics. In 1951, the mathematician Stephen Kleene wrote events similar to today's RegEx.

You want to search for mail addresses in a text and extract only these addresses from the text. Or do you want to replace all Internet addresses in a text with a new Internet address of your own? In this case, regular expressions are a good solution for this task. What regular expressions are and how you can use them in FileMaker I will show you in this Door.

What are regular expressions?

With regular expressions you can search for certain patterns in a text or check a string if it meets certain criteria, e.g. if the chosen password contains upper and lower case letters, at least one number, one special character and is at least 8 characters long. If we search for something in a text search then we actually always search for a regular expression. For example, if we enter the word "Miss", then we search for a pattern in the text that searches for the letters M-i-s-s that stand behind each other. We find the word Miss but also the word Mississippi and if we tell the program that we don't care about upper and lower case letters we also find the word missed. So we are looking for a pattern where the 4 letters appear exactly in this order. This is already a regular expression. But now we can build it even further. E.g. for a mail address that the local part is separated from the domain with an @ sign. After the Domain is the Domaintail separated by a dot. [email protected] but also [email protected] are valid mailaddresses. We now want to develop a regular expression that finds both mail addresses in one text. So we have to formulate how the pattern looks like. The local part can be composed of upper and lower case letters, numbers and the characters .!#$%&'*+-/=?^_`{|}~ the string has no fixed length. Let's see how to define something like this. If we specify a range from which a character can be taken, we write the characters in square brackets. In this example, all characters that are an a, b or c would be found. 

[abc]

If we now want the character we are looking for to be any lowercase letter, we do not have to list all 26 letters but can also write [a-z] instead of the list. The dash will then not be found because it only acts as an indicator for a range. If we want that also capital letters are found, we enter [A-Za-z]. If we want the numbers 0-9 to be found, we add [A-Za-z0-9]. We can add to this any character. If we define the range for the local part of the mail it looks like this [a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]. 

If we now specify this as a regular expression, exactly one character would always be found that is contained in this set. To indicate that these characters can occur several times in a row we have different possibilities. First we can use the star, which indicates that a character can occur any number of times. With [a-z]* we could find a string of lowercase letters of any length. But the star is not what we would choose for the mail address, because the star excludes that the string can have a length of 0. We want our local part to consist of at least one character. For this we have the + which should be behind the range. If we would have the condition that the string should consist of at least 3 and at most 20 characters, we could indicate this range also in curly brackets. [a-z]{3,20}. For three to infinity, we would simply remove the 20. So our definition for the local part would look like this [a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+ now. We add an @ as separation between the local and domain part. 

[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@ 

Since the @ character should occur exactly once, we don't need a multiplicity here. The domain part has now again its own range definition, which characters can be taken and how many characters this part contains. Let's assume that we limit the character range to upper and lower case letters, numbers and the underline. For this range of characters there is already a predefined range called \w. Pay attention to the upper and lower case because \W means that it can be all characters except the characters just mentioned. So this is the negation. Since there should be at least 1 character here, too, it looks like this: 

[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@\w+

Now only the domain tail is missing. This can be .de, .com but also .info. That means we have a dot followed by 2-4 upper and lower case letters. Because the dot as such can be any character in a regular expression we have to escape it with a prefixed backslash if we really mean the dot character. Then follows the domain tail. The regular expression looks like this: 

[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@\w+\.[a-zA-Z]{2,4}

If we now leave the regular expression as it is and our text contains, for example, the misspelled mail address [email protected], then the text [email protected] would be found because we have not specified that there is a word limit. We can specify this with \b before and after the expression. 

\b[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@\w+\.[a-zA-Z]{2,4}\b

How to Implement this in FileMaker

Now we want to implement such a search in FileMaker. So that you can also create your own regex later, we create a field in FileMaker in which you write the regex expression (Search). A field in which we specify the text to be searched (Text), a field that specifies whether there were any matches at all in the text for the pattern (FindMatches), a field that specifies how many matches there are (MatchCount) and a field in which the matches are listed in a list (Catches).

Advent23_D19_B1.png

With the RegEx.Match function, we first test whether there is at least one structure in the text that matches our pattern. If this is the case, the function returns a 1. In the parameters of this function, we start by entering the pattern, followed by the text we want to search. This is followed by the compiler options. Here we can specify further options for compiling. The following options are available here:

Compile option Number Description
Caseless 1 Do caseless matching
Multiline 2 ^ and $ match newlines within data
Dot All 4 . matches anything including NL
Extended 8 Ignore white space and # comments
Anchored 16 Force pattern anchoring
Dollar End Only 32 $ not to match newline at end
Ungreedy 512 Invert greediness of quantifiers
No Auto Capture 4096 Disable numbered capturing parentheses (named ones available)
Auto Callout 16384 Compile automatic callouts
FirstLine 262144 Force matching to be before newline
Dup Names 524288 Allow duplicate names for subpatterns
Newline CR 1048576 Set CR as the newline sequence
Newline LF 2097152 Set LF as the newline sequence
Newline CRLF 3145728 Set CRLF as the newline sequence
Newline Any 4194304 Recognize any Unicode newline sequence
Newline Any CRLF 5242880 Recognize CR, LF, and CRLF as newline sequences
BSR Any CRLF 8388608 \R matches only CR, LF, or CRLF
BSR Unicode 16777216 \R matches all Unicode line endings
JavaScript Compatible 33554432 JavaScript compatibility
No start optimize 67108864 Disable match-time start optimizations

If you want to combine these options, you can add the individual values together. In this example, we have selected the option 512 and 1. So we want our pattern evaluation to be Ungreedy, which means that we want the smallest possible match to be displayed and the 1 stands for the fact that we don't care about upper and lower case in the evaluation. 

The function then has another parameter and this contains the options that we set for the execution. Here, too, we can choose from various values and combine them.

Execute option Number Description
Anchored 16 Force pattern anchoring
Not BOL 128 Subject string is not the beginning of a line
Not EOL 256 Subject string is not the end of a line
Not Empty 1024 An empty string is not a valid match
Partial 32768 Allow partial results.
Newline CR 1048576 Set CR as the newline sequence
Newline LF 2097152 Set LF as the newline sequence
Newline CRLF 3145728 Set CRLF as the newline sequence
Newline Any 4194304 Recognize any Unicode newline sequence
Newline Any CRLF 5242880 Recognize CR, LF, and CRLF as newline sequences
BSR Any CRLF 8388608 \R matches only CR, LF, or CRLF
BSR Unicode 16777216 \R matches all Unicode line endings
No start optimize 67108864 Disable match-time start optimizations
Partial Hard 134217728 Return partial result if found before .
Not Empty At Start 268435456 An empty string at the start of the subject is not a valid match
UCP 536870912 Use Unicode properties for \d, \w, etc.

In our case, we do not want to enter a specific value here and therefore enter a 0.

Set Field [ DoorNineteen::FindMatches ; MBS( "RegEx.Match"; 
	DoorNineteen::Search; DoorNineteen::Text; 512+1; 0 ) ] 

Now we also want to know what these hits look like. To do this, we first compile the pattern with the RegEx.Compile function. We pass our search pattern to the function and again the compiler options that we have seen above. The function gives us a reference of the pattern with which we can continue working in the RegEx.FindMatches function. This function returns a list with the results that have been found for the searched pattern.

Set Variable [ $regex ; Value: MBS("RegEx.Compile"; DoorNineteen::Search; 512+1) ] 
Set Variable [ $Match ; Value: MBS("RegEx.FindMatches"; $regex; DoorNineteen::Text; 0; 1) ] 
Set Field [ DoorNineteen::Catches ; $Match ]

Using this list, we can also use the ValueCount function in FileMaker to determine how many hits we have in the text.

Set Variable [ $count ; Value: ValueCount ( $Match ) ] 
Set Field [ DoorNineteen::MatchCount ; $count ] 

Last but not least, we need to release the references that we have created. We use ReleaseAll for this. 

Not only can we search for the entries in a text, we can also replace them. For example, if you want to delete the mail addresses from the text for data protection reasons or replace them with a placeholder, we can use the RegEx.Replace and RegEx.ReplaceAll functions. With the RegEx.Replace function, we can replace individual hits by specifying the index, whereby RegEx.ReplaceAll replaces all hits. In this example, we have a field in which we can enter the text we want to replace all hits with. We then write the result text in a separate field.

Set Field [ DoorNineteen::Result ; MBS( "RegEx.ReplaceAll"; 
	DoorNineteen::Text; DoorNineteen::Search; DoorNineteen::Replace ) ] 

I hope you enjoyed this door too and we'll see you again tomorrow.

Link to comment
Share on other sites

Door 20 - MapView

Fact of the day
The advantage of Apple Mapview is that you do not have a total quota of calls that are assigned to your app, but these calls apply per device and are therefore almost impossible to reach.

Did you know that you can use the map material from Apple Mapview on your Mac and iOS devices in FileMaker? I'll show you how it works in this door. 

Show map with options 

First of all, we want to display a map that we can then work with. Two functions are available to us for this purpose. One is MapView.CreateWithSize with which we can create the map in a window by specifying the position and size. We would like to use the second option MapView.CreateWithControl, because here we position the map with the help of a control. The control can be a rectangle, for example. We then give this control a name so that we can also address it. In our case Map. Then we call the function. First we specify the window reference. If the window is in the foreground, it is sufficient to enter a 0. Then the name of the control follows. If necessary, we can then specify an offset with X and Y. This is oriented to the top left corner of the control and causes the map to be shifted.

Set Variable [ $$MapView ; Value: MBS("MapView.CreateWithControl"; 0; "Map")

We can now go to the Exploration gate on the map. In the plugin we have some functions that can show and hide various options on the map. For example, you can display the outlines of buildings, traffic, points of interest such as places of interest and stores or your own position on the map. But also controls such as the compass, the scaling bar or the zoom controls can be displayed. We have a separate function for each of these properties. In our solution, we have now built in buttons with which you can switch these features on and off. For each feature there is also a field in the database that holds the current value for us. When creating the card, we switch all these properties off once and fill the corresponding fields with the value 0.

Set Variable [ $r ; Value: MBS("MapView.SetShowsBuildings"; $$MapView; 0) ] 
Set Field [ DoortTwenty::Buildings ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsTraffic"; $$MapView; 0) ] 
Set Field [ DoortTwenty::Traffic ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsPointsOfInterest"; $$MapView; 0) ] 
Set Field [ DoortTwenty::POI ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsUserLocation"; $$MapView; 0) ] 
Set Field [ DoortTwenty::UserLocation ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsCompass"; $$MapView; 0) ] 
Set Field [ DoortTwenty::Compass ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsScale"; $$MapView; 0) ] 
Set Field [ DoortTwenty::Scale ; 0 ] 
# 
Set Variable [ $r ; Value: MBS("MapView.SetShowsZoomControls"; $$MapView; 0) ] 
Set Field [ DoortTwenty::Zoom ; 0 ] 

Each button then gets a script that changes the value when the button is pressed. For the display of buildings, such a script looks like this: 

If [ DoortTwenty::Buildings = 1 ] 
	Set Variable [ $buildings ; Value: 0 ] 
Else
	Set Variable [ $buildings ; Value: 1 ] 
End If
Set Field [ DoortTwenty::Buildings ; $buildings ] 
Set Variable [ $r ; Value: MBS( "MapView.SetShowsBuildings"; $$MapView; $buildings ) ] 

If the current value was 1, then the value of the variable is set to 0 and if it was not 1, then the value of the variable is set to 0. The variable now contains the new value, which is saved once in the corresponding field and is then used to set the appropriate function.

The button has a conditional formatting that colors the button green if the value of the corresponding field is one. Here you can see what the map can look like when the options are switched on.

Advent23_D20_B1.png Advent23_D20_B2.png

Plan a route

But we can not only display a map, we can also calculate routes. To do this, we first need our starting point and our destination point. For the starting point, you could also use CurrentLocation to determine your location, for example. You saw how this works in door 8 of this calendar. We can use the MapView.PlanRoute function to plan a route between the start and end points. In addition to these two parameters, we pass a mode to the function. This mode determines how we see our route on the map and what our return looks like. We can choose from various options and combine them by adding the values.

Value Description
1 show route on map
2 show alternative routes on the map
4 show start of route with a pin
8 show end of route with a pin
16 zoom map to show whole of the router
32 return result as JSON
64 include poly lines in JSON

In this example, we have decided that the individual route should be displayed on the map. This should have a start and an end pin and should be displayed as large as possible in the center of the map. We would like to have the result for this route as JSON, because the JSON also provides us with some information that we will take a closer look at in a moment. 

In the function we can also specify optional additional parameters to determine the transport method for which the route should be calculated. In addition, we can specify the names for the destination and start pin and also select the color of the two pins.

Set Variable [ $JSON ; Value: MBS( "MapView.PlanRoute"; 
      $$MapView; DoortTwenty::Start; DoortTwenty::Target; 1+4+8+16+32 ; 1  ) ]

We now receive a JSON as a return that we can continue to work with. This contains some information. Here you can see such a JSON. We have shortened the JSON in the route instructions.

 {
  "routes" : [
    {
      "distance" : 39063,
      "steps" : [
        {
          "notice" : null,
          "distance" : 0,
          "instructions" : null,
          "distanceText" : "0 m",
          "transportType" : 1
        },
        {
          "notice" : null,
          "distance" : 213.63,
          "instructions" : "Turn left onto Benrather Straße",
          "distanceText" : "200 m",
          "transportType" : 1
        },
        {
          "notice" : null,
          "distance" : 249.81999999999999,
          "instructions" : "Turn right onto Kasernenstraße",
          "distanceText" : "250 m",
          "transportType" : 1
        },
		…
        {
          "notice" : null,
          "distance" : 186.06,
          "instructions" : "Arrive at the destination",
          "distanceText" : "200 m",
          "transportType" : 1
        }
      ],
      "advisoryNotices" : [
        "Düsseldorf, DE, This zone comprises most major roads entering the city. Nevertheless, 
         the driving restriction does not apply to highways, thus an emissions stickers is not required 
         on the A44 north of the city and on the A46 south of the city.",
        "Cologne, DE, The zone covers a large part of the city of Cologne, 
         it extends beyond the city centre and affects both the east and west side of the Rhine.",
        "Directions begin at closest open road."
      ],
      "expectedTravelTime" : 3539,
      "distanceText" : "39 km",
      "name" : "A57",
      "transportType" : 1
    }
  ],
  "source" : {
    "ISOcountryCode" : "DE",
    "subThoroughfare" : null,
    "areasOfInterest" : null,
    "subLocality" : null,
    "administrativeArea" : "North Rhine-Westphalia",
    "country" : "Germany",
    "thoroughfare" : null,
    "ocean" : null,
    "latitude" : 51.225863400000001,
    "name" : "Düsseldorf",
    "altitude" : 0,
    "timeZone" : "CET",
    "longitude" : 6.7722986000000001,
    "postalCode" : null,
    "inlandWater" : null,
    "locality" : "Düsseldorf",
    "subAdministrativeArea" : "Düsseldorf"
  },
  "destination" : {
    "ISOcountryCode" : "DE",
    "subThoroughfare" : null,
    "areasOfInterest" : null,
    "subLocality" : null,
    "administrativeArea" : "North Rhine-Westphalia",
    "country" : "Germany",
    "thoroughfare" : null,
    "ocean" : null,
    "latitude" : 50.937522899999998,
    "name" : "Cologne",
    "altitude" : 0,
    "timeZone" : "CET",
    "longitude" : 6.9594800000000001,
    "postalCode" : null,
    "inlandWater" : null,
    "locality" : "Cologne",
    "subAdministrativeArea" : "Cologne"
  }
}

Now we can use JSON functions to read out the required data. In our case, this is the travel time, which we get in seconds and then have to divide this by 60 again to get the travel time in minutes.

Set Variable [ $time ; Value: JSONGetElement ($JSON ; "routes.[0].expectedTravelTime" ) ] 
Set Variable [ $time ; Value: Int ( $time/60) ] 
Set Field [ DoortTwenty::Time ; $time & " minutes" ] 

We also want to read out the route instructions. To do this, we first determine how many instructions are in the array and then go through them in a loop and put the instruction together with the distance information. Finally, we write the combined text into the appropriate field.

Set Variable [ $routeArray ; Value: JSONGetElement ( $JSON; "routes.[0].steps" ) ] 
Set Variable [ $count ; Value: MBS( "JSON.GetArraySize"; $RouteArray  ) ] 
Set Variable [ $i ; Value: 1 ] 
Set Variable [ $text ; Value: "Start: " & DoortTwenty::Start ] 
Loop
	Set Variable [ $inst ; Value: JSONGetElement ( $JSON ; "routes.[0].steps.["& $i &"].instructions" ) & 
		" in " & JSONGetElement ( $JSON ; "routes.[0].steps.["& $i &"].distanceText" ) ] 
	Set Variable [ $Text ; Value: $text & "¶¶" & $inst ] 
	Exit Loop If [ $i ≥ $count-1 ] 
	Set Variable [ $i ; Value: $i+1 ] 
End Loop
Set Variable [ $text ; Value: $text & "¶¶You have reached your destination: " & DoortTwenty::Target ] 
Set Field [ DoortTwenty::Route ; $text ] 
Advent23_D20_B3.png

If we experiment with different start and destination locations, we see that the old routes and pins remain on the map. To avoid this, we remove the pins with MapView.RemoveAnnotations and the routes with MapView.RemoveOverlays when starting the route planning script.

Set Variable [ $r ; Value: MBS( "MapView.RemoveAnnotations"; $$MapView ) ] 
Set Variable [ $r ; Value: MBS( "MapView.RemoveOverlays"; $$MapView ) ] 
...

When we are finished and want to hide the map again, we call the MapView.ReleaseAll function. This and much more awaits you in the MapView component.

Then I hope that Santa Claus has also installed the navigation system in his sleigh so that he can find you at Christmas.

Link to comment
Share on other sites

Door 21 - SendMail

Fact of the day
The first e-mail as we know it today was sent by Ray Tomlinson in 1971.

Did you know that you can also use the MBS FileMaker Plugin to create mails according to your wishes and send them to one or more people? I will show you how this works in today's door. 

First of all, we create all the fields we need in our project. Today we want to add a graphic header and footer to our email. We put the image files for each of these in a container. Then we need a field for the subject and a field for the recipient. In this example we only send the mail to one recipient, but I will explain how you can send the mail to other recipients. We also need a field for the mail text. We also want to send an attachment with the mail. We also need a container for this. In addition, we can now also create fields containing our data for the email layout. The layout can then look like this, for example:

Advent23_D21_B1.png

Now we can write the script. First we convert the text we want to send by mail into HTML. For this we have the Text.TextToHTML function. The cool thing about this function is that we not only transfer the text into the HTML, but also the formatting of the text is transferred and thus an HTML with the matching styling tags is created.

# Read HTML Text from field
Set Variable [ $HTML ; Value: MBS( "Text.TextToHTML"; DoorTwentyOne::Text; 1) ] 

In the next step, we place a placeholder for the header in front of the HTML and append a placeholder for the footer at the end. We replace the strings $$Header$$ and $$Footer$$ in a moment. Then we build the rest of the HTML structure around it.

Set Variable [ $HTML ; Value: "$$Header$$" & $HTML & "$$Footer$$" ] 
Set Variable [ $HTML ; Value: "<html><body>" & $HTML & "</body></html>" ] 

We then replace the header and footer with an img tag that refers to an attached image in our mail. With the cid in front of the image name we say that we want to attach the image and also integrate and display it in the mail. We will come to the attaching of these images in a moment. For now, we'll just create the template for the images.

Set Variable [ $HTML ; Value: Substitute ($HTML; "$$Header$$"; "<img src=\"cid:Header.png\">") ] 
Set Variable [ $HTML ; Value: Substitute ($HTML; "$$Footer$$"; "<img src=\"cid:Footer.jpg\">") ] 

There is a bit more to a mail than just the content HTML, which is why we create a mail in the working memory. With the reference as a parameter in other functions, we can then set further information for this mail.

Set Variable [ $Mail ; Value: MBS("SendMail.CreateEmail") ]

In order to be able to send the email, we need a valid mail account for which we have to enter information. We need the user name (SendMail.SetSMTPUserName), which is usually also the mail address, the password (SendMail.SetSMTPPassword) for the account, as well as the SMTP outgoing server ("SendMail.SetSMTPServer). In the SendMail.SetSMTPServer function we can also specify whether we want to use TLS encryption. This is what we want in our example and for this reason we specify 1 in the parameters.

Set Variable [ $r ; Value: MBS( "SendMail.SetSMTPUserName"; $Mail; DoorTwentyOne::UserName ) ] 
Set Variable [ $r ; Value: MBS( "SendMail.SetSMTPPassword"; $Mail; DoorTwentyOne::Password ) ] 
Set Variable [ $r ; Value: MBS( "SendMail.SetSMTPServer"; $Mail; DoorTwentyOne::SSLServer ; 1 ) ] 

Now we can set the individual information for the email itself. For example, we set the recipient of the email with SendMail.AddTo. In addition to this one recipient that we use here in the example, you can, for example, also loop through data records in a relational table and use this function in each loop call to add another recipient. But you can not only add standard recipients, you also have other functions for other types of recipients. You can add recipients in the CC with the SendMail.AddCC function or in the BCC with SendMail.AddBCC. If you would like to use different types in this loop, you can use the SendMail.AddRecipient function. Here you simply enter the recipient type as an additional parameter. You can choose between the types TO, BCC, CC or ReplyTo. 

Now, of course, we also want to determine the content of the mail. With SendMail.SetSubject we specify the subject for the e-mail. We can also set the HTML text that we have already put together. To do this we use the SendMail.SetHTMLText function.

# Recipient
Set Variable [ $r ; Value: MBS( "SendMail.AddTo"; $Mail; DoorTwentyOne::Recipient ) ] 
# Subject
Set Variable [ $r ; Value: MBS( "SendMail.SetSubject"; $Mail; DoorTwentyOne::Subject ) ] 
# Content
Set Variable [ $r ; Value: MBS("SendMail.SetHTMLText"; $Mail; $HTML) ] 

Now we need to add our attachments. As already mentioned, we now also need to attach our inline graphics. To do this, we use the SendMail.AddAttachmentContainer function. In the parameters, we specify the mail reference and the container in which the image is currently located. We also enter the file name, the type and the InlineID that we have previously defined in the HTML.

Set Variable [ $r ; Value: MBS("SendMail.AddAttachmentContainer"; $Mail; DoorTwentyOne::Header; "Header.png"; "image/png"; "Header.png") ] 
Set Variable [ $r ; Value: MBS("SendMail.AddAttachmentContainer"; $Mail; DoorTwentyOne::Footer; "Footer.png"; "image/png"; "Footer.png") ] 

You can use the same function to attach normal attachments that are not displayed in the email. You do not need all the parameters here. The mail ID and the container in which the attachment is located are sufficient here.

Set Variable [ $r ; Value: MBS( "SendMail.AddAttachmentContainer"; $Mail; DoorTwentyOne::Attachment ) ] 

Now we have to create the CURL connection via which we want to send the mail. First we create a new CURL connection with CURL.New. With SendMail.PrepareCURL we then set the settings that we have already made for the mail in the CURL. We then use CURL.Perform to send the mail.

Set Variable [ $CURL ; Value: MBS("CURL.New") ] 
Set Variable [ $r ; Value: MBS("SendMail.PrepareCURL"; $Mail; $CURL) ] 
Set Variable [ $r ; Value: MBS("CURL.Perform"; $CURL) ] 
Advent23_D21_B2.png

If we were able to send the mail, we receive an "OK" back from the function. If there was a problem with the connection, a text with an error number is returned instead of the OK. We have to be careful here. If there was an error during sending, we cannot intercept this with the MBS("IsError") function, because there was no error with the function but in the connection. For this reason, we must check here whether the function has returned OK and, if not, display an error message to inform the user.

If [ MBS("IsError") ] 
	Show Custom Dialog [ "Error" ; "An error occurred while sending the email" ] 
End If
If [ $r ≠ "OK" ] 
	Show Custom Dialog [ "Error" ; "An error occurred while sending the email:¶" & $r ] 
End If

This is what such an error message might look like. In this case, the corresponding sender address was written incorrectly.

Last but not least, we have to release the reference of the CURL connection and the reference to our mail.

Set Variable [ $r ; Value: MBS("CURL.Release"; $CURL) ] 
Set Variable [ $r ; Value: MBS("SendMail.Release"; $Mail) ] 
Advent23_D21_B3.png

Now you can send your Christmas mail to your hearts desire. Have fun with it.

Link to comment
Share on other sites

Door 22 - ListDialog

Fact of the day
Over the last few days we have become familiar with some dialogs that you can display with MBS. One that we were unable to present this Advent due to time constraints is the FileDialog. With this you can create a dialog to select a file.

Sometimes you want to be able to display a dialog from which the user can select one or more options. The ListDialog component allows you to do this. I will introduce this component to you in this door.

The ListDialog is again another dialog that is accessible across the entire solution. This means that I can set settings for this dialog in various scripts and these are then applied to the dialog. So we have to reset the settings of the dialog first if we want to create a new and fresh dialog. For this we have the function ListDialog.Reset.

Set Variable [ $r ; Value: MBS( "ListDialog.Reset" ) ] 

Now we can make settings for the dialog: First, we want a text to be displayed in the dialog. We set this with the ListDialog.SetPrompt function. We set the title for the dialog with ListDialog.SetWindowTitle. 

The dialog can have up to 3 buttons. One button indicates a cancel button. Then we have a Select button, which confirms the entries in the dialog. The third button, which is not displayed by default, can be passed an expression with ListDialog.SetOtherButtonEvaluate, which is executed when the button is used. This button appears when we set the text of the button with ListDialog.SetOtherButtonLabel. In our case, we do not need this. We can also change the other two buttons with the functions ListDialog.SetCancelButtonLabel and ListDialog.SetSelectButtonLabel.

Set Variable [ $r ; Value: MBS( "ListDialog.SetPrompt"; "Please make your selection") ] 
Set Variable [ $r ; Value: MBS( "ListDialog.SetWindowTitle"; "Door 22" ) ] 
Set Variable [ $r ; Value: MBS("ListDialog.SetSelectButtonLabel"; "My Presents") ]
Advent23_D22_B1.png

If we now create a very long list in this dialog, then it can happen that this list quickly becomes confusing. How good it is that you can display a filter above the list with which you can limit the list. To show or hide the filter, use the ListDialog.SetShowsFilter function and pass a 1 in the parameters to show or a 0 to hide. If you already want to display a filter in the field in the beginning, you can specify this with ListDialog.SetFilter.

Set Variable [ $r ; Value: MBS("ListDialog.SetShowsFilter" ; 1) ] 

Advent23_D22_B2.png

As of this year, you can also use checkboxes for multiple selections in the list dialog. To display the checkboxes, we use the ListDialog.SetShowCheckboxes function. We can then use these in the dialog. If we have checked several checkboxes and then use a filter, the already selected checkboxes remain active even though they are no longer displayed by the filter.

Set Variable [ $r ; Value: MBS("ListDialog.SetShowCheckboxes"; 1) ] 
 

Now, of course, we have to fill the list dialog with entries. The entry can consist of up to 5 columns in total. How many columns we see depends on the value set in the ListDialog.SetColumnCount function. By default this value is 1. With the function ListDialog.AddItemToList we can add a single item to the list. If we have an entry with more than one column, we must separate the individual columns with a Char(9) character. In addition to the visible entries in the columns, we can also specify a tag for each column. This is not displayed, but can be queried later.

Set Variable [ $r ; Value: MBS("ListDialog.SetColumnCount"; 5) ] 
Set Variable [ $r ; Value: MBS("ListDialog.AddItemToList"; "1st Column" & Char(9) & "2nd Column" 
   & Char(9) & "3rd Column" & Char(9) & "4th Column" & Char(9) & "5th Column"; "Tag123") ] 
Advent23_D22_B3.png
Advent23_D22_B4.png

We can also specify in the Optional whether the entry is a header. A header delimits an area and looks a little different. Here, for example, you can see the header: Fruit. A header can also not be selected.


Advent23_D22_B5.png

With the ListDialog.AddItemToList function, we can only add single entries to the list. If we want to add several items to the list at the same time, we can use the ListDialog.AddItemsToList function. The entries are then transferred as a list. So we can first pass a list of titles to the function and also a list with the corresponding tags.

Set Variable [ $r ; Value: MBS( "ListDialog.AddItemToList"; "Hello" ; "Greetings" ) ] 
Set Variable [ $r ; Value: MBS( "ListDialog.AddItemToList"; "Good Morning" ; "Greetings" ) ] 
Set Variable [ $r ; Value: MBS( "ListDialog.AddItemToList"; "Banana" ; "Fruit" ) ] 
Set Variable [ $r ; Value: MBS( "ListDialog.AddItemToList"; "Orange" ; "Fruit" ) ] 
Set Variable [ $r ; Value: MBS( "ListDialog.AddItemsToList"; "Broccoli¶Apple¶GoodEvening"; "Vegetable¶Fruit¶Greetings") ] 
 

To display the dialog, we need to call the ListDialog.ShowDialog function. The dialog is hidden again by pressing one of the buttons on the dialog.

Set Variable [ $r ; Value: MBS("ListDialog.ShowDialog") ] 

Now, of course, we also want to know what has been selected in the dialog. We can query this with various functions. With the functions ListDialog.GetCheckedTitles and ListDialog.GetCheckedTags we get information about the lines that have been checked in the dialog. The two functions provide us with a list of either the titles that were selected with the checkbox or the tags.

Set Variable [ $CheckedTitles ; Value: MBS("ListDialog.GetCheckedTitles") ] 
Set Variable [ $CheckedTags ; Value: MBS("ListDialog.GetCheckedTags") ] 

With the functions ListDialog.GetSelectedTitle and ListDialog.GetSelectedTag we get two lists with the titles and tags of the rows that have been selected at the time of confirmation.

Set Variable [ $SelectedTitle ; Value: MBS("ListDialog.GetSelectedTitle") ] 
Set Variable [ $SelectedTag ; Value: MBS("ListDialog.GetSelectedTag") ] 
Advent23_D22_B6.png

I hope you enjoyed this excursion into the world of list dialogs and that we'll see us again tomorrow in door 23.

Link to comment
Share on other sites

Door 23 - MailParser

Fact of the day
With the MBS FileMaker Plugin you can not only analyze or send mails. You can also retrieve mails directly from your email account via CURL using an IMAP connection.

Our advent calendar is slowly coming to an end and so we have already reached door 23. In door 21 we saw how to send emails with inline graphics. Today we would like to look at how we can find out what is in an email if we only have the mail file. For this we have the component EmailParser.

The mail file can either be in a container, as a file on the hard disk or we have the source code of an email. In all cases, we can read the data from the mail with the MBS FileMaker Plugin. For each case we have a separate function that loads the mail into the working memory and returns the reference with which we can work. If we have a sourecode of the email we use the function EmailParser.Parse to parse the email. Do we have the mail as a file on the disk then we use the function EmailParser.ParseFile. In our example, we place the email file in a container. To parse this mail we use the function EmailParser.ParseContainer

Set Variable [ $Mail ; Value: MBS( "EmailParser.ParseContainer"; DoortTwentyThree::Mail ) ] 

Now, of course, we want to know where the mail comes from and whether it has been sent to other people. We have all this information available as addresses which we can read out using the EmailParser.Addressfunction by specifying the index. In order to know how many addresses are involved, we query this number with EmailParser.AddressCount. Now we can loop through the individual addresses. The loop starts at index zero and runs up to index $AdressCount-1. As we go through the addresses, we want to divide them into two groups. One group stands for the sender (Sender and From) and the other group for the different recipients (TO, CC and BCC). With the function EmailParser.Address we can query not only the address, but also the type and the name of the address. In the parameters of the function we first enter the mail reference and the address index. This is followed by the selector, in which we specify which information we want. You can choose from the following selectors: type, name or email. We want the email and also the type with which we can then assign the addresses to the correct group. Depending on which group the address belongs to, it will be appended to the text in the variable $From or $To.

Set Variable [ $AddressCount ; Value: MBS( "EmailParser.AddressCount"; $Mail ) ] 
Set Variable [ $AddressIndex ; Value: 0 ] 
Set Variable [ $From ; Value: "" ] 
Set Variable [ $To ; Value: "" ] 
Loop
	Set Variable [ $Address ; Value: MBS( "EmailParser.Address"; $Mail; $AddressIndex; "email" ) ] 
	Set Variable [ $AddressType ; Value: MBS( "EmailParser.Address"; $Mail; $AddressIndex; "type" ) ] 
	If [ $AddressType = "Sender"  or  $AddressType = "From" ] 
		Set Variable [ $From ; Value: $From  & $Address & "¶" ] 
	Else If [ $AddressType = "TO"  or  $AddressType = "CC" or $AddressType = "BCC" ] 
		Set Variable [ $To ; Value: $To  & $Address & "¶" ] 
	End If
	Set Variable [ $AddressIndex ; Value: $AddressIndex + 1 ] 
	Exit Loop If [ $AddressIndex  ≥ $AddressCount ] 
End Loop
Set Field [ DoortTwentyThree::To ; $To ] 
Set Field [ DoortTwentyThree::From ; $From ] 

It would also be good to know what the mail is about, so we read the subject with EmailParser.Subject. 

Set Variable [ $Subject ; Value: MBS( "EmailParser.Subject"; $Mail ) ] 
Set Field [ DoortTwentyThree::Subject ; $Subject ]

We can also use the EmailParser.ReceiveDate and EmailParser.SentDate functions to get the send and receive dates.

Set Variable [ $RecDate ; Value: MBS( "EmailParser.ReceiveDate"; $Mail ) ] 
Set Field [ DoortTwentyThree::ReceiveDate ; $RecDate ] 
# 
Set Variable [ $SentDate ; Value: MBS( "EmailParser.SentDate"; $Mail ) ] 
Set Field [ DoortTwentyThree::SentDate ; $SentDate ] 

We can either query the content of the mail as plain text or we can directly retrieve the HTML in which the formatting can also be recognized. We can then also display this in a web viewer, for example.

Set Variable [ $PlainText ; Value: MBS( "EmailParser.PlainText"; $Mail ) ] 
Set Field [ DoortTwentyThree::PlainText ; $PlainText ] 

Set Variable [ $HTML ; Value: MBS( "EmailParser.HTMLText"; $Mail ) ] 
Set Field [ DoortTwentyThree::HTML ; $HTML ] 

Now only the attachments are missing. For the attachments, we differentiate between normal attachments and inline graphics. If we use the EmailParser.AttachmentCount function to determine the number of attachments, we only determine the number of standard attachments. As with the addresses, we also have the option of querying various values with the EmailParser.Attachment function. The selectors we can use here are Filename, MimeType, MimeVersion, ContentType, ContentTransferEncoding, ContentDisposition, ContentDescription, contentId, text or container. In our case, we only want a list of the filenames of the attachments.

Set Variable [ $AttachmentCount ; Value: MBS( "EmailParser.AttachmentCount"; $Mail ) ] 
Set Variable [ $AttachmentIndex ; Value: 0 ] 
Set Variable [ $FileNames ; Value: "" ] 
Loop
	Set Variable [ $AttachmentName ; Value: MBS( "EmailParser.Attachment"; $Mail; $AttachmentIndex; "Filename" ) ] 
	Set Variable [ $FileNames ; Value: $FileNames & $AttachmentName & "¶" ] 
	Set Variable [ $AttachmentIndex ; Value: $AttachmentIndex + 1 ] 
	Exit Loop If [ $AttachmentIndex  ≥ $AttachmentCount ] 
End Loop
Set Field [ DoortTwentyThree::Attatchments ; $FileNames ] 

If you want to write a email directly as a file, you can use the EmailParser.WriteAttachment function. This writes an attachment that is defined via the index to a storage location specified with a path.

Working with InlienAttachments is very similar to the standard attachment. We use the EmailParser.InlineCount function to determine the number of inline attachments and query the information in a loop with the EmailParser.Inline function. The already known selectors are also used here. We can then use the EmailParser.WriteInline function to write the inline graphic to a file again.

Set Variable [ $InlineCount ; Value: MBS( "EmailParser.InlineCount"; $Mail ) ] 
Set Variable [ $InlineIndex ; Value: 0 ] 
Set Variable [ $InlineFileNames ; Value: "" ] 
Loop
	Set Variable [ $InlineName ; Value: MBS( "EmailParser.Inline"; $Mail; $InlineIndex; "Filename" ) ] 
	Set Variable [ $InlineFileNames ; Value: $InlineFileNames & $InlineName & "¶" ] 
	Set Variable [ $InlineIndex ; Value: $InlineIndex + 1 ] 
	Exit Loop If [ $InlineIndex  ≥ $InlineCount ] 
End Loop
Set Field [ DoortTwentyThree::InlineGraphics ; $InlineFileNames ] 

When we have finished our work, we release the storage address for the email with EmailParser.Free.

Set Variable [ $r ; Value: MBS( "EmailParser.Free"; $Mail ) ] 
Advent23_D23_B1.png

Have a look to see if Santa Claus has sent you an e-mail and analyze its content. I look forward to seeing you again tomorrow for the last door.

Link to comment
Share on other sites

Door 24 - Goodies

Fact of the day
Did you know that we have over 2000 functions in the plugin that you can use without a license?

Today we come to the last door of our Advent calendar. I would like to introduce you to some of the free developer functions. MBS has been providing free functions for a number of years to make life easier for you as a developer. Unfortunately, most of these functions only work on the Mac. This is not because we wouldn't like to make the features available to Windows users, but because we don't have the access to Windows, so we can only offer most things on Mac. This year, we were able to make one function available to Windows users. So if you have installed the plugin on Windows and the function is activated, there is an input field with which you can search in the relationship graph.

WinSearch1.jpg

If you want to use the functions, you need to download and install the MBS FileMaker Plugin and the functions you want to use have to be activated. To do this, you have a checkbox in the Windows preferences dialog that you need to check. 

FMPluginPreferencesWin.jpg

On Mac, this dialog is a little more extensive and you can make several selections. You will find the dialog under Settings -> Plug-Ins and then click on the MBS plug-in and click on the Configure button. You get this dialog, which lists the developer goodies. In this dialog you can activate and deactivate the functions with the checkboxes.

PreferencesDialogMac.png

First, you can color your scripts and formulas. This helps you to keep a better overview and find errors more easily

ScriptColors.jpg

In this image you can see, for example, that the Perform Script on Server script step has been highlighted in red because no script has been set. The Set Field script step is orange because we have not yet entered a field. The special thing about the colouring of scripts is that you can change the colors yourself using the fmSyntaxColorizer database, which is included with the plugin in the examples.

Color highlighting of matching brackets also helps you to find errors in your calculations more quickly. This can be a blessing, especially with nested calculations or SQL commands. Simply click on a square, curly or regular bracket and the matching bracket will turn blue.

 

You will also see a blue background if you click on the appropriate structures in loops or conditions. The structures matching the appropriate script step are then highlighted in blue. This helps to maintain an overview, especially with nested loops or conditions.

 

While we're on the subject of loops and conditions, I don't want to leave our codefolding unmentioned, because here you can temporarily collapse parts of the script so that you can keep the overview in large scripts. Codefolding has no influence on the execution of the various parts.


autocomplete.png

Do you sometimes find it difficult to remember a variable name that you have previously used in your script and now need in your calculation? 

With the auto-completion of variables, this should now be easier for you. If you have previously used the variable in your script, you only need to enter the first few letters and you will be shown a list of variables from which you can select the correct one.

There is also a completion for the MBS function name to make your work even easier.


missingvariable.jpg

If you make a mistake when entering a variable, you will see a note in red in the line that the variable you are currently using in the calculation does not yet exist if the variable has not yet been set. 

 

If you do not know where your error is in the script and you want to share the script but not the whole file in a forum, or you write an article about a cool thing in FileMaker, then you do not have to type the script by hand, instead you can copy the text to the clipboard by clicking on the Copy Script Text button at the top right of the Script Workspace, and you can use it from there.

missingvariable.jpg

Very close to this button there are three more buttons with minus and plus. These buttons can be used to enlarge the font in the script area. This is particularly useful for presentations or when you are looking at a screen with several people. If your eyes are not getting any better, you can also set a font size for script texts and formulas directly in the dialog in which you can also activate the goodies. And if you no longer like the font, you can also change it here. 

 

When I write a script, sometimes after several days I don't remember why I wrote the script the way I did and didn't choose another way. That's why comments in a script are essential. With the MBS FileMaker Plugin, you now also have the option of using links in your comments, which you can send directly to a suitable website with one click. 

CommentLinks.jpg

Since 13.5 you even have the possibility to jump to a certain line or to the end or the beginning of your script by clicking on the comment. Here you can see such a comment.

# History on the bottom goto:end ➜🌎 

# back to top: goto:start ➜🌎 

# * added loop in line 15   goto:15 ➜🌎 

debuggerVariable.jpg

Another very useful function is Tooltips for Script Debugger. When using other development tools, we frequently have a feature to inspect values for variables or fields. Just move the mouse over the variable or field and see what's inside. This is very convenient for debugging a script to quickly see values, especially if all the local variables and object properties don't fit a single variable viewer window.

 

I hope you enjoyed the journey to our Developer Goodies. There are many more of these goodies. You can find a list and an even more detailed description of these goodies here.

I wish you lots of fun trying them out. 

The MonkeyBread Software team wishes you a Merry Christmas.

Monkeybread Software Logo with Monkey with Santa hat
23  👈 24 of 24   

You can download the example file here: Advent2023.fmp12.

Link to comment
Share on other sites

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.