Bug 336408 - Allow the caret to be positioned at the end of trimmed whitespace, as if the whitespace wasn't trimmed [p=roc r=smontagu sr=mrbkap a=blocking1.9+]
authorreed@reedloden.com
Wed, 07 Nov 2007 20:07:00 -0800
changeset 7677 6ffcd95f1533aebf97d28501ec9b500e69d795e1
parent 7676 547075a3fedd23206fe6581f215cc4851eaccc40
child 7678 2b91bbb618415ba0850eda6bba0655fc62bb0bcf
push idunknown
push userunknown
push dateunknown
reviewerssmontagu, mrbkap, blocking1.9
bugs336408
milestone1.9b2pre
Bug 336408 - Allow the caret to be positioned at the end of trimmed whitespace, as if the whitespace wasn't trimmed [p=roc r=smontagu sr=mrbkap a=blocking1.9+]
layout/base/nsCaret.cpp
layout/generic/nsTextFrame.h
layout/generic/nsTextFrameThebes.cpp
layout/generic/test/Makefile.in
layout/generic/test/test_character_movement.html
--- a/layout/base/nsCaret.cpp
+++ b/layout/base/nsCaret.cpp
@@ -63,16 +63,17 @@
 #include "nsIScrollableView.h"
 #include "nsIViewManager.h"
 #include "nsPresContext.h"
 #include "nsILookAndFeel.h"
 #include "nsBlockFrame.h"
 #include "nsISelectionController.h"
 #include "nsDisplayList.h"
 #include "nsCaret.h"
+#include "nsTextFrame.h"
 
 // The bidi indicator hangs off the caret to one side, to show which
 // direction the typing is in. It needs to be at least 2x2 to avoid looking like 
 // an insignificant dot
 static const PRUint32 kMinBidiIndicatorPixels = 2;
 
 #ifdef IBMBIDI
 #include "nsIBidiKeyboard.h"
@@ -603,16 +604,64 @@ nsCaret::DrawAtPositionWithHint(nsIDOMNo
   }
 
   if (aInvalidate)
     InvalidateRects(mCaretRect, mHookRect, theFrame);
 
   return PR_TRUE;
 }
 
