Commit 2aa340b8 authored by Bas Lijnse's avatar Bas Lijnse

Added double buffering mechanism for hiding slow form updates

git-svn-id: https://svn.cs.ru.nl/repos/iTask-system/branches/fancyTasks@345 63da3aa8-80fd-4f01-9db8-e6ea747a3da2
parent 3ce7d50a
......@@ -7,11 +7,16 @@ Ext.ns('itasks');
itasks.WorkTabPanel = Ext.extend(Ext.Panel, {
updates: {}, //Dictionary with form updates
inputs: [],
state: undefined, //The encoded state that is temporarily stored in the tab
busy: false, //Lock to prevent multiple requests at once
debugPanel: undefined, //An optional reference to a debug panel to find trace options
applicationPanel: undefined, //A reference to the application panel to find the session id
lastFocus: undefined, //The id of the last focused input
firstBuffer: false, //Use the first buffer panel (double buffering is used for rendering)
prefixCounter: 0, //Prefix counter
contentPanel: undefined, //A reference to the panel which is currently visible
initComponent: function () {
......@@ -47,7 +52,13 @@ itasks.WorkTabPanel = Ext.extend(Ext.Panel, {
}],
activeItem: 0,
items: [{
xtype: 'panel',
xtype: 'panel', //Task panel (no trace) 1
border: false,
cls: 'worktab-content',
autoWidth: true,
autoScroll: true
},{
xtype: 'panel', //Task panel (no trace) 2
border: false,
cls: 'worktab-content',
autoWidth: true,
......@@ -61,7 +72,12 @@ itasks.WorkTabPanel = Ext.extend(Ext.Panel, {
layoutOnTabChange: true,
activeTab: 0,
items: [{
xtype: 'panel',
xtype: 'panel', //Task panel (trace) 1
autoWidth: true,
title: 'Task',
cls: 'worktab-content'
},{
xtype: 'panel', //Task panel (trace) 2
autoWidth: true,
title: 'Task',
cls: 'worktab-content'
......@@ -124,243 +140,258 @@ itasks.WorkTabPanel = Ext.extend(Ext.Panel, {
if(success) {
var data = Ext.decode(response.responseText);
var trace = (data.stateTrace != undefined || data.updateTrace != undefined || data.subtreeTrace != undefined);
//Check for session errors.
this.applicationPanel.checkSessionResponse(data);
//Clear the updates list
this.updates = {};
//Determine in which panel the new content must be put
this.setNextContentPanel(trace);
//Save the state
this.state = data.state;
//Fill the content and trace panels
this.setupContentPanel(trace, data);
this.setupTracePanels(trace, data);
var mainPanel = this.getComponent(1);
var tracePanel = mainPanel.getComponent(1);
//Check if trace information is available
if(data.stateTrace != undefined || data.updateTrace != undefined || data.subtreeTrace != undefined) {
//Hide the current content panel and switch to new content
this.switchContentPanels(trace);
mainPanel.layout.setActiveItem(1);
this.contentPanel = tracePanel.getComponent(0);
emptyPanel = mainPanel.getComponent(0);
var statePanel = tracePanel.getComponent(1);
if(data.stateTrace != undefined) {
statePanel.getEl().dom.innerHTML = data.stateTrace;
statePanel.enable();
} else {
statePanel.disable();
}
var updatePanel = tracePanel.getComponent(2);
if(data.updateTrace != undefined) {
updatePanel.getEl().dom.innerHTML = data.updateTrace;
updatePanel.enable();
} else {
updatePanel.disable();
}
var subtreePanel = tracePanel.getComponent(3);
if(data.subtreeTrace != undefined) {
subtreePanel.getEl().dom.innerHTML = data.subtreeTrace;
subtreePanel.enable();
} else {
subtreePanel.disable();
}
//Reset for new updates
this.updates = {};
this.inputs = data.inputs;
this.state = data.state;
}
//Release the busy lock
this.setBusy(false);
},
setNextContentPanel: function (trace) {
if(trace) {
this.contentPanel = this.getComponent(1).getComponent(2).getComponent(this.firstBuffer ? 0 : 1);
} else {
this.contentPanel = this.getComponent(1).getComponent(this.firstBuffer ? 0 : 1);
}
},
setupContentPanel: function (trace, data) {
//Replace the content
this.contentPanel.body.dom.innerHTML = data.html;
tracePanel.setActiveTab(0);
} else {
mainPanel.layout.setActiveItem(0);
this.contentPanel = mainPanel.getComponent(0);
emptyPanel = tracePanel.getComponent(0);
}
//"ExtJS-ify" the inputs and attach event handlers
var num = data.inputs.length;
var forms = {};
for(var i = 0; i < num; i++) {
//Clear the panel which may contain content
//of the previous request
emptyPanel.body.dom.innerHTML = "";
if (data.error != null) {
this.autoClose(this.makeErrorMessage(data.error), 5);
} else if(data.status == 'TaskFinished') { //Check if the task is done
this.fireEvent('taskfinished', this.id);
this.autoClose(this.makeFinishedMessage(), 5);
} else if(data.status == 'TaskDeleted') {
this.fireEvent('taskdeleted', this.id);
this.autoClose(this.makeDeletedMessage(), 5);
} else {
//Update the tab content
this.contentPanel.body.dom.innerHTML = data.html;
//"ExtJS-ify" the inputs and attach event handlers
var num = data.inputs.length;
var forms = {};
var inputid = data.prefix + data.inputs[i].formid + '-' + data.inputs[i].inputid;
var inputname = data.inputs[i].formid + '-' + data.inputs[i].inputid;
var input = Ext.get(inputid);
//Record the formid
forms[data.inputs[i].formid] = true;
//ExtJS-ify
switch(data.inputs[i].type) {
for(var i = 0; i < num; i++) {
case "Int":
case "Real":
case "String":
var value = input.getValue();
var parent = input.parent();
var next = input.next();
var inputid = data.inputs[i].formid + '-' + data.inputs[i].inputid;
var input = Ext.get(inputid);
//Replace
input.remove();
if(data.inputs[i].type == "String") {
input = new Ext.form.TextField({
id: inputid,
name: inputname,
value: value
});
} else {
input = new Ext.form.NumberField({
id: inputid,
name: inputname,
value: value,
allowDecimals: (data.inputs[i].type == "Real"),
decimalPrecision: Number.MAX_VALUE,
style: "width: 5em"
});
}
input.render(parent, next);
//Record the formid
forms[data.inputs[i].formid] = true;
//ExtJS-ify
switch(data.inputs[i].type) {
//Event handlers
if(data.inputs[i].updateon == "OnChange") {
input.on("change", function (inp, newVal, oldVal) {
this.addUpdate(inp.name, newVal);
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
}
if(data.inputs[i].updateon == "OnSubmit") {
input.on("change", function (inp, newVal, oldVal) {
this.addUpdate(inp.name, newVal);
},this);
}
input.on("focus", function (inp) {
this.lastFocus = inp.id;
},this);
break;
case "Bool":
case "Maybe":
var checked = input.dom.checked;
var parent = input.parent();
var next = input.next();
//Replace
input.remove();
input = new Ext.form.Checkbox({
id: inputid,
name: inputname,
checked: checked
});
input.render(parent,next);
//Attach event handlers
if(data.inputs[i].updateon == "OnChange") {
input.on("check", function (inp, checked) {
this.addUpdate(inp.name, checked ? "checked" : "unchecked");
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
}
if(data.inputs[i].updateon == "OnSubmit") {
input.on("check", function (inp, checked) {
this.addUpdate(inp.name, checked ? "checked" : "unchecked");
},this);
}
input.on("focus", function (inp) {
this.lastFocus = inp.id;
},this);
break;
case "HtmlButton":
var label = input.dom.innerHTML;
var parent = input.parent();
var next = input.next();
//Replace
input.remove();
input = new Ext.Button({
id: inputid,
name: inputname,
text: label,
style: "display: inline;"
});
input.render(parent,next);
//Attach event handler
input.on("click", function(but, e) {
this.addUpdate(but.name, "click");
this.refresh();
},this);
break;
case "Int":
case "Real":
case "String":
var value = input.getValue();
var parent = input.parent();
var next = input.next();
//Replace
input.remove();
if(data.inputs[i].type == "String") {
input = new Ext.form.TextField({
id: inputid,
value: value
});
} else {
input = new Ext.form.NumberField({
id: inputid,
value: value,
allowDecimals: (data.inputs[i].type == "Real"),
decimalPrecision: Number.MAX_VALUE,
style: "width: 5em"
});
}
input.render(parent, next);
//Event handlers
if(data.inputs[i].updateon == "OnChange") {
input.on("change", function (inp, newVal, oldVal) {
this.addUpdate(inp.id, newVal);
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
}
if(data.inputs[i].updateon == "OnSubmit") {
input.on("change", function (inp, newVal, oldVal) {
this.addUpdate(inp.id, newVal);
},this);
}
input.on("focus", function (inp) {
this.lastFocus = inp.id;
},this);
break;
case "Bool":
case "Maybe":
var checked = input.dom.checked;
var parent = input.parent();
var next = input.next();
//Replace
input.remove();
input = new Ext.form.Checkbox({
id: inputid,
checked: checked
});
input.render(parent,next);
//Attach event handlers
if(data.inputs[i].updateon == "OnChange") {
input.on("check", function (inp, checked) {
this.addUpdate(inp.id, checked ? "checked" : "unchecked");
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
}
if(data.inputs[i].updateon == "OnSubmit") {
input.on("check", function (inp, checked) {
this.addUpdate(inp.id, checked ? "checked" : "unchecked");
},this);
}
input.on("focus", function (inp) {
this.lastFocus = inp.id;
//Default: Attach event handlers
default:
switch(data.inputs[i].updateon) {
case "OnChange":
input.on("change", function (e) {
this.addUpdate(e.target.name,e.target.value);
//Slightly delayed refresh. There could be click event right after this event.
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
break;
case "HtmlButton":
var label = input.dom.innerHTML;
var parent = input.parent();
var next = input.next();
//Replace
input.remove();
input = new Ext.Button({
id: inputid,
text: label,
style: "display: inline;"
});
input.render(parent,next);
//Attach event handler
input.on("click", function(but, e) {
this.addUpdate(but.id, "click");
case "OnClick":
input.on("click", function (e) {
this.addUpdate(e.target.name,"click");
this.refresh();
},this);
break;
//Default: Attach event handlers
default:
switch(data.inputs[i].updateon) {
case "OnChange":
input.on("change", function (e) {
this.addUpdate(e.target.id,e.target.value);
//Slightly delayed refresh. There could be click event right after this event.
new Ext.util.DelayedTask().delay(150,this.refresh,this);
},this);
break;
case "OnClick":
input.on("click", function (e) {
this.addUpdate(e.target.id,"click");
this.refresh();
},this);
break;
case "OnSubmit":
input.on("change", function (e) {
//Track changes, but don't send any data
this.addUpdate(e.target.id,e.target.value);
},this);
break;
}
//Attach focus tracking handler
input.on("focus", function (e) {
this.lastFocus = e.target.id;
case "OnSubmit":
input.on("change", function (e) {
//Track changes, but don't send any data
this.addUpdate(e.target.name,e.target.value);
},this);
break;
}
//Refocus
if(this.lastFocus == inputid) {
input.focus();
}
}
//Attach the submit handlers of the forms
for(var formid in forms) {
var form = Ext.get(formid);
if(form != undefined) {
//Cancel the form submit;
form.dom.onsubmit = function() {return false;}
//Attach our replacement event handler
form.on("submit", function (e) {
this.refresh();
},this);
}
}
//Attach focus tracking handler
input.on("focus", function (e) {
this.lastFocus = e.target.id;
},this);
}
//Refocus
if(this.lastFocus == inputid) {
input.focus();
}
}
//Release the busy lock
this.setBusy(false);
//Attach the submit handlers of the forms
for(var formid in forms) {
var form = Ext.get(data.prefix + formid);
if(form != undefined) {
//Cancel the form submit;
form.dom.onsubmit = function() {return false;}
//Attach our replacement event handler
form.on("submit", function (e) {
this.refresh();
},this);
}
}
},
setupTracePanels: function (trace, data) {
if(!trace) {
return;
}
var tabPanel = this.getComponent(1).getComponent(2);
var statePanel = tabPanel.getComponent(2);
if(data.stateTrace != undefined) {
statePanel.getEl().dom.innerHTML = data.stateTrace;
statePanel.enable();
} else {
statePanel.disable();
}
var updatePanel = tabPanel.getComponent(3);
if(data.updateTrace != undefined) {
updatePanel.getEl().dom.innerHTML = data.updateTrace;
updatePanel.enable();
} else {
updatePanel.disable();
}
var subtreePanel = tabPanel.getComponent(4);
if(data.subtreeTrace != undefined) {
subtreePanel.getEl().dom.innerHTML = data.subtreeTrace;
subtreePanel.enable();
} else {
subtreePanel.disable();
}
},
switchContentPanels: function (trace) {
var mainPanel = this.getComponent(1);
if(trace) {
mainPanel.layout.setActiveItem(2);
var tabPanel = mainPanel.getComponent(2);
tabPanel.setActiveTab(this.firstBuffer ? 0 : 1);
tabPanel.unhideTabStripItem(this.firstBuffer ? 0 : 1);
tabPanel.hideTabStripItem(this.firstBuffer ? 1 : 0);
} else {
mainPanel.layout.setActiveItem(this.firstBuffer ? 0 : 1);
}
//Toggle firstbuffer flag
this.firstBuffer = !this.firstBuffer;
},
addUpdate: function (inputid, value) {
this.updates[inputid] = value;
},
......@@ -380,6 +411,9 @@ itasks.WorkTabPanel = Ext.extend(Ext.Panel, {
//Add the state to the params
params['state'] = Ext.encode(this.state);
//Add the prefix to the params
params['prefix'] = 'tb' + (this.prefixCounter++) + '_';
//Check if we need to request trace info
if (this.debugPanel != undefined && this.debugPanel.traceEnabled()) {
params['trace'] = 1;
......
......@@ -11,6 +11,7 @@ from Http import :: HTTPRequest
from StdFile import class FileSystem
:: *HSt = { cntr :: !Int // counts position in expression
, prefix :: !String // global prefix used in all generated html id's
, request :: !HTTPRequest // to enable access to the current HTTP request
, states :: !*FormStates // all form states are collected here ...
, world :: *NWorld // to enable all kinds of I/O
......@@ -23,13 +24,14 @@ appWorldHSt :: !.(*World -> *World) !*HSt -> *HSt // enabling World
accWorldHSt :: !.(*World -> *(.a,*World)) !*HSt -> (.a,!*HSt) // enabling World operations on HSt
// Create a new HSt
mkHSt :: HTTPRequest *FormStates *NWorld -> *HSt
mkHSt :: String HTTPRequest *FormStates *NWorld -> *HSt
// Access on the HSt structure
getHStCntr :: !*HSt -> (!Int,!*HSt) // HSt.cntr
setHStCntr :: !Int !*HSt -> *HSt // HSt.cntr := HSt.cntr
incrHStCntr :: !Int !*HSt -> *HSt // HSt.cntr := HSt.cntr + n
setHStPrefix :: !String !*HSt -> *HSt
// Explicit removal of all (Persistent) IData for with the same prefix IData form id
// Change lifespan of all IData with the same prefix IData form id
......
......@@ -35,8 +35,8 @@ accWorldHSt f hst=:{world}
= (a,{hst & world=world})
// Create a new HSt
mkHSt :: HTTPRequest *FormStates *NWorld -> *HSt
mkHSt request states nworld = {cntr=0, states=states, request= request, world=nworld }
mkHSt :: String HTTPRequest *FormStates *NWorld -> *HSt
mkHSt prefix request states nworld = {cntr=0, prefix = prefix, states=states, request= request, world=nworld }
// Access on the HSt structure
getHStCntr :: !*HSt -> (!Int,!*HSt)
......@@ -48,6 +48,10 @@ setHStCntr i hst = {hst & cntr = i}
incrHStCntr :: !Int !*HSt -> *HSt
incrHStCntr i hst = {hst & cntr = hst.cntr + i}
setHStPrefix :: !String !*HSt -> *HSt
setHStPrefix s hst = {hst & prefix = s}
// It can be convenient to explicitly delete IData, in particular for persistent IData object
// or to optimize iTasks
// All IData objects administrated in the state satisfying the predicate will be deleted, no matter where they are stored.
......
......@@ -26,7 +26,7 @@ gPrint{|(->)|} gArg gRes _ _ = abort "functions can only be used with dynamic st
// TODO: Try to make it do just a little less :)
mkViewForm :: !(InIDataId d) !(HBimap d v) !*HSt -> (Form d,!*HSt) | iData v
mkViewForm (init,formid) bm=:{toForm, updForm, fromForm, resetForm} hst=:{request,states,world}
mkViewForm (init,formid) bm=:{toForm, updForm, fromForm, resetForm} hst=:{prefix, request,states,world}
| init == Const && formid.FormId.lifespan <> LSTemp
= mkViewForm (init,{FormId| formid & lifespan = LSTemp}) bm hst // constant i-data are never stored
| init == Const // constant i-data, no look up of previous value
......@@ -52,10 +52,10 @@ where
, form = []
, inputs = []
}
, mkHSt request states world)
, mkHSt prefix request states world)
# (viewform,{states,world}) // make a form for it
= mkForm (init,if (init == Const) vformid (reuseFormId formid view)) (mkHSt request states world)
= mkForm (init,if (init == Const) vformid (reuseFormId formid view)) (mkHSt prefix request states world)
| viewform.changed && not isupdated // important: redo it all to handle the case that a user defined specialisation is updated !!
= calcnextView True (Just viewform.Form.value) states world
......@@ -67,7 +67,7 @@ where
, form = viewform.form
, inputs = viewform.inputs
}
,mkHSt request states world)
,mkHSt prefix request states world)
replaceState` vformid view states world
| init <> Const = setState vformid view states world
......@@ -418,19 +418,19 @@ derive gUpd (,), (,,), (,,,), Void
// gForm: automatically derives a Html form for any Clean type
mkForm :: !(InIDataId a) *HSt -> *(Form a, !*HSt) | gForm {|*|} a
mkForm (init, formid =: {issub}) hst
mkForm (init, formid =: {issub}) hst =:{prefix}
# (form, hst) = gForm{|*|} (init, formid) hst
| issub = (form, hst) //Subforms are contained in the <form> tags of their parent