Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "ui/base/x/x11_window_cache.h" | |
| 6 | |
| 7 #include <X11/Xlib.h> | |
| 8 #include <X11/Xlib-xcb.h> | |
| 9 #include <xcb/xcbext.h> | |
| 10 | |
| 11 #include <algorithm> | |
| 12 #include <cstdio> | |
| 13 #include <cstdlib> | |
| 14 #include <cstring> | |
| 15 | |
| 16 #include "base/logging.h" | |
| 17 #include "ui/events/platform/platform_event_source.h" | |
| 18 #include "ui/events/platform/x11/x11_event_source.h" | |
| 19 | |
| 20 namespace ui { | |
| 21 | |
| 22 namespace { | |
| 23 | |
| 24 const char kNetWmIcon[] = "_NET_WM_ICON"; | |
| 25 | |
| 26 // In case a property's value is huge, only cache the first 64KiB, and | |
| 27 // indicate in Property that the cached value is not the entire value. | |
| 28 static constexpr auto kMaxPropertySize = 0xffff; | |
| 29 | |
| 30 template <typename T> | |
| 31 void CacheWindowGeometryFromResponse(XWindowCache::Window* window, | |
| 32 const T& response) { | |
| 33 window->x = response.x; | |
| 34 window->y = response.y; | |
| 35 window->width = response.width; | |
| 36 window->height = response.height; | |
| 37 window->border_width = response.border_width; | |
| 38 } | |
| 39 | |
| 40 auto FindChild(XWindowCache::Window* parent, xcb_window_t child_id) | |
| 41 -> decltype(parent->children.begin()) { | |
| 42 return std::find_if(parent->children.begin(), parent->children.end(), | |
| 43 [child_id](std::unique_ptr<XWindowCache::Window>& child) { | |
| 44 return child->id == child_id; | |
| 45 }); | |
| 46 } | |
| 47 | |
| 48 } // anonymous namespace | |
| 49 | |
| 50 const XWindowCache::Property* XWindowCache::Window::GetProperty( | |
| 51 XID atom) const { | |
| 52 auto it = properties.find(atom); | |
| 53 return it == properties.end() ? nullptr : it->second.get(); | |
| 54 } | |
| 55 | |
| 56 struct XWindowCache::GetWindowAttributesRequest | |
| 57 : public X11EventSource::Request { | |
| 58 GetWindowAttributesRequest(Window* window, XWindowCache* cache) | |
| 59 : Request( | |
| 60 xcb_get_window_attributes(cache->connection_, window->id).sequence), | |
| 61 cache_(cache), | |
| 62 window_(window) { | |
| 63 window_->attributes_request = this; | |
| 64 } | |
| 65 | |
| 66 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override { | |
| 67 if (error) { | |
| 68 switch (error->error_code) { | |
| 69 case BadDrawable: | |
| 70 case BadWindow: | |
| 71 cache_->DestroyWindow(window_); | |
| 72 return; | |
| 73 default: | |
| 74 NOTREACHED(); | |
| 75 } | |
| 76 } | |
| 77 auto* reply = reinterpret_cast<xcb_get_window_attributes_reply_t*>(r); | |
| 78 | |
| 79 window_->attributes_request = nullptr; | |
| 80 window_->override_redirect = reply->override_redirect; | |
| 81 window_->is_mapped = reply->map_state != XCB_MAP_STATE_UNMAPPED; | |
| 82 } | |
| 83 | |
| 84 protected: | |
| 85 XWindowCache* cache_; | |
| 86 Window* window_; | |
| 87 }; | |
| 88 | |
| 89 struct XWindowCache::GetGeometryRequest : public X11EventSource::Request { | |
| 90 GetGeometryRequest(Window* window, XWindowCache* cache) | |
| 91 : Request(xcb_get_geometry(cache->connection_, window->id).sequence), | |
| 92 cache_(cache), | |
| 93 window_(window) { | |
| 94 window_->geometry_request = this; | |
| 95 } | |
| 96 | |
| 97 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override { | |
| 98 if (error) { | |
| 99 switch (error->error_code) { | |
| 100 case BadDrawable: | |
| 101 cache_->DestroyWindow(window_); | |
| 102 return; | |
| 103 default: | |
| 104 NOTREACHED(); | |
| 105 } | |
| 106 } | |
| 107 auto* reply = reinterpret_cast<xcb_get_geometry_reply_t*>(r); | |
| 108 | |
| 109 window_->geometry_request = nullptr; | |
| 110 CacheWindowGeometryFromResponse(window_, *reply); | |
| 111 } | |
| 112 | |
| 113 protected: | |
| 114 XWindowCache* cache_; | |
| 115 Window* window_; | |
| 116 }; | |
| 117 | |
| 118 struct XWindowCache::ListPropertiesRequest : public X11EventSource::Request { | |
| 119 ListPropertiesRequest(Window* window, XWindowCache* cache) | |
| 120 : Request(xcb_list_properties(cache->connection_, window->id).sequence), | |
| 121 cache_(cache), | |
| 122 window_(window) { | |
| 123 window_->properties_request = this; | |
| 124 } | |
| 125 | |
| 126 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override { | |
| 127 if (error) { | |
| 128 switch (error->error_code) { | |
| 129 case BadWindow: | |
| 130 cache_->DestroyWindow(window_); | |
| 131 return; | |
| 132 default: | |
| 133 NOTREACHED(); | |
| 134 } | |
| 135 } | |
| 136 auto* reply = reinterpret_cast<xcb_list_properties_reply_t*>(r); | |
| 137 | |
| 138 window_->properties_request = nullptr; | |
| 139 for (int i = 0; i < xcb_list_properties_atoms_length(reply); i++) { | |
| 140 xcb_atom_t atom = xcb_list_properties_atoms(reply)[i]; | |
| 141 cache_->CacheProperty(window_, atom); | |
| 142 } | |
| 143 } | |
| 144 | |
| 145 protected: | |
| 146 XWindowCache* cache_; | |
| 147 Window* window_; | |
| 148 }; | |
| 149 | |
| 150 struct XWindowCache::QueryTreeRequest : public X11EventSource::Request { | |
| 151 QueryTreeRequest(Window* window, XWindowCache* cache) | |
| 152 : Request(xcb_query_tree(cache->connection_, window->id).sequence), | |
| 153 cache_(cache), | |
| 154 window_(window) { | |
| 155 window_->children_request = this; | |
| 156 } | |
| 157 | |
| 158 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override { | |
| 159 if (error) { | |
| 160 switch (error->error_code) { | |
| 161 case BadWindow: | |
| 162 cache_->DestroyWindow(window_); | |
| 163 return; | |
| 164 default: | |
| 165 NOTREACHED(); | |
| 166 } | |
| 167 } | |
| 168 auto* reply = reinterpret_cast<xcb_query_tree_reply_t*>(r); | |
| 169 | |
| 170 DCHECK(window_->children.empty()); | |
| 171 | |
| 172 xcb_window_t* children = xcb_query_tree_children(reply); | |
| 173 int n_children = xcb_query_tree_children_length(reply); | |
| 174 | |
| 175 window_->children_request = nullptr; | |
| 176 | |
| 177 // Iterate over children from top-to-bottom. | |
| 178 for (int i = 0; i < n_children; i++) | |
| 179 cache_->CreateWindow(children[i], window_); | |
| 180 } | |
| 181 | |
| 182 protected: | |
| 183 XWindowCache* cache_; | |
| 184 Window* window_; | |
| 185 }; | |
| 186 | |
| 187 struct XWindowCache::GetPropertyRequest : public X11EventSource::Request { | |
| 188 GetPropertyRequest(Window* window, | |
| 189 Property* property, | |
| 190 xcb_atom_t atom, | |
| 191 XWindowCache* cache) | |
| 192 : Request(xcb_get_property(cache->connection_, | |
| 193 false, | |
| 194 window->id, | |
| 195 atom, | |
| 196 XCB_ATOM_ANY, | |
| 197 0, | |
| 198 kMaxPropertySize) | |
| 199 .sequence), | |
| 200 cache_(cache), | |
| 201 window_(window), | |
| 202 property_(property), | |
| 203 atom_(atom) { | |
| 204 property->property_request = this; | |
| 205 } | |
| 206 | |
| 207 void OnReply(xcb_generic_reply_t* r, xcb_generic_error_t* error) override { | |
| 208 if (error) { | |
| 209 switch (error->error_code) { | |
| 210 case BadWindow: | |
| 211 cache_->DestroyWindow(window_); | |
| 212 return; | |
| 213 case BadValue: | |
| 214 case BadAtom: | |
| 215 default: | |
| 216 NOTREACHED(); | |
| 217 } | |
| 218 } | |
| 219 auto* reply = reinterpret_cast<xcb_get_property_reply_t*>(r); | |
| 220 | |
| 221 if (reply->format == 0) { | |
| 222 // According to Xlib, a format of anything other than 8, 16, or 32 is a | |
| 223 // BadImplementation error. However, this occurs when creating a new | |
| 224 // xterm window, so just forget about the property in question. | |
| 225 window_->properties.erase(atom_); | |
| 226 return; | |
| 227 } | |
| 228 | |
| 229 property_->property_request = nullptr; | |
| 230 property_->type = reply->type; | |
| 231 DCHECK(reply->format == 8 || reply->format == 16 || reply->format == 32); | |
| 232 property_->data_format = reply->format; | |
| 233 property_->data_length = xcb_get_property_value_length(reply); | |
| 234 uint32_t data_bytes = property_->data_length * (property_->data_format / 8); | |
| 235 property_->data.bits_8 = new uint8_t[data_bytes]; | |
| 236 std::memcpy(property_->data.bits_8, xcb_get_property_value(reply), | |
| 237 data_bytes); | |
| 238 } | |
| 239 | |
| 240 protected: | |
| 241 XWindowCache* cache_; | |
| 242 Window* window_; | |
| 243 Property* property_; | |
| 244 xcb_atom_t atom_; | |
| 245 }; | |
| 246 | |
| 247 XWindowCache::Window::Window(xcb_window_t id, | |
| 248 XWindowCache::Window* parent, | |
| 249 XWindowCache* cache) | |
| 250 : id(id), | |
| 251 parent(parent), | |
| 252 attributes_request(nullptr), | |
| 253 geometry_request(nullptr), | |
| 254 properties_request(nullptr), | |
| 255 children_request(nullptr), | |
| 256 cache_(cache) { | |
| 257 uint32_t event_mask = | |
| 258 XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE; | |
| 259 if (id == cache->root_id_) { | |
| 260 event_mask |= XCB_EVENT_MASK_STRUCTURE_NOTIFY; | |
| 261 } | |
| 262 // TODO(thomasanderson): Use XForeignWindowManager to select on the mask | |
| 263 // instead of resetting it. | |
| 264 auto cookie = xcb_change_window_attributes(cache->connection_, id, | |
| 265 XCB_CW_EVENT_MASK, &event_mask); | |
| 266 xcb_discard_reply(cache->connection_, cookie.sequence); | |
| 267 | |
| 268 // Get the desired window state AFTER requesting state change events to avoid | |
| 269 // race conditions. | |
| 270 | |
| 271 cache->event_source_->EnqueueRequest(new QueryTreeRequest(this, cache)); | |
| 272 cache->event_source_->EnqueueRequest(new ListPropertiesRequest(this, cache)); | |
| 273 cache->event_source_->EnqueueRequest( | |
| 274 new GetWindowAttributesRequest(this, cache)); | |
| 275 cache->event_source_->EnqueueRequest(new GetGeometryRequest(this, cache)); | |
| 276 xcb_flush(cache->connection_); | |
| 277 } | |
| 278 | |
| 279 XWindowCache::Window::~Window() { | |
| 280 // The window tree was created top-down, so must be destroyed bottom-up. | |
| 281 while (!children.empty()) | |
| 282 cache_->DestroyWindow(children.front().get()); | |
| 283 | |
| 284 if (attributes_request) | |
| 285 cache_->event_source_->DiscardRequest(attributes_request); | |
| 286 if (geometry_request) | |
| 287 cache_->event_source_->DiscardRequest(geometry_request); | |
| 288 if (properties_request) | |
| 289 cache_->event_source_->DiscardRequest(properties_request); | |
| 290 if (children_request) | |
| 291 cache_->event_source_->DiscardRequest(children_request); | |
| 292 | |
| 293 // TODO(thomasanderson): Use XForeignWindowManager to deselect on the mask | |
| 294 // instead of resetting it. | |
| 295 uint32_t event_mask = XCB_EVENT_MASK_NO_EVENT; | |
| 296 auto cookie = xcb_change_window_attributes(cache_->connection_, id, | |
| 297 XCB_CW_EVENT_MASK, &event_mask); | |
| 298 // Window |id| may already be destroyed at this point, so the | |
| 299 // change_attributes request may give a BadWindow error. In this case, just | |
| 300 // ignore the error. | |
| 301 xcb_discard_reply(cache_->connection_, cookie.sequence); | |
| 302 } | |
| 303 | |
| 304 XWindowCache::Property::Property(xcb_atom_t name, | |
| 305 Window* window, | |
| 306 XWindowCache* cache) | |
| 307 : name(name), property_request(nullptr), cache_(cache) { | |
| 308 data.bits_8 = nullptr; | |
| 309 | |
| 310 cache->event_source_->EnqueueRequest( | |
| 311 new GetPropertyRequest(window, this, name, cache)); | |
| 312 xcb_flush(cache->connection_); | |
| 313 } | |
| 314 | |
| 315 XWindowCache::Property::~Property() { | |
| 316 if (property_request) | |
| 317 cache_->event_source_->DiscardRequest(property_request); | |
| 318 | |
| 319 delete[] data.bits_8; | |
| 320 } | |
| 321 | |
| 322 // Xlib shall own the event queue. | |
| 323 XWindowCache::XWindowCache(XDisplay* display, | |
| 324 X11EventSource* event_source, | |
| 325 XID root) | |
| 326 : display_(display), | |
| 327 connection_(XGetXCBConnection(display_)), | |
| 328 root_id_(root), | |
| 329 event_source_(event_source), | |
| 330 root_(nullptr), | |
| 331 net_wm_icon_(0) { | |
| 332 DCHECK(event_source); | |
| 333 DCHECK(!xcb_connection_has_error(connection_)); | |
| 334 | |
| 335 if (PlatformEventSource::GetInstance()) | |
| 336 PlatformEventSource::GetInstance()->AddPlatformEventObserver(this); | |
| 337 | |
| 338 net_wm_icon_cookie_ = | |
| 339 xcb_intern_atom(connection_, false, sizeof(kNetWmIcon) - 1, kNetWmIcon); | |
| 340 | |
| 341 CreateWindow(root, nullptr); | |
| 342 } | |
| 343 | |
| 344 XWindowCache::~XWindowCache() { | |
| 345 if (PlatformEventSource::GetInstance()) | |
| 346 PlatformEventSource::GetInstance()->RemovePlatformEventObserver(this); | |
| 347 } | |
| 348 | |
| 349 const XWindowCache::Window* XWindowCache::GetWindow(XID id) const { | |
| 350 return GetWindowInternal(id); | |
| 351 } | |
| 352 | |
| 353 XWindowCache::Window* XWindowCache::GetWindowInternal(XID id) const { | |
| 354 auto it = windows_.find(id); | |
| 355 return it == windows_.end() ? nullptr : it->second; | |
| 356 } | |
| 357 | |
| 358 // TODO(thomasanderson): Call ProcessEvent directly from X11EventSource. Have | |
| 359 // ProcessEvent return an indication of if it is possible for clients other | |
| 360 // than XWindowCache to be interested in the event, so that event dispatchers | |
| 361 // don't get bogged down with the many events that XWindowCache selects. | |
| 362 void XWindowCache::WillProcessEvent(const PlatformEvent& event) { | |
| 363 ProcessEvent(event); | |
| 364 } | |
| 365 | |
| 366 void XWindowCache::DidProcessEvent(const PlatformEvent& event) {} | |
| 367 | |
| 368 void XWindowCache::ProcessEvent(const XEvent* e) { | |
| 369 switch (e->type) { | |
| 370 case PropertyNotify: { | |
| 371 Window* window = GetWindowInternal(e->xproperty.window); | |
| 372 if (!window) | |
| 373 break; | |
| 374 | |
| 375 switch (e->xproperty.state) { | |
| 376 case PropertyDelete: | |
| 377 window->properties.erase(e->xproperty.atom); | |
| 378 break; | |
| 379 case PropertyNewValue: | |
| 380 CacheProperty(window, e->xproperty.atom); | |
| 381 break; | |
| 382 default: | |
| 383 NOTREACHED(); | |
| 384 } | |
| 385 break; | |
| 386 } | |
| 387 case CirculateNotify: { | |
| 388 Window* window = GetWindowInternal(e->xcirculate.window); | |
| 389 if (!window) | |
| 390 break; | |
| 391 | |
| 392 if (e->xcirculate.event == e->xcirculate.window) | |
| 393 break; // This is our root window | |
| 394 | |
| 395 Window* parent = window->parent; | |
| 396 if (parent->id != e->xcirculate.event) | |
| 397 ResetCache(); | |
| 398 | |
| 399 auto it = FindChild(parent, window->id); | |
| 400 switch (e->xcirculate.place) { | |
| 401 case PlaceOnTop: | |
| 402 parent->children.push_front(std::move(*it)); | |
| 403 break; | |
| 404 case PlaceOnBottom: | |
| 405 parent->children.push_back(std::move(*it)); | |
| 406 break; | |
| 407 default: | |
| 408 NOTREACHED(); | |
| 409 } | |
| 410 parent->children.erase(it); | |
| 411 break; | |
| 412 } | |
| 413 case ConfigureNotify: { | |
| 414 Window* window = GetWindowInternal(e->xconfigure.window); | |
| 415 if (!window) | |
| 416 break; | |
| 417 | |
| 418 CacheWindowGeometryFromResponse(window, e->xconfigure); | |
| 419 | |
| 420 if (e->xconfigure.event == e->xconfigure.window) | |
| 421 break; | |
| 422 | |
| 423 Window* parent = window->parent; | |
| 424 if (parent->id != e->xconfigure.event) | |
| 425 ResetCache(); | |
| 426 | |
| 427 auto it = FindChild(parent, window->id); | |
| 428 if (e->xconfigure.above) { | |
| 429 auto it_above = FindChild(parent, e->xconfigure.above); | |
| 430 if (it == parent->children.end()) | |
| 431 ResetCache(); | |
| 432 else | |
| 433 parent->children.insert(it_above, std::move(*it)); | |
| 434 } else { | |
| 435 // |window| is not above any other sibling window | |
| 436 parent->children.push_back(std::move(*it)); | |
| 437 } | |
| 438 parent->children.erase(it); | |
| 439 break; | |
| 440 } | |
| 441 case CreateNotify: { | |
| 442 Window* parent = GetWindowInternal(e->xcreatewindow.parent); | |
| 443 if (!parent) | |
| 444 break; | |
| 445 | |
| 446 if (parent->children_request) { | |
| 447 // |parent| is in the process of being cached, so we will pick up this | |
| 448 // window in the near future. | |
| 449 break; | |
| 450 } | |
| 451 | |
| 452 CreateWindow(e->xcreatewindow.window, parent); | |
| 453 break; | |
| 454 } | |
| 455 case DestroyNotify: { | |
| 456 Window* window = GetWindowInternal(e->xdestroywindow.window); | |
| 457 if (!window) | |
| 458 break; | |
| 459 DestroyWindow(window); | |
| 460 break; | |
| 461 } | |
| 462 case GravityNotify: { | |
| 463 Window* window = GetWindowInternal(e->xgravity.window); | |
| 464 if (!window) | |
| 465 break; | |
| 466 | |
| 467 window->x = e->xgravity.x; | |
| 468 window->y = e->xgravity.y; | |
| 469 break; | |
| 470 } | |
| 471 case MapNotify: { | |
| 472 Window* window = GetWindowInternal(e->xmap.window); | |
| 473 if (!window) | |
| 474 break; | |
| 475 | |
| 476 window->override_redirect = e->xmap.override_redirect; | |
| 477 window->is_mapped = true; | |
| 478 break; | |
| 479 } | |
| 480 case ReparentNotify: { | |
| 481 Window* window = GetWindowInternal(e->xreparent.window); | |
| 482 if (!window) | |
| 483 break; | |
| 484 | |
| 485 window->x = e->xreparent.x; | |
| 486 window->y = e->xreparent.y; | |
| 487 | |
| 488 window->override_redirect = e->xreparent.override_redirect; | |
| 489 window->is_mapped = false; // Reparenting a window unmaps it | |
| 490 | |
| 491 Window* old_parent = window->parent; | |
| 492 if (!old_parent) | |
| 493 break; // Don't worry about caching windows above our root. | |
| 494 | |
| 495 Window* new_parent = GetWindowInternal(e->xreparent.parent); | |
| 496 if (!new_parent || new_parent->children_request) { | |
| 497 // |window| is either no longer in our tree, or we are already waiting | |
| 498 // to receive a list of |new_parent|'s children. | |
| 499 DestroyWindow(window); | |
| 500 break; | |
| 501 } | |
| 502 window->parent = new_parent; | |
| 503 | |
| 504 auto it = FindChild(old_parent, window->id); | |
| 505 new_parent->children.push_front(std::move(*it)); | |
| 506 old_parent->children.erase(it); | |
| 507 break; | |
| 508 } | |
| 509 case UnmapNotify: { | |
| 510 Window* window = GetWindowInternal(e->xunmap.window); | |
| 511 if (!window) | |
| 512 break; | |
| 513 | |
| 514 window->is_mapped = false; | |
| 515 break; | |
| 516 } | |
| 517 default: | |
| 518 break; | |
| 519 } | |
| 520 } | |
| 521 | |
| 522 void XWindowCache::ResetCache() { | |
| 523 NOTREACHED(); | |
| 524 | |
| 525 // On release builds, try to fix our state. | |
| 526 ResetCacheImpl(); | |
| 527 } | |
| 528 | |
| 529 void XWindowCache::ResetCacheImpl() { | |
| 530 // TODO(thomasanderson): Log something in UMA. | |
| 531 if (root_) { | |
| 532 DestroyWindow(root_.get()); | |
| 533 DCHECK(windows_.empty()); | |
| 534 CreateWindow(root_id_, nullptr); | |
| 535 } | |
| 536 } | |
| 537 | |
| 538 void XWindowCache::CacheProperty(XWindowCache::Window* window, | |
| 539 xcb_atom_t atom) { | |
| 540 if (!net_wm_icon_ && net_wm_icon_cookie_.sequence) { | |
| 541 auto reply = | |
| 542 xcb_intern_atom_reply(connection_, net_wm_icon_cookie_, nullptr); | |
| 543 if (reply) { | |
| 544 net_wm_icon_ = reply->atom; | |
| 545 free(reply); | |
| 546 } | |
| 547 net_wm_icon_cookie_.sequence = 0; | |
| 548 } | |
| 549 if (atom == net_wm_icon_) | |
| 550 return; | |
| 551 window->properties[atom].reset(new Property(atom, window, this)); | |
| 552 } | |
| 553 | |
| 554 void XWindowCache::CreateWindow(xcb_window_t id, XWindowCache::Window* parent) { | |
| 555 auto it = windows_.find(id); | |
| 556 if (it != windows_.end()) { | |
| 557 // We're already tracking window |id|. | |
| 558 return; | |
| 559 } | |
| 560 | |
| 561 Window* window = new Window(id, parent, this); | |
| 562 windows_[id] = window; | |
| 563 if (parent) { | |
| 564 parent->children.emplace_front(window); | |
| 565 } else { | |
| 566 DCHECK(!root_); | |
| 567 root_.reset(window); | |
| 568 } | |
| 569 } | |
| 570 | |
| 571 void XWindowCache::DestroyWindow(Window* window) { | |
| 572 DCHECK(window); | |
| 573 xcb_window_t id = window->id; | |
| 574 if (window->parent) { | |
| 575 auto it = FindChild(window->parent, window->id); | |
| 576 DCHECK(it != window->parent->children.end()); | |
| 577 window->parent->children.erase(it); | |
| 578 } else { | |
| 579 DCHECK_EQ(window, root_.get()); | |
| 580 root_.reset(); | |
| 581 } | |
| 582 windows_.erase(id); | |
|
Daniel Erat
2016/09/06 20:35:58
are you leaking Window objects here? please use st
| |
| 583 } | |
| 584 | |
| 585 } // namespace ui | |
| OLD | NEW |