LayoutTests:
authoradele <adele@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 31 Oct 2006 22:10:51 +0000 (22:10 +0000)
committeradele <adele@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 31 Oct 2006 22:10:51 +0000 (22:10 +0000)
        Reviewed by Adam.

        Tests for http://bugs.webkit.org/show_bug.cgi?id=11127 NativeListBox: arrow and drag selection should pivot around one list item
        and http://bugs.webkit.org/show_bug.cgi?id=11173 REGRESSION (NativeListBox): Shift-clicking items in list box doesn't expand the current selection
        and http://bugs.webkit.org/show_bug.cgi?id=11417 REGRESSION: onchange does not fire for list-style select elements

        * fast/forms/listbox-onchange-expected.txt: Added.
        * fast/forms/listbox-onchange.html: Added.
        * fast/forms/listbox-selection-expected.txt: Added.
        * fast/forms/listbox-selection.html: Added.

WebCore:

        Reviewed by Adam.

        - Fix for http://bugs.webkit.org/show_bug.cgi?id=11127 NativeListBox: arrow and drag selection should pivot around one list item
        and http://bugs.webkit.org/show_bug.cgi?id=11173 REGRESSION (NativeListBox): Shift-clicking items in list box doesn't expand the current selection
        and http://bugs.webkit.org/show_bug.cgi?id=11417 REGRESSION: onchange does not fire for list-style select elements

        Tests:
        * LayoutTests/fast/forms/listbox-selection.html
        * LayoutTests/fast/forms/listbox-onchange.html

        * html/HTMLSelectElement.h: Added m_selectedListIndexBase and m_selectedListIndexExtent to track indices for the active selection in progress.
          Added 2 vectors to cache selection state.  One is kept so that the previous selection state can be restored as the active selection grows and shrinks.
          And one for onChange, that is updated after onChange is fired.
          Added m_activeSelectionState to keep track of whether the current drag selection is selecting or deselecting.

        * html/HTMLSelectElement.cpp:
        (WebCore::HTMLSelectElement::HTMLSelectElement): Initialized new variables.
        (WebCore::HTMLSelectElement::setSelectedIndex): If needed, initialize m_selectedListIndexBase and m_selectedListIndexExtent.
        (WebCore::HTMLSelectElement::dispatchBlurEvent): Only fire the onChange event here for menu lists.
        (WebCore::HTMLSelectElement::listBoxDefaultEventHandler): Updates base and extent variables for mouse and key events.
        (WebCore::HTMLSelectElement::setBase): Added. Also caches the selection state.
        (WebCore::HTMLSelectElement::setExtent): Added.
        (WebCore::HTMLSelectElement::updateListBoxSelection): Added.
        (WebCore::HTMLSelectElement::listBoxOnChange): Added.

        * rendering/RenderListBox.cpp:
        (WebCore::RenderListBox::updateFromElement): Only scroll to reveal the first index if both the first and last indices aren't visible.
        (WebCore::RenderListBox::listIndexAtOffset): Added.  Replaces optionAtPoint, which is no longer used.
        (WebCore::RenderListBox::autoscroll): Now sets the selection using the select's base and extent.
        (WebCore::RenderListBox::stopAutoscroll): Added.  Tells the select element to fire onChange.  This is needed because the autoscroll can end from a mouseUp
         outside of the list box, and the select element won't get a mouseUp event directly.  But the frame will stop the autoscroll at that point, and now we can
         notify the select element from here.
        (WebCore::RenderListBox::scrollToRevealElementAtListIndex): Checks new listIndexIsVisible method.
        (WebCore::RenderListBox::listIndexIsVisible): Added.
        (WebCore::RenderListBox::valueChanged): Removed unnecessary printf.

        * page/Frame.cpp: (WebCore::Frame::stopAutoscrollTimer): Added rendererIsBeingDestroyed argument, so when the renderer calls this during destruction,
        we don't try to use the pointer to that renderer to call stopAutoscroll.  This is done so a renderer that's still alive has a chance to do some cleanup after autoscroll.
        * rendering/RenderListBox.h: (WebCore::RenderListBox::shouldAutoscroll): Always returns true now, since we're also updating selection from the autoscroll timer.
        * rendering/RenderObject.h: (WebCore::RenderObject::stopAutoscroll): Added.
        * rendering/RenderObject.cpp: (WebCore::RenderObject::destroy): Calls stopAutoscrollTimer with rendererIsBeingDestroyed argument.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@17498 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/forms/listbox-onchange-expected.txt [new file with mode: 0644]
LayoutTests/fast/forms/listbox-onchange.html [new file with mode: 0644]
LayoutTests/fast/forms/listbox-selection-expected.txt [new file with mode: 0644]
LayoutTests/fast/forms/listbox-selection.html [new file with mode: 0644]
WebCore/ChangeLog
WebCore/html/HTMLSelectElement.cpp
WebCore/html/HTMLSelectElement.h
WebCore/page/Frame.cpp
WebCore/page/Frame.h
WebCore/rendering/RenderListBox.cpp
WebCore/rendering/RenderListBox.h
WebCore/rendering/RenderObject.cpp
WebCore/rendering/RenderObject.h

index 6de8dbbbbb4151bf1357752d702c29b66e38283a..c6bc05ad9932f3b02c191f2bfb10a3af4003d055 100644 (file)
@@ -1,3 +1,16 @@
+2006-10-31  Adele Peterson  <adele@apple.com>
+
+        Reviewed by Adam.
+
+        Tests for http://bugs.webkit.org/show_bug.cgi?id=11127 NativeListBox: arrow and drag selection should pivot around one list item
+        and http://bugs.webkit.org/show_bug.cgi?id=11173 REGRESSION (NativeListBox): Shift-clicking items in list box doesn't expand the current selection
+        and http://bugs.webkit.org/show_bug.cgi?id=11417 REGRESSION: onchange does not fire for list-style select elements
+
+        * fast/forms/listbox-onchange-expected.txt: Added.
+        * fast/forms/listbox-onchange.html: Added.
+        * fast/forms/listbox-selection-expected.txt: Added.
+        * fast/forms/listbox-selection.html: Added.
+
 2006-10-31  Justin Garcia  <justin.garcia@apple.com>
 
         Reviewed by harrison
