Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(135)

Side by Side Diff: third_party/WebKit/Source/core/editing/InputMethodController.cpp

Issue 2530843003: Introduce InsertIncrementalTextCommand to respect existing style for composition (Closed)
Patch Set: Address xiaochengh@'s review Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved. 2 * Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) 3 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
4 * 4 *
5 * Redistribution and use in source and binary forms, with or without 5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions 6 * modification, are permitted provided that the following conditions
7 * are met: 7 * are met:
8 * 1. Redistributions of source code must retain the above copyright 8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright 10 * 2. Redistributions in binary form must reproduce the above copyright
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
61 void dispatchCompositionEndEvent(LocalFrame& frame, const String& text) { 61 void dispatchCompositionEndEvent(LocalFrame& frame, const String& text) {
62 Element* target = frame.document()->focusedElement(); 62 Element* target = frame.document()->focusedElement();
63 if (!target) 63 if (!target)
64 return; 64 return;
65 65
66 CompositionEvent* event = CompositionEvent::create( 66 CompositionEvent* event = CompositionEvent::create(
67 EventTypeNames::compositionend, frame.domWindow(), text); 67 EventTypeNames::compositionend, frame.domWindow(), text);
68 target->dispatchEvent(event); 68 target->dispatchEvent(event);
69 } 69 }
70 70
71 bool needsIncrementalInsertion(const LocalFrame& frame, const String& newText) {
72 // No need to apply incremental insertion if it doesn't support formated text.
73 if (!frame.editor().canEditRichly())
74 return false;
75
76 // No need to apply incremental insertion if the old text (text to be
77 // replaced) or the new text (text to be inserted) is empty.
78 if (frame.selectedText().isEmpty() || newText.isEmpty())
79 return false;
80
81 return true;
82 }
83
71 // Used to insert/replace text during composition update and confirm 84 // Used to insert/replace text during composition update and confirm
72 // composition. 85 // composition.
73 // Procedure: 86 // Procedure:
74 // 1. Fire 'beforeinput' event for (TODO(chongz): deleted composed text) and 87 // 1. Fire 'beforeinput' event for (TODO(chongz): deleted composed text) and
75 // inserted text 88 // inserted text
76 // 2. Fire 'compositionupdate' event 89 // 2. Fire 'compositionupdate' event
77 // 3. Fire TextEvent and modify DOM 90 // 3. Fire TextEvent and modify DOM
78 // TODO(chongz): 4. Fire 'input' event 91 // TODO(chongz): 4. Fire 'input' event
79 void insertTextDuringCompositionWithEvents( 92 void insertTextDuringCompositionWithEvents(
80 LocalFrame& frame, 93 LocalFrame& frame,
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
115 128
116 // 'beforeinput' event handler may destroy document. 129 // 'beforeinput' event handler may destroy document.
117 if (!frame.document()) 130 if (!frame.document())
118 return; 131 return;
119 132
120 dispatchCompositionUpdateEvent(frame, text); 133 dispatchCompositionUpdateEvent(frame, text);
121 // 'compositionupdate' event handler may destroy document. 134 // 'compositionupdate' event handler may destroy document.
122 if (!frame.document()) 135 if (!frame.document())
123 return; 136 return;
124 137
138 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
139 // needs to be audited. see http://crbug.com/590369 for more details.
140 frame.document()->updateStyleAndLayoutIgnorePendingStylesheets();
141
142 const bool isIncrementalInsertion = needsIncrementalInsertion(frame, text);
143
125 switch (compositionType) { 144 switch (compositionType) {
126 case TypingCommand::TextCompositionType::TextCompositionUpdate: 145 case TypingCommand::TextCompositionType::TextCompositionUpdate:
146 case TypingCommand::TextCompositionType::TextCompositionConfirm:
127 TypingCommand::insertText(*frame.document(), text, options, 147 TypingCommand::insertText(*frame.document(), text, options,
128 compositionType); 148 compositionType, isIncrementalInsertion);
129 break; 149 break;
130 case TypingCommand::TextCompositionType::TextCompositionConfirm:
131 case TypingCommand::TextCompositionType::TextCompositionCancel: 150 case TypingCommand::TextCompositionType::TextCompositionCancel:
132 // TODO(chongz): Use TypingCommand::insertText after TextEvent was 151 // TODO(chongz): Use TypingCommand::insertText after TextEvent was
133 // removed. (Removed from spec since 2012) 152 // removed. (Removed from spec since 2012)
134 // See TextEvent.idl. 153 // See TextEvent.idl.
135 frame.eventHandler().handleTextInputEvent(text, 0, 154 frame.eventHandler().handleTextInputEvent(text, 0,
136 TextEventInputComposition); 155 TextEventInputComposition);
137 break; 156 break;
138 default: 157 default:
139 NOTREACHED(); 158 NOTREACHED();
140 } 159 }
(...skipping 23 matching lines...) Expand all
164 return element->fastGetAttribute(HTMLNames::inputmodeAttr).lower(); 183 return element->fastGetAttribute(HTMLNames::inputmodeAttr).lower();
165 } 184 }
166 185
167 } // anonymous namespace 186 } // anonymous namespace
168 187
169 InputMethodController* InputMethodController::create(LocalFrame& frame) { 188 InputMethodController* InputMethodController::create(LocalFrame& frame) {
170 return new InputMethodController(frame); 189 return new InputMethodController(frame);
171 } 190 }
172 191
173 InputMethodController::InputMethodController(LocalFrame& frame) 192 InputMethodController::InputMethodController(LocalFrame& frame)
174 : m_frame(&frame), m_isDirty(false), m_hasComposition(false) {} 193 : m_frame(&frame), m_hasComposition(false) {}
175 194
176 InputMethodController::~InputMethodController() = default; 195 InputMethodController::~InputMethodController() = default;
177 196
178 bool InputMethodController::isAvailable() const { 197 bool InputMethodController::isAvailable() const {
179 return frame().document(); 198 return frame().document();
180 } 199 }
181 200
182 Document& InputMethodController::document() const { 201 Document& InputMethodController::document() const {
183 DCHECK(isAvailable()); 202 DCHECK(isAvailable());
184 return *frame().document(); 203 return *frame().document();
185 } 204 }
186 205
187 bool InputMethodController::hasComposition() const { 206 bool InputMethodController::hasComposition() const {
188 return m_hasComposition && !m_compositionRange->collapsed() && 207 return m_hasComposition && !m_compositionRange->collapsed() &&
189 m_compositionRange->isConnected(); 208 m_compositionRange->isConnected();
190 } 209 }
191 210
192 inline Editor& InputMethodController::editor() const { 211 inline Editor& InputMethodController::editor() const {
193 return frame().editor(); 212 return frame().editor();
194 } 213 }
195 214
196 void InputMethodController::clear() { 215 void InputMethodController::clear() {
197 m_hasComposition = false; 216 m_hasComposition = false;
198 if (m_compositionRange) { 217 if (m_compositionRange) {
199 m_compositionRange->setStart(&document(), 0); 218 m_compositionRange->setStart(&document(), 0);
200 m_compositionRange->collapse(true); 219 m_compositionRange->collapse(true);
201 } 220 }
202 document().markers().removeMarkers(DocumentMarker::Composition); 221 document().markers().removeMarkers(DocumentMarker::Composition);
203 m_isDirty = false;
204 } 222 }
205 223
206 void InputMethodController::contextDestroyed() { 224 void InputMethodController::contextDestroyed() {
207 clear(); 225 clear();
208 m_compositionRange = nullptr; 226 m_compositionRange = nullptr;
209 } 227 }
210 228
211 void InputMethodController::documentAttached(Document* document) { 229 void InputMethodController::documentAttached(Document* document) {
212 DCHECK(document); 230 DCHECK(document);
213 setContext(document); 231 setContext(document);
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
262 // duplicate selection change event. 280 // duplicate selection change event.
263 if (!text.length() && !relativeCaretPosition) 281 if (!text.length() && !relativeCaretPosition)
264 return false; 282 return false;
265 return insertTextAndMoveCaret(text, relativeCaretPosition); 283 return insertTextAndMoveCaret(text, relativeCaretPosition);
266 } 284 }
267 285
268 bool InputMethodController::replaceComposition(const String& text) { 286 bool InputMethodController::replaceComposition(const String& text) {
269 if (!hasComposition()) 287 if (!hasComposition())
270 return false; 288 return false;
271 289
272 // If the composition was set from existing text and didn't change, then
273 // there's nothing to do here (and we should avoid doing anything as that
274 // may clobber multi-node styled text).
275 if (!m_isDirty && composingText() == text) {
276 clear();
277 return true;
278 }
279
280 // Select the text that will be deleted or replaced. 290 // Select the text that will be deleted or replaced.
281 selectComposition(); 291 selectComposition();
282 292
283 if (frame().selection().isNone()) 293 if (frame().selection().isNone())
284 return false; 294 return false;
285 295
286 if (!isAvailable()) 296 if (!isAvailable())
287 return false; 297 return false;
288 298
289 // If text is empty, then delete the old composition here. If text is 299 // If text is empty, then delete the old composition here. If text is
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after
404 if (!selection.isNone() && !m_compositionRange->collapsed()) { 414 if (!selection.isNone() && !m_compositionRange->collapsed()) {
405 if (selection.start().compareTo(m_compositionRange->startPosition()) >= 0 && 415 if (selection.start().compareTo(m_compositionRange->startPosition()) >= 0 &&
406 selection.end().compareTo(m_compositionRange->endPosition()) <= 0) 416 selection.end().compareTo(m_compositionRange->endPosition()) <= 0)
407 return; 417 return;
408 } 418 }
409 419
410 cancelComposition(); 420 cancelComposition();
411 frame().chromeClient().didCancelCompositionOnSelectionChange(); 421 frame().chromeClient().didCancelCompositionOnSelectionChange();
412 } 422 }
413 423
414 static size_t computeCommonPrefixLength(const String& str1,
415 const String& str2) {
416 const size_t maxCommonPrefixLength = std::min(str1.length(), str2.length());
417 for (size_t index = 0; index < maxCommonPrefixLength; ++index) {
418 if (str1[index] != str2[index])
419 return index;
420 }
421 return maxCommonPrefixLength;
422 }
423
424 static size_t computeCommonSuffixLength(const String& str1,
425 const String& str2) {
426 const size_t length1 = str1.length();
427 const size_t length2 = str2.length();
428 const size_t maxCommonSuffixLength = std::min(length1, length2);
429 for (size_t index = 0; index < maxCommonSuffixLength; ++index) {
430 if (str1[length1 - index - 1] != str2[length2 - index - 1])
431 return index;
432 }
433 return maxCommonSuffixLength;
434 }
435
436 // If current position is at grapheme boundary, return 0; otherwise, return the 424 // If current position is at grapheme boundary, return 0; otherwise, return the
437 // distance to its nearest left grapheme boundary. 425 // distance to its nearest left grapheme boundary.
438 static size_t computeDistanceToLeftGraphemeBoundary(const Position& position) { 426 static size_t computeDistanceToLeftGraphemeBoundary(const Position& position) {
439 const Position& adjustedPosition = previousPositionOf( 427 const Position& adjustedPosition = previousPositionOf(
440 nextPositionOf(position, PositionMoveType::GraphemeCluster), 428 nextPositionOf(position, PositionMoveType::GraphemeCluster),
441 PositionMoveType::GraphemeCluster); 429 PositionMoveType::GraphemeCluster);
442 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); 430 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode());
443 DCHECK_GE(position.computeOffsetInContainerNode(), 431 DCHECK_GE(position.computeOffsetInContainerNode(),
444 adjustedPosition.computeOffsetInContainerNode()); 432 adjustedPosition.computeOffsetInContainerNode());
445 return static_cast<size_t>(position.computeOffsetInContainerNode() - 433 return static_cast<size_t>(position.computeOffsetInContainerNode() -
446 adjustedPosition.computeOffsetInContainerNode()); 434 adjustedPosition.computeOffsetInContainerNode());
447 } 435 }
448 436
449 static size_t computeCommonGraphemeClusterPrefixLengthForSetComposition(
450 const String& oldText,
451 const String& newText,
452 const Element* rootEditableElement) {
453 const size_t commonPrefixLength = computeCommonPrefixLength(oldText, newText);
454
455 // For grapheme cluster, we should adjust it for grapheme boundary.
456 const EphemeralRange& range =
457 PlainTextRange(0, commonPrefixLength).createRange(*rootEditableElement);
458 if (range.isNull())
459 return 0;
460 const Position& position = range.endPosition();
461 const size_t diff = computeDistanceToLeftGraphemeBoundary(position);
462 DCHECK_GE(commonPrefixLength, diff);
463 return commonPrefixLength - diff;
464 }
465
466 // If current position is at grapheme boundary, return 0; otherwise, return the 437 // If current position is at grapheme boundary, return 0; otherwise, return the
467 // distance to its nearest right grapheme boundary. 438 // distance to its nearest right grapheme boundary.
468 static size_t computeDistanceToRightGraphemeBoundary(const Position& position) { 439 static size_t computeDistanceToRightGraphemeBoundary(const Position& position) {
469 const Position& adjustedPosition = nextPositionOf( 440 const Position& adjustedPosition = nextPositionOf(
470 previousPositionOf(position, PositionMoveType::GraphemeCluster), 441 previousPositionOf(position, PositionMoveType::GraphemeCluster),
471 PositionMoveType::GraphemeCluster); 442 PositionMoveType::GraphemeCluster);
472 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); 443 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode());
473 DCHECK_GE(adjustedPosition.computeOffsetInContainerNode(), 444 DCHECK_GE(adjustedPosition.computeOffsetInContainerNode(),
474 position.computeOffsetInContainerNode()); 445 position.computeOffsetInContainerNode());
475 return static_cast<size_t>(adjustedPosition.computeOffsetInContainerNode() - 446 return static_cast<size_t>(adjustedPosition.computeOffsetInContainerNode() -
476 position.computeOffsetInContainerNode()); 447 position.computeOffsetInContainerNode());
477 } 448 }
478 449
479 static size_t computeCommonGraphemeClusterSuffixLengthForSetComposition(
480 const String& oldText,
481 const String& newText,
482 const Element* rootEditableElement) {
483 const size_t commonSuffixLength = computeCommonSuffixLength(oldText, newText);
484
485 // For grapheme cluster, we should adjust it for grapheme boundary.
486 const EphemeralRange& range =
487 PlainTextRange(0, oldText.length() - commonSuffixLength)
488 .createRange(*rootEditableElement);
489 if (range.isNull())
490 return 0;
491 const Position& position = range.endPosition();
492 const size_t diff = computeDistanceToRightGraphemeBoundary(position);
493 DCHECK_GE(commonSuffixLength, diff);
494 return commonSuffixLength - diff;
495 }
496
497 void InputMethodController::setCompositionWithIncrementalText(
498 const String& text,
499 const Vector<CompositionUnderline>& underlines,
500 int selectionStart,
501 int selectionEnd) {
502 Element* editable = frame().selection().rootEditableElement();
503 if (!editable)
504 return;
505
506 DCHECK_LE(selectionStart, selectionEnd);
507 String composing = composingText();
508 const size_t commonPrefixLength =
509 computeCommonGraphemeClusterPrefixLengthForSetComposition(composing, text,
510 editable);
511
512 // We should ignore common prefix when finding common suffix.
513 const size_t commonSuffixLength =
514 computeCommonGraphemeClusterSuffixLengthForSetComposition(
515 composing.right(composing.length() - commonPrefixLength),
516 text.right(text.length() - commonPrefixLength), editable);
517
518 const bool inserting =
519 text.length() > commonPrefixLength + commonSuffixLength;
520 const bool deleting =
521 composing.length() > commonPrefixLength + commonSuffixLength;
522
523 if (inserting || deleting) {
524 // Select the text to be deleted.
525 const size_t compositionStart =
526 PlainTextRange::create(*editable, compositionEphemeralRange()).start();
527 const size_t deletionStart = compositionStart + commonPrefixLength;
528 const size_t deletionEnd =
529 compositionStart + composing.length() - commonSuffixLength;
530 const EphemeralRange& deletionRange =
531 PlainTextRange(deletionStart, deletionEnd).createRange(*editable);
532 Document& currentDocument = document();
533 frame().selection().setSelection(
534 SelectionInDOMTree::Builder().setBaseAndExtent(deletionRange).build(),
535 0);
536 clear();
537
538 // FrameSeleciton::setSelection() can change document associate to |frame|.
539 if (!isAvailable() || currentDocument != document())
540 return;
541 if (!currentDocument.focusedElement())
542 return;
543
544 // Insert the incremental text.
545 const size_t insertionLength =
546 text.length() - commonPrefixLength - commonSuffixLength;
547 const String& insertingText =
548 text.substring(commonPrefixLength, insertionLength);
549 insertTextDuringCompositionWithEvents(frame(), insertingText,
550 TypingCommand::PreventSpellChecking,
551 TypingCommand::TextCompositionUpdate);
552
553 // Event handlers might destroy document.
554 if (!isAvailable() || currentDocument != document())
555 return;
556
557 // TODO(yosin): The use of updateStyleAndLayoutIgnorePendingStylesheets
558 // needs to be audited. see http://crbug.com/590369 for more details.
559 document().updateStyleAndLayoutIgnorePendingStylesheets();
560
561 // Now recreate the composition starting at its original start, and
562 // apply the specified final selection offsets.
563 setCompositionFromExistingText(underlines, compositionStart,
564 compositionStart + text.length());
565 }
566
567 selectComposition();
568
569 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
570 // needs to be audited. see http://crbug.com/590369 for more details.
571 document().updateStyleAndLayoutIgnorePendingStylesheets();
572
573 const PlainTextRange& selectedRange = createSelectionRangeForSetComposition(
574 selectionStart, selectionEnd, text.length());
575 // We shouldn't close typing in the middle of setComposition.
576 setEditableSelectionOffsets(selectedRange, NotUserTriggered);
577 m_isDirty = true;
578 }
579
580 void InputMethodController::setComposition( 450 void InputMethodController::setComposition(
581 const String& text, 451 const String& text,
582 const Vector<CompositionUnderline>& underlines, 452 const Vector<CompositionUnderline>& underlines,
583 int selectionStart, 453 int selectionStart,
584 int selectionEnd) { 454 int selectionEnd) {
585 Editor::RevealSelectionScope revealSelectionScope(&editor()); 455 Editor::RevealSelectionScope revealSelectionScope(&editor());
586 456
587 // Updates styles before setting selection for composition to prevent 457 // Updates styles before setting selection for composition to prevent
588 // inserting the previous composition text into text nodes oddly. 458 // inserting the previous composition text into text nodes oddly.
589 // See https://bugs.webkit.org/show_bug.cgi?id=46868 459 // See https://bugs.webkit.org/show_bug.cgi?id=46868
590 document().updateStyleAndLayoutTree(); 460 document().updateStyleAndLayoutTree();
591 461
592 // When the IME only wants to change a few characters at the end of the
593 // composition, only touch those characters in order to preserve rich text
594 // substructure.
595 if (hasComposition() && text.length()) {
596 return setCompositionWithIncrementalText(text, underlines, selectionStart,
597 selectionEnd);
598 }
599
600 selectComposition(); 462 selectComposition();
601 463
602 if (frame().selection().isNone()) 464 if (frame().selection().isNone())
603 return; 465 return;
604 466
605 Element* target = document().focusedElement(); 467 Element* target = document().focusedElement();
606 if (!target) 468 if (!target)
607 return; 469 return;
608 470
609 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets 471 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after
678 document().updateStyleAndLayoutIgnorePendingStylesheets(); 540 document().updateStyleAndLayoutIgnorePendingStylesheets();
679 541
680 // Find out what node has the composition now. 542 // Find out what node has the composition now.
681 Position base = mostForwardCaretPosition(frame().selection().base()); 543 Position base = mostForwardCaretPosition(frame().selection().base());
682 Node* baseNode = base.anchorNode(); 544 Node* baseNode = base.anchorNode();
683 if (!baseNode || !baseNode->isTextNode()) 545 if (!baseNode || !baseNode->isTextNode())
684 return; 546 return;
685 547
686 Position extent = frame().selection().extent(); 548 Position extent = frame().selection().extent();
687 Node* extentNode = extent.anchorNode(); 549 Node* extentNode = extent.anchorNode();
688 if (baseNode != extentNode)
689 return;
690 550
691 unsigned extentOffset = extent.computeOffsetInContainerNode(); 551 unsigned extentOffset = extent.computeOffsetInContainerNode();
692 unsigned baseOffset = base.computeOffsetInContainerNode(); 552 unsigned baseOffset = base.computeOffsetInContainerNode();
693 if (baseOffset + text.length() != extentOffset)
694 return;
695 553
696 m_isDirty = true;
697 m_hasComposition = true; 554 m_hasComposition = true;
698 if (!m_compositionRange) 555 if (!m_compositionRange)
699 m_compositionRange = Range::create(document()); 556 m_compositionRange = Range::create(document());
700 m_compositionRange->setStart(baseNode, baseOffset); 557 m_compositionRange->setStart(baseNode, baseOffset);
701 m_compositionRange->setEnd(baseNode, extentOffset); 558 m_compositionRange->setEnd(extentNode, extentOffset);
702 559
703 if (baseNode->layoutObject()) 560 if (baseNode->layoutObject())
704 baseNode->layoutObject()->setShouldDoFullPaintInvalidation(); 561 baseNode->layoutObject()->setShouldDoFullPaintInvalidation();
705 562
706 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets 563 // TODO(xiaochengh): The use of updateStyleAndLayoutIgnorePendingStylesheets
707 // needs to be audited. see http://crbug.com/590369 for more details. 564 // needs to be audited. see http://crbug.com/590369 for more details.
708 document().updateStyleAndLayoutIgnorePendingStylesheets(); 565 document().updateStyleAndLayoutIgnorePendingStylesheets();
709 566
710 // We shouldn't close typing in the middle of setComposition. 567 // We shouldn't close typing in the middle of setComposition.
711 setEditableSelectionOffsets(selectedRange, NotUserTriggered); 568 setEditableSelectionOffsets(selectedRange, NotUserTriggered);
(...skipping 481 matching lines...) Expand 10 before | Expand all | Expand 10 after
1193 return WebTextInputTypeNone; 1050 return WebTextInputTypeNone;
1194 } 1051 }
1195 1052
1196 DEFINE_TRACE(InputMethodController) { 1053 DEFINE_TRACE(InputMethodController) {
1197 visitor->trace(m_frame); 1054 visitor->trace(m_frame);
1198 visitor->trace(m_compositionRange); 1055 visitor->trace(m_compositionRange);
1199 SynchronousMutationObserver::trace(visitor); 1056 SynchronousMutationObserver::trace(visitor);
1200 } 1057 }
1201 1058
1202 } // namespace blink 1059 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698