+/**
+ * Find the first frame in an in-order traversal of the frame subtree rooted
+ * at aFrame which is either a text frame logically at the end of a line,
+ * or which is aStopAtFrame. Return null if no such frame is found. We don't
+ * descend into the children of non-eLineParticipant frames.
+ */
+static nsIFrame*
+CheckForTrailingTextFrameRecursive(nsIFrame* aFrame, nsIFrame* aStopAtFrame)
+{
+  if (aFrame == aStopAtFrame ||
+      ((aFrame->GetType() == nsGkAtoms::textFrame &&
+       (static_cast<nsTextFrame*>(aFrame))->IsAtEndOfLine())))
+    return aFrame;
+  if (!aFrame->IsFrameOfType(nsIFrame::eLineParticipant))
+    return nsnull;
+
+  for (nsIFrame* f = aFrame->GetFirstChild(nsnull); f; f = f->GetNextSibling())
+  {
+    nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame);
+    if (r)
+      return r;
+  }
+  return nsnull;
+}
+
+static void
+AdjustCaretFrameForLineEnd(nsIFrame** aFrame, PRInt32* aOffset)
+{
+  nsBlockFrame* block = nsLayoutUtils::FindNearestBlockAncestor(*aFrame);
+  if (!block)
+    return;
+  nsLineBox* line = block->FindLineFor(nsLayoutUtils::FindChildContainingDescendant(block, *aFrame));
+  PRInt32 count = line->GetChildCount();
+  for (nsIFrame* f = line->mFirstChild; count > 0; --count, f = f->GetNextSibling())
+  {
+    nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame);
+    if (r == *aFrame)
+      return;
+    if (r)
+    {
+      *aFrame = r;
+      NS_ASSERTION(r->GetType() == nsGkAtoms::textFrame, "Expected text frame");
+      *aOffset = (static_cast<nsTextFrame*>(r))->GetContentEnd();
+      return;
+    }
+  }
+}
+
 NS_IMETHODIMP 
 nsCaret::GetCaretFrameForNodeOffset(nsIContent*             aContentNode,
                                     PRInt32                 aOffset,
                                     nsFrameSelection::HINT aFrameHint,
                                     PRUint8                 aBidiLevel,
                                     nsIFrame**              aReturnFrame,
                                     PRInt32*                aReturnOffset)
 {
@@ -629,16 +678,22 @@ nsCaret::GetCaretFrameForNodeOffset(nsIC
   nsIFrame* theFrame = nsnull;
   PRInt32   theFrameOffset = 0;
 
   theFrame = frameSelection->GetFrameForNodeOffset(aContentNode, aOffset,
                                                    aFrameHint, &theFrameOffset);
   if (!theFrame)
     return NS_ERROR_FAILURE;
 
+  // if theFrame is after a text frame that's logically at the end of the line
+  // (e.g. if theFrame is a <br> frame), then put the caret at the end of
+  // that text frame instead. This way, the caret will be positioned as if
+  // trailing whitespace was not trimmed.
+  AdjustCaretFrameForLineEnd(&theFrame, &theFrameOffset);
+  
   // Mamdouh : modification of the caret to work at rtl and ltr with Bidi
   //
   // Direction Style from this->GetStyleData()
   // now in (visibility->mDirection)
   // ------------------
   // NS_STYLE_DIRECTION_LTR : LTR or Default
   // NS_STYLE_DIRECTION_RTL
   // NS_STYLE_DIRECTION_INHERIT
--- a/layout/generic/nsTextFrame.h
+++ b/layout/generic/nsTextFrame.h
@@ -175,16 +175,22 @@ public:
   virtual PRBool IsEmpty();
   virtual PRBool IsSelfEmpty() { return IsEmpty(); }
   
   /**
    * @return PR_TRUE if this text frame ends with a newline character.  It
    * should return PR_FALSE if this is not a text frame.
    */
   virtual PRBool HasTerminalNewline() const;
+
+  /**
+   * Returns true if this text frame is logically adjacent to the end of the
+   * line.
+   */
+  PRBool IsAtEndOfLine() const;
   
 #ifdef ACCESSIBILITY
   NS_IMETHOD GetAccessible(nsIAccessible** aAccessible);
 #endif
   
   virtual void MarkIntrinsicWidthsDirty();
   virtual nscoord GetMinWidth(nsIRenderingContext *aRenderingContext);
   virtual nscoord GetPrefWidth(nsIRenderingContext *aRenderingContext);
--- a/layout/generic/nsTextFrameThebes.cpp
+++ b/layout/generic/nsTextFrameThebes.cpp
@@ -4452,17 +4452,17 @@ nsTextFrame::PeekOffsetCharacter(PRBool 
   IsSelectable(&selectable, &selectStyle);
   if (selectStyle == NS_STYLE_USER_SELECT_ALL)
     return PR_FALSE;
 
   gfxSkipCharsIterator iter = EnsureTextRun();
   if (!mTextRun)
     return PR_FALSE;
 
-  TrimmedOffsets trimmed = GetTrimmedOffsets(mContent->GetText(), PR_TRUE);
+  TrimmedOffsets trimmed = GetTrimmedOffsets(mContent->GetText(), PR_FALSE);
 
   // A negative offset means "end of frame".
   PRInt32 startOffset = GetContentOffset() + (*aOffset < 0 ? contentLength : *aOffset);
 
   if (!aForward) {
     PRInt32 i;
     for (i = PR_MIN(trimmed.GetEnd(), startOffset) - 1;
          i >= trimmed.mStart; --i) {
@@ -5884,8 +5884,14 @@ nsTextFrame::AdjustOffsetsForBidi(PRInt3
  * @return PR_TRUE if this text frame ends with a newline character.  It should return
  * PR_FALSE if it is not a text frame.
  */
 PRBool
 nsTextFrame::HasTerminalNewline() const
 {
   return ::HasTerminalNewline(this);
 }
+
+PRBool
+nsTextFrame::IsAtEndOfLine() const
+{
+  return (GetStateBits() & TEXT_END_OF_LINE) != 0;
+}
--- a/layout/generic/test/Makefile.in
+++ b/layout/generic/test/Makefile.in
@@ -50,13 +50,14 @@ include $(topsrcdir)/config/rules.mk
 		test_bug382429.html \
 		test_bug384527.html \
 		test_bug385751.html \
 		test_bug389630.html \
 		test_bug391747.html \
 		test_bug392923.html \
 		test_bug394173.html \
 		test_bug394239.html \
+		test_character_movement.html \
 		test_word_movement.html \
 		$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/layout/generic/test/test_character_movement.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test Character Movement (including nsTextFrame::PeekOffsetCharacter)</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: block">
+<div contentEditable id="editor"></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript;version=1.7">
+
+SimpleTest.waitForExplicitFinish();
+
+// This seems to be necessary because the selection is not set up properly otherwise
+setTimeout(test, 0);
+
+function test() {
+  var sel = window.getSelection();
+  var editor = document.getElementById("editor");
+
+  function testRight(node, offset) {
+    synthesizeKey("VK_RIGHT", {});
+    is(sel.anchorNode, node, "Right movement broken in " + editor.innerHTML);
+    is(sel.anchorOffset, offset, "Right movement broken in " + editor.innerHTML);
+  }
+
+  function testLeft(node, offset) {
+    synthesizeKey("VK_LEFT", {});
+    is(sel.anchorNode, node, "Left movement broken in " + editor.innerHTML);
+    is(sel.anchorOffset, offset, "Left movement broken in " + editor.innerHTML);
+  }
+
+  editor.innerHTML = "H K";
+  sel.collapse(editor.firstChild, 0);
+  testRight(editor.firstChild, 1);
+  testRight(editor.firstChild, 2);
+  testRight(editor.firstChild, 3);
+  testLeft(editor.firstChild, 2);
+  testLeft(editor.firstChild, 1);
+  testLeft(editor.firstChild, 0);
+
+  editor.innerHTML = "<b>H</b> K";
+  sel.collapse(editor.firstChild.firstChild, 0);
+  testRight(editor.firstChild.firstChild, 1);
+  testRight(editor.firstChild.nextSibling, 1);
+  testRight(editor.firstChild.nextSibling, 2);
+  testLeft(editor.firstChild.nextSibling, 1);
+  testLeft(editor.firstChild.nextSibling, 0);
+  testLeft(editor.firstChild.firstChild, 0);
+
+  editor.innerHTML = "H <br>K";
+  sel.collapse(editor.firstChild, 0);
+  testRight(editor.firstChild, 1);
+  testRight(editor.firstChild, 2);
+  testRight(editor.firstChild.nextSibling.nextSibling, 0);
+  testRight(editor.firstChild.nextSibling.nextSibling, 1);
+  testLeft(editor.firstChild.nextSibling.nextSibling, 0);
+  testLeft(editor, 1);
+  testLeft(editor.firstChild, 1);
+  testLeft(editor.firstChild, 0);
+
+  SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+</body>
+</html>