diff --git a/LayoutTests/fast/forms/listbox-onchange-expected.txt b/LayoutTests/fast/forms/listbox-onchange-expected.txt
new file mode 100644 (file)
index 0000000..f9e2d75
--- /dev/null
@@ -0,0 +1,10 @@
+EDITING DELEGATE: shouldChangeSelectedDOMRange:(null) toDOMRange:(null) affinity:NSSelectionAffinityDownstream stillSelecting:FALSE
+
+Results:
+1) Make sure onChange fires when clicking
+onChange fired
+2) Make sure onChange doesn't fire when clicking again on the same item
+3) Make sure onChange fires when clicking on a new item
+onChange fired
+4) Make sure onChange fires when changing the selection with the keyboard
+onChange fired
diff --git a/LayoutTests/fast/forms/listbox-onchange.html b/LayoutTests/fast/forms/listbox-onchange.html
new file mode 100644 (file)
index 0000000..7a568eb
--- /dev/null
@@ -0,0 +1,68 @@
+<html>
+    <head>
+        <script>
+            function setup() {
+                var results = document.createElement('div');
+                results.id = "res";
+                results.appendChild(document.createTextNode("Results:"));
+                document.body.appendChild(results);
+            }
+            function test() {
+                setup();
+                
+                if (window.layoutTestController) {
+                    layoutTestController.dumpAsText();
+                    layoutTestController.waitUntilDone();
+                }
+                
+                log("1) Make sure onChange fires when clicking");
+                clickOnSelect("sl1", 2);
+
+                log("2) Make sure onChange doesn't fire when clicking again on the same item");
+                clickOnSelect("sl1", 2);
+
+                log("3) Make sure onChange fires when clicking on a new item");
+                clickOnSelect("sl1", 0);
+                
+                log("4) Make sure onChange fires when changing the selection with the keyboard");
+                keyPressOnSelect("sl1", "Down", true, false);
+                                
+                if (window.layoutTestController)
+                    layoutTestController.notifyDone();
+                
+            }
+            
+            function clickOnSelect(selId, index) {
+                var sl = document.getElementById(selId);
+                var itemHeight = 15;
+                var y = 10 + index * itemHeight;
+                
+                if (window.layoutTestController) {
+                    eventSender.mouseMoveTo(sl.offsetLeft + 10, sl.offsetTop + y);
+                    eventSender.mouseDown();
+                    eventSender.mouseUp();
+                }
+            }
+                        
+            function keyPressOnSelect(selId, identifier, shift, meta) {
+                var sl = document.getElementById(selId);
+                var event = document.createEvent("KeyboardEvents");
+                event.initKeyboardEvent("keypress", true, true, document.defaultView, identifier, 0, false, false, shift, meta, false);
+                sl.dispatchEvent(event);
+            }
+            
+            function log(msg) {
+                var r = document.getElementById('res');
+                r.innerHTML = r.innerHTML + "<br>" + msg;
+            }
+        </script>
+    </head>
+    <body onload="test()">
+    <select id="sl1" size=5 multiple onchange="log('onChange fired')">
+    <option>item 0
+    <option>item 1
+    <option>item 2
+    <option>item 3
+    </select>
+    </body>
+</html>
diff --git a/LayoutTests/fast/forms/listbox-selection-expected.txt b/LayoutTests/fast/forms/listbox-selection-expected.txt
new file mode 100644 (file)
index 0000000..4fa2b24
--- /dev/null
@@ -0,0 +1,29 @@
+ 1) Select one item with mouse (no previous selection)
+ 2) Select one item with mouse (with previous selection)
+ 3) Select one item with the keyboard (no previous selection)
+ 4) Select one item with the keyboard (with previous selection)
+ 5) Attempt to select an item cmd-clicking
+ 6) Attempt to select a range shift-clicking
+ 7) Attempt to select a range with the keyboard
+ 8) Select one item with mouse (no previous selection)
+ 9) Select one item with mouse (with previous selection)
+ 10) Select one item with the keyboard (no previous selection)
+ 11) Select one item with the keyboard (with previous selection)
+ 12) Select an item cmd-clicking
+ 13) Select a range shift-clicking
+ 14) Select a range with the keyboard
+Results:
+Test 1 Passed
+Test 2 Passed
+Test 3 Passed
+Test 4 Passed
+Test 5 Passed
+Test 6 Passed
+Test 7 Passed
+Test 8 Passed
+Test 9 Passed
+Test 10 Passed
+Test 11 Passed
+Test 12 Passed
+Test 13 Passed
+Test 14 Passed
diff --git a/LayoutTests/fast/forms/listbox-selection.html b/LayoutTests/fast/forms/listbox-selection.html
new file mode 100644 (file)
index 0000000..f4fd4b8
--- /dev/null
@@ -0,0 +1,178 @@
+<html>
+    <head>
+        <script>
+            function setup() {
+                createSelect("sl1", 5, false, -1, " 1) Select one item with mouse (no previous selection)");
+                createSelect("sl2", 5, false, 1, " 2) Select one item with mouse (with previous selection)");
+                createSelect("sl3", 5, false, -1, " 3) Select one item with the keyboard (no previous selection)");
+                createSelect("sl4", 5, false, 1, " 4) Select one item with the keyboard (with previous selection)");
+                createSelect("sl5", 5, false, 2, " 5) Attempt to select an item cmd-clicking");
+                createSelect("sl6", 5, false, 3, " 6) Attempt to select a range shift-clicking");
+                createSelect("sl7", 5, false, 1, " 7) Attempt to select a range with the keyboard");
+
+                createSelect("sl8", 5, true, -1, " 8) Select one item with mouse (no previous selection)");
+                createSelect("sl9", 5, true, 1, " 9) Select one item with mouse (with previous selection)");
+                createSelect("sl10", 5, true, -1, " 10) Select one item with the keyboard (no previous selection)");
+                createSelect("sl11", 5, true, 1, " 11) Select one item with the keyboard (with previous selection)");
+                createSelect("sl12", 5, true, 2, " 12) Select an item cmd-clicking");
+                createSelect("sl13", 5, true, 0, " 13) Select a range shift-clicking");
+                createSelect("sl14", 5, true, 1, " 14) Select a range with the keyboard");
+                
+                var results = document.createElement('div');
+                results.id = "res";
+                results.appendChild(document.createTextNode("Results:"));
+                document.body.appendChild(results);
+            }
+            function test() {
+                setup();
+                
+                if (window.layoutTestController) {
+                    layoutTestController.dumpAsText();
+                    layoutTestController.waitUntilDone();
+                }
+                
+                // 1) Select one item with mouse (no previous selection)
+                mouseDownOnSelect("sl1", 0, false, false);                
+                var expectedSelectionResults = new Array(true, false, false, false, false);
+                testResults(expectedSelectionResults, 1);
+
+                // 2) Select one item with mouse (with previous selection)
+                mouseDownOnSelect("sl2", 0, false, false);                
+                expectedSelectionResults = new Array(true, false, false, false, false);
+                testResults(expectedSelectionResults, 2);
+                                
+                // 3) Select one item with the keyboard (no previous selection)
+                keyPressOnSelect("sl3", "Up", false, false);               
+                expectedSelectionResults = new Array(false, false, false, false, true);
+                testResults(expectedSelectionResults, 3);
+
+                // 4) Select one item with the keyboard (with previous selection)
+                keyPressOnSelect("sl4", "Down", false, false);
+                expectedSelectionResults = new Array(false, false, true, false, false);
+                testResults(expectedSelectionResults, 4);
+
+                //  5) Attempt to select an item cmd-clicking
+                mouseDownOnSelect("sl5", 1, false, true);                    
+                expectedSelectionResults = new Array(false, true, false, false, false);
+                testResults(expectedSelectionResults, 5);
+                
+                // 6) Attempt to select a range shift-clicking
+                mouseDownOnSelect("sl6", 1, true, false);                    
+                expectedSelectionResults = new Array(false, true, false, false, false);
+                testResults(expectedSelectionResults, 6);
+                
+                // 7) Attempt to select a range with the keyboard
+                keyPressOnSelect("sl7", "Down", true, false);
+                expectedSelectionResults = new Array(false, false, true, false, false);
+                testResults(expectedSelectionResults, 7);
+                
+                // Multiple selection tests
+                
+                // 8) Select one item with mouse (no previous selection)
+                mouseDownOnSelect("sl8", 0, false, false);                    
+                expectedSelectionResults = new Array(true, false, false, false, false);
+                testResults(expectedSelectionResults, 8);
+                
+                // 9) Select one item with mouse (with previous selection)
+                var sl0 = document.getElementById('sl9');
+                mouseDownOnSelect("sl9", 0, false, false);                    
+                expectedSelectionResults = new Array(true, false, false, false, false);
+                testResults(expectedSelectionResults, 9);
+                
+                // 10) Select one item with the keyboard (no previous selection)
+                keyPressOnSelect("sl10", "Up", false, false);
+                expectedSelectionResults = new Array(false, false, false, false, true);
+                testResults(expectedSelectionResults, 10);
+                                
+                // 11) Select one item with the keyboard (with previous selection)
+                keyPressOnSelect("sl11", "Down", false, false);
+                expectedSelectionResults = new Array(false, false, true, false, false);
+                testResults(expectedSelectionResults, 11);
+                
+                // 12) Select an item cmd-clicking
+                mouseDownOnSelect("sl12", 1, false, true);                    
+                expectedSelectionResults = new Array(false, true, true, false, false);
+                testResults(expectedSelectionResults, 12);
+                                
+                // 13) Select a range shift-clicking
+                mouseDownOnSelect("sl13", 3, true, false);
+                expectedSelectionResults = new Array(true, true, true, true, false);
+                testResults(expectedSelectionResults, 13);
+                                
+                // 14) Select a range with the keyboard
+                keyPressOnSelect("sl14", "Down", true, false);
+                expectedSelectionResults = new Array(false, true, true, false, false);
+                testResults(expectedSelectionResults, 14);
+                
+                if (window.layoutTestController)
+                    layoutTestController.notifyDone();
+                
+            }
+            
+            function mouseDownOnSelect(selId, index, shift, meta) {
+                var sl = document.getElementById(selId);
+                var itemHeight = 15;
+                var y = 10 + index * itemHeight - window.pageYOffset;
+                var event = document.createEvent("MouseEvent");
+                event.initMouseEvent("mousedown", true, true, document.defaultView, 1, sl.offsetLeft + 10, sl.offsetTop + y, sl.offsetLeft + 10, sl.offsetTop + y, false, false, shift, meta, 0, document);
+                sl.dispatchEvent(event);
+            }
+            
+            function keyPressOnSelect(selId, identifier, shift, meta) {
+                var sl = document.getElementById(selId);
+                sl.focus();
+                var event = document.createEvent("KeyboardEvents");
+                event.initKeyboardEvent("keypress", true, true, document.defaultView, identifier, 0, false, false, shift, meta, false);
+                sl.dispatchEvent(event);
+            }
+            
+            function createSelect(idName, sz, mlt, selIndex, testMsg) {
+                var sl = document.createElement("select");
+                var i = 0;
+                while (i < sz) {
+                    var opt = document.createElement("option");
+                    if (i == selIndex)
+                        opt.selected = true;
+                    opt.innerText = "item " + i;
+                    sl.appendChild(opt);
+                    i++;
+                }
+                sl.size = sz;
+                sl.multiple = mlt;
+                sl.id = idName;
+                document.body.appendChild(sl);
+                document.body.appendChild(document.createTextNode(testMsg));
+                document.body.appendChild(document.createElement("br"));
+            }
+            
+            function testResults(expectedArr, testNum) {
+                var sl = document.getElementById("sl" + testNum);
+                var res = document.getElementById('res');
+                var resultsArr = new Array(sl.options.length);
+                
+                var i;
+                for (i=0; i < sl.options.length; i++) {
+                    resultsArr[i] = sl.options[i].selected;
+                }
+                var successString = "Failed";
+                var success = false;
+                if (expectedArr.join() == resultsArr.join()) {
+                    success = true;
+                    successString = "Passed";
+                }
+                
+               log("Test " + testNum + " " + successString);
+                if (!success) {
+                    log("<pre>     Expected: " + expectedArr.join() + "<br>" + "     Actual: " + resultsArr.join() + "</pre>");
+                }
+            }
+            
+            function log(msg) {
+                var r = document.getElementById('res');
+                r.innerHTML = r.innerHTML + "<br>" + msg;
+            }
+        </script>
+    </head>
+    <body onload="test()">
+    </body>
+</html>
index ecf5f05761d1b146ab9e9b36f07dbbf106acd6ec..ac7c5199e501657e63c1cf6b689ef0a5438813c4 100644 (file)
@@ -1,3 +1,47 @@
+2006-10-31  Adele Peterson  <adele@apple.com>
+
+        Reviewed by Adam.
+
+        - Fix for http://bugs.webkit.org/show_bug.cgi?id=11127 NativeListBox: arrow and drag selection should pivot around one list item
+        and http://bugs.webkit.org/show_bug.cgi?id=11173 REGRESSION (NativeListBox): Shift-clicking items in list box doesn't expand the current selection
+        and http://bugs.webkit.org/show_bug.cgi?id=11417 REGRESSION: onchange does not fire for list-style select elements
+
+        Tests: 
+        * LayoutTests/fast/forms/listbox-selection.html
+        * LayoutTests/fast/forms/listbox-onchange.html
+
+        * html/HTMLSelectElement.h: Added m_selectedListIndexBase and m_selectedListIndexExtent to track indices for the active selection in progress.
+          Added 2 vectors to cache selection state.  One is kept so that the previous selection state can be restored as the active selection grows and shrinks.
+          And one for onChange, that is updated after onChange is fired.
+          Added m_activeSelectionState to keep track of whether the current drag selection is selecting or deselecting.
+        * html/HTMLSelectElement.cpp:
+        (WebCore::HTMLSelectElement::HTMLSelectElement): Initialized new variables.
+        (WebCore::HTMLSelectElement::setSelectedIndex): If needed, initialize m_selectedListIndexBase and m_selectedListIndexExtent.
+        (WebCore::HTMLSelectElement::dispatchBlurEvent): Only fire the onChange event here for menu lists.
+        (WebCore::HTMLSelectElement::listBoxDefaultEventHandler): Updates base and extent variables for mouse and key events.
+        (WebCore::HTMLSelectElement::setBase): Added. Also caches the selection state.
+        (WebCore::HTMLSelectElement::setExtent): Added.
+        (WebCore::HTMLSelectElement::updateListBoxSelection): Added.
+        (WebCore::HTMLSelectElement::listBoxOnChange): Added.
+
+        * rendering/RenderListBox.cpp:
+        (WebCore::RenderListBox::updateFromElement): Only scroll to reveal the first index if both the first and last indices aren't visible.
+        (WebCore::RenderListBox::listIndexAtOffset): Added.  Replaces optionAtPoint, which is no longer used.
+        (WebCore::RenderListBox::autoscroll): Now sets the selection using the select's base and extent.
+        (WebCore::RenderListBox::stopAutoscroll): Added.  Tells the select element to fire onChange.  This is needed because the autoscroll can end from a mouseUp
+         outside of the list box, and the select element won't get a mouseUp event directly.  But the frame will stop the autoscroll at that point, and now we can
+         notify the select element from here.
+        (WebCore::RenderListBox::scrollToRevealElementAtListIndex): Checks new listIndexIsVisible method.
+        (WebCore::RenderListBox::listIndexIsVisible): Added.
+        (WebCore::RenderListBox::valueChanged): Removed unnecessary printf.
+
+        * page/Frame.cpp: (WebCore::Frame::stopAutoscrollTimer): Added rendererIsBeingDestroyed argument, so when the renderer calls this during destruction, 
+        we don't try to use the pointer to that renderer to call stopAutoscroll.  This is done so a renderer that's still alive has a chance to do some cleanup after autoscroll.
+        * rendering/RenderListBox.h: (WebCore::RenderListBox::shouldAutoscroll): Always returns true now, since we're also updating selection from the autoscroll timer.
+        * rendering/RenderObject.h: (WebCore::RenderObject::stopAutoscroll): Added.
+        * rendering/RenderObject.cpp: (WebCore::RenderObject::destroy): Calls stopAutoscrollTimer with rendererIsBeingDestroyed argument.
+
 2006-10-31  Beth Dakin  <bdakin@apple.com>
 
         Forgot to check this in a minute ago. Oops!! Thanks Mitz!
index fb4c2a4d87ea92f84797511eb74f7e92c85b003d..0bc4ba3499658d90acab3c3f11e4e244968bafbb 100644 (file)
@@ -66,6 +66,9 @@ HTMLSelectElement::HTMLSelectElement(Document* doc, HTMLFormElement* f)
     , m_multiple(false)
     , m_recalcListItems(false)
     , m_lastOnChangeIndex(0)
+    , m_activeSelectionAnchorIndex(-1)
+    , m_activeSelectionEndIndex(-1)
+    , m_activeSelectionState(false)
     , m_repeatingChar(0)
     , m_lastCharTime(0)
     , m_typedString(String())
@@ -161,13 +164,23 @@ void HTMLSelectElement::setSelectedIndex(int optionIndex, bool deselect, bool fi
     const Vector<HTMLElement*>& items = listItems();
     int listIndex = optionToListIndex(optionIndex);
     HTMLOptionElement* element = 0;
+
     if (listIndex >= 0) {
         element = static_cast<HTMLOptionElement*>(items[listIndex]);
         element->setSelected(true);
     }
+
     if (deselect)
         deselectItems(element);
-    if (fireOnChange && m_lastOnChangeIndex != optionIndex) {
+
+    if (listIndex >= 0) {
+        if (m_activeSelectionAnchorIndex < 0 || deselect)
+            setActiveSelectionAnchorIndex(listIndex);
+        if (m_activeSelectionEndIndex < 0 || deselect)
+            setActiveSelectionEndIndex(listIndex);
+    }
+
+    if (usesMenuList() && fireOnChange && m_lastOnChangeIndex != optionIndex) {
         m_lastOnChangeIndex = optionIndex;
         onChange();
     }
@@ -522,7 +535,7 @@ void HTMLSelectElement::notifyOptionSelected(HTMLOptionElement *selectedOption,
 void HTMLSelectElement::dispatchBlurEvent()
 {
 #if !ARROW_KEYS_POP_MENU
-    if (selectedIndex() != m_lastOnChangeIndex) {
+    if (usesMenuList() && selectedIndex() != m_lastOnChangeIndex) {
         m_lastOnChangeIndex = selectedIndex();
         onChange();
     }
@@ -616,9 +629,9 @@ void HTMLSelectElement::listBoxDefaultEventHandler(Event* evt)
 {
     if (evt->type() == mousedownEvent) {
         MouseEvent* mEvt = static_cast<MouseEvent*>(evt);
-        if (HTMLOptionElement* element = static_cast<RenderListBox*>(renderer())->optionAtPoint(mEvt->x(), mEvt->y())) {
-            bool deselectOtherOptions = true;
-            bool shouldSelect = true;
+        int listIndex = static_cast<RenderListBox*>(renderer())->listIndexAtOffset(mEvt->offsetX(), mEvt->offsetY());
+        if (listIndex >= 0) {
+            m_activeSelectionState = true;
             
             bool multiSelectKeyPressed = false;
 #if PLATFORM(MAC)
@@ -626,44 +639,165 @@ void HTMLSelectElement::listBoxDefaultEventHandler(Event* evt)
 #else
             multiSelectKeyPressed = mEvt->ctrlKey();
 #endif
-            if (multiple() && multiSelectKeyPressed)
-                deselectOtherOptions = false;
-            if (element->selected() && multiSelectKeyPressed)
-                shouldSelect = false;
+
+            bool shiftSelect = multiple() && mEvt->shiftKey();
+            bool multiSelect = multiple() && multiSelectKeyPressed && !mEvt->shiftKey();
             
-            int optionIndex = element->index();
-            if (!shouldSelect) {
-                optionIndex = -1;
-                element->m_selected = false;
+            HTMLElement* clickedElement = listItems()[listIndex];            
+            HTMLOptionElement* option = 0;
+            if (clickedElement->hasLocalName(optionTag)) {
+                option = static_cast<HTMLOptionElement*>(clickedElement);
+                
+                // Keep track of whether an active selection (like during drag selection), should select or deselect
+                if (option->selected() && multiSelectKeyPressed)
+                    m_activeSelectionState = false;
+
+                if (!m_activeSelectionState)
+                    option->m_selected = false;
             }
-            setSelectedIndex(optionIndex, deselectOtherOptions);
+            
+            // If we're not in any special multiple selection mode, then deselect all other items, excluding the clicked option.
+            // If no option was clicked, then this will deselect all items in the list.
+            if (!shiftSelect && !multiSelect)
+                deselectItems(option);
+
+            // If the anchor hasn't been set, and we're doing a single selection or a shift selection, then initialize the anchor to the first selected index.
+            if (m_activeSelectionAnchorIndex < 0 && !multiSelect)
+                setActiveSelectionAnchorIndex(selectedIndex());
+
+            // Set the selection state of the clicked option
+            if (option && !option->disabled())
+                option->m_selected = true;
+            
+            // If there was no selectedIndex() for the previous initialization, or
+            // If we're doing a single selection, or a multiple selection (using cmd or ctrl), then initialize the anchor index to the listIndex that just got clicked.
+            if (listIndex >= 0 && (m_activeSelectionAnchorIndex < 0 || !shiftSelect))
+                setActiveSelectionAnchorIndex(listIndex);
+            
+            setActiveSelectionEndIndex(listIndex);
+            updateListBoxSelection(!multiSelect);
+            renderer()->repaint();
         }
-    } else if (evt->type() == keypressEvent) {
+    } else if (evt->type() == mouseupEvent && document()->frame()->autoscrollRenderer() != renderer())
+        // This makes sure we fire onChange for a single click.  For drag selection, onChange will fire when the autoscroll timer stops.
+        listBoxOnChange();
+    else if (evt->type() == keypressEvent) {
         if (!evt->isKeyboardEvent())
             return;
         String keyIdentifier = static_cast<KeyboardEvent*>(evt)->keyIdentifier();
         
-        int index = 0;
+        int endIndex = 0;
         const Vector<HTMLElement*>& items = listItems();
-
-        if (keyIdentifier == "Down") {
-            index = nextSelectableListIndex(lastSelectedListIndex());
-            
-        } else if (keyIdentifier == "Up") {
-            index = previousSelectableListIndex(optionToListIndex(selectedIndex()));
+        
+        if (m_activeSelectionEndIndex < 0) {
+            // Initialize the end index
+            if (keyIdentifier == "Down")
+                endIndex = nextSelectableListIndex(lastSelectedListIndex());
+            else if (keyIdentifier == "Up")
+                endIndex = previousSelectableListIndex(optionToListIndex(selectedIndex()));
+        } else {
+            // Set the end index based on the current end index
+            if (keyIdentifier == "Down")
+                endIndex = nextSelectableListIndex(m_activeSelectionEndIndex);
+            else if (keyIdentifier == "Up")
+                endIndex = previousSelectableListIndex(m_activeSelectionEndIndex);    
         }
+        
         if (keyIdentifier == "Down" || keyIdentifier == "Up") {
-            ASSERT(index >= 0 && (unsigned)index < items.size()); 
-            HTMLOptionElement* element = static_cast<HTMLOptionElement*>(items[index]);
-            
-            setSelectedIndex(element->index(), !multiple() || !static_cast<KeyboardEvent*>(evt)->shiftKey());
-            static_cast<RenderListBox*>(renderer())->scrollToRevealElementAtListIndex(index);
+            ASSERT(endIndex >= 0 && (unsigned)endIndex < items.size()); 
+            setActiveSelectionEndIndex(endIndex);
             
+            // If the anchor is unitialized, or if we're going to deselect all other options, then set the anchor index equal to the end index.
+            bool deselectOthers = !multiple() || !static_cast<KeyboardEvent*>(evt)->shiftKey();
+            if (m_activeSelectionAnchorIndex < 0 || deselectOthers) {
+                m_activeSelectionState = true;
+                if (deselectOthers)
+                    deselectItems();
+                setActiveSelectionAnchorIndex(m_activeSelectionEndIndex);
+            }
+
+            static_cast<RenderListBox*>(renderer())->scrollToRevealElementAtListIndex(endIndex);
             evt->setDefaultHandled();
-            setChanged();
+            updateListBoxSelection(deselectOthers);
             renderer()->repaint();
+            
+            listBoxOnChange();
+        }
+    }
+}
+
+void HTMLSelectElement::setActiveSelectionAnchorIndex(int index)
+{
+    m_activeSelectionAnchorIndex = index;
+    
+    // Cache the selection state so we can restore the old selection as the new selection pivots around this anchor index
+    const Vector<HTMLElement*>& items = listItems();
+    m_cachedStateForActiveSelection.clear();
+    for (unsigned i = 0; i < items.size(); i++) {
+        if (items[i]->hasLocalName(optionTag)) {
+            HTMLOptionElement* option = static_cast<HTMLOptionElement*>(items[i]);
+            m_cachedStateForActiveSelection.append(option->selected());
+        } else
+            m_cachedStateForActiveSelection.append(false);
+    }
+}
+
+void HTMLSelectElement::updateListBoxSelection(bool deselectOtherOptions)
+{
+    unsigned start;
+    unsigned end;
+    ASSERT(m_activeSelectionAnchorIndex >= 0);
+    start = min(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex);
+    end = max(m_activeSelectionAnchorIndex, m_activeSelectionEndIndex);
+
+    const Vector<HTMLElement*>& items = listItems();
+    for (unsigned i = 0; i < items.size(); i++) {
+        if (items[i]->hasLocalName(optionTag)) {
+            HTMLOptionElement* option = static_cast<HTMLOptionElement*>(items[i]);
+            if (!option->disabled()) {
+                if (i >= start && i <= end)
+                    option->m_selected = m_activeSelectionState;
+                else if (deselectOtherOptions)
+                    option->m_selected = false;
+                else
+                    option->m_selected = m_cachedStateForActiveSelection[i];
+            }
         }
     }
+    if (renderer()->isListBox())
+        static_cast<RenderListBox*>(renderer())->setSelectionChanged(true);
+}
+
+void HTMLSelectElement::listBoxOnChange()
+{
+    const Vector<HTMLElement*>& items = listItems();
+    
+    // If the cached selection list is empty, or the size has changed, then rebuild the list, fire onChange, and return early.
+    if (m_lastOnChangeSelection.isEmpty() || m_lastOnChangeSelection.size() != items.size()) {
+        m_lastOnChangeSelection.clear();
+        for (unsigned i = 0; i < items.size(); i++) {
+            if (items[i]->hasLocalName(optionTag)) {
+                HTMLOptionElement* option = static_cast<HTMLOptionElement*>(items[i]);
+                m_lastOnChangeSelection.append(option->selected());
+            } else
+                m_lastOnChangeSelection.append(false);
+        }
+        onChange();
+        return;
+    }
+    
+    // Update m_lastOnChangeSelection and fire onChange
+    bool fireOnChange = false;
+    for (unsigned i = 0; i < items.size(); i++) {
+        bool selected = false;
+        if (items[i]->hasLocalName(optionTag))
+            selected = static_cast<HTMLOptionElement*>(items[i])->selected();
+        if (selected != m_lastOnChangeSelection[i])      
+            fireOnChange = true;
+        m_lastOnChangeSelection[i] = selected;
+    }
+    if (fireOnChange)
+        onChange();
 }
 
 static String stripLeadingWhiteSpace(const String& string)
index baded059d48853c5c68d379b9371d530ee4f208d..0935195b7f6a160335f3fd13f1db47b3cf28d018 100644 (file)
@@ -61,7 +61,8 @@ public:
     int selectedIndex() const;
     void setSelectedIndex(int index, bool deselect = true, bool fireOnChange = false);
     void notifyOptionSelected(HTMLOptionElement* selectedOption, bool selected);
-    
+    int lastSelectedListIndex() const;
+
     virtual bool isEnumeratable() const { return true; }
 
     int length() const;
@@ -125,11 +126,15 @@ public:
 
     HTMLCollection::CollectionInfo* collectionInfo() { return &m_collectionInfo; }
     
+    void setActiveSelectionAnchorIndex(int index);
+    void setActiveSelectionEndIndex(int index) { m_activeSelectionEndIndex = index; }
+    void updateListBoxSelection(bool deselectOtherOptions);
+    void listBoxOnChange();
+
 private:
     void recalcListItems() const;
-    void deselectItems(HTMLOptionElement* excludeElement);
+    void deselectItems(HTMLOptionElement* excludeElement = 0);
     bool usesMenuList() const { return !m_multiple && m_size <= 1; }
-    int lastSelectedListIndex() const;
     int nextSelectableListIndex(int startIndex);
     int previousSelectableListIndex(int startIndex);
     void menuListDefaultEventHandler(Event*);
@@ -137,11 +142,17 @@ private:
     void typeAheadFind(KeyboardEvent*);
 
     mutable Vector<HTMLElement*> m_listItems;
+    Vector<bool> m_cachedStateForActiveSelection;
+    Vector<bool> m_lastOnChangeSelection;
     int m_minwidth;
     int m_size;
     bool m_multiple;
     mutable bool m_recalcListItems;
     int m_lastOnChangeIndex;
+    
+    int m_activeSelectionAnchorIndex;
+    int m_activeSelectionEndIndex;  
+    bool m_activeSelectionState;
 
     // Instance variables for type-ahead find
     UChar m_repeatingChar;
index b541ac028ab37c52bc9a7d128d2a009bb0fa64f1..cc56b7ae19db7c319e28af7f4b5f915423d73caa 100644 (file)
@@ -2855,8 +2855,10 @@ void Frame::startAutoscrollTimer()
     d->m_autoscrollTimer.startRepeating(autoscrollInterval);
 }
 
-void Frame::stopAutoscrollTimer()
+void Frame::stopAutoscrollTimer(bool rendererIsBeingDestroyed)
 {
+    if (!rendererIsBeingDestroyed && autoscrollRenderer())
+        autoscrollRenderer()->stopAutoscroll();
     setAutoscrollRenderer(0);
     d->m_autoscrollTimer.stop();
 }
index f1b5eebde45165aba85c435d5154efb0fcdc22a4..cc485f70e6acbc0e80ba636ee81d118d4ae85d4d 100644 (file)
@@ -726,7 +726,7 @@ public:
 
   void setNeedsReapplyStyles();
 
-  void stopAutoscrollTimer();
+  void stopAutoscrollTimer(bool rendererIsBeingDestroyed = false);
   RenderObject* autoscrollRenderer() const;
 
 protected:
index ddabc46f3b2a211bdd0f381f2056587122b229f0..cd88fd5aae4613d20cf4b3d23ca9cbd2b534f1f5 100644 (file)
@@ -110,7 +110,10 @@ void RenderListBox::updateFromElement()
         setNeedsLayoutAndMinMaxRecalc();
     }
     
-    scrollToRevealElementAtListIndex(select->optionToListIndex(select->selectedIndex()));
+    int firstIndex = select->optionToListIndex(select->selectedIndex());
+    int lastIndex = select->lastSelectedListIndex();
+    if (firstIndex >= 0 && !listIndexIsVisible(firstIndex) && !listIndexIsVisible(lastIndex))
+        scrollToRevealElementAtListIndex(firstIndex);
 }
 
 void RenderListBox::calcMinMaxWidth()
@@ -343,48 +346,62 @@ bool RenderListBox::isPointInScrollbar(HitTestResult& result, int _x, int _y, in
     return false;
 }
 
-HTMLOptionElement* RenderListBox::optionAtPoint(int x, int y)
+int RenderListBox::listIndexAtOffset(int offsetX, int offsetY)
 {
     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
     const Vector<HTMLElement*>& listItems = select->listItems();
-    int yOffset = y - absoluteBoundingBoxRect().y();
-    int newOffset = max(0, yOffset / (style()->font().height() + optionsSpacingMiddle)) + m_indexOffset;
+
+    int newOffset = max(0, offsetY / (style()->font().height() + optionsSpacingMiddle)) + m_indexOffset;
     newOffset = max(0, min((int)listItems.size() - 1, newOffset));
     int scrollbarWidth = m_vBar ? m_vBar->width() : 0;
-    if (x >= absoluteBoundingBoxRect().x() + borderLeft() + paddingLeft() && x < absoluteBoundingBoxRect().right() - borderRight() - paddingRight() - scrollbarWidth)
-        return static_cast<HTMLOptionElement*>(listItems[newOffset]);
-    return 0;
+    if (offsetX >= borderLeft() + paddingLeft() && offsetX < absoluteBoundingBoxRect().width() - borderRight() - paddingRight() - scrollbarWidth)
+        return newOffset;
+            
+    return -1;
 }
 
 void RenderListBox::autoscroll()
 {
     IntPoint pos = document()->frame()->view()->windowToContents(document()->frame()->view()->currentMousePosition());
-    IntRect bounds = absoluteBoundingBoxRect();
 
-    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
-    const Vector<HTMLElement*>& items = select->listItems();
-    HTMLOptionElement* element = 0;
+    int rx = 0;
+    int ry = 0;
+    absolutePosition(rx, ry);
+    int offsetX = pos.x() - rx;
+    int offsetY = pos.y() - ry;
+    
+    int endIndex = -1;
     int rows = size();
     int offset = m_indexOffset;
-    if (pos.y() < bounds.y() && scrollToRevealElementAtListIndex(offset - 1) && items[offset - 1]->hasTagName(optionTag))
-        element = static_cast<HTMLOptionElement*>(items[offset - 1]);
-    else if (pos.y() > bounds.bottom() && scrollToRevealElementAtListIndex(offset + rows) && items[offset + rows - 1]->hasTagName(optionTag))
-        element = static_cast<HTMLOptionElement*>(items[offset + rows - 1]);
+    if (offsetY <  0 && scrollToRevealElementAtListIndex(offset - 1))
+        endIndex = offset - 1;
+    else if (offsetY > absoluteBoundingBoxRect().height() && scrollToRevealElementAtListIndex(offset + rows))
+        endIndex = offset + rows - 1;
     else
-        element = optionAtPoint(pos.x(), pos.y());
-        
-    if (element) {
-        select->setSelectedIndex(element->index(), !select->multiple());
+        endIndex = listIndexAtOffset(offsetX, offsetY);
+
+    HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
+    if (endIndex >= 0 && select) {
+        if (!select->multiple())
+            select->setActiveSelectionAnchorIndex(endIndex);
+        select->setActiveSelectionEndIndex(endIndex);
+        select->updateListBoxSelection(!select->multiple());
         repaint();
     }
 }
 
+void RenderListBox::stopAutoscroll()
+{
+    if ( HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node()))
+        select->listBoxOnChange();
+}
+
 bool RenderListBox::scrollToRevealElementAtListIndex(int index)
 {
     HTMLSelectElement* select = static_cast<HTMLSelectElement*>(node());
     const Vector<HTMLElement*>& listItems = select->listItems();
     
-    if (index < 0 || index > (int)listItems.size() - 1 || (index >= m_indexOffset && index < m_indexOffset + size()))
+    if (index < 0 || index > (int)listItems.size() - 1 || listIndexIsVisible(index))
         return false;
 
     int newOffset;
@@ -402,6 +419,11 @@ bool RenderListBox::scrollToRevealElementAtListIndex(int index)
     return true;
 }
 
+bool RenderListBox::listIndexIsVisible(int index)
+{    
+    return index >= m_indexOffset && index < m_indexOffset + size();
+}
+
 bool RenderListBox::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier)
 {
     return m_vBar && m_vBar->scroll(direction, granularity, multiplier);
@@ -420,7 +442,6 @@ void RenderListBox::valueChanged(Scrollbar*)
         int newOffset = max(0, m_vBar->value() / (style()->font().height() + optionsSpacingMiddle));
         if (newOffset != m_indexOffset) {
             m_indexOffset = newOffset;
-        //    printf("value changed: new offset index: %d\n", newOffset);
             repaint();
             // Fire the scroll DOM event.
             EventTargetNodeCast(node())->dispatchHTMLEvent(scrollEvent, true, false);
index fbf7db5313c767ca986e56b2bc65ac7b59994010..1c35664564caa7600dfa3e8a71859a5e9c56d707 100644 (file)
@@ -27,6 +27,7 @@
 
 namespace WebCore {
 
+class HTMLElement;
 class HTMLSelectElement;
 class HTMLOptionElement;
 
@@ -60,12 +61,13 @@ public:
     virtual void valueChanged(Scrollbar*);
     virtual IntRect windowClipRect() const;
 
-    HTMLOptionElement* optionAtPoint(int x, int y);
+    int listIndexAtOffset(int x, int y);
 
     bool scrollToRevealElementAtListIndex(int index);
 
-    virtual bool shouldAutoscroll() const { return numItems() > size(); }
+    virtual bool shouldAutoscroll() const { return true; }
     virtual void autoscroll();
+    virtual void stopAutoscroll();
 
 private:
     int size() const;
@@ -74,6 +76,7 @@ private:
     void paintScrollbar(PaintInfo&);
     void paintItemForeground(PaintInfo&, int tx, int ty, int listIndex);
     void paintItemBackground(PaintInfo&, int tx, int ty, int listIndex);
+    bool listIndexIsVisible(int index);
 
     bool m_optionsChanged;
     int m_optionsWidth;
index defa01e653c1522e2e1998c6241b57a73211c535..12b65f895742f826f0370f2ea381e648c30ecbe1 100644 (file)
@@ -2465,7 +2465,7 @@ void RenderObject::destroy()
 {
     // If this renderer is being autoscrolled, stop the autoscroll timer
     if (document() && document()->frame() && document()->frame()->autoscrollRenderer() == this)
-        document()->frame()->stopAutoscrollTimer();
+        document()->frame()->stopAutoscrollTimer(true);
 
     if (m_hasCounterNodeMap) {
         RenderObjectsToCounterNodeMaps* objectsMap = getRenderObjectsToCounterNodeMaps();
index c88c046640ee06bf3bd8c51dd081a93114943f7e..a8a646b00329043c5898c9ceb25dbd168e872bd8 100644 (file)
@@ -608,6 +608,7 @@ public:
     virtual bool scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier=1.0);
     virtual bool shouldAutoscroll() const;
     virtual void autoscroll();
+    virtual void stopAutoscroll() {};
 
     // The following seven functions are used to implement collapsing margins.
     // All objects know their maximal positive and negative margins.  The