OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "ui/chromeos/touch_exploration_controller.h" | 5 #include "ui/chromeos/touch_exploration_controller.h" |
6 | 6 |
7 #include "base/strings/string_number_conversions.h" | 7 #include "base/strings/string_number_conversions.h" |
8 #include "ui/aura/client/cursor_client.h" | 8 #include "ui/aura/client/cursor_client.h" |
9 #include "ui/aura/window.h" | 9 #include "ui/aura/window.h" |
10 #include "ui/aura/window_event_dispatcher.h" | 10 #include "ui/aura/window_event_dispatcher.h" |
11 #include "ui/aura/window_tree_host.h" | 11 #include "ui/aura/window_tree_host.h" |
12 #include "ui/events/event.h" | 12 #include "ui/events/event.h" |
13 #include "ui/events/event_processor.h" | 13 #include "ui/events/event_processor.h" |
14 #include "ui/gfx/geometry/rect.h" | 14 #include "ui/gfx/geometry/rect.h" |
15 | 15 |
16 #define VLOG_STATE() if (VLOG_IS_ON(0)) VlogState(__func__) | 16 #define VLOG_STATE() if (VLOG_IS_ON(0)) VlogState(__func__) |
17 #define VLOG_EVENT(event) if (VLOG_IS_ON(0)) VlogEvent(event, __func__) | 17 #define VLOG_EVENT(event) if (VLOG_IS_ON(0)) VlogEvent(event, __func__) |
18 | 18 |
19 namespace ui { | 19 namespace ui { |
20 | 20 |
21 namespace { | 21 namespace { |
22 | 22 |
23 // Delay between adjustment sounds. | 23 // Delay between adjustment sounds. |
24 const base::TimeDelta kSoundDelay = base::TimeDelta::FromMilliseconds(150); | 24 const base::TimeDelta kSoundDelay = base::TimeDelta::FromMilliseconds(150); |
25 | 25 |
26 // Delay before corner passthrough activates. | |
27 const base::TimeDelta kCornerPassthroughDelay = | |
28 base::TimeDelta::FromMilliseconds(500); | |
29 | |
26 // In ChromeOS, VKEY_LWIN is synonymous for the search key. | 30 // In ChromeOS, VKEY_LWIN is synonymous for the search key. |
27 const ui::KeyboardCode kChromeOSSearchKey = ui::VKEY_LWIN; | 31 const ui::KeyboardCode kChromeOSSearchKey = ui::VKEY_LWIN; |
28 } // namespace | 32 } // namespace |
29 | 33 |
30 TouchExplorationController::TouchExplorationController( | 34 TouchExplorationController::TouchExplorationController( |
31 aura::Window* root_window, | 35 aura::Window* root_window, |
32 TouchExplorationControllerDelegate* delegate) | 36 TouchExplorationControllerDelegate* delegate) |
33 : root_window_(root_window), | 37 : root_window_(root_window), |
34 delegate_(delegate), | 38 delegate_(delegate), |
35 state_(NO_FINGERS_DOWN), | 39 state_(NO_FINGERS_DOWN), |
36 event_handler_for_testing_(NULL), | 40 event_handler_for_testing_(NULL), |
37 gesture_provider_(this), | 41 gesture_provider_(this), |
38 prev_state_(NO_FINGERS_DOWN), | 42 prev_state_(NO_FINGERS_DOWN), |
39 VLOG_on_(true) { | 43 VLOG_on_(true), |
44 waiting_for_corner_passthrough_(false) { | |
40 CHECK(root_window); | 45 CHECK(root_window); |
41 root_window->GetHost()->GetEventSource()->AddEventRewriter(this); | 46 root_window->GetHost()->GetEventSource()->AddEventRewriter(this); |
42 } | 47 } |
43 | 48 |
44 TouchExplorationController::~TouchExplorationController() { | 49 TouchExplorationController::~TouchExplorationController() { |
45 root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this); | 50 root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this); |
46 } | 51 } |
47 | 52 |
48 ui::EventRewriteStatus TouchExplorationController::RewriteEvent( | 53 ui::EventRewriteStatus TouchExplorationController::RewriteEvent( |
49 const ui::Event& event, | 54 const ui::Event& event, |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
125 case DOUBLE_TAP_PRESSED: | 130 case DOUBLE_TAP_PRESSED: |
126 return InDoubleTapPressed(touch_event, rewritten_event); | 131 return InDoubleTapPressed(touch_event, rewritten_event); |
127 case TOUCH_EXPLORATION: | 132 case TOUCH_EXPLORATION: |
128 return InTouchExploration(touch_event, rewritten_event); | 133 return InTouchExploration(touch_event, rewritten_event); |
129 case GESTURE_IN_PROGRESS: | 134 case GESTURE_IN_PROGRESS: |
130 return InGestureInProgress(touch_event, rewritten_event); | 135 return InGestureInProgress(touch_event, rewritten_event); |
131 case TOUCH_EXPLORE_SECOND_PRESS: | 136 case TOUCH_EXPLORE_SECOND_PRESS: |
132 return InTouchExploreSecondPress(touch_event, rewritten_event); | 137 return InTouchExploreSecondPress(touch_event, rewritten_event); |
133 case TWO_TO_ONE_FINGER: | 138 case TWO_TO_ONE_FINGER: |
134 return InTwoToOneFinger(touch_event, rewritten_event); | 139 return InTwoToOneFinger(touch_event, rewritten_event); |
140 case CORNER_PASSTHROUGH: | |
141 return InCornerPassthrough(touch_event, rewritten_event); | |
135 case PASSTHROUGH: | 142 case PASSTHROUGH: |
136 return InPassthrough(touch_event, rewritten_event); | 143 return InPassthrough(touch_event, rewritten_event); |
137 case WAIT_FOR_RELEASE: | 144 case WAIT_FOR_RELEASE: |
138 return InWaitForRelease(touch_event, rewritten_event); | 145 return InWaitForRelease(touch_event, rewritten_event); |
139 case SLIDE_GESTURE: | 146 case SLIDE_GESTURE: |
140 return InSlideGesture(touch_event, rewritten_event); | 147 return InSlideGesture(touch_event, rewritten_event); |
141 } | 148 } |
142 NOTREACHED(); | 149 NOTREACHED(); |
143 return ui::EVENT_REWRITE_CONTINUE; | 150 return ui::EVENT_REWRITE_CONTINUE; |
144 } | 151 } |
145 | 152 |
146 ui::EventRewriteStatus TouchExplorationController::NextDispatchEvent( | 153 ui::EventRewriteStatus TouchExplorationController::NextDispatchEvent( |
147 const ui::Event& last_event, scoped_ptr<ui::Event>* new_event) { | 154 const ui::Event& last_event, scoped_ptr<ui::Event>* new_event) { |
148 NOTREACHED(); | 155 NOTREACHED(); |
149 return ui::EVENT_REWRITE_CONTINUE; | 156 return ui::EVENT_REWRITE_CONTINUE; |
150 } | 157 } |
151 | 158 |
152 ui::EventRewriteStatus TouchExplorationController::InNoFingersDown( | 159 ui::EventRewriteStatus TouchExplorationController::InNoFingersDown( |
153 const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) { | 160 const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) { |
154 const ui::EventType type = event.type(); | 161 const ui::EventType type = event.type(); |
155 if (type == ui::ET_TOUCH_PRESSED) { | 162 if (type != ui::ET_TOUCH_PRESSED) { |
156 initial_press_.reset(new TouchEvent(event)); | 163 NOTREACHED() << "Unexpected event type received: " << event.name(); |
157 last_unused_finger_event_.reset(new TouchEvent(event)); | 164 return ui::EVENT_REWRITE_CONTINUE; |
158 tap_timer_.Start(FROM_HERE, | |
159 gesture_detector_config_.double_tap_timeout, | |
160 this, | |
161 &TouchExplorationController::OnTapTimerFired); | |
162 gesture_provider_.OnTouchEvent(event); | |
163 gesture_provider_.OnTouchEventAck(false); | |
164 ProcessGestureEvents(); | |
165 state_ = SINGLE_TAP_PRESSED; | |
166 VLOG_STATE(); | |
167 return ui::EVENT_REWRITE_DISCARD; | |
168 } | 165 } |
169 NOTREACHED() << "Unexpected event type received: " << event.name();; | 166 int location = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge); |
170 return ui::EVENT_REWRITE_CONTINUE; | 167 base::TimeDelta timeout; |
168 | |
169 // If the press was at a corner, the user might go into corner passthrough | |
170 // instead. | |
171 bool in_a_bottom_corner = | |
172 (BOTTOM_LEFT_CORNER == location) || (BOTTOM_RIGHT_CORNER == location); | |
173 if (in_a_bottom_corner) { | |
174 VLOG(0) << "Location: " << location; | |
175 timeout = kCornerPassthroughDelay; | |
176 waiting_for_corner_passthrough_ = true; | |
177 } else { | |
178 timeout = gesture_detector_config_.double_tap_timeout; | |
179 waiting_for_corner_passthrough_ = false; | |
180 } | |
181 | |
182 initial_press_.reset(new TouchEvent(event)); | |
183 last_unused_finger_event_.reset(new TouchEvent(event)); | |
184 tap_timer_.Start( | |
185 FROM_HERE, timeout, this, &TouchExplorationController::OnTapTimerFired); | |
186 gesture_provider_.OnTouchEvent(event); | |
187 gesture_provider_.OnTouchEventAck(false); | |
188 ProcessGestureEvents(); | |
189 state_ = SINGLE_TAP_PRESSED; | |
190 VLOG_STATE(); | |
191 return ui::EVENT_REWRITE_DISCARD; | |
171 } | 192 } |
172 | 193 |
173 ui::EventRewriteStatus TouchExplorationController::InSingleTapPressed( | 194 ui::EventRewriteStatus TouchExplorationController::InSingleTapPressed( |
174 const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) { | 195 const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event) { |
175 const ui::EventType type = event.type(); | 196 const ui::EventType type = event.type(); |
176 | 197 |
198 int location = FindEdgesWithinBounds(event.location(), kMaxDistanceFromEdge); | |
199 bool in_a_bottom_corner = | |
200 (location == BOTTOM_LEFT_CORNER) || (location == BOTTOM_RIGHT_CORNER); | |
201 // If the event is from the initial press and the location is no longer in the | |
202 // corner, then we are not waiting for a corner passthrough anymore. | |
203 if (event.touch_id() == initial_press_->touch_id() && in_a_bottom_corner) { | |
aboxhall
2014/07/24 18:26:34
I think |in_a_bottom_corner| should be negated...
lisayin
2014/07/25 20:11:57
Opps...
| |
204 waiting_for_corner_passthrough_ = false; | |
205 // If the initial press was in a corner, then more than the double tap | |
206 // timeout could have elapsed. | |
207 if (tap_timer_.IsRunning() && | |
208 event.time_stamp() - initial_press_->time_stamp() > | |
209 gesture_detector_config_.double_tap_timeout) { | |
210 tap_timer_.Stop(); | |
211 OnTapTimerFired(); | |
212 return EVENT_REWRITE_DISCARD; | |
213 } | |
214 } | |
215 | |
177 if (type == ui::ET_TOUCH_PRESSED) { | 216 if (type == ui::ET_TOUCH_PRESSED) { |
178 // Adding a second finger within the timeout period switches to | 217 // Adding a second finger within the timeout period switches to |
179 // passing through every event from the second finger and none form the | 218 // passing through every event from the second finger and none form the |
180 // first. The event from the first finger is still saved in initial_press_. | 219 // first. The event from the first finger is still saved in initial_press_. |
181 state_ = TWO_TO_ONE_FINGER; | 220 state_ = TWO_TO_ONE_FINGER; |
182 last_two_to_one_.reset(new TouchEvent(event)); | 221 last_two_to_one_.reset(new TouchEvent(event)); |
183 rewritten_event->reset(new ui::TouchEvent(ui::ET_TOUCH_PRESSED, | 222 rewritten_event->reset(new ui::TouchEvent(ui::ET_TOUCH_PRESSED, |
184 event.location(), | 223 event.location(), |
185 event.touch_id(), | 224 event.touch_id(), |
186 event.time_stamp())); | 225 event.time_stamp())); |
(...skipping 208 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
395 event.time_stamp())); | 434 event.time_stamp())); |
396 (*rewritten_event)->set_flags(event.flags()); | 435 (*rewritten_event)->set_flags(event.flags()); |
397 state_ = WAIT_FOR_RELEASE; | 436 state_ = WAIT_FOR_RELEASE; |
398 return ui::EVENT_REWRITE_REWRITTEN; | 437 return ui::EVENT_REWRITE_REWRITTEN; |
399 } | 438 } |
400 } else if (type == ui::ET_TOUCH_PRESSED) { | 439 } else if (type == ui::ET_TOUCH_PRESSED) { |
401 DCHECK(current_touch_ids_.size() == 3); | 440 DCHECK(current_touch_ids_.size() == 3); |
402 // If a third finger is pressed, we are now going into passthrough mode | 441 // If a third finger is pressed, we are now going into passthrough mode |
403 // and now need to dispatch the first finger into a press, as well as the | 442 // and now need to dispatch the first finger into a press, as well as the |
404 // recent press. | 443 // recent press. |
405 if (current_touch_ids_.size() == 3){ | 444 if (current_touch_ids_.size() == 3) { |
406 state_ = PASSTHROUGH; | 445 state_ = PASSTHROUGH; |
407 scoped_ptr<ui::TouchEvent> first_finger_press; | 446 scoped_ptr<ui::TouchEvent> first_finger_press; |
408 first_finger_press.reset( | 447 first_finger_press.reset( |
409 new ui::TouchEvent(ui::ET_TOUCH_PRESSED, | 448 new ui::TouchEvent(ui::ET_TOUCH_PRESSED, |
410 last_unused_finger_event_->location(), | 449 last_unused_finger_event_->location(), |
411 last_unused_finger_event_->touch_id(), | 450 last_unused_finger_event_->touch_id(), |
412 event.time_stamp())); | 451 event.time_stamp())); |
413 DispatchEvent(first_finger_press.get()); | 452 DispatchEvent(first_finger_press.get()); |
414 rewritten_event->reset(new ui::TouchEvent(ui::ET_TOUCH_PRESSED, | 453 rewritten_event->reset(new ui::TouchEvent(ui::ET_TOUCH_PRESSED, |
415 event.location(), | 454 event.location(), |
(...skipping 18 matching lines...) Expand all Loading... | |
434 event.touch_id(), | 473 event.touch_id(), |
435 event.time_stamp())); | 474 event.time_stamp())); |
436 (*rewritten_event)->set_flags(event.flags()); | 475 (*rewritten_event)->set_flags(event.flags()); |
437 return ui::EVENT_REWRITE_REWRITTEN; | 476 return ui::EVENT_REWRITE_REWRITTEN; |
438 } | 477 } |
439 } | 478 } |
440 NOTREACHED() << "Unexpected event type received: " << event.name(); | 479 NOTREACHED() << "Unexpected event type received: " << event.name(); |
441 return ui::EVENT_REWRITE_CONTINUE; | 480 return ui::EVENT_REWRITE_CONTINUE; |
442 } | 481 } |
443 | 482 |
483 ui::EventRewriteStatus TouchExplorationController::InCornerPassthrough( | |
484 const ui::TouchEvent& event, | |
485 scoped_ptr<ui::Event>* rewritten_event) { | |
486 ui::EventType type = event.type(); | |
487 if (current_touch_ids_.size() == 0) { | |
488 ResetToNoFingersDown(); | |
489 } | |
490 | |
491 if (event.touch_id() == initial_press_->touch_id()) { | |
aboxhall
2014/07/24 18:26:34
Some comments explaining this block would be helpf
lisayin
2014/07/25 20:11:57
Done.
| |
492 int edge = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge); | |
aboxhall
2014/07/24 18:26:34
suggestion: s/edge/edges/
lisayin
2014/07/25 20:11:57
Done.
| |
493 bool in_a_bottom_corner = (edge == BOTTOM_LEFT_CORNER) || | |
494 (edge == BOTTOM_RIGHT_CORNER); | |
495 if (type == ui::ET_TOUCH_MOVED && in_a_bottom_corner) | |
496 return ui::EVENT_REWRITE_DISCARD; | |
497 | |
498 waiting_for_corner_passthrough_ = false; | |
499 state_ = WAIT_FOR_RELEASE; | |
500 return ui::EVENT_REWRITE_DISCARD; | |
501 } | |
502 | |
503 rewritten_event->reset(new ui::TouchEvent( | |
504 type, event.location(), event.touch_id(), event.time_stamp())); | |
505 (*rewritten_event)->set_flags(event.flags()); | |
506 | |
507 return ui::EVENT_REWRITE_REWRITTEN; | |
508 } | |
509 | |
444 ui::EventRewriteStatus TouchExplorationController::InPassthrough( | 510 ui::EventRewriteStatus TouchExplorationController::InPassthrough( |
445 const ui::TouchEvent& event, | 511 const ui::TouchEvent& event, |
446 scoped_ptr<ui::Event>* rewritten_event) { | 512 scoped_ptr<ui::Event>* rewritten_event) { |
447 ui::EventType type = event.type(); | 513 ui::EventType type = event.type(); |
448 | 514 |
449 if (!(type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED || | 515 if (!(type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED || |
450 type == ui::ET_TOUCH_MOVED || type == ui::ET_TOUCH_PRESSED)) { | 516 type == ui::ET_TOUCH_MOVED || type == ui::ET_TOUCH_PRESSED)) { |
451 NOTREACHED() << "Unexpected event type received: " << event.name(); | 517 NOTREACHED() << "Unexpected event type received: " << event.name(); |
452 return ui::EVENT_REWRITE_CONTINUE; | 518 return ui::EVENT_REWRITE_CONTINUE; |
453 } | 519 } |
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
579 if (current_touch_ids_.size() == 0) | 645 if (current_touch_ids_.size() == 0) |
580 ResetToNoFingersDown(); | 646 ResetToNoFingersDown(); |
581 return ui::EVENT_REWRITE_DISCARD; | 647 return ui::EVENT_REWRITE_DISCARD; |
582 } | 648 } |
583 | 649 |
584 ProcessGestureEvents(); | 650 ProcessGestureEvents(); |
585 return ui::EVENT_REWRITE_DISCARD; | 651 return ui::EVENT_REWRITE_DISCARD; |
586 } | 652 } |
587 | 653 |
588 void TouchExplorationController::OnTapTimerFired() { | 654 void TouchExplorationController::OnTapTimerFired() { |
655 if (waiting_for_corner_passthrough_) { | |
656 delegate_->PlayEarCon(); | |
657 delete gesture_provider_.GetAndResetPendingGestures(); | |
658 state_ = CORNER_PASSTHROUGH; | |
659 VLOG_STATE(); | |
660 return; | |
661 } | |
662 | |
589 switch (state_) { | 663 switch (state_) { |
590 case SINGLE_TAP_RELEASED: | 664 case SINGLE_TAP_RELEASED: |
591 ResetToNoFingersDown(); | 665 ResetToNoFingersDown(); |
592 break; | 666 break; |
593 case TOUCH_EXPLORE_RELEASED: | 667 case TOUCH_EXPLORE_RELEASED: |
594 ResetToNoFingersDown(); | 668 ResetToNoFingersDown(); |
595 last_touch_exploration_.reset(new TouchEvent(*initial_press_)); | 669 last_touch_exploration_.reset(new TouchEvent(*initial_press_)); |
596 return; | 670 return; |
597 case SINGLE_TAP_PRESSED: | 671 case SINGLE_TAP_PRESSED: |
598 case GESTURE_IN_PROGRESS: | 672 case GESTURE_IN_PROGRESS: |
(...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
798 void TouchExplorationController::EnterTouchToMouseMode() { | 872 void TouchExplorationController::EnterTouchToMouseMode() { |
799 aura::client::CursorClient* cursor_client = | 873 aura::client::CursorClient* cursor_client = |
800 aura::client::GetCursorClient(root_window_); | 874 aura::client::GetCursorClient(root_window_); |
801 if (cursor_client && !cursor_client->IsMouseEventsEnabled()) | 875 if (cursor_client && !cursor_client->IsMouseEventsEnabled()) |
802 cursor_client->EnableMouseEvents(); | 876 cursor_client->EnableMouseEvents(); |
803 if (cursor_client && cursor_client->IsCursorVisible()) | 877 if (cursor_client && cursor_client->IsCursorVisible()) |
804 cursor_client->HideCursor(); | 878 cursor_client->HideCursor(); |
805 } | 879 } |
806 | 880 |
807 void TouchExplorationController::ResetToNoFingersDown() { | 881 void TouchExplorationController::ResetToNoFingersDown() { |
882 waiting_for_corner_passthrough_ = false; | |
808 ProcessGestureEvents(); | 883 ProcessGestureEvents(); |
809 if (sound_timer_.IsRunning()) | 884 if (sound_timer_.IsRunning()) |
810 sound_timer_.Stop(); | 885 sound_timer_.Stop(); |
811 state_ = NO_FINGERS_DOWN; | 886 state_ = NO_FINGERS_DOWN; |
812 VLOG_STATE(); | 887 VLOG_STATE(); |
813 if (tap_timer_.IsRunning()) | 888 if (tap_timer_.IsRunning()) |
814 tap_timer_.Stop(); | 889 tap_timer_.Stop(); |
815 } | 890 } |
816 | 891 |
817 void TouchExplorationController::VlogState(const char* function_name) { | 892 void TouchExplorationController::VlogState(const char* function_name) { |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
868 case DOUBLE_TAP_PRESSED: | 943 case DOUBLE_TAP_PRESSED: |
869 return "DOUBLE_TAP_PRESSED"; | 944 return "DOUBLE_TAP_PRESSED"; |
870 case TOUCH_EXPLORATION: | 945 case TOUCH_EXPLORATION: |
871 return "TOUCH_EXPLORATION"; | 946 return "TOUCH_EXPLORATION"; |
872 case GESTURE_IN_PROGRESS: | 947 case GESTURE_IN_PROGRESS: |
873 return "GESTURE_IN_PROGRESS"; | 948 return "GESTURE_IN_PROGRESS"; |
874 case TOUCH_EXPLORE_SECOND_PRESS: | 949 case TOUCH_EXPLORE_SECOND_PRESS: |
875 return "TOUCH_EXPLORE_SECOND_PRESS"; | 950 return "TOUCH_EXPLORE_SECOND_PRESS"; |
876 case TWO_TO_ONE_FINGER: | 951 case TWO_TO_ONE_FINGER: |
877 return "TWO_TO_ONE_FINGER"; | 952 return "TWO_TO_ONE_FINGER"; |
953 case CORNER_PASSTHROUGH: | |
954 return "CORNER_PASSTHROUGH"; | |
878 case PASSTHROUGH: | 955 case PASSTHROUGH: |
879 return "PASSTHROUGH"; | 956 return "PASSTHROUGH"; |
880 case WAIT_FOR_RELEASE: | 957 case WAIT_FOR_RELEASE: |
881 return "WAIT_FOR_RELEASE"; | 958 return "WAIT_FOR_RELEASE"; |
882 case SLIDE_GESTURE: | 959 case SLIDE_GESTURE: |
883 return "SLIDE_GESTURE"; | 960 return "SLIDE_GESTURE"; |
884 } | 961 } |
885 return "Not a state"; | 962 return "Not a state"; |
886 } | 963 } |
887 | 964 |
888 } // namespace ui | 965 } // namespace ui |
OLD | NEW |