Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 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 "views/touchui/touch_factory.h" | 5 #include "views/touchui/touch_factory.h" |
| 6 | 6 |
| 7 #include <gtk/gtk.h> | 7 #include <gtk/gtk.h> |
| 8 #include <gdk/gdkx.h> | 8 #include <gdk/gdkx.h> |
| 9 #include <X11/cursorfont.h> | 9 #include <X11/cursorfont.h> |
| 10 #include <X11/extensions/XInput.h> | 10 #include <X11/extensions/XInput.h> |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 22 // The X cursor is hidden if it is idle for kCursorIdleSeconds seconds. | 22 // The X cursor is hidden if it is idle for kCursorIdleSeconds seconds. |
| 23 int kCursorIdleSeconds = 5; | 23 int kCursorIdleSeconds = 5; |
| 24 | 24 |
| 25 // Given the TouchParam, return the correspoding XIValuatorClassInfo using | 25 // Given the TouchParam, return the correspoding XIValuatorClassInfo using |
| 26 // the X device information through Atom name matching. | 26 // the X device information through Atom name matching. |
| 27 XIValuatorClassInfo* FindTPValuator(Display* display, | 27 XIValuatorClassInfo* FindTPValuator(Display* display, |
| 28 XIDeviceInfo* info, | 28 XIDeviceInfo* info, |
| 29 views::TouchFactory::TouchParam tp) { | 29 views::TouchFactory::TouchParam tp) { |
| 30 // Lookup table for mapping TouchParam to Atom string used in X. | 30 // Lookup table for mapping TouchParam to Atom string used in X. |
| 31 // A full set of Atom strings can be found at xserver-properties.h. | 31 // A full set of Atom strings can be found at xserver-properties.h. |
| 32 // For Slot ID, See this chromeos revision: http://git.chromium.org/gitweb/? | |
| 33 // p=chromiumos/overlays/chromiumos-overlay.git; | |
| 34 // a=commit;h=9164d0a75e48c4867e4ef4ab51f743ae231c059a | |
| 35 static struct { | 32 static struct { |
| 36 views::TouchFactory::TouchParam tp; | 33 views::TouchFactory::TouchParam tp; |
| 37 const char* atom; | 34 const char* atom; |
| 38 } kTouchParamAtom[] = { | 35 } kTouchParamAtom[] = { |
| 39 { views::TouchFactory::TP_TOUCH_MAJOR, "Abs MT Touch Major" }, | 36 { views::TouchFactory::TP_TOUCH_MAJOR, "Abs MT Touch Major" }, |
| 40 { views::TouchFactory::TP_TOUCH_MINOR, "Abs MT Touch Minor" }, | 37 { views::TouchFactory::TP_TOUCH_MINOR, "Abs MT Touch Minor" }, |
| 41 { views::TouchFactory::TP_ORIENTATION, "Abs MT Orientation" }, | 38 { views::TouchFactory::TP_ORIENTATION, "Abs MT Orientation" }, |
| 42 { views::TouchFactory::TP_PRESSURE, "Abs MT Pressure" }, | 39 { views::TouchFactory::TP_PRESSURE, "Abs MT Pressure" }, |
| 40 #if !defined(USE_XI2_MT) | |
| 41 // For Slot ID, See this chromeos revision: http://git.chromium.org/gitweb/? | |
| 42 // p=chromiumos/overlays/chromiumos-overlay.git; | |
| 43 // a=commit;h=9164d0a75e48c4867e4ef4ab51f743ae231c059a | |
| 43 { views::TouchFactory::TP_SLOT_ID, "Abs MT Slot ID" }, | 44 { views::TouchFactory::TP_SLOT_ID, "Abs MT Slot ID" }, |
| 45 #endif | |
| 44 { views::TouchFactory::TP_TRACKING_ID, "Abs MT Tracking ID" }, | 46 { views::TouchFactory::TP_TRACKING_ID, "Abs MT Tracking ID" }, |
| 45 { views::TouchFactory::TP_LAST_ENTRY, NULL }, | 47 { views::TouchFactory::TP_LAST_ENTRY, NULL }, |
| 46 }; | 48 }; |
| 47 | 49 |
| 48 const char* atom_tp = NULL; | 50 const char* atom_tp = NULL; |
| 49 | 51 |
| 50 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTouchParamAtom); i++) { | 52 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTouchParamAtom); i++) { |
| 51 if (tp == kTouchParamAtom[i].tp) { | 53 if (tp == kTouchParamAtom[i].tp) { |
| 52 atom_tp = kTouchParamAtom[i].atom; | 54 atom_tp = kTouchParamAtom[i].atom; |
| 53 break; | 55 break; |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 120 // static | 122 // static |
| 121 TouchFactory* TouchFactory::GetInstance() { | 123 TouchFactory* TouchFactory::GetInstance() { |
| 122 return Singleton<TouchFactory>::get(); | 124 return Singleton<TouchFactory>::get(); |
| 123 } | 125 } |
| 124 | 126 |
| 125 TouchFactory::TouchFactory() | 127 TouchFactory::TouchFactory() |
| 126 : is_cursor_visible_(true), | 128 : is_cursor_visible_(true), |
| 127 keep_mouse_cursor_(false), | 129 keep_mouse_cursor_(false), |
| 128 cursor_timer_(), | 130 cursor_timer_(), |
| 129 pointer_device_lookup_(), | 131 pointer_device_lookup_(), |
| 132 #if defined(USE_XI2_MT) | |
| 133 touch_device_list_() { | |
| 134 #else | |
| 130 touch_device_list_(), | 135 touch_device_list_(), |
| 131 slots_used_() { | 136 slots_used_() { |
| 137 #endif | |
| 132 #if defined(TOUCH_UI) | 138 #if defined(TOUCH_UI) |
| 133 if (!base::MessagePumpForUI::HasXInput2()) | 139 if (!base::MessagePumpForUI::HasXInput2()) |
| 134 return; | 140 return; |
| 135 #endif | 141 #endif |
| 136 | 142 |
| 137 char nodata[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; | 143 char nodata[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; |
| 138 XColor black; | 144 XColor black; |
| 139 black.red = black.green = black.blue = 0; | 145 black.red = black.green = black.blue = 0; |
| 140 Display* display = ui::GetXDisplay(); | 146 Display* display = ui::GetXDisplay(); |
| 141 Pixmap blank = XCreateBitmapFromData(display, ui::GetX11RootWindow(), | 147 Pixmap blank = XCreateBitmapFromData(display, ui::GetX11RootWindow(), |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 183 void TouchFactory::UpdateDeviceList(Display* display) { | 189 void TouchFactory::UpdateDeviceList(Display* display) { |
| 184 // Detect touch devices. | 190 // Detect touch devices. |
| 185 // NOTE: The new API for retrieving the list of devices (XIQueryDevice) does | 191 // NOTE: The new API for retrieving the list of devices (XIQueryDevice) does |
| 186 // not provide enough information to detect a touch device. As a result, the | 192 // not provide enough information to detect a touch device. As a result, the |
| 187 // old version of query function (XListInputDevices) is used instead. | 193 // old version of query function (XListInputDevices) is used instead. |
| 188 // If XInput2 is not supported, this will return null (with count of -1) so | 194 // If XInput2 is not supported, this will return null (with count of -1) so |
| 189 // we assume there cannot be any touch devices. | 195 // we assume there cannot be any touch devices. |
| 190 int count = 0; | 196 int count = 0; |
| 191 touch_device_lookup_.reset(); | 197 touch_device_lookup_.reset(); |
| 192 touch_device_list_.clear(); | 198 touch_device_list_.clear(); |
| 199 #if !defined(USE_XI2_MT) | |
| 193 XDeviceInfo* devlist = XListInputDevices(display, &count); | 200 XDeviceInfo* devlist = XListInputDevices(display, &count); |
| 194 for (int i = 0; i < count; i++) { | 201 for (int i = 0; i < count; i++) { |
| 195 if (devlist[i].type) { | 202 if (devlist[i].type) { |
| 196 const char* devtype = XGetAtomName(display, devlist[i].type); | 203 const char* devtype = XGetAtomName(display, devlist[i].type); |
| 197 if (devtype && !strcmp(devtype, XI_TOUCHSCREEN)) { | 204 if (devtype && !strcmp(devtype, XI_TOUCHSCREEN)) { |
| 198 touch_device_lookup_[devlist[i].id] = true; | 205 touch_device_lookup_[devlist[i].id] = true; |
| 199 touch_device_list_.push_back(devlist[i].id); | 206 touch_device_list_.push_back(devlist[i].id); |
| 200 } | 207 } |
| 201 } | 208 } |
| 202 } | 209 } |
| 203 if (devlist) | 210 if (devlist) |
| 204 XFreeDeviceList(devlist); | 211 XFreeDeviceList(devlist); |
| 212 #endif | |
| 205 | 213 |
| 206 // Instead of asking X for the list of devices all the time, let's maintain a | 214 // Instead of asking X for the list of devices all the time, let's maintain a |
| 207 // list of pointer devices we care about. | 215 // list of pointer devices we care about. |
| 208 // It should not be necessary to select for slave devices. XInput2 provides | 216 // It should not be necessary to select for slave devices. XInput2 provides |
| 209 // enough information to the event callback to decide which slave device | 217 // enough information to the event callback to decide which slave device |
| 210 // triggered the event, thus decide whether the 'pointer event' is a | 218 // triggered the event, thus decide whether the 'pointer event' is a |
| 211 // 'mouse event' or a 'touch event'. | 219 // 'mouse event' or a 'touch event'. |
| 212 // However, on some desktops, some events from a master pointer are | 220 // However, on some desktops, some events from a master pointer are |
| 213 // not delivered to the client. So we select for slave devices instead. | 221 // not delivered to the client. So we select for slave devices instead. |
| 214 // If the touch device has 'GrabDevice' set and 'SendCoreEvents' unset (which | 222 // If the touch device has 'GrabDevice' set and 'SendCoreEvents' unset (which |
| 215 // is possible), then the device is detected as a floating device, and a | 223 // is possible), then the device is detected as a floating device, and a |
| 216 // floating device is not connected to a master device. So it is necessary to | 224 // floating device is not connected to a master device. So it is necessary to |
| 217 // also select on the floating devices. | 225 // also select on the floating devices. |
| 218 pointer_device_lookup_.reset(); | 226 pointer_device_lookup_.reset(); |
| 219 XIDeviceInfo* devices = XIQueryDevice(display, XIAllDevices, &count); | 227 XIDeviceInfo* devices = XIQueryDevice(display, XIAllDevices, &count); |
| 220 for (int i = 0; i < count; i++) { | 228 if (devices) { |
|
Daniel Kurtz
2011/09/09 14:26:18
This isn't needed. On error, devices = NULL, but
ningxin.hu
2011/09/10 17:09:45
Thanks for the comments. I will correct this.
| |
| 221 XIDeviceInfo* devinfo = devices + i; | 229 for (int i = 0; i < count; i++) { |
| 222 if (devinfo->use == XIFloatingSlave || devinfo->use == XISlavePointer) { | 230 if (!devices[i].enabled) |
| 223 pointer_device_lookup_[devinfo->deviceid] = true; | 231 continue; |
| 232 XIDeviceInfo* devinfo = devices + i; | |
|
Daniel Kurtz
2011/09/09 14:26:18
For consistency, assign devinfo first, then do:
ningxin.hu
2011/09/10 17:09:45
OK. I will follow up with a separated patch for th
| |
| 233 #if defined(USE_XI2_MT) | |
| 234 for (int k = 0; k < devinfo->num_classes; ++k) { | |
| 235 XIAnyClassInfo* xiclassinfo = devinfo->classes[k]; | |
| 236 if (xiclassinfo->type == XITouchClass) { | |
| 237 XITouchClassInfo* tci = (XITouchClassInfo *)xiclassinfo; | |
|
Daniel Kurtz
2011/09/09 14:26:18
reinterpret_cast<XITouchClassInfo*>xiclassinfo;
ningxin.hu
2011/09/10 17:09:45
I will correct this.
| |
| 238 // Only care direct touch device (such as touch screen) right now | |
| 239 if (tci->mode == XIDirectTouch) { | |
| 240 touch_device_lookup_[devinfo->deviceid] = true; | |
| 241 touch_device_list_.push_back(devinfo->deviceid); | |
| 242 } | |
| 243 } | |
| 244 } | |
| 245 #endif | |
| 246 if (devinfo->use == XIFloatingSlave || devinfo->use == XISlavePointer) | |
| 247 pointer_device_lookup_[devinfo->deviceid] = true; | |
| 224 } | 248 } |
| 225 } | 249 } |
| 226 if (devices) | 250 if (devices) |
| 227 XIFreeDeviceInfo(devices); | 251 XIFreeDeviceInfo(devices); |
| 228 | 252 |
| 229 SetupValuator(); | 253 SetupValuator(); |
| 230 } | 254 } |
| 231 | 255 |
| 232 bool TouchFactory::ShouldProcessXI2Event(XEvent* xev) { | 256 bool TouchFactory::ShouldProcessXI2Event(XEvent* xev) { |
| 233 DCHECK_EQ(GenericEvent, xev->type); | 257 DCHECK_EQ(GenericEvent, xev->type); |
| 258 XIEvent* event = static_cast<XIEvent*>(xev->xcookie.data); | |
| 259 XIDeviceEvent* xiev = reinterpret_cast<XIDeviceEvent*>(event); | |
| 234 | 260 |
| 235 XGenericEventCookie* cookie = &xev->xcookie; | 261 #if defined(USE_XI2_MT) |
| 236 if (cookie->evtype != XI_ButtonPress && | 262 if (event->evtype == XI_TouchBegin || |
| 237 cookie->evtype != XI_ButtonRelease && | 263 event->evtype == XI_TouchUpdate || |
| 238 cookie->evtype != XI_Motion) | 264 event->evtype == XI_TouchEnd) { |
| 265 return touch_device_lookup_[xiev->sourceid]; | |
| 266 } | |
| 267 #endif | |
| 268 if (event->evtype != XI_ButtonPress && | |
| 269 event->evtype != XI_ButtonRelease && | |
| 270 event->evtype != XI_Motion) | |
| 239 return true; | 271 return true; |
| 240 | 272 |
| 241 XIDeviceEvent* xiev = static_cast<XIDeviceEvent*>(cookie->data); | |
| 242 return pointer_device_lookup_[xiev->deviceid]; | 273 return pointer_device_lookup_[xiev->deviceid]; |
| 243 } | 274 } |
| 244 | 275 |
| 245 void TouchFactory::SetupXI2ForXWindow(Window window) { | 276 void TouchFactory::SetupXI2ForXWindow(Window window) { |
| 246 // Setup mask for mouse events. It is possible that a device is loaded/plugged | 277 // Setup mask for mouse events. It is possible that a device is loaded/plugged |
| 247 // in after we have setup XInput2 on a window. In such cases, we need to | 278 // in after we have setup XInput2 on a window. In such cases, we need to |
| 248 // either resetup XInput2 for the window, so that we get events from the new | 279 // either resetup XInput2 for the window, so that we get events from the new |
| 249 // device, or we need to listen to events from all devices, and then filter | 280 // device, or we need to listen to events from all devices, and then filter |
| 250 // the events from uninteresting devices. We do the latter because that's | 281 // the events from uninteresting devices. We do the latter because that's |
| 251 // simpler. | 282 // simpler. |
| 252 | 283 |
| 253 Display* display = ui::GetXDisplay(); | 284 Display* display = ui::GetXDisplay(); |
| 254 | 285 |
| 255 unsigned char mask[XIMaskLen(XI_LASTEVENT)]; | 286 unsigned char mask[XIMaskLen(XI_LASTEVENT)]; |
| 256 memset(mask, 0, sizeof(mask)); | 287 memset(mask, 0, sizeof(mask)); |
| 257 | 288 |
| 289 #if defined(USE_XI2_MT) | |
| 290 XISetMask(mask, XI_TouchBegin); | |
| 291 XISetMask(mask, XI_TouchUpdate); | |
| 292 XISetMask(mask, XI_TouchEnd); | |
| 293 #endif | |
| 258 XISetMask(mask, XI_ButtonPress); | 294 XISetMask(mask, XI_ButtonPress); |
| 259 XISetMask(mask, XI_ButtonRelease); | 295 XISetMask(mask, XI_ButtonRelease); |
| 260 XISetMask(mask, XI_Motion); | 296 XISetMask(mask, XI_Motion); |
| 261 | 297 |
| 262 XIEventMask evmask; | 298 XIEventMask evmask; |
| 263 evmask.deviceid = XIAllDevices; | 299 evmask.deviceid = XIAllDevices; |
| 264 evmask.mask_len = sizeof(mask); | 300 evmask.mask_len = sizeof(mask); |
| 265 evmask.mask = mask; | 301 evmask.mask = mask; |
| 266 XISelectEvents(display, window, &evmask, 1); | 302 XISelectEvents(display, window, &evmask, 1); |
| 267 XFlush(display); | 303 XFlush(display); |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 279 } | 315 } |
| 280 | 316 |
| 281 SetupValuator(); | 317 SetupValuator(); |
| 282 } | 318 } |
| 283 | 319 |
| 284 bool TouchFactory::IsTouchDevice(unsigned deviceid) const { | 320 bool TouchFactory::IsTouchDevice(unsigned deviceid) const { |
| 285 return deviceid < touch_device_lookup_.size() ? | 321 return deviceid < touch_device_lookup_.size() ? |
| 286 touch_device_lookup_[deviceid] : false; | 322 touch_device_lookup_[deviceid] : false; |
| 287 } | 323 } |
| 288 | 324 |
| 325 #if !defined(USE_XI2_MT) | |
| 289 bool TouchFactory::IsSlotUsed(int slot) const { | 326 bool TouchFactory::IsSlotUsed(int slot) const { |
| 290 CHECK_LT(slot, kMaxTouchPoints); | 327 CHECK_LT(slot, kMaxTouchPoints); |
| 291 return slots_used_[slot]; | 328 return slots_used_[slot]; |
| 292 } | 329 } |
| 293 | 330 |
| 294 void TouchFactory::SetSlotUsed(int slot, bool used) { | 331 void TouchFactory::SetSlotUsed(int slot, bool used) { |
| 295 CHECK_LT(slot, kMaxTouchPoints); | 332 CHECK_LT(slot, kMaxTouchPoints); |
| 296 slots_used_[slot] = used; | 333 slots_used_[slot] = used; |
| 297 } | 334 } |
| 335 #endif | |
| 298 | 336 |
| 299 bool TouchFactory::GrabTouchDevices(Display* display, ::Window window) { | 337 bool TouchFactory::GrabTouchDevices(Display* display, ::Window window) { |
| 300 #if defined(TOUCH_UI) | 338 #if defined(TOUCH_UI) |
| 301 if (!base::MessagePumpForUI::HasXInput2() || | 339 if (!base::MessagePumpForUI::HasXInput2() || |
| 302 touch_device_list_.empty()) | 340 touch_device_list_.empty()) |
| 303 return true; | 341 return true; |
| 304 #endif | 342 #endif |
| 305 | 343 |
| 306 unsigned char mask[XIMaskLen(XI_LASTEVENT)]; | 344 unsigned char mask[XIMaskLen(XI_LASTEVENT)]; |
| 307 bool success = true; | 345 bool success = true; |
| 308 | 346 |
| 309 memset(mask, 0, sizeof(mask)); | 347 memset(mask, 0, sizeof(mask)); |
| 348 #if defined(USE_XI2_MT) | |
| 349 XISetMask(mask, XI_TouchBegin); | |
| 350 XISetMask(mask, XI_TouchUpdate); | |
| 351 XISetMask(mask, XI_TouchEnd); | |
| 352 #endif | |
| 310 XISetMask(mask, XI_ButtonPress); | 353 XISetMask(mask, XI_ButtonPress); |
| 311 XISetMask(mask, XI_ButtonRelease); | 354 XISetMask(mask, XI_ButtonRelease); |
| 312 XISetMask(mask, XI_Motion); | 355 XISetMask(mask, XI_Motion); |
| 313 | 356 |
| 314 XIEventMask evmask; | 357 XIEventMask evmask; |
| 315 evmask.mask_len = sizeof(mask); | 358 evmask.mask_len = sizeof(mask); |
| 316 evmask.mask = mask; | 359 evmask.mask = mask; |
| 317 for (std::vector<int>::const_iterator iter = | 360 for (std::vector<int>::const_iterator iter = |
| 318 touch_device_list_.begin(); | 361 touch_device_list_.begin(); |
| 319 iter != touch_device_list_.end(); ++iter) { | 362 iter != touch_device_list_.end(); ++iter) { |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 408 float* value) { | 451 float* value) { |
| 409 XIDeviceEvent* xiev = static_cast<XIDeviceEvent*>(xev.xcookie.data); | 452 XIDeviceEvent* xiev = static_cast<XIDeviceEvent*>(xev.xcookie.data); |
| 410 if (xiev->sourceid >= kMaxDeviceNum) | 453 if (xiev->sourceid >= kMaxDeviceNum) |
| 411 return false; | 454 return false; |
| 412 int v = valuator_lookup_[xiev->sourceid][tp]; | 455 int v = valuator_lookup_[xiev->sourceid][tp]; |
| 413 if (v >= 0 && XIMaskIsSet(xiev->valuators.mask, v)) { | 456 if (v >= 0 && XIMaskIsSet(xiev->valuators.mask, v)) { |
| 414 *value = xiev->valuators.values[v]; | 457 *value = xiev->valuators.values[v]; |
| 415 return true; | 458 return true; |
| 416 } | 459 } |
| 417 | 460 |
| 461 // With XInput 2.1, Tracking ID could be provided in the detail field | |
| 462 if (tp == TP_TRACKING_ID) { | |
| 463 *value = xiev->detail; | |
| 464 return true; | |
| 465 } | |
| 466 | |
| 418 return false; | 467 return false; |
| 419 } | 468 } |
| 420 | 469 |
| 421 bool TouchFactory::NormalizeTouchParam(unsigned int deviceid, | 470 bool TouchFactory::NormalizeTouchParam(unsigned int deviceid, |
| 422 TouchParam tp, | 471 TouchParam tp, |
| 423 float* value) { | 472 float* value) { |
| 424 float max_value; | 473 float max_value; |
| 425 float min_value; | 474 float min_value; |
| 426 if (GetTouchParamRange(deviceid, tp, &min_value, &max_value)) { | 475 if (GetTouchParamRange(deviceid, tp, &min_value, &max_value)) { |
| 427 *value = (*value - min_value) / (max_value - min_value); | 476 *value = (*value - min_value) / (max_value - min_value); |
| 428 DCHECK(*value >= 0.0 && *value <= 1.0); | 477 DCHECK(*value >= 0.0 && *value <= 1.0); |
| 429 return true; | 478 return true; |
| 430 } | 479 } |
| 431 return false; | 480 return false; |
| 432 } | 481 } |
| 433 | 482 |
| 434 bool TouchFactory::GetTouchParamRange(unsigned int deviceid, | 483 bool TouchFactory::GetTouchParamRange(unsigned int deviceid, |
| 435 TouchParam tp, | 484 TouchParam tp, |
| 436 float* min, | 485 float* min, |
| 437 float* max) { | 486 float* max) { |
| 438 if (valuator_lookup_[deviceid][tp] >= 0) { | 487 if (valuator_lookup_[deviceid][tp] >= 0) { |
| 439 *min = touch_param_min_[deviceid][tp]; | 488 *min = touch_param_min_[deviceid][tp]; |
| 440 *max = touch_param_max_[deviceid][tp]; | 489 *max = touch_param_max_[deviceid][tp]; |
| 441 return true; | 490 return true; |
| 442 } | 491 } |
| 443 return false; | 492 return false; |
| 444 } | 493 } |
| 445 | 494 |
| 446 } // namespace views | 495 } // namespace views |
| OLD | NEW |