Friday, July 13, 2007

Hidden controls retain focus; child dialogs and DS_CONTROL

Version 1.4.03 (and previous versions) crash deterministically if you load (via drag/drop) a plugin with at least one parameter, then load (again via drag/drop) a plugin with NO parameters, and then spin the mouse wheel, or press one of the arrow keys or editing keys. This turns out to be an example of a more general problem: if a control has focus, hiding its parent dialog does NOT take focus away from the control. The control continues to receive mouse and keyboard input. This can cause unexpected behavior (e.g. the wheel moving an invisible control) or even crash the app. But why do we have hidden dialogs? Here's why.

FFRend makes much use of the row view. This custom UI object is similar to a list view or grid control, but it's implemented as a form view containing a vertical list of child dialogs, one per row. A row dialog is just like any other dialog: it has a resource, contains controls, can be built using the Class Wizard, and can even be tested outside the containing view. The main advantage of this approach is encapsulation: row dialogs derive from a base class which makes it easier to operate on entire rows at once (e.g. for cut/copy/paste). By contrast, in a grid control, everything is contained in a single window, so there's no equivalent to a row object.

FFRend uses the row view to display plugin parameters, patchbay, MIDI setup, and metaparameters. In the case of plugin parameters, there's a complication: each plugin has different parameters, but only one plugin's parameters are visible at a time. FFRend allows each plugin to have its own set of row dialogs, and then shows and hides entire sets of row dialogs as needed. For example, when a plugin is selected, the previously selected plugin's rows are hidden, and the new plugin's rows are shown. This approach is wasteful in terms of memory, windows, and GDI objects, but efficient in terms of CPU usage: hiding/showing windows is cheap compared to creating and destroying them. The approach has another advantage: since we can assume that while a plugin exists, its parameter row dialogs also exist, we can store the parameter and automation data in the row controls, instead of storing them elsewhere and updating the controls on demand. This is elegant, and simplifies undo handling. So that's why we have hidden dialogs.

As it turns out, a dialog has to have the DS_CONTROL style in order to behave well as a child of another window. People often encounter this issue when they try to make a home-grown property sheet, i.e. a series of child dialogs that can be overlaid onto a parent container dialog. The usual problem is that without DS_CONTROL, tabbing doesn't work as expected: the entire child dialog is treated as a single tab stop. DS_CONTROL integrates the child dialog's tab layout into the tab layout of the parent window. FFRend used to handle tabbing in row views explicitly, but now that it's using DS_CONTROL, Windows handles the tabbing.

DS_CONTROL also improves window activation behavior: without DS_CONTROL, showing the parent window unexpectedly focuses the last control in the child dialog, and this can lead to the problem described above (hidden controls with focus). So DS_CONTROL is a very good discovery, and makes the row view UI much more robust, but it's still necessary to test for a hidden control with focus after updating the plugin parameters view. That's not such a big deal.

No comments: