how to detach a shortcut command on Qooxdoo? - qooxdoo

I have this code:
qx.Class.define('my.Window', {
extend: qx.ui.window.Window,
construct: function(caption, icon) {
this.base(arguments, caption, icon);
this.setLayout(new qx.ui.layout.Basic());
this.__btn = new qx.ui.form.Button('Shortcut Test');
this.__cmd = new qx.ui.command.Command('Alt+T');
this.__cmd.addListener("execute", function() { alert('FOOBAR'); });
this.__btn.setCommand(this.__cmd);
this.add(this.__btn);
},
members: {
__btn: null,
__cmd: null
}
});
qx.Class.define('my.Compo', {
extend: qx.ui.container.Composite,
construct: function() {
this.base(arguments);
this.setLayout(new qx.ui.layout.HBox());
this.__btnShow = new qx.ui.form.Button("Show Window", "icon/22/apps/internet-web-browser.png");
this.__btnDestroy = new qx.ui.form.Button('Destroy window');
this.__btnNull = new qx.ui.form.Button('Null window');
this.__btnDestroy.addListener('execute', function(){
this.__window.destroy();
}, this);
this.__btnNull.addListener('execute', function(){
this.__window = null;
}, this);
this.__btnShow.addListener("execute", function(e){
if(this.__window) {
console.info('Window exist');
this.__window.open();
this.__window.center();
}
else {
console.info('Window do not exist!');
this.__window = new my.Window("Shortcut test window");
this.__window.setWidth(300);
this.__window.setHeight(200);
this.__window.setShowMinimize(false);
this.__window.open();
this.__window.center();
}
}, this);
this.add(this.__btnShow);
this.add(this.__btnDestroy);
this.add(this.__btnNull);
},
members: {
__btnShow: null,
__btnDestroy: null,
__window: null
}
});
var compo = new my.Compo();
this.getRoot().add(compo);
So, if you try "Alt+T" shortcut before clicking "Show Window" button, nothing happen. After show the Window, the shortcut is available and an alert is showed.
Well, the issue to me is the remaining availability of the shortcut in scenarios where that shortcut must do not exist any more:
When the window is close normally.
When Null window button is execute, and after that if you use "Alt+T" then the alert is showed twice, and so on as many time you switch between the Null and Show buttons.
Same behavior as (2) even if destroy() method of window is called explicitly.
On playground if "Run" button is clicked several times, also equal times is showed alert after use the shortcut.
Thanks no all for your time. :)
On Playground

The qx.ui.command.Command wraps the qx.bom.Shortcut that attaches 2 listeners to document element. When you close the window qx.ui.command.Command instance was not set inactive or destroyed. You'll have to handle the window close event properly.
destruct: function()
{
this.__cmd = null;
}
it does not destroy the command. If you try:
qx.core.ObjectRegistry.fromHashCode("the command object hash code").getActive()
You will find that it exists and is active. You forgot to destroy the command calling
this.__cmd.dispose()
Qooxdoo Playground App object registry is not initialized every time someone pushes "Run". So, qooxdoo object's lifetime is bound to page lifetime or a disposal event.

Related

Command handling in Qooxdoo multi window application

