merge master
[xtuple] / lib / enyo-x / source / app.js
1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
2 newcap:true, noarg:true, regexp:true, undef:true, trailing:true,
3 white:true*/
4 /*global enyo:true, XT:true, _:true, document:true, window:true, XM:true */
5
6 (function () {
7
8   var UNINITIALIZED = 0;
9   var LOADING_SESSION = 1;
10   var LOADING_EXTENSIONS = 3;
11   var LOADING_SCHEMA = 4;
12   var LOADING_APP_DATA = 5;
13   var RUNNING = 6;
14
15   /**
16     Application kind. Contains two components, "postbooks", which is a module container,
17     and the pullout.
18    */
19   enyo.kind(
20     /** @lends XV.App# */{
21     name: "XV.App",
22     classes: "enyo-fit enyo-unselectable",
23     published: {
24       isStarted: false,
25       debug: false,
26       keyCapturePatterns: []
27     },
28     handlers: {
29       onListAdded: "addPulloutItem",
30       onModelChange: "modelChanged",
31       onParameterChange: "parameterDidChange",
32       onNavigatorEvent: "togglePullout",
33       onHistoryChange: "refreshHistoryPanel",
34       onHistoryItemSelected: "selectHistoryItem",
35       onSearch: "waterfallSearch",
36       onWorkspace: "waterfallWorkspace",
37       onColumnsChange: "columnsDidChange",
38       onWorkspaceAction: "waterfallWorkspaceAction"
39     },
40     /*
41       Three use cases:
42       hotkey mode (triggered by alt)
43       notify popup mode (if the notify popup is showing)
44       pattern-match mode
45     */
46     handleKeyDown: function (inSender, inEvent) {
47       var that = this,
48         keyCode = inEvent.keyCode;
49
50       // remember the last 10 key presses
51       this._keyBufferArray.push(keyCode);
52       this._keyBufferArray = this._keyBufferArray.slice(-10);
53
54       // not working
55       if (this.$.postbooks.isNotifyPopupShowing()) {
56         this.$.postbooks.notifyKey(keyCode, inEvent.shiftKey);
57         return;
58       }
59
60       if (keyCode === 189) {
61         // XXX FIXME hack. Dashes aren't coming through as dashes from my keyboard
62         keyCode = '-'.charCodeAt(0);
63       }
64       if (inEvent.altKey) {
65         inEvent.cancelBubble = true;
66         inEvent.returnValue = false;
67         this.processHotKey(keyCode);
68         return true;
69       }
70       if (this._keyBufferEndPattern) {
71         // we're in record mode, so record.
72         this._keyBuffer = this._keyBuffer + String.fromCharCode(keyCode);
73       }
74
75       if (this._keyBufferEndPattern &&
76           _.isEqual(this._keyBufferArray.slice(-1 * this._keyBufferEndPattern.length), this._keyBufferEndPattern) &&
77           this._falsePositives) {
78         this._falsePositives--;
79
80       } else if (this._keyBufferEndPattern &&
81           _.isEqual(this._keyBufferArray.slice(-1 * this._keyBufferEndPattern.length), this._keyBufferEndPattern)) {
82
83         // first slice the end pattern off the payload
84         this._keyBuffer = this._keyBuffer.substring(0, this._keyBuffer.length - this._keyBufferEndPattern.length);
85
86         // we've matched an end pattern. Send the recorded buffer to the appropriate method
87         this[this._keyBufferMethod](this._keyBuffer);
88         this._keyBuffer = "";
89         this._keyBufferEndPattern = undefined;
90         this._falsePositives = undefined;
91       }
92
93       // specification of the key capture patterns themselves are up to the subkind
94       _.each(this.getKeyCapturePatterns(), function (pattern) {
95         if (_.isEqual(that._keyBufferArray.slice(-1 * pattern.start.length), pattern.start)) {
96           // we've matched a start pattern. Now we're in record mode, waiting for a match to the end pattern
97           that._keyBuffer = that._keyBuffer || "";
98           that._keyBufferEndPattern = pattern.end;
99           that._keyBufferMethod = pattern.method;
100           that._falsePositives = pattern.falsePositives;
101         }
102       });
103
104     },
105     // the components array is overriden by the subkind
106     components: [
107       {name: "postbooks", kind: "XV.ModuleContainer",  onTransitionStart: "handlePullout"},
108       {name: "pullout", kind: "XV.Pullout", onAnimateFinish: "pulloutAnimateFinish"},
109       {name: "signals", kind: "enyo.Signals", onkeydown: "handleKeyDown"},
110     ],
111     state: UNINITIALIZED,
112     /**
113       Passes the pullout payload straight from the sender (presumably the list
114       containing the pullout parameter) to the pullout, who will deal with
115       adding it.
116      */
117     addPulloutItem: function (inSender, inEvent) {
118       if (!this.$.pullout) {
119         this._cachePullouts.push(inEvent);
120         return;
121       }
122       this.$.pullout.addPulloutItem(inSender, inEvent);
123     },
124     columnsDidChange: function (inSender, inEvent) {
125       this.$.postbooks.getNavigator().waterfall("onColumnsChange", inEvent);
126     },
127     create: function () {
128       this._cachePullouts = [];
129       this._keyBufferArray = [];
130       this.inherited(arguments);
131       XT.app = this;
132       // make even modal popups hear keydown (necessary for keyboarding the notify popup)
133       enyo.dispatcher.autoForwardEvents.keydown = 1;
134       window.onbeforeunload = function () {
135         return "_exitPageWarning".loc();
136       };
137     },
138     handlePullout: function (inSender, inEvent) {
139       var showing = inSender.getActive().showPullout || false;
140       this.$.pullout.setShowing(showing);
141     },
142     /*
143       When a model is changed, we want to reflect the new state across
144       the app, so we bubble all the way up there and waterfall down.
145       Currently, the only things that are updated by this process are the lists.
146     */
147     modelChanged: function (inSender, inEvent) {
148       //
149       // Waterfall down to all lists to tell them to update themselves
150       //
151       this.$.postbooks.getNavigator().waterfall("onModelChange", inEvent);
152     },
153     parameterDidChange: function (inSender, inEvent) {
154       if (this.getIsStarted()) {
155         this.$.postbooks.getNavigator().waterfall("onParameterChange", inEvent);
156       }
157     },
158     /**
159       Implemented by the subkind
160     */
161     processHotKey: function (keyCode) {
162     },
163     /**
164      * Manages the "lit-up-ness" of the icon buttons based on the pullout.
165      * If the pull-out is put away, we want all buttons to dim. If the pull-out
166      * is activated, we want the button related to the active pullout pane
167      * to light up. The presentation of these buttons take care of themselves
168      * if the user actually clicks on the buttons.
169      */
170     pulloutAnimateFinish: function (inSender, inEvent) {
171       var activeIconButton;
172
173       if (inSender.value === inSender.max) {
174         // pullout is active
175         if (this.$.pullout.getSelectedPanel() === 'history') {
176           activeIconButton = 'history';
177         } else {
178           activeIconButton = 'search';
179         }
180       } else if (inSender.value === inSender.min) {
181         // pullout is inactive
182         activeIconButton = null;
183       }
184       this.$.postbooks.getNavigator().setActiveIconButton(activeIconButton);
185     },
186     refreshHistoryPanel: function (inSender, inEvent) {
187       this.$.pullout.refreshHistoryList();
188     },
189     /**
190       When a history item is selected we bubble an event way up the application.
191       Note that we create a sort of ersatz model to mimic the way the handler
192       expects to have a model with the event to know what to drill down into.
193     */
194     selectHistoryItem: function (inSender, inEvent) {
195       inEvent.eventName = "onWorkspace";
196       this.waterfall("onWorkspace", inEvent);
197     },
198     startupProcess: function () {
199       var startupManager = XT.getStartupManager(),
200         progressBar = XT.app.$.postbooks.getStartupProgressBar(),
201         that = this,
202         prop,
203         ext,
204         extprop,
205         i = 0,
206         task,
207         startupTaskCount,
208         text,
209         inEvent,
210         ajax,
211         extensionSuccess,
212         extensionError,
213         extensionLocation,
214         extensionName,
215         extensionPrivilegeName,
216         extensionCount = 0,
217         extensionsDownloaded = 0,
218         extensionPayloads = [],
219         loginInfo,
220         details,
221         eachCallback = function () {
222           var completed = startupManager.get('completed').length;
223           progressBar.animateProgressTo(completed);
224           if (completed === startupTaskCount) {
225             that.startupProcess();
226           }
227         };
228
229       // 1: Load session data
230       if (this.state === UNINITIALIZED) {
231         this.state = LOADING_SESSION;
232         startupManager.registerCallback(eachCallback, true);
233         XT.dataSource.connect();
234         startupTaskCount = startupManager.get('queue').length + startupManager.get('completed').length;
235         progressBar.setMax(startupTaskCount);
236
237       // #2 is off high-wire walking the Grand Canyon
238
239       // 3: Initialize extensions
240       } else if (this.state === LOADING_SESSION) {
241
242         this.state = LOADING_EXTENSIONS;
243         text = "_loadingExtensions".loc() + "...";
244         XT.app.$.postbooks.getStartupText().setContent(text);
245         for (prop in XT.extensions) {
246           if (XT.extensions.hasOwnProperty(prop)) {
247             ext = XT.extensions[prop];
248             for (extprop in ext) {
249               if (ext.hasOwnProperty(extprop) &&
250                   typeof ext[extprop] === "function") {
251                 //XT.log('Installing ' + prop + ' ' + extprop);
252                 ext[extprop]();
253               }
254             }
255           }
256           i++;
257         }
258         this.startupProcess();
259
260       // 4. Load Schema
261       } else if (this.state === LOADING_EXTENSIONS) {
262         this.state = LOADING_SCHEMA;
263         text = "_loadingSchema".loc() + "...";
264         XT.app.$.postbooks.getStartupText().setContent(text);
265         XT.StartupTask.create({
266           taskName: "loadSessionSchema",
267           task: function () {
268             var task = this,
269               options = {
270                 success: function () {
271                   task.didComplete();
272                   that.startupProcess();
273                 }
274               };
275             XT.session.loadSessionObjects(XT.session.SCHEMA, options);
276           }
277         });
278
279       // 5 Load Application Data
280       } else if (this.state === LOADING_SCHEMA) {
281         // Run startup tasks
282         this.state = LOADING_APP_DATA;
283         text = "_loadingApplicationData".loc() + "...";
284         XT.app.$.postbooks.getStartupText().setContent(text);
285         progressBar.setMax(XT.StartupTasks.length);
286         progressBar.setProgress(0);
287
288         // there's a new startup task count now that
289         // the second stage of tasks are being loaded.
290         startupTaskCount = startupManager.get('queue').length +
291           startupManager.get('completed').length +
292           XT.StartupTasks.length;
293
294         // create a new each callback to manage the completion of this step
295         // the previously registered callback isn't doing any harm. It would be
296         // best to unregister the previous eachCallback and replicate the code
297         // here that advances the progress bar. This would make the animation look
298         // better. There's not a great way to unregister a startup callback, though.
299         eachCallback = function () {
300           var completed = startupManager.get('completed').length;
301           if (completed === startupTaskCount) {
302             that.startupProcess();
303           }
304         };
305
306         startupManager.registerCallback(eachCallback, true);
307         for (i = 0; i < XT.StartupTasks.length; i++) {
308           task = XT.StartupTasks[i];
309           XT.StartupTask.create(task);
310         }
311
312       // 6. Finish up
313       } else if (this.state === LOADING_APP_DATA) {
314         // Go to the navigator
315         for (i = 0; i < this._cachePullouts.length; i++) {
316           inEvent = this._cachePullouts[i];
317           this.$.pullout.addPulloutItem(null, inEvent);
318         }
319         loginInfo = XT.app.$.postbooks.getNavigator().loginInfo();
320         details = XT.session.details;
321         loginInfo.setContent(details.username + " ยท " + details.organization);
322         this.state = RUNNING;
323         XT.app.$.postbooks.activate();
324       }
325     },
326     start: function (debug) {
327       if (this.getIsStarted()) { return; }
328       XT.app = this;
329       this.setDebug(debug);
330
331       // Run through the multi-step start process
332       this.startupProcess();
333
334       // lets not allow this to happen again
335       this.setIsStarted(true);
336     },
337     show: function () {
338       if (this.getShowing() && this.getIsStarted()) {
339         this.renderInto(document.body);
340       } else {
341         this.inherited(arguments);
342       }
343     },
344     togglePullout: function (inSender, inEvent) {
345       this.$.pullout.togglePullout(inSender, inEvent);
346     },
347     waterfallSearch: function (inSender, inEvent) {
348       this.$.postbooks.waterfall("onSearch", inEvent);
349       return true; // don't want to double up
350     },
351     waterfallWorkspace: function (inSender, inEvent) {
352       this.$.postbooks.waterfall("onWorkspace", inEvent);
353       return true; // don't want to double up
354     },
355     waterfallWorkspaceAction: function (inSender, inEvent) {
356       this.$.postbooks.waterfall("onWorkspaceAction", inEvent);
357       return true; // don't want to double up
358     }
359   });
360 }());