1 dojo.declare( 'JBrowse.View.TrackList.Faceted', null,
  2    /**
  3     * @lends JBrowse.View.TrackList.Faceted.prototype
  4     */
  5    {
  6 
  7    /**
  8      * Track selector with facets and text searching.
  9      * @constructs
 10      */
 11    constructor: function(args) {
 12        dojo.require('dojox.grid.EnhancedGrid');
 13        dojo.require('dojox.grid.enhanced.plugins.IndirectSelection');
 14        dojo.require('dijit.layout.AccordionContainer');
 15        dojo.require('dijit.layout.AccordionPane');
 16 
 17        this.browser = args.browser;
 18        this.tracksActive = {};
 19        this.config = args;
 20 
 21        // construct the discriminator for whether we will display a
 22        // facet selector for this facet
 23        this._isSelectableFacet = this._coerceFilter(
 24                args.selectableFacetFilter
 25                // default facet filtering function
 26                || function( facetName, store ){
 27                    return (
 28                        // has an avg bucket size > 1
 29                        store.getFacetStats( facetName ).avgBucketSize > 1
 30                     &&
 31                        // and not an ident or label attribute
 32                        ! dojo.some( store.getLabelAttributes()
 33                                     .concat( store.getIdentityAttributes() ),
 34                                     function(l) {return l == facetName;}
 35                                   )
 36                    );
 37                }
 38            );
 39 
 40        // construct a similar discriminator for which columns will be displayed
 41        this.displayColumns = args.displayColumns;
 42        this._isDisplayableColumn = this._coerceFilter(
 43            args.displayColumnFilter || function() { return true; }
 44        );
 45 
 46        // data store that fetches and filters our track metadata
 47        this.trackDataStore = args.trackMetaData;
 48 
 49        // subscribe to commands coming from the the controller
 50        dojo.subscribe( '/jbrowse/v1/c/tracks/show',
 51                        dojo.hitch( this, 'setTracksActive' ));
 52        // subscribe to commands coming from the the controller
 53        dojo.subscribe( '/jbrowse/v1/c/tracks/hide',
 54                        dojo.hitch( this, 'setTracksInactive' ));
 55 
 56        this.renderInitial();
 57 
 58        // once its data is loaded and ready
 59        this.trackDataStore.onReady( this, function() {
 60 
 61            // render our controls and so forth
 62            this.renderSelectors();
 63 
 64            // connect events so that when a grid row is selected or
 65            // deselected (with the checkbox), publish a message
 66            // indicating that the user wants that track turned on or
 67            // off
 68            dojo.connect( this.dataGrid.selection, 'onSelected', this, function(index) {
 69                          this._ifNotSuppressed( 'selectionEvents', function() {
 70                              this._suppress( 'gridUpdate', function() {
 71                                  dojo.publish( '/jbrowse/v1/v/tracks/show', [[this.dataGrid.getItem( index ).conf]] );
 72                              });
 73                          });
 74 
 75            });
 76            dojo.connect( this.dataGrid.selection, 'onDeselected', this, function(index) {
 77                          this._ifNotSuppressed( 'selectionEvents', function() {
 78                              this._suppress( 'gridUpdate', function() {
 79                                  dojo.publish( '/jbrowse/v1/v/tracks/hide', [[this.dataGrid.getItem( index ).conf]] );
 80                              });
 81                          });
 82            });
 83        });
 84        this.trackDataStore.onReady( this, '_updateFacetCounts' ); // just once at start
 85 
 86        dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateGridSelections' );
 87        dojo.connect( this.trackDataStore, 'onFetchSuccess', this, '_updateMatchCount' );
 88     },
 89 
 90     /**
 91      * Coerces a string or array of strings into a function that,
 92      * given a string, returns true if the string matches one of the
 93      * given strings.  If passed a function, just returns that
 94      * function.
 95      * @private
 96      */
 97     _coerceFilter: function( filter ) {
 98         // if we have a non-function filter, coerce to an array,
 99         // then convert that array to a function
100         if( typeof filter == 'string' )
101             filter = [filter];
102         if( Array.isArray( filter ) ) {
103             filter = function( store, facetName) {
104                 return dojo.some( filter, function(fn) {
105                                       return facetName == fn;
106                                   });
107             };
108         }
109         return filter;
110     },
111 
112     /**
113      * Call the given callback if none of the given event suppression flags are set.
114      * @private
115      */
116     _ifNotSuppressed: function( suppressFlags, callback ) {
117         if( typeof suppressFlags == 'string')
118             suppressFlags = [suppressFlags];
119         if( !this.suppress)
120             this.suppress = {};
121         if( dojo.some( suppressFlags, function(f) {return this.suppress[f];}, this) )
122             return undefined;
123         return callback.call(this);
124     },
125 
126     /**
127      * Call the given callback while setting the given event suppression flags.
128      * @private
129      */
130     _suppress: function( suppressFlags, callback ) {
131         if( typeof suppressFlags == 'string')
132             suppressFlags = [suppressFlags];
133         if( !this.suppress)
134             this.suppress = {};
135         dojo.forEach( suppressFlags, function(f) {this.suppress[f] = true; }, this);
136         var retval = callback.call( this );
137         dojo.forEach( suppressFlags, function(f) {this.suppress[f] = false;}, this);
138         return retval;
139     },
140 
141     /**
142      * Call a method of our object such that it cannot call itself
143      * by way of event cycles.
144      * @private
145      */
146     _suppressRecursion: function( methodName ) {
147         var flag   = ['method_'+methodName];
148         var method = this[methodName];
149         return this._ifNotSuppressed( flag, function() { this._suppress( flag, method );});
150     },
151 
152     renderInitial: function() {
153         this.containerElem = dojo.create( 'div', {
154             id: 'faceted_tracksel',
155             style: {
156                 left: '-95%',
157                 width: '95%',
158                 zIndex: 500
159             }
160         },
161         document.body );
162 
163         // make the tab that turns the selector on and off
164         dojo.create('div',
165                     {
166                         className: 'faceted_tracksel_on_off tab',
167                         innerHTML: '<img src="img/left_arrow.png"><div>Select<br>tracks</div>'
168                     },
169                     this.containerElem
170                    );
171         this.mainContainer = new dijit.layout.BorderContainer(
172             { design: 'headline', gutters: false },
173             dojo.create('div',{ className: 'mainContainer' }, this.containerElem)
174         );
175 
176 
177         this.topPane = new dijit.layout.ContentPane(
178             { region: 'top',
179               id: "faceted_tracksel_top",
180               content: '<div class="title">Select Tracks</div> '
181                        + '<div class="topLink" style="cursor: help"><a title="Track selector help">Help</a></div>'
182             });
183         dojo.query('div.topLink a[title="Track selector help"]',this.topPane.domNode)
184             .forEach(function(helplink){
185                 var helpdialog = new dijit.Dialog({
186                     "class": 'help_dialog',
187                     refocus: false,
188                     draggable: false,
189                     title: 'Track Selection',
190                     content: '<div class="main">'
191                              + '<p>The JBrowse Faceted Track Selector makes it easy to search through'
192                              + ' large numbers of available tracks to find exactly the ones you want.'
193                              + ' You can incrementally filter the track display to narrow it down to'
194                              + ' those your are interested in.  There are two types of filtering available,'
195                              + ' which can be used together:'
196                              + ' <b>filtering with data fields</b>, and free-form <b>filtering with text</b>.'
197                              + '</p>'
198                              + '  <dl><dt>Filtering with Data Fields</dt>'
199                              + '  <dd>The left column of the display contains the available <b>data fields</b>.  Click on the data field name to expand it, and then select one or more values for that field.  This narrows the search to display only tracks that have one of those values for that field.  You can do this for any number of fields.<dd>'
200                              + '  <dt>Filtering with Text</dt>'
201                              + '  <dd>Type text in the "Contains text" box to filter for tracks whose data contains that text.  If you type multiple words, tracks are filtered such that they must contain all of those words, in any order.  Placing "quotation marks" around the text filters for tracks that contain that phrase exactly.  All text matching is case insensitive.</dd>'
202                              + '  <dt>Activating Tracks</dt>'
203                              + "  <dd>To activate and deactivate a track, click its check-box in the left-most column.  When the box contains a check mark, the track is activated.  You can also turn whole groups of tracks on and off using the check-box in the table heading.</dd>"
204                              + "  </dl>"
205                              + "</div>"
206                  });
207                 dojo.connect( helplink, 'onclick', this, function(evt) {helpdialog.show(); return false;});
208             },this);
209 
210         this.mainContainer.addChild( this.topPane );
211 
212         // make both buttons toggle this track selector
213         dojo.query( '.faceted_tracksel_on_off' )
214             .onclick( dojo.hitch( this, 'toggle' ));
215 
216         this.centerPane = new dijit.layout.BorderContainer({region: 'center', "class": 'gridPane', gutters: false});
217         this.mainContainer.addChild( this.centerPane );
218         var textFilterContainer = this.renderTextFilter();
219 
220         this.busyIndicator = dojo.create(
221             'div', {
222                 innerHTML: '<img src="img/spinner.gif">',
223                 className: 'busy_indicator'
224             }, this.containerElem );
225 
226         this.centerPane.addChild(
227             new dijit.layout.ContentPane(
228                 { region: 'top',
229                   "class": 'gridControls',
230                   content: [
231                       dojo.create( 'button', {
232                                        className: 'faceted_tracksel_on_off',
233                                        innerHTML: '<img src="img/left_arrow.png"> <div>Back to browser</div>',
234                                        onclick: dojo.hitch( this, 'hide' )
235                                    }
236                                  ),
237                       dojo.create( 'button', {
238                                        className: 'clear_filters',
239                                        innerHTML:'<img src="img/red_x.png">'
240                                                  + '<div>Clear All Filters</div>',
241                                        onclick: dojo.hitch( this, function(evt) {
242                                            this._clearTextFilterControl();
243                                            this._clearAllFacetControls();
244                                            this._async( function() {
245                                                this.updateQuery();
246                                                this._updateFacetCounts();
247                                            },this).call();
248                                        })
249                                    }
250                                  ),
251                       this.busyIndicator,
252                       textFilterContainer,
253                       dojo.create('div', { className: 'matching_record_count' })
254                   ]
255                 }
256             )
257         );
258 
259 
260     },
261     renderSelectors: function() {
262 
263         // make our main components
264         var facetContainer = this.renderFacetSelectors();
265         // put them in their places in the overall layout of the track selector
266         facetContainer.set('region','left');
267         this.mainContainer.addChild( facetContainer );
268 
269         this.dataGrid = this.renderGrid();
270         this.dataGrid.set('region','center');
271         this.centerPane.addChild( this.dataGrid );
272 
273         this.mainContainer.startup();
274     },
275 
276     _async: function( func, scope ) {
277         var that = this;
278         return function() {
279             var args = arguments;
280             var nativeScope = this;
281             that._busy( true );
282             window.setTimeout(
283                 function() {
284                     func.apply( scope || nativeScope, args );
285                     that._busy( false );
286                 },
287                 50
288             );
289         };
290     },
291 
292     _busy: function( busy ) {
293         this.busyCount = Math.max( 0, (this.busyCount || 0) + ( busy ? 1 : -1 ) );
294         if( this.busyCount > 0 )
295             dojo.addClass( this.containerElem, 'busy' );
296         else
297             dojo.removeClass( this.containerElem, 'busy' );
298     },
299 
300     renderGrid: function() {
301         var grid = new dojox.grid.EnhancedGrid({
302                id: 'trackSelectGrid',
303                store: this.trackDataStore,
304                selectable: true,
305                noDataMessage: "No tracks match the filtering criteria.",
306                structure: [
307                    dojo.map(
308                        dojo.filter( this.displayColumns || this.trackDataStore.getFacetNames(),
309                                     dojo.hitch(this, '_isDisplayableColumn')
310                                   ),
311                        function(facetName) {
312                            return {'name': this._facetDisplayName(facetName), 'field': facetName, 'width': '100px'};
313                        },
314                        this
315                    )
316                ],
317                plugins: {
318                    indirectSelection: {
319                        headerSelector: true
320                    }
321                }
322            }
323         );
324 
325         this._monkeyPatchGrid( grid );
326         return grid;
327     },
328 
329     /**
330      * Given a raw facet name, format it for user-facing display.
331      * @private
332      */
333     _facetDisplayName: function( facetName ) {
334         // make renameFacets if needed
335         this.renameFacets = this.renameFacets || function(){
336             var rename = this.config.renameFacets || {};
337             rename.key = rename.key || 'Name';
338             return rename;
339         }.call(this);
340 
341         return this.renameFacets[facetName] || Util.ucFirst( facetName.replace('_',' ') );
342     },
343 
344     /**
345      * Apply several run-time patches to the dojox.grid.EnhancedGrid
346      * code to fix bugs and customize the behavior in ways that aren't
347      * quite possible using the regular Dojo APIs.
348      * @private
349      */
350     _monkeyPatchGrid: function( grid ) {
351 
352         // 1. monkey-patch the grid's onRowClick handler to not do
353         // anything.  without this, clicking on a row selects it, and
354         // deselects everything else, which is quite undesirable.
355         grid.onRowClick = function() {};
356 
357         // 2. monkey-patch the grid's range-selector to refuse to select
358         // if the selection is too big
359         var origSelectRange = grid.selection.selectRange;
360         grid.selection.selectRange = function( inFrom, inTo ) {
361             var selectionLimit = 30;
362             if( inTo - inFrom > selectionLimit ) {
363                 alert( "Too many tracks selected, please select fewer than "+selectionLimit+" tracks." );
364                 return undefined;
365             }
366             return origSelectRange.apply( this, arguments );
367         };
368 
369         // 3. monkey-patch the grid's scrolling handler to fix
370         // http://bugs.dojotoolkit.org/ticket/15343
371         // diff between this and its implementation in dojox.grid._View.js (1.6.1) is only:
372         // if(top !== this.lastTop)  --->  if( Math.abs( top - this.lastTop ) > 1 )
373         grid.views.views[0].doscroll = function(inEvent){
374                 //var s = dojo.marginBox(this.headerContentNode.firstChild);
375                 var isLtr = dojo._isBodyLtr();
376                 if(this.firstScroll < 2){
377                         if((!isLtr && this.firstScroll == 1) || (isLtr && this.firstScroll === 0)){
378                                 var s = dojo.marginBox(this.headerNodeContainer);
379                                 if(dojo.isIE){
380                                         this.headerNodeContainer.style.width = s.w + this.getScrollbarWidth() + 'px';
381                                 }else if(dojo.isMoz){
382                                         //TODO currently only for FF, not sure for safari and opera
383                                         this.headerNodeContainer.style.width = s.w - this.getScrollbarWidth() + 'px';
384                                         //this.headerNodeContainer.style.width = s.w + 'px';
385                                         //set scroll to right in FF
386                                         this.scrollboxNode.scrollLeft = isLtr ?
387                                                 this.scrollboxNode.clientWidth - this.scrollboxNode.scrollWidth :
388                                                 this.scrollboxNode.scrollWidth - this.scrollboxNode.clientWidth;
389                                 }
390                         }
391                         this.firstScroll++;
392                 }
393                 this.headerNode.scrollLeft = this.scrollboxNode.scrollLeft;
394                 // 'lastTop' is a semaphore to prevent feedback-loop with setScrollTop below
395                 var top = this.scrollboxNode.scrollTop;
396                 if(Math.abs( top - this.lastTop ) > 1 ){
397                         this.grid.scrollTo(top);
398                 }
399         };
400     },
401 
402     renderTextFilter: function( parent ) {
403         // make the text input for text filtering
404         this.textFilterLabel = dojo.create(
405             'label',
406             { className: 'textFilterControl',
407               innerHTML: 'Contains text ',
408               id: 'tracklist_textfilter',
409               style: {position: 'relative'}
410             },
411             parent
412         );
413         this.textFilterInput = dojo.create(
414             'input',
415             { type: 'text',
416               size: 40,
417               disabled: true, // disabled until shown
418               onkeypress: dojo.hitch( this, function(evt) {
419                   if( evt.keyCode == dojo.keys.SHIFT || evt.keyCode == dojo.keys.CTRL || evt.keyCode == dojo.keys.ALT )
420                       return;
421                   if( this.textFilterTimeout )
422                       window.clearTimeout( this.textFilterTimeout );
423                   this.textFilterTimeout = window.setTimeout(
424                       dojo.hitch( this, function() {
425                                       this._updateTextFilterControl();
426                                       this._async( function() {
427                                           this.updateQuery();
428                                           this._updateFacetCounts();
429                                           this.textFilterInput.focus();
430                                       },this).call();
431                                       this.textFilterInput.focus();
432                                   }),
433                       500
434                   );
435                   this._updateTextFilterControl();
436 
437                   evt.stopPropagation();
438               })
439             },
440             this.textFilterLabel
441         );
442         // make a "clear" button for the text filtering input
443         this.textFilterClearButton = dojo.create('img', {
444             src: 'img/red_x.png',
445             className: 'text_filter_clear',
446             onclick: dojo.hitch( this, function() {
447                 this._clearTextFilterControl();
448                 this._async( function() {
449                     this.updateQuery();
450                     this._updateFacetCounts();
451                 },this).call();
452             }),
453             style: {
454                 position: 'absolute',
455                 right: '4px',
456                 top: '20%'
457             }
458         }, this.textFilterLabel );
459 
460         return this.textFilterLabel;
461     },
462 
463    /**
464     * Clear the text filter control input.
465     * @private
466     */
467     _clearTextFilterControl: function() {
468         this.textFilterInput.value = '';
469         this._updateTextFilterControl();
470     },
471     /**
472      * Update the display of the text filter control based on whether
473      * it has any text in it.
474      * @private
475      */
476     _updateTextFilterControl: function() {
477         if( this.textFilterInput.value.length )
478             dojo.addClass( this.textFilterLabel, 'selected' );
479         else
480             dojo.removeClass( this.textFilterLabel, 'selected' );
481 
482     },
483 
484     /**
485      * Create selection boxes for each searchable facet.
486      */
487     renderFacetSelectors: function() {
488         var container = new dijit.layout.AccordionContainer({style: 'width: 200px'});
489 
490         var store = this.trackDataStore;
491         this.facetSelectors = {};
492 
493         // render a facet selector for a pseudo-facet holding
494         // attributes regarding the tracks the user has been working
495         // with
496         var usageFacet = this._renderFacetSelector(
497             'My Tracks', ['Currently Active', 'Recently Used'] );
498         usageFacet.set('class', 'myTracks' );
499         container.addChild( usageFacet );
500 
501         // for the facets from the store, only render facet selectors
502         // for ones that are not identity attributes, and have an
503         // average bucket size greater than 1
504         var selectableFacets =
505             dojo.filter( this.config.selectableFacets || store.getFacetNames(),
506                          function( facetName ) {
507                              return this._isSelectableFacet( facetName, this.trackDataStore );
508                          },
509                          this
510                        );
511 
512         dojo.forEach( selectableFacets, function(facetName) {
513 
514             // get the values of this facet
515             var values = store.getFacetValues(facetName).sort();
516             if( !values || !values.length )
517                 return;
518 
519             var facetPane = this._renderFacetSelector( facetName, values );
520             container.addChild( facetPane );
521         },this);
522 
523         return container;
524     },
525 
526     /**
527      * Make HTML elements for a single facet selector.
528      * @private
529      * @returns {dijit.layout.AccordionPane}
530      */
531     _renderFacetSelector: function( /**String*/ facetName, /**Array[String]*/ values ) {
532 
533         var facetPane = new dijit.layout.AccordionPane(
534             {
535                 title: '<div id="facet_title_' + facetName +'" '
536                     + 'class="facetTitle">'
537                     + this._facetDisplayName(facetName)
538                     + ' <a class="clearFacet"><img src="img/red_x.png" /></a>'
539                     + '</div>'
540             });
541 
542         // make a selection control for the values of this facet
543         var facetControl = dojo.create( 'table', {className: 'facetSelect'}, facetPane.containerNode );
544         // populate selector's options
545         this.facetSelectors[facetName] = dojo.map(
546             values,
547             function(val) {
548                 var that = this;
549                 var node = dojo.create(
550                     'tr',
551                     { className: 'facetValue',
552                       innerHTML: '<td class="count"></td><td class="value">'+ val + '</td>',
553                       onclick: function(evt) {
554                           dojo.toggleClass(this, 'selected');
555                           that._updateFacetControl( facetName );
556                           that._async( function() {
557                               that.updateQuery();
558                               that._updateFacetCounts( facetName );
559                           }).call();
560                       }
561                     },
562                     facetControl
563                 );
564                 node.facetValue = val;
565                 return node;
566             },
567             this
568         );
569 
570         return facetPane;
571     },
572 
573     /**
574      * Clear all the selections from all of the facet controls.
575      * @private
576      */
577     _clearAllFacetControls: function() {
578        dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) {
579            this._clearFacetControl( facetName );
580        },this);
581     },
582 
583     /**
584      * Clear all the selections from the facet control with the given name.
585      * @private
586      */
587     _clearFacetControl: function( facetName ) {
588         dojo.forEach( this.facetSelectors[facetName] || [], function(selector) {
589                           dojo.removeClass(selector,'selected');
590                       },this);
591         this._updateFacetControl( facetName );
592     },
593 
594     /**
595      * Incrementally update the facet counts as facet values are selected.
596      * @private
597      */
598     _updateFacetCounts: function( /**String*/ skipFacetName ) {
599         dojo.forEach( dojof.keys( this.facetSelectors ), function( facetName ) {
600             if( facetName == 'My Tracks' )// || facetName == skipFacetName )
601                 return;
602             var thisFacetCounts = this.trackDataStore.getFacetCounts( facetName );
603             dojo.forEach( this.facetSelectors[facetName] || [], function( selectorNode ) {
604                 dojo.query('.count',selectorNode)
605                     .forEach( function(countNode) {
606                          var count = thisFacetCounts ? thisFacetCounts[ selectorNode.facetValue ] || 0 : 0;
607                          countNode.innerHTML = Util.addCommas( count );
608                          if( count )
609                              dojo.removeClass( selectorNode, 'disabled');
610                          else
611                              dojo.addClass( selectorNode, 'disabled' );
612                      },this);
613                 //dojo.removeClass(selector,'selected');
614             },this);
615             this._updateFacetControl( facetName );
616        },this);
617     },
618 
619     /**
620      * Update the title bar of the given facet control to reflect
621      * whether it has selected values in it.
622      */
623     _updateFacetControl: function( facetName ) {
624         var titleContent = dojo.byId('facet_title_'+facetName);
625 
626         // if all our values are disabled, add 'disabled' to our
627         // title's CSS classes
628         if( dojo.every( this.facetSelectors[facetName] ||[], function(sel) {
629                             return dojo.hasClass( sel, 'disabled' );
630                         },this)
631           ) {
632                 dojo.addClass( titleContent, 'disabled' );
633         }
634 
635         // if we have some selected values, make a "clear" button, and
636         // add 'selected' to our title's CSS classes
637         if( dojo.some( this.facetSelectors[facetName] || [], function(sel) {
638                 return dojo.hasClass( sel, 'selected' );
639             }, this ) ) {
640                 var clearFunc = dojo.hitch( this, function(evt) {
641                     this._clearFacetControl( facetName );
642                     this._async( function() {
643                         this.updateQuery();
644                         this._updateFacetCounts( facetName );
645                     },this).call();
646                     evt.stopPropagation();
647                 });
648                 dojo.addClass( titleContent, 'selected' );
649                 dojo.query( '> a', titleContent )
650                     .forEach(function(node) { node.onclick = clearFunc; },this)
651                     .attr('title','clear selections');
652         }
653         // otherwise, no selected values
654         else {
655                 dojo.removeClass( titleContent, 'selected' );
656                 dojo.query( '> a', titleContent )
657                     .onclick( function(){return false;})
658                     .removeAttr('title');
659         }
660     },
661 
662     /**
663      * Update the query we are using with the track metadata store
664      * based on the values of the search form elements.
665      */
666     updateQuery: function() {
667         this._suppressRecursion( '_updateQuery' );
668     },
669     _updateQuery: function() {
670         var newQuery = {};
671 
672         var is_selected = function(node) {
673             return dojo.hasClass(node,'selected');
674         };
675 
676         // update from the My Tracks pseudofacet
677         (function() {
678              var mytracks_options = this.facetSelectors['My Tracks'];
679 
680              // index the optoins by name
681              var byname = {};
682              dojo.forEach( mytracks_options, function(opt){ byname[opt.facetValue] = opt;});
683 
684              // if filtering for active tracks, add the labels for the
685              // currently selected tracks to the query
686              if( is_selected( byname['Currently Active'] ) ) {
687                  var activeTrackLabels = dojof.keys(this.tracksActive || {});
688                  newQuery.label = Util.uniq(
689                      (newQuery.label ||[])
690                      .concat( activeTrackLabels )
691                  );
692              }
693 
694              // if filtering for recently used tracks, add the labels of recently used tracks
695              if( is_selected( byname['Recently Used'])) {
696                  var recentlyUsed = dojo.map(
697                      this.browser.getRecentlyUsedTracks(),
698                      function(t){
699                          return t.label;
700                      }
701                  );
702 
703                  newQuery.label = Util.uniq(
704                      (newQuery.label ||[])
705                      .concat(recentlyUsed)
706                  );
707              }
708 
709              // finally, if something is selected in here, but we have
710              // not come up with any track labels, then insert a dummy
711              // track label value that will never match, because the
712              // query engine ignores empty arrayrefs.
713              if( ( ! newQuery.label || ! newQuery.label.length )
714                  && dojo.some( mytracks_options, is_selected )
715                ) {
716                    newQuery.label = ['FAKE LABEL THAT IS HIGHLY UNLIKELY TO EVER MATCH ANYTHING'];
717              }
718 
719         }).call(this);
720 
721         // update from the text filter
722         if( this.textFilterInput.value.length ) {
723             newQuery.text = this.textFilterInput.value;
724         }
725 
726         // update from the data-based facet selectors
727         dojo.forEach( this.trackDataStore.getFacetNames(), function(facetName) {
728             var options = this.facetSelectors[facetName];
729             if( !options ) return;
730 
731             var selectedFacets = dojo.map(
732                 dojo.filter( options, is_selected ),
733                 function(opt) {return opt.facetValue;}
734             );
735             if( selectedFacets.length )
736                 newQuery[facetName] = selectedFacets;
737         },this);
738 
739         this.query = newQuery;
740         this.dataGrid.setQuery( this.query );
741         this._updateMatchCount();
742     },
743 
744     /**
745      * Update the match-count text in the grid controls bar based
746      * on the last query that was run against the store.
747      * @private
748      */
749     _updateMatchCount: function() {
750         var count = this.dataGrid.store.getCount();
751         dojo.query( '.matching_record_count', this.containerElem )
752             .forEach( function(n) {
753                           n.innerHTML = Util.addCommas(count) + ' matching track' + ( count == 1 ? '' : 's' );
754                       }
755                     );
756     },
757 
758     /**
759      * Update the grid to have only rows checked that correspond to
760      * tracks that are currently active.
761      * @private
762      */
763     _updateGridSelections: function() {
764         // keep selection events from firing while we mess with the
765         // grid
766         this._ifNotSuppressed('gridUpdate', function(){
767             this._suppress('selectionEvents', function() {
768                 this.dataGrid.selection.deselectAll();
769 
770                 // check the boxes that should be checked, based on our
771                 // internal memory of what tracks should be on.
772                 for( var i= 0; i < Math.min( this.dataGrid.get('rowCount'), this.dataGrid.get('rowsPerPage') ); i++ ) {
773                     var item = this.dataGrid.getItem( i );
774                     var label = this.dataGrid.store.getIdentity( item );
775                     if( this.tracksActive[label] )
776                         this.dataGrid.rowSelectCell.toggleRow( i, true );
777                 }
778 
779             });
780         });
781     },
782 
783     /**
784      * Given an array of track configs, update the track list to show
785      * that they are turned on.
786      */
787     setTracksActive: function( /**Array[Object]*/ trackConfigs ) {
788         dojo.forEach( trackConfigs, function(conf) {
789             this.tracksActive[conf.label] = true;
790         },this);
791     },
792 
793     /**
794      * Given an array of track configs, update the track list to show
795      * that they are turned off.
796      */
797     setTracksInactive: function( /**Array[Object]*/ trackConfigs ) {
798         dojo.forEach( trackConfigs, function(conf) {
799             delete this.tracksActive[conf.label];
800         },this);
801     },
802 
803     /**
804      * Make the track selector visible.
805      */
806     show: function() {
807         window.setTimeout( dojo.hitch( this, function() {
808             this.textFilterInput.disabled = false;
809             this.textFilterInput.focus();
810         }), 300);
811 
812         dojo.addClass( this.containerElem, 'active' );
813         dojo.animateProperty({
814             node: this.containerElem,
815             properties: {
816                 left: { start: -95, end: 0, units: '%' }
817             }
818         }).play();
819 
820         this.shown = true;
821     },
822 
823     /**
824      * Make the track selector invisible.
825      */
826     hide: function() {
827 
828         dojo.removeClass( this.containerElem, 'active' );
829 
830         dojo.animateProperty({
831             node: this.containerElem,
832             properties: {
833                 left: { start: 0, end: -95, units: '%' }
834             }
835         }).play();
836 
837         this.textFilterInput.blur();
838         this.textFilterInput.disabled = true;
839 
840         this.shown = false;
841     },
842 
843     /**
844      * Toggle whether the track selector is visible.
845      */
846     toggle: function() {
847         this.shown ? this.hide() : this.show();
848     }
849 });