I want to create a Qooxdoo application that consists of a Desktop with multiple Windows. The Desktop (and not each Window) also has a (common) ToolBar.
Now I want to have a command to "save" the document of the active window. This command can be triggered by the keyboard shortcut "Ctrl+S" as well as a button on the toolbar.
To handle the "Ctrl+S" to reach the currently active window the qx.ui.command.GroupManager (as described by https://qooxdoo.org/documentation/v7.5/#/desktop/gui/interaction?id=working-with-commands and https://qooxdoo.org/qxl.demobrowser/#ui~CommandGroupManager.html) is supposed to be the best solution. And I could easily implement that in my code.
But now I'm struggling to make the toolbar save button also call the save command of the currently active window as I don't know how to bind it correctly to the GroupManager.
An example code to get started in the playground https://qooxdoo.org/qxl.playground/:
// NOTE: run this script only once. Before running it again you need to reload
// the page as it seems that the commands are accumulation and not reset.
// I guess that this is a bug in the Playground
const root = this.getRoot();
qx.Class.define('test.Application',
{
extend : qx.application.Standalone,
members: {
main: function() {
const layout = new qx.ui.layout.VBox(5);
const container = new qx.ui.container.Composite(layout);
root.add(container, {edge: 0});
const windowManager = new qx.ui.window.Manager();
const desktop = new qx.ui.window.Desktop(windowManager);
this._manager = new qx.ui.command.GroupManager();
const menuBar = new qx.ui.menubar.MenuBar();
let menu = new qx.ui.menu.Menu();
///////////////////////////
// TODO: Call _doSave() of the active window!
let saveMenuButton = new qx.ui.menu.Button('Save','#MaterialIcons/save/16');
///////////////////////////
menu.add(saveMenuButton);
var fileMenu = new qx.ui.menubar.Button('File', null, menu);
menuBar.add(fileMenu);
const toolBar = new qx.ui.toolbar.ToolBar();
///////////////////////////
// TODO: Call _doSave() of the active window!
let saveToolBarButton = new qx.ui.toolbar.Button('Save','#MaterialIcons/save/16');
///////////////////////////
toolBar.add(saveToolBarButton);
container.add(menuBar,{flex:0});
container.add(toolBar,{flex:0});
container.add(desktop,{flex:1});
this._foo1 = new test.Window('foo1', this);
desktop.add(this._foo1);
this._foo1.open();
this._foo1.moveTo(100,20);
this._foo2 = new test.Window('foo2', this);
desktop.add(this._foo2);
this._foo2.open();
this._foo2.moveTo(200,100);
this._foo3 = new test.Window('foo3', this);
desktop.add(this._foo3);
this._foo3.open();
this._foo3.moveTo(300,180);
},
getGroupManager() {
return this._manager;
}
}
});
qx.Class.define('test.Window', {
extend: qx.ui.window.Window,
construct(windowName, controller) {
this.base(arguments, windowName);
this._name = windowName;
let commandGroup = new qx.ui.command.Group();
const cmd = new qx.ui.command.Command("Ctrl+S");
cmd.addListener('execute', this._doSave, this);
commandGroup.add('save', cmd);
controller.getGroupManager().add(commandGroup);
this.addListener('changeActive', () => {
if (this.isActive()) {
controller.getGroupManager().setActive(commandGroup);
}
}, this);
},
members: {
_doSave() {
alert("save " + this._name);
}
}
});
a = new test.Application();
How should the saveMenuButton.setCommand() and saveToolBarButton.setCommand() should look like to always call the command of the active window?
You can control a current active window via Desktop class:
let saveToolBarButton = new qx.ui.toolbar.Button('Save');
saveToolBarButton.addListener("click", function(){
desktop.getActiveWindow()._doSave();
}, this);
Would be great for your solution imo is to create a separate command and add this command to buttons:
const saveActiveWindowCommand = new qx.ui.command.Command();
saveActiveWindowCommand.addListener("execute", function(){
desktop.getActiveWindow()._doSave();
}, this);
let saveMenuButton = new qx.ui.menu.Button('Save');
saveMenuButton.setCommand(saveActiveWindowCommand);
let saveToolBarButton = new qx.ui.toolbar.Button('Save');
saveToolBarButton.setCommand(saveActiveWindowCommand);
EDIT:
You could set commands dynamically for "Main Panel" menu buttons. Because there is only one instance of command pressing "Ctrl+S" will trigger only one command but maybe you would like that main bar save buttons have extra logic.
You have in application class next method which will be called from window class when changeActive event happens.
setSaveCommand(command){
this.saveMenuButton.setCommand(command);
this.saveToolBarButton.setCommand(command);
},
and in your Window class:
if (this.isActive()) {
controller.setSaveCommand(cmd);
controller.getGroupManager().setActive(commandGroup);
}

how to add an event that fires after scrolling the scrollbar to the end

I am working with standalone (not mobile) and I think it is _getScroll method for reaching it.
how to implement it here qooxdoo selectbox example
I found similar for mobile implementing virtual scrolling list console.log says container._getScroll is not a function.
The idea is to get scrollbar from a widget, the scrollbar you are needed is NativeScrollbar of the widget qx.ui.list.List. Then add event handler for a "scroll" event. In handler u have to compare current position of scroll and maximum.
Try the code below (eg copy and paste into the Qooxdoo playground).
qx.Class.define("SelectBoxWithScrollEndEvent", {
extend: qx.ui.form.SelectBox,
construct: function(){
this.base(arguments);
this.__setupScroll();
},
events: {
"scrollEndHappened": "qx.event.type.Event"
},
members: {
__setupScroll: function(){
const list = this.getChildControl("list");
const scrollbar = list.getChildControl("scrollbar-y");
scrollbar.addListener("scroll", function(e){
if (scrollbar.getMaximum() === scrollbar.getPosition()){
this.fireEvent("scrollEndHappened");
}}, this);
}
}
});
const box = new SelectBoxWithScrollEndEvent();
const data = new qx.data.Array([1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5]);
const controller = new qx.data.controller.List(data, box);
box.addListener("scrollEndHappened", function(){
alert("SCROLL HAPPENED ALERT");
}, this);
this.getRoot().add(box);

ExtJS 4: Problems with Message confirmation box on a modal window

I am currently facing a problem which seems to be a very common one for ExtJS or JavaScript users. I am facing a problem due to the Message Confirmation box's Asynchronous nature. I have tried to find the solution on many forums, but none of the solutions haven't given me a simpler way to deal with the below problem. Or may be I failed to understand that hints that were given.
Below is mycode:
var modalWindow = new Ext.Window({
id: 'modalWindow',
...
...
listeners: {
beforeclose: function ( window, eOpts ){
if ( dirtyCheck() ){
//perform abc()
// and don't close the modal window
return false;
}
else {
//perform xyz()
// close the modal window
return true;
}
},
close: function ( panel, eOpts ){
//will trigger beforeclose event to check if all is well
//refresh screen
}
},
buttons: [{
text: 'Close',
handler: function () {
modalWindow.close() // Same close() method is called when we click on window close ( X symbol ) tool.
}
}]
});
function dirtyCheck(){
// Code to check if dirty records exist
if ( /* Found dirty records */ ){
Ext.MessageBox.confirm('Please Confirm', 'Changes not saved. Do you want to close the window?', function (btn) {
if (btn === 'yes') {
console.log('*** clicked yes ****');
window.close();
}
});
}
}
Case Description: I have a grid and double clicking on a grid row opens a modal window coded above. I make some modifications on the modal window. If the user either clicks on the 'Close' button or the modal window close tool ( X symbol on top-right corner ) or even ESC button, I should prompt a confirmation messsage. If the user clicks on 'Yes', I should close the modal window else leave it open.
I have learnt that clicking on the X ( close tool ) or the ESC button fires a 'close' event which in turn fires 'beforeclose' and waits for true/false to either destroy the modal window or keep it open.
Problem: When the user clicks on X ( close tol ) or the ESC button, and while I debug, I see the control going to beforeclose and then the dirtyCheck() function that I wrote. The dirtyCheck function also displays a Message Confirmation box, but the control doesn't wait for me to click on Yes/No. It returns to beforeclose in the background and then to close. So, by the time user decides to click on either 'Yes' or 'No', ExtJS has decided what to do with the modal window and hence I am losing control on whether to close the modal window or not.
Could anyone please help me on what to do in such a case?
You might need to use window.destroy() to remove it after confirmation, your beforeClose function can be
beforeclose: function(window, eOpts) {
Ext.Msg.confirm('Please Confirm', 'Changes not saved. Do you want to close the window?', function(answer) {
if (answer == "yes") {
window.destroy();
}
});
return false;
}
Pass the window to your check function and close it once user have made a choice.
var modalWindow = new Ext.Window({
id: 'modalWindow',
...
...
listeners: {
beforeclose: function ( window, eOpts ){
dirtyCheck(window);
return false;
},
close: function ( panel, eOpts ){
//will trigger beforeclose event to check if all is well
//refresh screen
}
},
buttons: [{
text: 'Close',
handler: function () {
modalWindow.close() // Same close() method is called when we click on window close ( X symbol ) tool.
}
}]
});
function dirtyCheck(window){
// Code to check if dirty records exist
Ext.MessageBox.confirm('Please Confirm', 'Changes not saved. Do you want to close the window?', function (btn) {
confirmation = true;
if (btn === 'yes') {
console.log('*** clicked yes ****');
window.close();
}
});
}
BTW, you can use your way but then you will need to use native browser dialog, see confirm function http://www.w3schools.com/js/js_popup.asp

ExtJS 4.1: Modal Window seems to return control before submitting the window

I wasn't sure how to describe my question in the question title. But here is my problem:
(a) When I double click on a row in an Ext.grid.Panel, I open a modal window with it's relavant details to update the record.
(b) After I make the needed modifications and close the modal window, I want to return to the Grid with the Grid filtered on a certain code i.e selectedSalesOrderNum.
jobSlotsGrid.on('celldblclick', function(tableview, td, cellIndex, record, tr, rowIndex, e, eOpts){
modalStatus = loadWindow();
jobSlotStore.filterBy(function(rec) {
alert('Filtering data');
return rec.get('salesOrderNum') === selectedSalesOrderNum;
});
});
(c) Below is the function, which creates the model window. It also has the call to method submitCreateJobSlotHandler() which basically saves the changes and reloads the original Grid with all the data. ( Hence the necessity to filter it back with a certain code i.e selectedSalesOrderNum ).
function loadWindow()
{
getAllTabsForEditJobSlot();
var createJobSlotWin = new Ext.Window({
id:'salesOrder-win-jobSlot',
applyTo : 'hello-win',
modal : true,
layout : 'fit',
width : 900,
height : 500,
closeAction :'destroy',
plain : true,
model : true,
stateful : false,
title :'Create Job Slot',
items : [editJobSlotInformationPanel],
buttons : [{
text : 'Save',
handler : function(){
submitCreateJobSlotHandler();
//createJobSlotWin.destroy();
}
},{
text : 'Close',
handler : function(){
createJobSlotWin.destroy();
}
}]
});
createJobSlotWin.show();
}
The Issue:
In the first block of code, as soon as the loadWindow method is called, both a modal window is popped up along with the filterBy code getting executed in parallel and showing up the alerts ( 'Filtering data' ). I then enter the data in the modal and save. So, basically, the filtering is not done after the Save/Close on Modal. The code ( if/else ) is immediately reached after loading the modal window. It is as if, the modal window opens and goes to the next line of code while waiting for the user to perform some action on the modal window later.
Hope I am clear on my question. Could anyone please advice how do I handle this?
EDIT:
The more I think about it now, I guess the loadWindow() method just creates the Modal Window as we just have a new Ext.Window() call and doesn't bother about other user actions inside the modal and returns the control. And hence, executes the subsequent filterBy event immediately. In that case, I want to filter the store after I am reloading the store upon the Save in Modal Window. The save on Modal window has this handler code:
function submitCreateJobSlotHandler () {
alert('Into Submit');
var formPanel = Ext.getCmp('salesOrderJobSlotForm');
formPanel.getForm().submit({
url : 'someUrl',
method : 'POST',
success : function() {
alert('Success');
jobSlotStore.load({
scope : this,
url : 'salesOrderJobSlot/listJSON'
});
jobSlotStore.filterBy(function(rec) {
alert(rec.get('salesOrderNum')+"--"+selectedSalesOrderNum)
return rec.get('salesOrderNum') === selectedSalesOrderNum;
});
},
failure : function() {
alert('PSO save failed!');
}
});
}
But the issue here is, the jobSlotStore.load() though gets called, it holds until the filterBy gets executed. Because, I see the alerts coming up one by one and then after all the alerts are done, the store loads. So, the filterBy gets overriden by the 'late' store load.
Any suggestions to deal with the issue in any of the ways?
The store load is asynchronous, you need to wait til it completes before you can filter the data set on the client:
store.on('load', function() {
store.filter();
}, null, {single: true});
store.load();

Sencha Touch: Clicking a button rapidly will push a view twice

Say I have a button that triggers a push of a new view.
I noticed that if I click it more than once, fast enough, it will push the same view twice.
You can mimic this behavior using their official docs on this page, where they have a live sample:
http://docs.sencha.com/touch/2-0/#!/guide/navigation_view
the clear question is, simply how to prevent it?
Another method is to check what the active view is, and only push if it is not the same as the view you are about to push. I've tested this and it works.
E.g.
if (this.getNavigationView().getActiveItem().xtype != "someView") {
this.getNavigationView().push({ xtype: "someView" });
}
Extending jayteejee's answer, I've overridden the push method in a custom navigation view, like this:
Ext.define('BT.navigation.View', {
extend: 'Ext.navigation.View',
xtype: 'btnavigationview',
push: function (view) {
if(this.getActiveItem().xtype != view.xtype)
this.callParent(arguments);
else
console.warn("Prevented pushing a potentially duplicate view of xtype: " + view.xtype);
}
});
I'm not totally sure if the xtype assumption is safe enough, but I can't think of any situation in my current app that would require one view pushing another view of the same type onto the navigation stack. So, the solution works for me, and it's pretty neat. The warning is there to save me headache later on and possibly pulling my hair out trying to work out why push wouldn't work!
Masking successfully prevents double tapping problem.
In my code I'm using two functions for mask/unmask navigation container:
/**
* Mask container with rolling wheel. Usually need if Ajax-request is sent to the server and app waiting for response
* Best practice is masking the current navigator container, to prevent blocking whole app. Method warns if no container
* is defined. In some cases warning could be suppress with parameter
*
* #param container
* #param {boolean} [suppressWarning]
*/
startLoading: function(container, suppressWarning) {
var loadingComponent = container;
if (!loadingComponent) {
// <debug>
if (!suppressWarning) {
console.warn('Please define navigator container for non-blocking operation, or define suppressWarning parameter');
}
// </debug>
loadingComponent = Ext.Viewport;
}
// var lastMaskedContainer = container;
this.lastMaskedContainer = container;
loadingComponent.setMasked({
xtype: 'loadmask',
message: 'Loading...'
});
/*
Ext.defer(function() {
lastMaskedContainer.setMasked(false);
}, Pipedrive.app.maskingTimeout * 1000)
*/
},
/**
*
* #param {Ext.Container} container
* #param {boolean} [suppressWarning]
*/
stopLoading: function(container, suppressWarning) {
var loadingComponent = container;
if (!loadingComponent) {
// <debug>
if (!suppressWarning) {
console.warn('Please define either navigator container for non-blocking operation, or define suppressWarning parameter');
}
// </debug>
loadingComponent = Ext.Viewport;
}
var alreadyMasked = loadingComponent.getMasked();
var lastMaskedContainer = this.lastMaskedContainer;
if (!alreadyMasked && !suppressWarning) {
// <debug>
if (lastMaskedContainer != container) {
console.warn('Found Start/Stop Loading inconsistency. Please revise code'
+ (container ? '. Container: ' + container.getId() : 'Ext.Viewport')
+ (lastMaskedContainer ? ', last masked container: ' + lastMaskedContainer.getId() : '')
);
}
// </debug>
loadingComponent = Ext.Viewport;
}
loadingComponent.setMasked(false);
}
than in the tap handler:
onDealDetailsTap: function(ct) {
console.log('onDealDetailsTap', ct);
var form = ct.getReferenceForm(),
navigatorContainer = this.getNavigatorContainer(form),
model = form.getRecord();
UiHelper.startLoading(navigatorContainer);
Ext.Viewport.fireEvent('detailfields', {
title: model.get('title'),
id: model.get('id'),
store: 'DealFields',
navigatorContainer: navigatorContainer
})
},
to cleanup the loading mask:
control : {
activitiesContainer: {
push: 'onPushActivitiesContainer'
},
onPushActivitiesContainer: function(ct) {
//console.log('onPushActivitiesContainer', ct);
UiHelper.stopLoading(ct);
},
especially it is cool for waiting for long-timed ajax requests....
Cheers, Oleg
Just suspend the events on the button when it's tapped and resume them when the view is pushed
button.suspendEvents();
...
button.resumeEvents();
I don't think there is another way. As a developer or a user, when you tap a button twice, you expect the event handler to be called twice.
Hope this helps
simply mask the entire container and then unmask it; create a ref for the container or panel in which the button exists in your controller and on tap set:
ref.setMasked(true)
After the new view is pushed simply unmask by
ref.setMasked(false)
Another way is to flip a parameter once the list item has been tapped once, like this:
{
onListItemTap: function () {
if (!this.tapped) {
this.tapped = true;
...
}
}
}
Of course, that only works if you are destroying the list view as soon as the user goes to a different screen.
I created a method for checking this:
ENSURE_NO_DOUBLE_TAP : function(classNameToPush) {
if (Ext.getClassName(Ext.getCmp('MyViewport').getActiveItem()) == classNameToPush) {
return false;
}
return true;
}
Then from your app before anything that could be double tapped is processed:
if (!ENSURE_NO_DOUBLE_TAP('MyApp.view.View')) {
return;
}
If you are listening to the tap event of a button using listeners,then here is
my solution:
listeners : {
release : function(){
if(this.getDisabled())return false;
this.setDisabled(true);
this.fireEvent('tap');
},
tap : function() {
//do what you want
}
}
Extending on jayteejee's and Merott's answers, I've added some code to intercept on multiple fast pushes to not only prevent duplicates but to prevent pushing of different views as well before the page transition completes. Think of a user tapping different list items.
Also notice the view.destroy(); method in the else block to prevent view instances from heaping up in memory.
Ext.define('Overrides.navigation.View', {
extend: 'Ext.navigation.View',
xtype: 'ovrnavigationview',
interceptPush: false,
push: function (view) {
var activeItem = this.getActiveItem();
// Prevent multiple pushes & duplicates
if (!this.interceptPush && activeItem.xtype !== view.xtype) {
// Set interceptPush
this.interceptPush = true;
// Reset interceptPush after 500 ms
Ext.defer(function() {
this.interceptPush = false;
}, 500, this);
// Handle push
this.callParent(arguments);
} else {
// Warn developer
console.warn("Prevented pushing view of xtype: " + view.xtype);
// Destroy view
view.destroy();
return false;
}
}
});
You can just use the "itemsingletap" event.
If you want to support double taps as well, make a second listener for "itemdoubletap" and invoke the same function, both listeners work fine together.

Resources