- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / task_manager / main.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /** @constructor */
6 function TaskManager() { }
7
8 cr.addSingletonGetter(TaskManager);
9
10 TaskManager.prototype = {
11   /**
12    * Handle window close.
13    * @this
14    */
15   onClose: function() {
16     if (!this.disabled_) {
17       this.disabled_ = true;
18       commands.disableTaskManager();
19     }
20   },
21
22   /**
23    * Handles selection changes.
24    * This is also called when data of tasks are refreshed, even if selection
25    * has not been changed.
26    * @this
27    */
28   onSelectionChange: function() {
29     var sm = this.selectionModel_;
30     var dm = this.dataModel_;
31     var selectedIndexes = sm.selectedIndexes;
32     var isEndProcessEnabled = true;
33     if (selectedIndexes.length == 0)
34       isEndProcessEnabled = false;
35     for (var i = 0; i < selectedIndexes.length; i++) {
36       var index = selectedIndexes[i];
37       var task = dm.item(index);
38       if (task['type'] == 'BROWSER')
39         isEndProcessEnabled = false;
40     }
41     if (this.isEndProcessEnabled_ != isEndProcessEnabled) {
42       if (isEndProcessEnabled)
43         $('kill-process').removeAttribute('disabled');
44       else
45         $('kill-process').setAttribute('disabled', 'true');
46
47       this.isEndProcessEnabled_ = isEndProcessEnabled;
48     }
49   },
50
51   /**
52    * Closes taskmanager dialog.
53    * After this function is called, onClose() will be called.
54    * @this
55    */
56   close: function() {
57     window.close();
58   },
59
60   /**
61    * Sends commands to kill selected processes.
62    * @this
63    */
64   killSelectedProcesses: function() {
65     var selectedIndexes = this.selectionModel_.selectedIndexes;
66     var dm = this.dataModel_;
67     var uniqueIds = [];
68     for (var i = 0; i < selectedIndexes.length; i++) {
69       var index = selectedIndexes[i];
70       var task = dm.item(index);
71       uniqueIds.push(task['uniqueId'][0]);
72     }
73
74     commands.killSelectedProcesses(uniqueIds);
75   },
76
77   /**
78    * Initializes taskmanager.
79    * @this
80    */
81   initialize: function(dialogDom, opt) {
82     if (!dialogDom) {
83       console.log('ERROR: dialogDom is not defined.');
84       return;
85     }
86
87     measureTime.startInterval('Load.DOM');
88
89     this.opt_ = opt;
90
91     this.initialized_ = true;
92
93     this.elementsCache_ = {};
94     this.dialogDom_ = dialogDom;
95     this.document_ = dialogDom.ownerDocument;
96
97     this.localized_column_ = [];
98     for (var i = 0; i < DEFAULT_COLUMNS.length; i++) {
99       var columnLabelId = DEFAULT_COLUMNS[i][1];
100       this.localized_column_[i] = loadTimeData.getString(columnLabelId);
101     }
102
103     this.initElements_();
104     this.initColumnModel_();
105     this.selectionModel_ = new cr.ui.ListSelectionModel();
106     this.dataModel_ = new cr.ui.ArrayDataModel([]);
107
108     this.selectionModel_.addEventListener('change',
109                                           this.onSelectionChange.bind(this));
110
111     // Initializes compare functions for column sort.
112     var dm = this.dataModel_;
113     // List of columns to sort by its numerical value as opposed to the
114     // formatted value, e.g., 20480 vs. 20KB.
115     var COLUMNS_SORTED_BY_VALUE = [
116         'cpuUsage', 'physicalMemory', 'sharedMemory', 'privateMemory',
117         'networkUsage', 'webCoreImageCacheSize', 'webCoreScriptsCacheSize',
118         'webCoreCSSCacheSize', 'fps', 'videoMemory', 'sqliteMemoryUsed',
119         'goatsTeleported', 'v8MemoryAllocatedSize'];
120
121     for (var i = 0; i < DEFAULT_COLUMNS.length; i++) {
122       var columnId = DEFAULT_COLUMNS[i][0];
123       var compareFunc = (function() {
124           var columnIdToSort = columnId;
125           if (COLUMNS_SORTED_BY_VALUE.indexOf(columnId) != -1)
126             columnIdToSort += 'Value';
127
128           return function(a, b) {
129               var aValues = a[columnIdToSort];
130               var bValues = b[columnIdToSort];
131               var aValue = aValues && aValues[0] || 0;
132               var bvalue = bValues && bValues[0] || 0;
133               return dm.defaultValuesCompareFunction(aValue, bvalue);
134           };
135       })();
136       dm.setCompareFunction(columnId, compareFunc);
137     }
138
139     if (isColumnEnabled(DEFAULT_SORT_COLUMN))
140       dm.sort(DEFAULT_SORT_COLUMN, DEFAULT_SORT_DIRECTION);
141
142     this.initTable_();
143
144     commands.enableTaskManager();
145
146     // Populate the static localized strings.
147     i18nTemplate.process(this.document_, loadTimeData);
148
149     measureTime.recordInterval('Load.DOM');
150     measureTime.recordInterval('Load.Total');
151
152     loadDelayedIncludes(this);
153   },
154
155   /**
156    * Initializes the visibilities and handlers of the elements.
157    * This method is called by initialize().
158    * @private
159    * @this
160    */
161   initElements_: function() {
162     // <if expr="pp_ifdef('chromeos')">
163     // The 'close-window' element exists only on ChromeOS.
164     // This <if ... /if> section is removed while flattening HTML if chrome is
165     // built as Desktop Chrome.
166     if (!this.opt_['isShowCloseButton'])
167       $('close-window').style.display = 'none';
168     $('close-window').addEventListener('click', this.close.bind(this));
169     // </if>
170
171     $('kill-process').addEventListener('click',
172                                        this.killSelectedProcesses.bind(this));
173     $('about-memory-link').addEventListener('click', commands.openAboutMemory);
174   },
175
176   /**
177    * Additional initialization of taskmanager. This function is called when
178    * the loading of delayed scripts finished.
179    * @this
180    */
181   delayedInitialize: function() {
182     this.initColumnMenu_();
183     this.initTableMenu_();
184
185     var dm = this.dataModel_;
186     for (var i = 0; i < dm.length; i++) {
187       var processId = dm.item(i)['processId'][0];
188       for (var j = 0; j < DEFAULT_COLUMNS.length; j++) {
189         var columnId = DEFAULT_COLUMNS[j][0];
190
191         var row = dm.item(i)[columnId];
192         if (!row)
193           continue;
194
195         for (var k = 0; k < row.length; k++) {
196           var labelId = 'detail-' + columnId + '-pid' + processId + '-' + k;
197           var label = $(labelId);
198
199           // Initialize a context-menu, if the label exists and its context-
200           // menu is not initialized yet.
201           if (label && !label.contextMenu)
202             cr.ui.contextMenuHandler.setContextMenu(label,
203                                                     this.tableContextMenu_);
204         }
205       }
206     }
207
208     this.isFinishedInitDelayed_ = true;
209     var t = this.table_;
210     t.redraw();
211     addEventListener('resize', t.redraw.bind(t));
212   },
213
214   initColumnModel_: function() {
215     var tableColumns = new Array();
216     for (var i = 0; i < DEFAULT_COLUMNS.length; i++) {
217       var column = DEFAULT_COLUMNS[i];
218       var columnId = column[0];
219       if (!isColumnEnabled(columnId))
220         continue;
221
222       tableColumns.push(new cr.ui.table.TableColumn(columnId,
223                                                      this.localized_column_[i],
224                                                      column[2]));
225     }
226
227     for (var i = 0; i < tableColumns.length; i++) {
228       tableColumns[i].renderFunction = this.renderColumn_.bind(this);
229     }
230
231     this.columnModel_ = new cr.ui.table.TableColumnModel(tableColumns);
232   },
233
234   initColumnMenu_: function() {
235     this.column_menu_commands_ = [];
236
237     this.commandsElement_ = this.document_.createElement('commands');
238     this.document_.body.appendChild(this.commandsElement_);
239
240     this.columnSelectContextMenu_ = this.document_.createElement('menu');
241     for (var i = 0; i < DEFAULT_COLUMNS.length; i++) {
242       var column = DEFAULT_COLUMNS[i];
243
244       // Creates command element to receive event.
245       var command = this.document_.createElement('command');
246       command.id = COMMAND_CONTEXTMENU_COLUMN_PREFIX + '-' + column[0];
247       cr.ui.Command.decorate(command);
248       this.column_menu_commands_[command.id] = command;
249       this.commandsElement_.appendChild(command);
250
251       // Creates menuitem element.
252       var item = this.document_.createElement('menuitem');
253       item.command = command;
254       command.menuitem = item;
255       item.textContent = this.localized_column_[i];
256       if (isColumnEnabled(column[0]))
257         item.setAttributeNode(this.document_.createAttribute('checked'));
258       this.columnSelectContextMenu_.appendChild(item);
259     }
260
261     this.document_.body.appendChild(this.columnSelectContextMenu_);
262     cr.ui.Menu.decorate(this.columnSelectContextMenu_);
263
264     cr.ui.contextMenuHandler.setContextMenu(this.table_.header,
265                                             this.columnSelectContextMenu_);
266     cr.ui.contextMenuHandler.setContextMenu(this.table_.list,
267                                             this.columnSelectContextMenu_);
268
269     this.document_.addEventListener('command', this.onCommand_.bind(this));
270     this.document_.addEventListener('canExecute',
271                                     this.onCommandCanExecute_.bind(this));
272   },
273
274   initTableMenu_: function() {
275     this.table_menu_commands_ = [];
276     this.tableContextMenu_ = this.document_.createElement('menu');
277
278     var addMenuItem = function(tm, commandId, string_id) {
279       // Creates command element to receive event.
280       var command = tm.document_.createElement('command');
281       command.id = COMMAND_CONTEXTMENU_TABLE_PREFIX + '-' + commandId;
282       cr.ui.Command.decorate(command);
283       tm.table_menu_commands_[command.id] = command;
284       tm.commandsElement_.appendChild(command);
285
286       // Creates menuitem element.
287       var item = tm.document_.createElement('menuitem');
288       item.command = command;
289       command.menuitem = item;
290       item.textContent = loadTimeData.getString(string_id);
291       tm.tableContextMenu_.appendChild(item);
292     };
293
294     addMenuItem(this, 'inspect', 'inspect');
295     addMenuItem(this, 'activate', 'activate');
296
297     this.document_.body.appendChild(this.tableContextMenu_);
298     cr.ui.Menu.decorate(this.tableContextMenu_);
299   },
300
301   initTable_: function() {
302     if (!this.dataModel_ || !this.selectionModel_ || !this.columnModel_) {
303       console.log('ERROR: some models are not defined.');
304       return;
305     }
306
307     this.table_ = this.dialogDom_.querySelector('.detail-table');
308     cr.ui.Table.decorate(this.table_);
309
310     this.table_.dataModel = this.dataModel_;
311     this.table_.selectionModel = this.selectionModel_;
312     this.table_.columnModel = this.columnModel_;
313
314     // Expands height of row when a process has some tasks.
315     this.table_.fixedHeight = false;
316
317     this.table_.list.addEventListener('contextmenu',
318                                       this.onTableContextMenuOpened_.bind(this),
319                                       true);
320
321     // Sets custom row render function.
322     this.table_.setRenderFunction(this.getRow_.bind(this));
323   },
324
325   /**
326    * Returns a list item element of the list. This method trys to reuse the
327    * cached element, or creates a new element.
328    * @return {cr.ui.ListItem}  list item element which contains the given data.
329    * @private
330    * @this
331    */
332   getRow_: function(data, table) {
333     // Trys to reuse the cached row;
334     var listItemElement = this.renderRowFromCache_(data, table);
335     if (listItemElement)
336       return listItemElement;
337
338     // Initializes the cache.
339     var pid = data['processId'][0];
340     this.elementsCache_[pid] = {
341       listItem: null,
342       cell: [],
343       icon: [],
344       columns: {}
345     };
346
347     // Create new row.
348     return this.renderRow_(data, table);
349   },
350
351   /**
352    * Returns a list item element with re-using the previous cached element, or
353    * returns null if failed.
354    * @return {cr.ui.ListItem} cached un-used element to be reused.
355    * @private
356    * @this
357    */
358   renderRowFromCache_: function(data, table) {
359     var pid = data['processId'][0];
360
361     // Checks whether the cache exists or not.
362     var cache = this.elementsCache_[pid];
363     if (!cache)
364       return null;
365
366     var listItemElement = cache.listItem;
367     var cm = table.columnModel;
368     // Checks whether the number of columns has been changed or not.
369     if (cache.cachedColumnSize != cm.size)
370       return null;
371     // Checks whether the number of childlen tasks has been changed or not.
372     if (cache.cachedChildSize != data['uniqueId'].length)
373       return null;
374
375     // Updates informations of the task if necessary.
376     for (var i = 0; i < cm.size; i++) {
377       var columnId = cm.getId(i);
378       var columnData = data[columnId];
379       var oldColumnData = listItemElement.data[columnId];
380       var columnElements = cache.columns[columnId];
381
382       if (!columnData || !oldColumnData || !columnElements)
383         return null;
384
385       // Sets new width of the cell.
386       var cellElement = cache.cell[i];
387       cellElement.style.width = cm.getWidth(i) + '%';
388
389       for (var j = 0; j < columnData.length; j++) {
390         // Sets the new text, if the text has been changed.
391         if (oldColumnData[j] != columnData[j]) {
392           var textElement = columnElements[j];
393           textElement.textContent = columnData[j];
394         }
395       }
396     }
397
398     // Updates icon of the task if necessary.
399     var oldIcons = listItemElement.data['icon'];
400     var newIcons = data['icon'];
401     if (oldIcons && newIcons) {
402       for (var j = 0; j < columnData.length; j++) {
403         var oldIcon = oldIcons[j];
404         var newIcon = newIcons[j];
405         if (oldIcon != newIcon) {
406           var iconElement = cache.icon[j];
407           iconElement.src = newIcon;
408         }
409       }
410     }
411     listItemElement.data = data;
412
413     // Removes 'selected' and 'lead' attributes.
414     listItemElement.removeAttribute('selected');
415     listItemElement.removeAttribute('lead');
416
417     return listItemElement;
418   },
419
420   /**
421    * Create a new list item element.
422    * @return {cr.ui.ListItem} created new list item element.
423    * @private
424    * @this
425    */
426   renderRow_: function(data, table) {
427     var pid = data['processId'][0];
428     var cm = table.columnModel;
429     var listItem = new cr.ui.ListItem({label: ''});
430
431     listItem.className = 'table-row';
432
433     for (var i = 0; i < cm.size; i++) {
434       var cell = document.createElement('div');
435       cell.style.width = cm.getWidth(i) + '%';
436       cell.className = 'table-row-cell';
437       cell.id = 'column-' + pid + '-' + cm.getId(i);
438       cell.appendChild(
439           cm.getRenderFunction(i).call(null, data, cm.getId(i), table));
440
441       listItem.appendChild(cell);
442
443       // Stores the cell element to the dictionary.
444       this.elementsCache_[pid].cell[i] = cell;
445     }
446
447     // Specifies the height of the row. The height of each row is
448     // 'num_of_tasks * HEIGHT_OF_TASK' px.
449     listItem.style.height = (data['uniqueId'].length * HEIGHT_OF_TASK) + 'px';
450
451     listItem.data = data;
452
453     // Stores the list item element, the number of columns and the number of
454     // childlen.
455     this.elementsCache_[pid].listItem = listItem;
456     this.elementsCache_[pid].cachedColumnSize = cm.size;
457     this.elementsCache_[pid].cachedChildSize = data['uniqueId'].length;
458
459     return listItem;
460   },
461
462   /**
463    * Create a new element of the cell.
464    * @return {HTMLDIVElement} created cell
465    * @private
466    * @this
467    */
468   renderColumn_: function(entry, columnId, table) {
469     var container = this.document_.createElement('div');
470     container.className = 'detail-container-' + columnId;
471     var pid = entry['processId'][0];
472
473     var cache = [];
474     var cacheIcon = [];
475
476     if (entry && entry[columnId]) {
477       container.id = 'detail-container-' + columnId + '-pid' + entry.processId;
478
479       for (var i = 0; i < entry[columnId].length; i++) {
480         var label = document.createElement('div');
481         if (columnId == 'title') {
482           // Creates a page title element with icon.
483           var image = this.document_.createElement('img');
484           image.className = 'detail-title-image';
485           image.src = entry['icon'][i];
486           image.id = 'detail-title-icon-pid' + pid + '-' + i;
487           label.appendChild(image);
488           var text = this.document_.createElement('div');
489           text.className = 'detail-title-text';
490           text.id = 'detail-title-text-pid' + pid + '-' + i;
491           text.textContent = entry['title'][i];
492           label.appendChild(text);
493
494           // Chech if the delayed scripts (included in includes.js) have been
495           // loaded or not. If the delayed scripts ware not loaded yet, a
496           // context menu could not be initialized. In such case, it will be
497           // initialized at delayedInitialize() just after loading of delayed
498           // scripts instead of here.
499           if (this.isFinishedInitDelayed_)
500             cr.ui.contextMenuHandler.setContextMenu(label,
501                                                     this.tableContextMenu_);
502
503           label.addEventListener('dblclick', (function(uniqueId) {
504               commands.activatePage(uniqueId);
505           }).bind(this, entry['uniqueId'][i]));
506
507           label.data = entry;
508           label.index_in_group = i;
509
510           cache[i] = text;
511           cacheIcon[i] = image;
512         } else {
513           label.textContent = entry[columnId][i];
514           cache[i] = label;
515         }
516         label.id = 'detail-' + columnId + '-pid' + pid + '-' + i;
517         label.className = 'detail-' + columnId + ' pid' + pid;
518         container.appendChild(label);
519       }
520
521       this.elementsCache_[pid].columns[columnId] = cache;
522       if (columnId == 'title')
523         this.elementsCache_[pid].icon = cacheIcon;
524     }
525     return container;
526   },
527
528   /**
529    * Updates the task list with the supplied task.
530    * @private
531    * @this
532    */
533   processTaskChange: function(task) {
534     var dm = this.dataModel_;
535     var sm = this.selectionModel_;
536     if (!dm || !sm) return;
537
538     this.table_.list.startBatchUpdates();
539     sm.beginChange();
540
541     var type = task.type;
542     var start = task.start;
543     var length = task.length;
544     var tasks = task.tasks;
545
546     // We have to store the selected pids and restore them after
547     // splice(), because it might replace some items but the replaced
548     // items would lose the selection.
549     var oldSelectedIndexes = sm.selectedIndexes;
550
551     // Create map of selected PIDs.
552     var selectedPids = {};
553     for (var i = 0; i < oldSelectedIndexes.length; i++) {
554       var item = dm.item(oldSelectedIndexes[i]);
555       if (item) selectedPids[item['processId'][0]] = true;
556     }
557
558     var args = tasks.slice();
559     args.unshift(start, dm.length);
560     dm.splice.apply(dm, args);
561
562     // Create new array of selected indexes from map of old PIDs.
563     var newSelectedIndexes = [];
564     for (var i = 0; i < dm.length; i++) {
565       if (selectedPids[dm.item(i)['processId'][0]])
566         newSelectedIndexes.push(i);
567     }
568
569     sm.selectedIndexes = newSelectedIndexes;
570
571     var pids = [];
572     for (var i = 0; i < dm.length; i++) {
573       pids.push(dm.item(i)['processId'][0]);
574     }
575
576     // Sweeps unused caches, which elements no longer exist on the list.
577     for (var pid in this.elementsCache_) {
578       if (pids.indexOf(pid) == -1)
579         delete this.elementsCache_[pid];
580     }
581
582     sm.endChange();
583     this.table_.list.endBatchUpdates();
584   },
585
586   /**
587    * Respond to a command being executed.
588    * @this
589    */
590   onCommand_: function(event) {
591     var command = event.command;
592     var commandId = command.id.split('-', 2);
593
594     var mainCommand = commandId[0];
595     var subCommand = commandId[1];
596
597     if (mainCommand == COMMAND_CONTEXTMENU_COLUMN_PREFIX) {
598       this.onColumnContextMenu_(subCommand, command);
599     } else if (mainCommand == COMMAND_CONTEXTMENU_TABLE_PREFIX) {
600       var targetUniqueId = this.currentContextMenuTarget_;
601
602       if (!targetUniqueId)
603         return;
604
605       if (subCommand == 'inspect')
606         commands.inspect(targetUniqueId);
607       else if (subCommand == 'activate')
608         commands.activatePage(targetUniqueId);
609
610       this.currentContextMenuTarget_ = undefined;
611     }
612   },
613
614   onCommandCanExecute_: function(event) {
615     event.canExecute = true;
616   },
617
618   /**
619    * Store resourceIndex of target resource of context menu, because resource
620    * will be replaced when it is refreshed.
621    * @this
622    */
623   onTableContextMenuOpened_: function(e) {
624     if (!this.isFinishedInitDelayed_)
625       return;
626
627     var mc = this.table_menu_commands_;
628     var inspectMenuitem =
629         mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-inspect'].menuitem;
630     var activateMenuitem =
631         mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-activate'].menuitem;
632
633     // Disabled by default.
634     inspectMenuitem.disabled = true;
635     activateMenuitem.disabled = true;
636
637     var target = e.target;
638     for (;; target = target.parentNode) {
639       if (!target) return;
640       var classes = target.classList;
641       if (classes &&
642           Array.prototype.indexOf.call(classes, 'detail-title') != -1) break;
643     }
644
645     var indexInGroup = target.index_in_group;
646
647     // Sets the uniqueId for current target page under the mouse corsor.
648     this.currentContextMenuTarget_ = target.data['uniqueId'][indexInGroup];
649
650     // Enables if the page can be inspected.
651     if (target.data['canInspect'][indexInGroup])
652       inspectMenuitem.disabled = false;
653
654     // Enables if the page can be activated.
655     if (target.data['canActivate'][indexInGroup])
656       activateMenuitem.disabled = false;
657   },
658
659   onColumnContextMenu_: function(columnId, command) {
660     var menuitem = command.menuitem;
661     var checkedItemCount = 0;
662     var checked = isColumnEnabled(columnId);
663
664     // Leaves a item visible when user tries making invisible but it is the
665     // last one.
666     var enabledColumns = getEnabledColumns();
667     for (var id in enabledColumns) {
668       if (enabledColumns[id])
669         checkedItemCount++;
670     }
671     if (checkedItemCount == 1 && checked)
672       return;
673
674     // Toggles the visibility of the column.
675     var newChecked = !checked;
676     menuitem.checked = newChecked;
677     setColumnEnabled(columnId, newChecked);
678
679     this.initColumnModel_();
680     this.table_.columnModel = this.columnModel_;
681     this.table_.redraw();
682   },
683 };
684
685 // |taskmanager| has been declared in preload.js.
686 taskmanager = TaskManager.getInstance();
687
688 function init() {
689   var params = parseQueryParams(window.location);
690   var opt = {};
691   opt['isShowCloseButton'] = params.showclose;
692   taskmanager.initialize(document.body, opt);
693 }
694
695 document.addEventListener('DOMContentLoaded', init);
696 document.addEventListener('Close', taskmanager.onClose.bind(taskmanager));