For this week’s blog post we are going to start creating a script that will allow users to create a bulk filing for submission to EDGAR. In order to accomplish this we are going to split the blog into three parts: interface, project file processing, and finally the submission creation. Since this is the first blog in the series we are going to start with the interface.
Friday, December 14. 2018
LDC #114: Bulk Filings Part 1
Our interface needs a few things. The user will need to be able to specify a list of files to add to the bulk submission. They will also need to specify the name of the resulting XML file. We could just run the BrowseOpenFiles function and then the BrowseSaveFile function in succession but that isn’t very cohesive and the user would need to know everything that is required of him or her before running the script. Additionally, it would mean that all the files need to be selected from the same location. A dialog is a much better solution for this operation to allow the user flexibility in the workflow and process.
The first step of designing a dialog involves identifying what controls and what types of controls we need. For the list of files that will be added to the bulk submission, we can use a list box control. To add files to the list, we can have an “add” button that runs the BrowseOpenFiles function. If the user accidentally adds the wrong file, a button to remove a file would be nice. To save the bulk submission, we need a text edit control to store the name of the file and a browse button so the user doesn’t need to type in the full path to the location that submission will be created.
The code for the completed dialog could look something like this:
#define DC_BULK 201 #define DC_LIST 301 #define DC_ADD 101 #define DC_REMOVE 102 #define DC_BROWSE 103 BulkDialog DIALOGEX 0, 0, 344, 190 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Bulk Filing" FONT 8, "MS Shell Dlg", 400, 0 { CONTROL "Files", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 4, 20, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 26, 9, 314, 1, 0 CONTROL "", DC_LIST, "listbox", LBS_NOTIFY | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | WS_TABSTOP, 12, 16, 276, 120, 0 CONTROL "&Add...", DC_ADD, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 16, 45, 12, 0 CONTROL "&Remove", DC_REMOVE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 31, 45, 12, 0 CONTROL "Submission", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 137, 42, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 48, 142, 292, 1, 0 CONTROL "&Bulk Submission File:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 150, 72, 8, 0 CONTROL "", DC_BULK, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 88, 148, 200, 12 CONTROL "Browse...", DC_BROWSE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 148, 45, 12, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 166, 335, 1, 0 CONTROL "&Create", IDOK, "BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 232, 172, 50, 14 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 287, 172, 50, 14 }
This code will create a dialog that follows Novaworks’ dialog design specification, so it’s going to look like the other dialogs in GoFiler:
You can design your own dialog to your own specification but make sure to include the controls discussed above because we will be adding the supporting code for those controls next.
Since this script is going to be built upon in another blog post, we need to make sure the files list is available as a global variable. So we can start off with globals and function definitions. As a reminder this script on its own will not do anything other than display a functional dialog. All the processing after the dialog is closed will be added in a later post.
// // Function Definitions and Globals // -------------------------------- int bd_load (); void bd_set_state (); int bd_action (int id, int action); int bd_validate (); string files[]; string bulk;
For our functions we need to define several dialog handlers. The load function will handle loading the initial dialog state. The set state function will handle setting the state of the dialog controls after user actions. The action function handles all the user interaction with the dialog. Finally, the validate function will check the user supplied data, and, if the data is valid, save the data to our global variables.
We also have our two global variables. The files variable represents the list of files to be added to the bulk submission. The bulk variable is the name of the resulting bulk submission XML file. Pretty straight forward.
We also need a few defines for the dialog controls as well as a couple string constants for later. The dialog control defines were shown in the above dialog code to show a complete dialog but I moved them here when coding the script.
// // Dialog and Defines // -------------------------------- #define DC_BULK 201 #define DC_LIST 301 #define DC_ADD 101 #define DC_REMOVE 102 #define DC_BROWSE 103 #define ADD_FILTER "All Projects|*.gfp;*.eis;*.xml;&GoFiler Projects|*.gfp&EDGAR Internet Submissions|*.eis&XML Submission Files|*.xml&All Files *.*|*.*" #define SAVE_FILTER "XML Submission Files|*.xml&All Files *.*|*.*"
As stated above the first set of defines are the identifiers for the dialog controls. The second set are string defines we can use as filters for the browse functions. These defines give the BrowseOpenFiles and BrowseSaveFile functions a little more polish as the user is automatically filtered to the appropriate types of files. Making the defines means if we wanted to add support for other project formats at a later date we only need to change the define.
Now that all the easy stuff is out of the way let’s start working on the dialog functions. A logical place to start is the bd_load function.
int bd_load() { int ix, mx; // Load List mx = ArrayGetAxisDepth(files); for (ix = 0; ix < mx; ix++) { ListBoxAddItem(DC_LIST, files[ix]); } EditSetText(DC_BULK, bulk); bd_set_state(); return ERROR_NONE; }
This function does a little more than it needs to for our script. It loads the dialog’s list of files from our global variable as well as the bulk file name. Since all our script does is open this dialog all those global variables are going to be empty. However, by adding these few lines of code we allow for expanded functionality. If we wanted to add logic to fill the list or name from settings files or based on the active window in the application now the dialog would start with the list already entered and allow the user to edit it instead.
In order to accomplish this we use ArrayGetAxisDepth and a simple for loop to add each string from the files list to the list box. The ListBoxAddItem function simply adds the file to the end of the list. Then we use the EditSetText function to make sure the bulk variable is displayed in the DC_BULK text control. Finally we call the bd_set_state function. This last step is important since regardless of the data we did or didn’t load to the dialog we need to make sure the dialog buttons reflect what actions the user is able to perform.
Since we just wrote code that calls the bd_set_state function let’s take a look at that function now.
void bd_set_state() { int fx; fx = ListBoxGetSelectIndex(DC_LIST); if (fx >= 0) { ControlEnable(DC_REMOVE); } else { ControlDisable(DC_REMOVE); } }
Since all of the dialog controls are available at all times except the remove button this function checks if the remove button can be used. This button isn’t available unless there is a file selected in the file list. So we use the ListBoxGetSelectIndex function to check for a selection. If there is no selection we disable the button; otherwise we enable the button. With our logic for enabling controls out of the way we can move to the bd_action function.
int bd_action(int id, int action) { string tba[]; string path; int rc, ix, mx; switch (id) { case DC_ADD: tba = BrowseOpenFiles("Select Projects", ADD_FILTER); if (GetLastError() == ERROR_CANCEL) { break; } mx = ArrayGetAxisDepth(tba); for (ix = 0; ix < mx; ix++) { rc = ListBoxFindItem(DC_LIST, tba[ix]); if (IsError(rc)) { ListBoxAddItem(DC_LIST, tba[ix]); } } bd_set_state(); break; case DC_REMOVE: rc = ListBoxGetSelectIndex(DC_LIST); if (rc >= 0) { ListBoxDeleteItem(DC_LIST, rc); while (rc >= ListBoxGetItemCount(DC_LIST)) { rc--; } if (rc >= 0) { ListBoxSetSelectIndex(DC_LIST, rc); } } bd_set_state(); break; case DC_BROWSE: path = EditGetText(DC_BULK); path = BrowseSaveFile("Save Bulk Submission", SAVE_FILTER, path); if (GetLastError() == ERROR_CANCEL) { break; } EditSetText(DC_BULK, path); break; case DC_LIST: if (action == LBN_SELCHANGE) { bd_set_state(); } break; } return ERROR_NONE; }
This function controls the user interactions with the dialog. We need to add actions for several of the controls. The most obvious actions are the button controls so we can start there.
The DC_ADD button allows the user to add files to the file list. So we run the BrowseOpenFiles function to get a list of files. If the user canceled the dialog we immediately stop processing as there is nothing else to do. Otherwise, we iterate over the files in this new list and then check if they are already our current list using the ListBoxFindItem function. We don’t want to allow the user to accidentally add the same file to the submission twice as it would result in a duplicate filing. If the file was not in the list we add it to the list using ListBoxAddItem. Lastly, we run our bd_set_state function. This is not required given that adding items to the list does not change whether the remove button is available. However, it is a good practice to add this sort of logic in case more controls are added at a later date that do require state changes based on the number of items in the list. An example would be a label that contains the number of files in the submission.
Let’s move to the remove button. The logic for this button is pretty simple: we get the current selection; and if there is one we remove the item from the list using the ListBoxDeleteItem. Usually when you delete the selected item in a list box, the selected item resets. This means the user would need to click on the list box again to delete another item. As that is kind of onerous, we can automatically select the next file in the list or the last item if we are at the end of the list. This means the user can just repeatedly hit remove to quickly empty the list.
The last button is the browse button. For this we can get the current name, if any, using the EditGetText function and then call the BrowseSaveFile function. Once again if the user hits cancel we want to stop processing. Otherwise we can use the EditSetText function to update the control on screen.
Since we dealing with a list we also need to add an action for when the user selects something. This action is going to run our bd_set_state function so the remove button becomes available if a selection is made. For the list box we make sure we check the action variable as list boxes tend to send many notifications and we don’t need our bd_set_state to be called for all of them.
Now we have a dialog with some functionality to it. But there is one major component missing, which is the validate function. Without this function, when the user presses the Create button, there is no error checking and the data is never saved to our global variables. So let’s take a look at the bd_validate function.
int bd_validate() { string tba[]; string name; // Check List tba = ListBoxGetArray(DC_LIST); if (ArrayGetAxisDepth(tba) == 0) { MessageBox('x', "Submission must have at least one file."); return ERROR_SOFT | DC_LIST; } // Check Name name = EditGetText(DC_BULK); if (name == "") { MessageBox('x', "Bulk Submission File is a required field."); return ERROR_SOFT | DC_LIST; } // Save Values bulk = name; files = tba; return ERROR_NONE; }
It is a good practice to not edit the global variables until the entire dialog has been validated. For a script like this one, it is less important. But, take this example: we updated the list of files and the user forgets to fill in the bulk name. That global variable now has new data in it. Then the user hits the Cancel button and the script saves the file list for next time. Now, we have allowed the user to change the state of the script by hitting the Cancel button, yikes! Cancel should mean leave with no changes! So in general, it is better to validate everything and then store the results.
We start by using the ListBoxGetArray function which nicely gives us the entire list box as a string array. If that array is empty we put up a message box and exit the function with an error code. This stops the dialog from closing and sets the control focus to the offending control. Then we use the EditGetText function to get the name of the submission XML file. If the name is empty we complain as well. We could also add further checks here but for now this validation is acceptable. Lastly, we save our local data over the global data thus updating the script’s state with the user’s changes.
Here is the entire script with an added main function so we can test out the dialog:
// // Function Definitions and Globals // -------------------------------- int bd_load (); void bd_set_state (); int bd_action (int id, int action); int bd_validate (); string files[]; string bulk; int main() { DialogBox("BulkDialog", "bd_"); return 0; } // // Dialog and Defines // -------------------------------- #define DC_BULK 201 #define DC_LIST 301 #define DC_ADD 101 #define DC_REMOVE 102 #define DC_BROWSE 103 #define ADD_FILTER "All Projects|*.gfp;*.eis;*.xml;&GoFiler Projects|*.gfp&EDGAR Internet Submissions|*.eis&XML Submission Files|*.xml&All Files *.*|*.*" #define SAVE_FILTER "XML Submission Files|*.xml&All Files *.*|*.*" int bd_load() { int ix, mx; // Load List mx = ArrayGetAxisDepth(files); for (ix = 0; ix < mx; ix++) { ListBoxAddItem(DC_LIST, files[ix]); } EditSetText(DC_BULK, bulk); bd_set_state(); return ERROR_NONE; } void bd_set_state() { int fx; fx = ListBoxGetSelectIndex(DC_LIST); if (fx >= 0) { ControlEnable(DC_REMOVE); } else { ControlDisable(DC_REMOVE); } } int bd_action(int id, int action) { string tba[]; string path; int rc, ix, mx; switch (id) { case DC_ADD: tba = BrowseOpenFiles("Select Projects", ADD_FILTER); if (GetLastError() == ERROR_CANCEL) { break; } mx = ArrayGetAxisDepth(tba); for (ix = 0; ix < mx; ix++) { rc = ListBoxFindItem(DC_LIST, tba[ix]); if (IsError(rc)) { ListBoxAddItem(DC_LIST, tba[ix]); } } bd_set_state(); break; case DC_REMOVE: rc = ListBoxGetSelectIndex(DC_LIST); if (rc >= 0) { ListBoxDeleteItem(DC_LIST, rc); while (rc >= ListBoxGetItemCount(DC_LIST)) { rc--; } if (rc >= 0) { ListBoxSetSelectIndex(DC_LIST, rc); } } bd_set_state(); break; case DC_BROWSE: path = EditGetText(DC_BULK); path = BrowseSaveFile("Save Bulk Submission", SAVE_FILTER, path); if (GetLastError() == ERROR_CANCEL) { break; } EditSetText(DC_BULK, path); break; case DC_LIST: if (action == LBN_SELCHANGE) { bd_set_state(); } break; } return ERROR_NONE; } int bd_validate() { string tba[]; string name; // Check List tba = ListBoxGetArray(DC_LIST); if (ArrayGetAxisDepth(tba) == 0) { MessageBox('x', "Submission must have at least one file."); return ERROR_SOFT | DC_LIST; } // Check Name name = EditGetText(DC_BULK); if (name == "") { MessageBox('x', "Bulk Submission File is a required field."); return ERROR_SOFT | DC_LIST; } // Save Values bulk = name; files = tba; return ERROR_NONE; } #beginresource BulkDialog DIALOGEX 0, 0, 344, 190 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Bulk Filing" FONT 8, "MS Shell Dlg", 400, 0 { CONTROL "Files", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 4, 20, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 26, 9, 314, 1, 0 CONTROL "", DC_LIST, "listbox", LBS_NOTIFY | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | WS_TABSTOP, 12, 16, 276, 120, 0 CONTROL "&Add...", DC_ADD, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 16, 45, 12, 0 CONTROL "&Remove", DC_REMOVE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 31, 45, 12, 0 CONTROL "Submission", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 6, 137, 42, 8, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 48, 142, 292, 1, 0 CONTROL "&Bulk Submission File:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 150, 72, 8, 0 CONTROL "", DC_BULK, "edit", ES_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 88, 148, 200, 12 CONTROL "Browse...", DC_BROWSE, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 292, 148, 45, 12, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 166, 335, 1, 0 CONTROL "&Create", IDOK, "BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 232, 172, 50, 14 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 287, 172, 50, 14 } #endresource
We now have the user interface for a script to create bulk filings. Next week Steve will add project file processing so we can assemble the bulk filing XML.
David Theis has been developing software for Windows operating systems for over fifteen years. He has a Bachelor of Sciences in Computer Science from the Rochester Institute of Technology and co-founded Novaworks in 2006. He is the Vice President of Development and is one of the primary developers of GoFiler, a financial reporting software package designed to create and file EDGAR XML, HTML, and XBRL documents to the U.S. Securities and Exchange Commission. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato