Back when we first were designing the DataGrid we thought we would leave filtering and sorting up to the application. The problem with that approach is that it requires an extra copy of the data. For example, if we want the data sorted by a particular column or set of columns, we need to sort the data outside in the application and reset the DataGrid properties. This creates a whole new copy of the data. Same goes for basic filtering: if we want to select certain rows to display, we need to select the rows in the application and reset the DataGrid properties. This is not good.
It turns out the way we have implemented the grid makes it fairly easy to build in sorting and filtering. Assume the data is a matrix, m. Then at any given moment, the rows that are to be displayed in the available window space are given by a vector of (up till now) consecutive integers i:
m[i;]
If the user is on the bottom visible row and scrolls down one, then i will be effectively set to i+1. But there is no reason that i needs to be consecutive integers. We can display certain rows in a certain order by simply messing around with i.
In the DataGrid we now track an internal property RowIndices. This defaults to ⍳n where n is the number of data rows. That is, the default is to show all rows in the order given. We can pick out a subset of indices and permute them, assign this to RowIndices and thus display a subset of data in a different order:
m[RowIndices[i];]
And that's all there is to built-in sorting and filtering. The entire dataset is never sorted, nor is it ever selected out or copied. We limit built-in filtering to selecting where a column is equal to a particular value, or set of values. We can of course filter on multiple columns. More sophisticated filtering, for example selecting rows where a column is greater than a certain value, is left up to the application. There is a new RowMask property, a Boolean of the same length as the data. This allows the application to provide the entire dataset once to the DataGrid once, and then reset the visible rows. Built-in filtering can work on top of application filtering.
We only allow sorting and filtering if the property InsertRows is set to 0, that is the total number of rows cannot be changed. We may relax this in the future.
Way back in May of 2024 we made a first attempt at a ProgressBar component.
No battle plan survives first contact with the enemy, so now that one intrepid user is attempting some real work with Abacus, it's time to make the ProgressBar actually work.
While the operator approach is interesting and indeed useful, it does not allow us to easily integrate a progress bar into an existing codebase, especially one with looping control structures like :For, :While, and :Repeat. For this we need a Create function to create a progress bar, an Update function to update it inside the loop and to check for user action, and finally a Close function to get rid of it when done. For example if we know the number of iterations ahead of time, we might write something like:
p←d A.ProgressBar.Create ⊂'Iterations' 25
:For i :In ⍳25
r←p A.ProgressBar.Update'Doing iteration: ',⍕1+i
:If r≢'Resume' ⋄ :Leave ⋄ :EndIf
⎕DL 0.2
:EndFor
p A.ProgressBar.Close''
:Select r
:Case 'Cancel'
z←d A.Alert'You canceled the operation.'
:Case 'Resume'
z←d A.Alert'The operation completed.'
:EndSelect
Here the looping and logic is exposed and explicit. The result of Update is examined inside the loop on each iteration to determine if we should continue or not, and again after the loop to determine the final action. Of course this logic can be implemented many different ways.
If we don't know the number of iterations, we might write code like:
d A.ProgressBar.Create ''
t←0
:Repeat
r←A.ProgressBar.Update'Total time in seconds elapsed: ',⍕t
:If r≢'Resume' ⋄ :Leave ⋄ :EndIf
t+←⎕DL 0.2
:Until t>20
A.ProgressBar.Close''
We still provide the Run operator which has been significantly simplified, with one operator covering both the determinate and indeterminate cases:
Run←{
p←New 1⊃⍵
_←⍺ A.ShowModal p
r←p ⍺⍺{
s←((2=≡⍺.Status)/1+⍺.Iteration)⊃⍺.Status
c←⍺ Update s
c≡'Cancel':1 ⍺.Result
c≡'Truncate':2 ⍺.Result
⍺.Result,←⊂⍺ ⍺⍺ ⍵
⍺.Done:0 ⍺.Result
⍺.Iteration=⍺.Iterations-1:0 ⍺.Result
⍺ ∇ ⍵
}0⊃⍵
r⊣A.DeleteElement p
}
Both Create and Run cover the New function (passing through the right argument) :
New←{
w←(
'Title' 'Progress'
'Iterations' 0
'Truncate' 0
'UpdatePeriod' 1
'Width' '30rem'
'Status' 'Working...'
'LanguageTable'(LanguageTable'')
)A.Default ⍵
w.Iterations←(2=≡w.Status)⊃w.Iterations(≢w.Status)
s←A.New'p'
s.id←'progress-bar-status'
p←A.New'progress'
p.id←'progress'
p.max←w.Iterations
p.value←'0'
fp←A.New'p'(w A.Translate'Paused')
fp.id←'progress-paused-indicator'
fp.class←'pulse invisible'
w.BodyContent←s,((0<w.Iterations)/p),fp
w.Buttons←1 1 w.Truncate/w A.Translate'Cancel' 'Pause' 'Truncate'
w.OnClose←A.FQP'OnClose'
w.OnClick←A.FQP¨1 1 w.Truncate/'OnCancel' 'OnPause' 'OnTruncate'
d←A.DialogBox.New w
d.id←'progress-bar'
d.(Iteration Result)←¯1 ⍬
d.(Done Paused Canceled Truncated)←0
f←2⊃d.Content
f.Content.id←1 1 w.Truncate/'progress-'∘,¨'cancel' 'pause' 'truncate'
(d f).Unqueued←'close' 'click'
d.PauseToken←⎕TID
d
}
Here we can see the various options. The Iterations property defaults to 0, which indicates that we don't know the number of iterations ahead of time. If 0, then no actual graphical bar is displayed, only a status message.
The Truncate property indicates whether or not a Truncate option is offered in addition to Cancel and Resume when the work is paused.
The UpdatePeriod property determines how often the progress bar should be updated. It defaults to 1, meaning at every iteration. For loops with large numbers of iterations and small amounts of work in each iteration, it may make sense to set this higher.
The Status property is either a simple character vector or a vector of vectors. This property is only applicable when the Run operator is used, as in the case of Create and Close, the Update function is called explicitly by the programmer and passed the status as its argument. More on this below.
Let's look a little closer at the Run operator, which has changed significantly. It is called like so:
rc rv←d f ProgressBar.Run s p
Here f is the iteration function, s is an appropriate right argument to f, and p is an appropriate right argument to ProgressBar.New, and d is just a reference to the document. The result rc is a return code of 0 for complete, 1 for cancel, and 2 for truncate. The result rv is a vector of the results of f, one item for each iteration.
The iteration function f is passed a reference to the progress bar as its left argument. This lets f do and access a few important things. First, for the indeterminate case, allows f to set the Done property to 1, to indicate that no more iterations are necessary, and end the process. Second, it allows f to set the Status property to update the progress bar text. Finally, it allows f to reference the current iteration number.
The right argument to f may be anything, It might be a simple file tie number, or it could be a namespace. If the latter, in addition to providing a nice way for many named inputs to f, it provides storage for accumulating or aggregating results between iterations. Note that the progress bar itself, the left argument, can also provide storage that survives iterations.
The iteration function f must have a result. This explicit result may or may not be significant or useful, as the important result may be implicitly available in s.
As noted, the Status property may be set by f on each iteration. However, when the number of iterations is known, the status property may be set at startup to a vector of vectors, one item for each iteration. This defines the number of iterations and overrides the value of the Iterations property. The Run operator then automatically updates the status on each iteration.
There is an additional major change to the UI. Instead of a single Pause button that then presents a ConfirmBox with 'Cancel', 'Resume', and Truncate buttons, we provide Cancel, Pause, and Truncate buttons on the main progress element, avoiding the additional modal element. This means that Cancel and Truncate are always available and execute without additional confirmation. (Note that if using Create/Update/Close rather than Run, the programmer is in complete control of the loop and can add confirmation boxes when desired.) The Pause button switches to Resume and back as necessary. We generally don't like buttons that change captions, and we may change this, but in this case it seems reasonable.
Finally, with this version of the ProgressBar component, we introduce the LanguageTable property. This provides a way for the programmer to specify any or all of the visible text elements of the component. This may be used for language support, or simply to change the label of a particular element to something more appropriate for the task at hand.
You can see many examples of the ProgressBar component in the CSVEditor application.
Now that we have a few components, we need some convenient ways to manipulate them. Components are just nodes in the APL DOM with a Name property. Note the uppercase N, not to be confused with the HTML name attribute. Currently the existence of Name in an element is the only thing that identifies it as a component.
We can get a component by name, using the GetComponent function:
GetComponent←{
⍝ ⍺ ←→ APL DOM node
⍝ ⍵ ←→ Component Name (or already a component (pass through)
9=⎕NC'⍵':⍵
e←⍺ GetElementsWith'Name'
i←e.Name⍳⊂⍵
i=≢e:6 ⎕SIGNAL⍨'Component "',⍵,'" not found'
i⊃e
}
The left argument is a DOM node, perhaps the whole document, or more likely a <dialog> element. the right argument is the name of the component, or if a namespace, assumed to be a component already, and just passed though. This is a convenience for other functions that accept either a component or a component name as an argument. We simply find all the elements in the node with a Name property and pick the one we want.
We can get a namespace with all of the components in a node:
GetComponents←{
⍝ ⍵ ← APL DOM node
⍝ ⍺ ←→ 0 for namespace result, 1 for array
⍝ ← ←→ Namespace of components
⍺←0
e←⍵ GetElementsWith'Name'
⍺:e
0=≢⍵:()
()⎕VSET(↑e.Name)e
}
This allows us to access all the components under a node by name with dot syntax (or get an array of components back).
We can set the value of a component using SetComponentValue:
SetComponentValue←{
⍝ ⍺ ←→ APL DOM Node
⍝ ⍵ ←→ (Name|Component) Value
⍺←⊢
c←⍺ GetComponent 0⊃⍵
c(⍎c.class).SetValue 1⊃⍵
}
Here the right argument is name or component itself, and the value. If the name is provided, then the left argument must be provided. If the component is provided, the left argument should be omitted. There is no need for a corresponding GetComponentValue function, as the value may be directly accessed with dot syntax.
We also have GetComponentValues and SetComponentValues, which we have introduced before but now renamed to reflect that they work on components. These functions take or return a namespace of values. Finally we have SetComponentDisabled for making a component, in ⎕WC terms, active or inactive:
SetComponentDisabled←{
⍝ ⍺ ←→ DOM node
⍝ ⍵ ←→ Name or Component name, Boolean 1/0
⍺←⊢
c←⍺ GetComponent 0⊃⍵
e←Elements c
d←e/⍨e.Tag∊'button' 'fieldset' 'optgroup' 'option' 'select' 'textarea' 'input'
0=≢d:0
0⊣d{⍺ SetBoolean'disabled'⍵}¨1⊃⍵
}
The explicit list of HTML elements are those that take the disabled attribute.
Let's play around with the functions. Consider a dialog box with a few input fields:
OnFileSettings←{
d←⍵.Document
s←A.FieldSet.New''
i←s A.DropList.New'Separator' 'Separator:' ''('Comma' 'Pipe' 'Tab')
i←s A.DropList.New'TextQualifier' 'Text qualifier:'('Options'('DoubleQuote' 'Quote')
i←s A.NumberInput.New'MaxRows' 'Maximum Rows:' 256 'CI15'
i←s A.CheckBox.New'AutoConvert' 'Auto convert'
_←d.FileSettings A.InitValues s
p←⎕NS''
p.Title←'CSV Import Settings'
p.Contents←s
p.OnOK←A.FQP'OnFileSettingsOK'
b←A.DialogBox.New p
d A.ShowModal b
}
When the OnFileSettingsOK callback fires we are given the <dialog> element as an argument. Let's call this d. Then we can grab a component and see its value:
c←d A.GetComponent 'Separator'
c.Value
Pipe
Then set its value, updating the APL DOM and the browser:
A.SetComponentValue c 'Comma'
0
c.Value
Comma
A namepsace of components:
A.GetComponents d
p←A.GetComponents d
p.MaxRows.Value
1024
p.AutoConvert.Value
1
Get a namespace of values, make some changes, refresh the screen:
v←A.GetComponentValues d
⎕JSON v
{"AutoConvert":1,"MaxRows":1024,"Separator":"Comma","TextQualifier":"DoubleQuote"}
v.(AutoConvert MaxRows)←1 1234567
v A.SetComponentValues d
0
v←A.GetComponentValues d
⎕JSON v
{"AutoConvert":0,"MaxRows":1234567,"Separator":"Pipe","TextQualifier":"Quote"}
An array of components:
a←1 A.GetComponents d
a.Name
Separator TextQualifier MaxRows AutoConvert
a.Value
Pipe Quote 1234567 0
Deactivate all the components:
A.SetComponentDisabled¨a,¨1
0 0 0 0
We have used these verbose functions names, all including the word component, to distinguish them from the functions used to operate on plain elements, as everything sits all together in the #.Abacus.Main namespace. It would be nice to just use properties with real classes and dispense with exposing all these functions, and we may do that in the future. We are going to see how many functions we will need to work with components over the coming months.