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

Side by Side Diff: ui/events/ozone/evdev/touch_event_converter_evdev.cc

Issue 2263693003: Add palm suppression feature to EventConverterEvdev (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@stylus
Patch Set: simplified cancelation logic Created 4 years, 3 months 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 // 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/events/ozone/evdev/touch_event_converter_evdev.h" 5 #include "ui/events/ozone/evdev/touch_event_converter_evdev.h"
6 6
7 #include <errno.h> 7 #include <errno.h>
8 #include <fcntl.h> 8 #include <fcntl.h>
9 #include <linux/input.h> 9 #include <linux/input.h>
10 #include <poll.h> 10 #include <poll.h>
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after
119 touch_noise_finder_.reset(new TouchNoiseFinder); 119 touch_noise_finder_.reset(new TouchNoiseFinder);
120 } 120 }
121 touch_evdev_debug_buffer_.Initialize(devinfo); 121 touch_evdev_debug_buffer_.Initialize(devinfo);
122 } 122 }
123 123
124 TouchEventConverterEvdev::~TouchEventConverterEvdev() { 124 TouchEventConverterEvdev::~TouchEventConverterEvdev() {
125 } 125 }
126 126
127 void TouchEventConverterEvdev::Initialize(const EventDeviceInfo& info) { 127 void TouchEventConverterEvdev::Initialize(const EventDeviceInfo& info) {
128 has_mt_ = info.HasMultitouch(); 128 has_mt_ = info.HasMultitouch();
129 has_pen_ = info.HasKeyEvent(BTN_TOOL_PEN);
129 130
130 if (has_mt_) { 131 if (has_mt_) {
131 pressure_min_ = info.GetAbsMinimum(ABS_MT_PRESSURE); 132 pressure_min_ = info.GetAbsMinimum(ABS_MT_PRESSURE);
132 pressure_max_ = info.GetAbsMaximum(ABS_MT_PRESSURE); 133 pressure_max_ = info.GetAbsMaximum(ABS_MT_PRESSURE);
133 x_min_tuxels_ = info.GetAbsMinimum(ABS_MT_POSITION_X); 134 x_min_tuxels_ = info.GetAbsMinimum(ABS_MT_POSITION_X);
134 x_num_tuxels_ = info.GetAbsMaximum(ABS_MT_POSITION_X) - x_min_tuxels_ + 1; 135 x_num_tuxels_ = info.GetAbsMaximum(ABS_MT_POSITION_X) - x_min_tuxels_ + 1;
135 y_min_tuxels_ = info.GetAbsMinimum(ABS_MT_POSITION_Y); 136 y_min_tuxels_ = info.GetAbsMinimum(ABS_MT_POSITION_Y);
136 y_num_tuxels_ = info.GetAbsMaximum(ABS_MT_POSITION_Y) - y_min_tuxels_ + 1; 137 y_num_tuxels_ = info.GetAbsMaximum(ABS_MT_POSITION_Y) - y_min_tuxels_ + 1;
137 touch_points_ = 138 touch_points_ =
138 std::min<int>(info.GetAbsMaximum(ABS_MT_SLOT) + 1, kNumTouchEvdevSlots); 139 std::min<int>(info.GetAbsMaximum(ABS_MT_SLOT) + 1, kNumTouchEvdevSlots);
140 major_max_ = info.GetAbsMaximum(ABS_MT_TOUCH_MAJOR);
139 current_slot_ = info.GetAbsValue(ABS_MT_SLOT); 141 current_slot_ = info.GetAbsValue(ABS_MT_SLOT);
140 } else { 142 } else {
141 pressure_min_ = info.GetAbsMinimum(ABS_PRESSURE); 143 pressure_min_ = info.GetAbsMinimum(ABS_PRESSURE);
142 pressure_max_ = info.GetAbsMaximum(ABS_PRESSURE); 144 pressure_max_ = info.GetAbsMaximum(ABS_PRESSURE);
143 x_min_tuxels_ = info.GetAbsMinimum(ABS_X); 145 x_min_tuxels_ = info.GetAbsMinimum(ABS_X);
144 x_num_tuxels_ = info.GetAbsMaximum(ABS_X) - x_min_tuxels_ + 1; 146 x_num_tuxels_ = info.GetAbsMaximum(ABS_X) - x_min_tuxels_ + 1;
145 y_min_tuxels_ = info.GetAbsMinimum(ABS_Y); 147 y_min_tuxels_ = info.GetAbsMinimum(ABS_Y);
146 y_num_tuxels_ = info.GetAbsMaximum(ABS_Y) - y_min_tuxels_ + 1; 148 y_num_tuxels_ = info.GetAbsMaximum(ABS_Y) - y_min_tuxels_ + 1;
147 touch_points_ = 1; 149 touch_points_ = 1;
150 major_max_ = 0;
148 current_slot_ = 0; 151 current_slot_ = 0;
149 } 152 }
150 153
151 quirk_left_mouse_button_ = 154 quirk_left_mouse_button_ =
152 !has_mt_ && !info.HasKeyEvent(BTN_TOUCH) && info.HasKeyEvent(BTN_LEFT); 155 !has_mt_ && !info.HasKeyEvent(BTN_TOUCH) && info.HasKeyEvent(BTN_LEFT);
153 156
154 // Apply --touch-calibration. 157 // Apply --touch-calibration.
155 if (type() == INPUT_DEVICE_INTERNAL) { 158 if (type() == INPUT_DEVICE_INTERNAL) {
156 TouchCalibration cal = {}; 159 TouchCalibration cal = {};
157 GetTouchCalibration(&cal); 160 GetTouchCalibration(&cal);
(...skipping 20 matching lines...) Expand all
178 ABS_MT_TRACKING_ID, i, kTrackingIdForUnusedSlot); 181 ABS_MT_TRACKING_ID, i, kTrackingIdForUnusedSlot);
179 events_[i].touching = (events_[i].tracking_id >= 0); 182 events_[i].touching = (events_[i].tracking_id >= 0);
180 events_[i].slot = i; 183 events_[i].slot = i;
181 184
182 // Dirty the slot so we'll update the consumer at the first opportunity. 185 // Dirty the slot so we'll update the consumer at the first opportunity.
183 // We can't dispatch here as this is currently called on the worker pool. 186 // We can't dispatch here as this is currently called on the worker pool.
184 // TODO(spang): Move initialization off worker pool. 187 // TODO(spang): Move initialization off worker pool.
185 events_[i].altered = true; 188 events_[i].altered = true;
186 189
187 // Optional bits. 190 // Optional bits.
188 events_[i].radius_x = 191 int touch_major =
189 info.GetAbsMtSlotValueWithDefault(ABS_MT_TOUCH_MAJOR, i, 0) / 2.0f; 192 info.GetAbsMtSlotValueWithDefault(ABS_MT_TOUCH_MAJOR, i, 0) / 2.0f;
193 events_[i].radius_x = touch_major / 2.0f;
190 events_[i].radius_y = 194 events_[i].radius_y =
191 info.GetAbsMtSlotValueWithDefault(ABS_MT_TOUCH_MINOR, i, 0) / 2.0f; 195 info.GetAbsMtSlotValueWithDefault(ABS_MT_TOUCH_MINOR, i, 0) / 2.0f;
192 events_[i].pressure = ScalePressure( 196 events_[i].pressure = ScalePressure(
193 info.GetAbsMtSlotValueWithDefault(ABS_MT_PRESSURE, i, 0)); 197 info.GetAbsMtSlotValueWithDefault(ABS_MT_PRESSURE, i, 0));
198 events_[i].cancelled = (major_max_ > 0 && touch_major == major_max_);
194 } 199 }
195 } else { 200 } else {
196 // TODO(spang): Add key state to EventDeviceInfo to allow initial contact. 201 // TODO(spang): Add key state to EventDeviceInfo to allow initial contact.
197 // (and make sure to take into account quirk_left_mouse_button_) 202 // (and make sure to take into account quirk_left_mouse_button_)
198 events_[0].x = 0; 203 events_[0].x = 0;
199 events_[0].y = 0; 204 events_[0].y = 0;
200 events_[0].tracking_id = kTrackingIdForUnusedSlot; 205 events_[0].tracking_id = kTrackingIdForUnusedSlot;
201 events_[0].touching = false; 206 events_[0].touching = false;
202 events_[0].slot = 0; 207 events_[0].slot = 0;
203 events_[0].radius_x = 0; 208 events_[0].radius_x = 0;
204 events_[0].radius_y = 0; 209 events_[0].radius_y = 0;
205 events_[0].pressure = 0; 210 events_[0].pressure = 0;
206 events_[0].tool_code = 0; 211 events_[0].tool_code = 0;
212 events_[0].cancelled = false;
207 } 213 }
208 } 214 }
209 215
210 void TouchEventConverterEvdev::Reinitialize() { 216 void TouchEventConverterEvdev::Reinitialize() {
211 ReleaseButtons(); 217 ReleaseButtons();
212 218
213 EventDeviceInfo info; 219 EventDeviceInfo info;
214 if (!info.Initialize(fd_, path_)) { 220 if (!info.Initialize(fd_, path_)) {
215 LOG(ERROR) << "Failed to synchronize state for touch device: " 221 LOG(ERROR) << "Failed to synchronize state for touch device: "
216 << path_.value(); 222 << path_.value();
217 Stop(); 223 Stop();
218 return; 224 return;
219 } 225 }
220 Initialize(info); 226 Initialize(info);
221 } 227 }
222 228
223 bool TouchEventConverterEvdev::HasTouchscreen() const { 229 bool TouchEventConverterEvdev::HasTouchscreen() const {
224 return true; 230 return true;
225 } 231 }
226 232
233 bool TouchEventConverterEvdev::HasPen() const {
234 return has_pen_;
235 }
236
227 gfx::Size TouchEventConverterEvdev::GetTouchscreenSize() const { 237 gfx::Size TouchEventConverterEvdev::GetTouchscreenSize() const {
228 return gfx::Size(x_num_tuxels_, y_num_tuxels_); 238 return gfx::Size(x_num_tuxels_, y_num_tuxels_);
229 } 239 }
230 240
231 int TouchEventConverterEvdev::GetTouchPoints() const { 241 int TouchEventConverterEvdev::GetTouchPoints() const {
232 return touch_points_; 242 return touch_points_;
233 } 243 }
234 244
235 void TouchEventConverterEvdev::OnEnabled() { 245 void TouchEventConverterEvdev::OnEnabled() {
236 ReportEvents(EventTimeForNow());
237 } 246 }
238 247
239 void TouchEventConverterEvdev::OnDisabled() { 248 void TouchEventConverterEvdev::OnDisabled() {
240 ReleaseTouches(); 249 ReleaseTouches();
241 ReleaseButtons(); 250 ReleaseButtons();
251 if (enable_palm_suppression_callback_) {
252 enable_palm_suppression_callback_.Run(false);
253 }
242 } 254 }
243 255
244 void TouchEventConverterEvdev::OnFileCanReadWithoutBlocking(int fd) { 256 void TouchEventConverterEvdev::OnFileCanReadWithoutBlocking(int fd) {
245 TRACE_EVENT1("evdev", 257 TRACE_EVENT1("evdev",
246 "TouchEventConverterEvdev::OnFileCanReadWithoutBlocking", "fd", 258 "TouchEventConverterEvdev::OnFileCanReadWithoutBlocking", "fd",
247 fd); 259 fd);
248 260
249 input_event inputs[kNumTouchEvdevSlots * 6 + 1]; 261 input_event inputs[kNumTouchEvdevSlots * 6 + 1];
250 ssize_t read_size = read(fd, inputs, sizeof(inputs)); 262 ssize_t read_size = read(fd, inputs, sizeof(inputs));
251 if (read_size < 0) { 263 if (read_size < 0) {
252 if (errno == EINTR || errno == EAGAIN) 264 if (errno == EINTR || errno == EAGAIN)
253 return; 265 return;
254 if (errno != ENODEV) 266 if (errno != ENODEV)
255 PLOG(ERROR) << "error reading device " << path_.value(); 267 PLOG(ERROR) << "error reading device " << path_.value();
256 Stop(); 268 Stop();
257 return; 269 return;
258 } 270 }
259 271
260 if (!enabled_) {
261 dropped_events_ = true;
262 return;
263 }
264
265 for (unsigned i = 0; i < read_size / sizeof(*inputs); i++) { 272 for (unsigned i = 0; i < read_size / sizeof(*inputs); i++) {
266 if (!has_mt_) { 273 if (!has_mt_) {
267 // Emulate the device as an MT device with only 1 slot by inserting extra 274 // Emulate the device as an MT device with only 1 slot by inserting extra
268 // MT protocol events in the stream. 275 // MT protocol events in the stream.
269 EmulateMultitouchEvent(inputs[i]); 276 EmulateMultitouchEvent(inputs[i]);
270 } 277 }
271 278
272 ProcessMultitouchEvent(inputs[i]); 279 ProcessMultitouchEvent(inputs[i]);
273 } 280 }
274 } 281 }
275 282
276 void TouchEventConverterEvdev::DumpTouchEventLog(const char* filename) { 283 void TouchEventConverterEvdev::DumpTouchEventLog(const char* filename) {
277 touch_evdev_debug_buffer_.DumpLog(filename); 284 touch_evdev_debug_buffer_.DumpLog(filename);
278 } 285 }
279 286
280 void TouchEventConverterEvdev::SetTouchEventLoggingEnabled(bool enabled) { 287 void TouchEventConverterEvdev::SetTouchEventLoggingEnabled(bool enabled) {
281 touch_logging_enabled_ = enabled; 288 touch_logging_enabled_ = enabled;
282 } 289 }
283 290
291 void TouchEventConverterEvdev::SetPalmSuppressionCallback(
292 const base::Callback<void(bool)>& callback) {
293 enable_palm_suppression_callback_ = callback;
294 }
295
284 void TouchEventConverterEvdev::ProcessMultitouchEvent( 296 void TouchEventConverterEvdev::ProcessMultitouchEvent(
285 const input_event& input) { 297 const input_event& input) {
286 if (touch_logging_enabled_) 298 if (touch_logging_enabled_)
287 touch_evdev_debug_buffer_.ProcessEvent(current_slot_, &input); 299 touch_evdev_debug_buffer_.ProcessEvent(current_slot_, &input);
288 300
289 if (input.type == EV_SYN) { 301 if (input.type == EV_SYN) {
290 ProcessSyn(input); 302 ProcessSyn(input);
291 } else if (dropped_events_) { 303 } else if (dropped_events_) {
292 // Do nothing. This branch indicates we have lost sync with the driver. 304 // Do nothing. This branch indicates we have lost sync with the driver.
293 } else if (input.type == EV_ABS) { 305 } else if (input.type == EV_ABS) {
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after
345 // Do not change tool types while touching to prevent inconsistencies 357 // Do not change tool types while touching to prevent inconsistencies
346 // from switching between Mouse and TouchEvents. 358 // from switching between Mouse and TouchEvents.
347 if (events_[current_slot_].was_touching) 359 if (events_[current_slot_].was_touching)
348 break; 360 break;
349 361
350 if (input.value > 0) { 362 if (input.value > 0) {
351 events_[current_slot_].tool_code = input.code; 363 events_[current_slot_].tool_code = input.code;
352 } else { 364 } else {
353 events_[current_slot_].tool_code = 0; 365 events_[current_slot_].tool_code = 0;
354 } 366 }
367 events_[current_slot_].altered = true;
355 break; 368 break;
356 default: 369 default:
357 NOTIMPLEMENTED() << "invalid code for EV_KEY: " << input.code; 370 NOTIMPLEMENTED() << "invalid code for EV_KEY: " << input.code;
358 } 371 }
359 } 372 }
360 373
361 void TouchEventConverterEvdev::ProcessAbs(const input_event& input) { 374 void TouchEventConverterEvdev::ProcessAbs(const input_event& input) {
362 switch (input.code) { 375 switch (input.code) {
363 case ABS_MT_TOUCH_MAJOR: 376 case ABS_MT_TOUCH_MAJOR:
364 // TODO(spang): If we have all of major, minor, and orientation, 377 // TODO(spang): If we have all of major, minor, and orientation,
365 // we can scale the ellipse correctly. However on the Pixel we get 378 // we can scale the ellipse correctly. However on the Pixel we get
366 // neither minor nor orientation, so this is all we can do. 379 // neither minor nor orientation, so this is all we can do.
367 events_[current_slot_].radius_x = input.value / 2.0f; 380 events_[current_slot_].radius_x = input.value / 2.0f;
381
382 // The MT protocol cannot communicate cancelled touches, so some kernel
383 // drivers will identify palms by setting touch major to max.
384 if (major_max_ > 0 && input.value == major_max_)
385 events_[current_slot_].cancelled = true;
368 break; 386 break;
369 case ABS_MT_TOUCH_MINOR: 387 case ABS_MT_TOUCH_MINOR:
370 events_[current_slot_].radius_y = input.value / 2.0f; 388 events_[current_slot_].radius_y = input.value / 2.0f;
371 break; 389 break;
372 case ABS_MT_POSITION_X: 390 case ABS_MT_POSITION_X:
373 events_[current_slot_].x = input.value; 391 events_[current_slot_].x = input.value;
374 break; 392 break;
375 case ABS_MT_POSITION_Y: 393 case ABS_MT_POSITION_Y:
376 events_[current_slot_].y = input.value; 394 events_[current_slot_].y = input.value;
377 break; 395 break;
(...skipping 29 matching lines...) Expand all
407 // including the next SYN_REPORT. 425 // including the next SYN_REPORT.
408 dropped_events_ = true; 426 dropped_events_ = true;
409 break; 427 break;
410 default: 428 default:
411 NOTIMPLEMENTED() << "invalid code for EV_SYN: " << input.code; 429 NOTIMPLEMENTED() << "invalid code for EV_SYN: " << input.code;
412 } 430 }
413 } 431 }
414 432
415 EventType TouchEventConverterEvdev::GetEventTypeForTouch( 433 EventType TouchEventConverterEvdev::GetEventTypeForTouch(
416 const InProgressTouchEvdev& touch) { 434 const InProgressTouchEvdev& touch) {
417 if (touch.cancelled) 435 if (touch.was_cancelled)
418 return ET_UNKNOWN; 436 return ET_UNKNOWN;
419 437
420 if (touch_noise_finder_ && touch_noise_finder_->SlotHasNoise(touch.slot)) { 438 if (touch.cancelled)
421 if (touch.touching && !touch.was_touching) 439 return touch.was_touching ? ET_TOUCH_CANCELLED : ET_UNKNOWN;
422 return ET_UNKNOWN;
423 return ET_TOUCH_CANCELLED;
424 }
425 440
426 if (touch.touching) 441 if (touch.touching)
427 return touch.was_touching ? ET_TOUCH_MOVED : ET_TOUCH_PRESSED; 442 return touch.was_touching ? ET_TOUCH_MOVED : ET_TOUCH_PRESSED;
428 return touch.was_touching ? ET_TOUCH_RELEASED : ET_UNKNOWN; 443 return touch.was_touching ? ET_TOUCH_RELEASED : ET_UNKNOWN;
429 } 444 }
430 445
431 void TouchEventConverterEvdev::ReportTouchEvent( 446 void TouchEventConverterEvdev::ReportTouchEvent(
432 const InProgressTouchEvdev& event, 447 const InProgressTouchEvdev& event,
433 EventType event_type, 448 EventType event_type,
434 base::TimeTicks timestamp) { 449 base::TimeTicks timestamp) {
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
468 } 483 }
469 484
470 if (touch_noise_finder_) 485 if (touch_noise_finder_)
471 touch_noise_finder_->HandleTouches(events_, timestamp); 486 touch_noise_finder_->HandleTouches(events_, timestamp);
472 487
473 for (size_t i = 0; i < events_.size(); i++) { 488 for (size_t i = 0; i < events_.size(); i++) {
474 InProgressTouchEvdev* event = &events_[i]; 489 InProgressTouchEvdev* event = &events_[i];
475 if (!event->altered) 490 if (!event->altered)
476 continue; 491 continue;
477 492
493 if (enable_palm_suppression_callback_)
494 enable_palm_suppression_callback_.Run(event->tool_code > 0);
495
496 if (touch_noise_finder_ && touch_noise_finder_->SlotHasNoise(event->slot))
497 event->cancelled = true;
498
478 if (event->tool_code > 0) { 499 if (event->tool_code > 0) {
479 ReportStylusEvent(*event, timestamp); 500 ReportStylusEvent(*event, timestamp);
480 } else { 501 } else {
481 EventType event_type = GetEventTypeForTouch(*event); 502 EventType event_type = GetEventTypeForTouch(*event);
482 if (event_type == ET_UNKNOWN || event_type == ET_TOUCH_CANCELLED)
483 event->cancelled = true;
484
485 if (event_type != ET_UNKNOWN) 503 if (event_type != ET_UNKNOWN)
486 ReportTouchEvent(*event, event_type, timestamp); 504 ReportTouchEvent(*event, event_type, timestamp);
487 } 505 }
488 506
507 event->was_cancelled = event->cancelled;
489 event->was_touching = event->touching; 508 event->was_touching = event->touching;
490 event->altered = false; 509 event->altered = false;
491 event->btn_left.changed = false; 510 event->btn_left.changed = false;
492 event->btn_right.changed = false; 511 event->btn_right.changed = false;
493 event->btn_middle.changed = false; 512 event->btn_middle.changed = false;
494 } 513 }
495 } 514 }
496 515
497 void TouchEventConverterEvdev::UpdateTrackingId(int slot, int tracking_id) { 516 void TouchEventConverterEvdev::UpdateTrackingId(int slot, int tracking_id) {
498 InProgressTouchEvdev* event = &events_[slot]; 517 InProgressTouchEvdev* event = &events_[slot];
499 518
500 if (event->tracking_id == tracking_id) 519 if (event->tracking_id == tracking_id)
501 return; 520 return;
502 521
503 event->tracking_id = tracking_id; 522 event->tracking_id = tracking_id;
504 event->touching = (tracking_id >= 0); 523 event->touching = (tracking_id >= 0);
505 event->altered = true; 524 event->altered = true;
506 525
507 if (tracking_id >= 0) 526 if (tracking_id >= 0) {
508 event->cancelled = false; 527 event->was_cancelled = false;
528 event->cancelled = !enabled_;
529 }
509 } 530 }
510 531
511 void TouchEventConverterEvdev::ReleaseTouches() { 532 void TouchEventConverterEvdev::ReleaseTouches() {
512 for (size_t slot = 0; slot < events_.size(); slot++) 533 for (size_t slot = 0; slot < events_.size(); slot++)
513 UpdateTrackingId(slot, kTrackingIdForUnusedSlot); 534 events_[slot].cancelled = true;
514 535
515 ReportEvents(EventTimeForNow()); 536 ReportEvents(EventTimeForNow());
516 } 537 }
517 538
518 void TouchEventConverterEvdev::ReleaseButtons() { 539 void TouchEventConverterEvdev::ReleaseButtons() {
519 for (size_t slot = 0; slot < events_.size(); slot++) { 540 for (size_t slot = 0; slot < events_.size(); slot++) {
520 InProgressTouchEvdev* event = &events_[slot]; 541 InProgressTouchEvdev* event = &events_[slot];
521 542
522 if (event->btn_left.down) { 543 if (event->btn_left.down) {
523 event->btn_left.down = false; 544 event->btn_left.down = false;
(...skipping 17 matching lines...) Expand all
541 if (pressure_max_ - pressure_min_) 562 if (pressure_max_ - pressure_min_)
542 pressure /= pressure_max_ - pressure_min_; 563 pressure /= pressure_max_ - pressure_min_;
543 return pressure; 564 return pressure;
544 } 565 }
545 566
546 int TouchEventConverterEvdev::NextTrackingId() { 567 int TouchEventConverterEvdev::NextTrackingId() {
547 return next_tracking_id_++ & kMaxTrackingId; 568 return next_tracking_id_++ & kMaxTrackingId;
548 } 569 }
549 570
550 } // namespace ui 571 } // namespace ui
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698