diff --git a/core/events.js b/core/events.js index 3e196dc56..a4f0706a7 100644 --- a/core/events.js +++ b/core/events.js @@ -174,45 +174,37 @@ Blockly.Events.filter = function(queueIn, forward) { // Undo is merged in reverse order. queue.reverse(); } - // Merge duplicates. O(n^2), but n should be very small. - for (var i = 0, event1; event1 = queue[i]; i++) { - for (var j = i + 1, event2; event2 = queue[j]; j++) { - if (event1.type == event2.type && - event1.blockId == event2.blockId && - event1.workspaceId == event2.workspaceId) { - if (event1.type == Blockly.Events.MOVE) { - // Merge move events. - event1.newParentId = event2.newParentId; - event1.newInputName = event2.newInputName; - event1.newCoordinate = event2.newCoordinate; - queue.splice(j, 1); - j--; - } else if (event1.type == Blockly.Events.CHANGE && - event1.element == event2.element && - event1.name == event2.name) { - // Merge change events. - event1.newValue = event2.newValue; - queue.splice(j, 1); - j--; - } else if (event1.type == Blockly.Events.UI && - event2.element == 'click' && - (event1.element == 'commentOpen' || - event1.element == 'mutatorOpen' || - event1.element == 'warningOpen')) { - // Merge change events. - event1.newValue = event2.newValue; - queue.splice(j, 1); - j--; - } + var mergedQueue = []; + var hash = Object.create(null); + // Merge duplicates. + for (var i = 0, event; event = queue[i]; i++) { + if (!event.isNull()) { + var key = [event.type, event.blockId, event.workspaceId].join(' '); + var lastEvent = hash[key]; + if (!lastEvent) { + hash[key] = event; + mergedQueue.push(event); + } else if (event.type == Blockly.Events.MOVE) { + // Merge move events. + lastEvent.newParentId = event.newParentId; + lastEvent.newInputName = event.newInputName; + lastEvent.newCoordinate = event.newCoordinate; + } else if (event.type == Blockly.Events.CHANGE && + event.element == lastEvent.element && + event.name == lastEvent.name) { + // Merge change events. + lastEvent.newValue = event.newValue; + } else if (event.type == Blockly.Events.UI && + event.element == 'click' && + (lastEvent.element == 'commentOpen' || + lastEvent.element == 'mutatorOpen' || + lastEvent.element == 'warningOpen')) { + // Merge click events. + lastEvent.newValue = event.newValue; } } } - // Remove null events. - for (var i = queue.length - 1; i >= 0; i--) { - if (queue[i].isNull()) { - queue.splice(i, 1); - } - } + queue = mergedQueue; if (!forward) { // Restore undo order. queue.reverse(); diff --git a/tests/jsunit/event_test.js b/tests/jsunit/event_test.js index c019515e9..d5583089e 100644 --- a/tests/jsunit/event_test.js +++ b/tests/jsunit/event_test.js @@ -392,3 +392,136 @@ function test_varBackard_runForward() { checkVariableValues(workspace, 'name1', 'type1', 'id1'); eventTest_tearDown(); } + +function test_events_filter() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.BlockCreate(block1), + new Blockly.Events.BlockMove(block1), + new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'item', 'item1'), + new Blockly.Events.Ui(block1, 'click') + ]; + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(4, filteredEvents.length); // no event should have been removed. + // test that the order hasn't changed + assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange); + assertTrue(filteredEvents[3] instanceof Blockly.Events.Ui); +} + +function test_events_filterForward() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.BlockCreate(block1), + ]; + helper_addMoveEvent(events, block1, 1, 1); + helper_addMoveEvent(events, block1, 2, 2); + helper_addMoveEvent(events, block1, 3, 3); + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(2, filteredEvents.length); // duplicate moves should have been removed. + // test that the order hasn't changed + assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + assertEquals(3, filteredEvents[1].newCoordinate.x); + assertEquals(3, filteredEvents[1].newCoordinate.y); + eventTest_tearDownWithMockBlocks(); +} + +function test_events_filterBackward() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.BlockCreate(block1), + ]; + helper_addMoveEvent(events, block1, 1, 1); + helper_addMoveEvent(events, block1, 2, 2); + helper_addMoveEvent(events, block1, 3, 3); + var filteredEvents = Blockly.Events.filter(events, false); + assertEquals(2, filteredEvents.length); // duplicate event should have been removed. + // test that the order hasn't changed + assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + assertEquals(1, filteredEvents[1].newCoordinate.x); + assertEquals(1, filteredEvents[1].newCoordinate.y); + eventTest_tearDownWithMockBlocks(); +} + +function test_events_filterDifferentBlocks() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var block2 = workspace.newBlock('field_variable_test_block', '2'); + var events = [ + new Blockly.Events.BlockCreate(block1), + new Blockly.Events.BlockMove(block1), + new Blockly.Events.BlockCreate(block2), + new Blockly.Events.BlockMove(block2) + ]; + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(4, filteredEvents.length); // no event should have been removed. + eventTest_tearDownWithMockBlocks(); +} + +function test_events_mergeMove() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var events = []; + helper_addMoveEvent(events, block1, 0, 0); + helper_addMoveEvent(events, block1, 1, 1); + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(1, filteredEvents.length); // second move event merged into first + assertEquals(1, filteredEvents[0].newCoordinate.x); + assertEquals(1, filteredEvents[0].newCoordinate.y); + eventTest_tearDownWithMockBlocks(); +} + +function test_events_mergeChange() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.Change(block1, 'field', 'VAR', 'item', 'item1'), + new Blockly.Events.Change(block1, 'field', 'VAR', 'item1', 'item2') + ]; + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(1, filteredEvents.length); // second change event merged into first + assertEquals('item', filteredEvents[0].oldValue); + assertEquals('item2', filteredEvents[0].newValue); + eventTest_tearDownWithMockBlocks(); +} + +function test_events_mergeUi() { + eventTest_setUpWithMockBlocks(); + var block1 = workspace.newBlock('field_variable_test_block', '1'); + var block2 = workspace.newBlock('field_variable_test_block', '2'); + var block3 = workspace.newBlock('field_variable_test_block', '3'); + var events = [ + new Blockly.Events.Ui(block1, 'commentOpen', 'false', 'true'), + new Blockly.Events.Ui(block1, 'click', 'false', 'true'), + new Blockly.Events.Ui(block2, 'mutatorOpen', 'false', 'true'), + new Blockly.Events.Ui(block2, 'click', 'false', 'true'), + new Blockly.Events.Ui(block3, 'warningOpen', 'false', 'true'), + new Blockly.Events.Ui(block3, 'click', 'false', 'true') + ]; + var filteredEvents = Blockly.Events.filter(events, true); + assertEquals(3, filteredEvents.length); // click event merged into corresponding *Open event + assertEquals('commentOpen', filteredEvents[0].element); + assertEquals('mutatorOpen', filteredEvents[1].element); + assertEquals('warningOpen', filteredEvents[2].element); + eventTest_tearDownWithMockBlocks(); +} + +/** + * Helper function to simulate block move events. + * + * @param {!Array.} events a queue of events. + * @param {!Blockly.Block} block the block to be moved + * @param {number} newX new X coordinate of the block + * @param {number} newY new Y coordinate of the block + */ +function helper_addMoveEvent(events, block, newX, newY) { + events.push(new Blockly.Events.BlockMove(block)); + block.xy_ = new goog.math.Coordinate(newX, newY); + events[events.length-1].recordNew(); +}