Loss of Focus
Version: FileMaker 18
Tuesday, October 15, 2019
I hear it over and over on the forums, "my Tab Control changes to the default tab when I run a script". What initially seems like an innocent little script that adds a new related record by setting a variable to a primary key, switching layouts, creating a record, setting the foreign key and returning to the original layout, becomes a nightmare of fixes to restore focus to the correct tab panel. In fact, loss of focus can happen with a variety of different objects including Portals, Slide Controls and fields. I don't think there's one solution that fits all situations so we'll discuss the ins and outs of a handful of techniques so you can choose the right one for the job at hand.
Defining the Issue
Rather than trying to tackle all focus issues, I'm going to focus on Tab Controls. If you haven't seen loss of focus on a Tab Control, try switching to layout mode and then back to browse mode and the default tab panel will be selected. Another way to replicate the behavior without leaving browse mode is to create a sample database with two layouts. One layout should contain a Tab Control with multiple tabs and the other layout can be empty if you like. Just select the non-default tab and manually switch layouts using the layout menu in the status toolbar. Re-select the original layout and you'll see the Tab Control object has the default tab panel selected. Since scripting is basically automation of manual steps, any script that switches layouts, reset The Tab Control object to the default tab panel.
FYI: Switching from browse mode to layout mode retains the currently selected tab panel. It's handy when you want to work on the tab in layout mode that is selected in browse mode. Only when you switch back to browse mode does the default tab get selected or reset.
My goal is to help you learn FileMaker, not confuse you with a complex example. Therefore, the example in this article will be a simple one-to-many relationship from a companies table to an employees table.
This example is used for it's simplicity and not necessarily because it represents a solution where you might need to run a script that changes layouts. In the example file that comes with this article, you'll see a button on Tab 2 titled "Loss of Tab Panel Focus" that will demonstrate the issue by adding a record to the portal via a script. Additional buttons and tab panels will be discussed below and referenced in the example file to demonstrate various methods to overcome loss of tab panel focus.
An Easy Fix?
The easiest fix is to create a new window and then switch layouts. When the script is done performing, simply close the window. The original window will still have the same Tab Control, Slide Control, Portal and field focus. Pretty easy, right? I use this technique all the time but there are some downsides.
What's not obvious are the issues that occur from opening a new window. On a desktop computer, there tends to be a lot of flashing from the parts of the window that dim when another window is at the front. This includes the title bar, status tool bar and any other window element that dims in the background. There's really no flashing from the layout change since FileMaker does a great job of freezing the content portion of the window. Unfortunately, the operating system is in charge of window management so there's nothing FileMaker can do about the window flashing.
Some developers like to spawn the new window off screen but this doesn't really stop the flashing of the window. The reasoning is pretty simple. The visible window is still moving to the background even though the new window is off screen. The only thing you can really do is minimize the flashing by hiding the status toolbar (which I always do anyhow). The title bar still flashes as it dims but at least it's minimal. Well, I'm one of those developers who doesn't like any interface idiosyncrasies, no matter how insignificant. If you're satisfied with this solution then there's no reason to read any further.
TIP: You can spawn a new window off screen by entering a large negative value for the left and top sides of the window (e.g. -10,000).
FYI: The change of layout has been incorporated into the New Window script step.
I don't mean to be flippant. It's just that I believe you need more than one tool on your tool belt. The new window solution is almost always the right solution because it's simple. But, what about those times when a new window step can't be incorporated into a script? Maybe it's a FileMaker Go solution where window management is completely different. FileMaker Pro Advanced and Go have more in common than not but this is one area where they are completely different. iOS devices simply don't have the concept of multiple windows. Sure, you can create as many windows as you want but only one window will show at a time. No side-by-side windows, no layering windows, just one window at a time. This common scenario and many other uncommon situations require different solutions which we will discuss below.
FYI: With all the changes to the window interface in the past couple of releases, window flashing isn't as big an issue since FileMaker 16. Changes include eliminating the mode and zoom controls in the lower left corner and the visible scroll bar.
Use a Card Window
One way around the title bar, and other elements flashing, is the use of a Card Window. It's like a new window within the existing window. It has all the properties of spawning a traditional window, like switching context, but without dimming any portions of the parent window. It's a very simple change to the existing script. Just change the window style to card.
BTW: Card Windows are supported in FileMaker Go!
The earliest published solution to restore loss of focus, that I can remember, was to implement Script Triggers to store the current tab pane. It was a little more complicated than the technique typically used in the modern FileMaker. All you had back in FileMaker 8.5 was the GetLayoutObjectAttribute with the "IsFrontTabPanel" attribute. What you did was store the tab pane object name in a global variable using the GetLayoutObjectAttribute function. The part that was hard to swallow was the hard coded conditional statement to determine which tab was selected. It might look something like this if you had three tabs named "TabOne", "TabTwo" and "TabThree":
Case(Another problem was the OnPanelSwitch Script Trigger didn't come till FileMaker 12 so you had to use the OnObjectModify trigger. Not a big deal. They essentially work the same in this scenario. However, don't forget that OnObjectModify runs the script after the action of clicking the tab and OnPanelSwitch runs the script before the action. It could make a big difference in some situations.
GetLayoutObjectAttribute("TabOne"; "IsFrontTabPanel"); "TabOne";
GetLayoutObjectAttribute("TabTwo"; "IsFrontTabPanel"); "TabTwo";
GetLayoutObjectAttribute("TabThree"; "IsFrontTabPanel"); "TabThree"
The only real choice for a tab based Script Trigger these days is OnPanelSwitch. OnPanelSwitch comes with two sibling functions that were designed especially for it. Both the Get(TriggerCurrentPanel) and Get(TriggerTargetPanel) functions return both the tab panel index and object name. The tab panel index isn't very useful so I simply grab the second of the return-separated values to store the tab panel object name
Set Variable [$$Tab; Value: GetValue(Get(TriggerTargetPanel); 2)]You'll notice I'm using the Get(TriggerTargetPanel) instead of the Get(TriggerCurrentPanel) function. If you try to use the Get(TriggerCurrentPanel) function, you'll always be one tab panel behind. Think about it. Get(TriggerCurrentPanel) returns the current panel, not the panel being selected. That's because the OnPanelSwitch script trigger runs the script before processing so the current panel is not the one being clicked. Now, if you used the OnObjectModify trigger instead, it would be a different story. Unfortunately, the Get(TriggerCurrentPanel) and Get(TriggerTargetPanel) only work with the OnPanelSwitch Script Trigger.
BTW: All the techniques that apply to Tab Controls also apply to Slide Controls. They are essentially the same object type with a slightly different interface.
Also, notice the tab panel name is being stored in a global variable ($$). If you try to use a local variable ($) the object name from the tab panel will be lost as soon as the script ends. And, don't forget to name each of your tab panes using the Inspector!
Each time you switch tabs, you should be able to view the tab object name in the Data Viewer. It's always a good idea to test each piece of a puzzle before moving onto the next step. The only other thing to do is modify the script for creating a related record by removing the New Window step and adding a Go to Object step.
Go to Object [Object Name: $$Tab]
It's probably best to modularize this step since you're likely to use it on multiple layouts with multiple Tab Control objects. There's nothing hard coded about the script for storing the current tab or restoring it so it's a pretty easy modularization to setup.
Here's the complete script for creating a new row in a portal:
Slide Control Mimicry
Another solution to preserving the currently selected tab pane is to use Slide Controls instead of Tab Controls. Slide Controls and Tab Controls are very closely related and are essentially the same object but with a different interface. This technique requires you to use buttons to imitate tabs so the Slide Control looks just like a Tab Control.
So, what does this require in terms of development effort? It's pretty much the same solution as the trigger solution mentioned previously but without a trigger. The big difference is you need to attach the script setting the global variable to each button and pass the tab name via a script parameter. Here's what the script might look like:
Go to Object [Object Name: Get(ScriptParameter)]
Set Variable [$$Tab2; Value: Get(ScriptParameter)]
Refresh Window [ ]
The first step selects the correct slide control pane so make sure you have each of them named. The second step places the slide control pane object name into a global variable. I used a different global variable to differentiate the variable values in the single example file. The Refresh Window will be discussed below when the conditional formatting is discussed.
At this point, you could stop and the solution would work great. The only issue is the buttons don't look like tabs. If you want the look of a tab then you'll need to remove the bottom border on each of the buttons so they can visually connect to the Slide Control object and appear as a single object. I also did some work on the hover state and added some conditional formatting but it all depends on what theme you are employing so I won't go over additional interface fiddling (see the example file that comes with this article if you are interested). It's important to note that the Refresh Window step in the previous script works with the conditional formatting on the buttons to make sure their state updates each time a tab is clicked. Basically, I wanted the selected tab to look selected. If you don't end up using the conditional formatting trick then remove the Refresh Window step.
One issue with the conditional formatting that changes the active tab to a different color, so it looks selected, is the slide control and the button getting out of synchronization. This happens when the slide control reverts back to the default pane without the clicking of the button (e.g. layout change). The global variable storing the current tab is not necessarily the same as the default tab so you end up seeing the contents of the default pane but a highlighted button above a different tab. To get around this issue, you need to restore the selected tab. The best way I found to do this was to run the following script OnPanelSwitch:
Go to Object [Object Name: $$Tab2]
FYI: In the example file, I have piggybacked the slide control restore script onto the Script Trigger that stores $$Tab value from the first technique.
It's also important to remove the navigational dots from the Slide Control so it looks like a Tab Control object. And, so that the first tab is selected on startup of your file, place the following script step in your open script:
Set Variable [$$Tab2; Value: "Tab A"]
The object name you set into the global variable depends on what tab you want as the default.
This method is a bit more work than the first. While the script is pretty easy to write, it's the construction and possible deconstruction of the buttons (if there are changes needed) that require the most work. I try to avoid interface work whenever possible which is why I love the built-in themes. Prior to themes, interface work was as much as fifty percent of the work. As for advantages, this technique requires no Script Triggers. I also try to avoid overusing Script Triggers because they tend to conflict as more are added.
As an adjunct for this technique, you could use a Tab Control object with invisible buttons laid over the tabs. They would run the same scripts as discussed above but wouldn't require as much interface work so they would be easier to construct and deconstruct. All you need to do is set the borders, fill and states of the buttons to clear and you're done. One tip that can save some grief is how to place the buttons. If they touch the Tab Control object, they may get sucked into it. Make sure you create your buttons outside the Tab Control and then gently nudge them into position.
Whether you use the New Window technique, Script Triggers or buttons as tabs, you'll create a far better interface for your users by preserving the tab focus. Nobody wants to click a button and then have their focus changed to the default tab. Not only is it disconcerting but it also creates an inefficient solution. Learn all these methods cause you never know when one might work better than the other in a particular scenario.
John Mark Osborne
Example File (available to patrons at $5.00 per month): Download here if you are a patron
This blog is completely free. Please support it by clicking on one of the advertisers at the left side of the window or becoming a patron. Thanks so much!