Home | History | Annotate | Download | only in drag_drop
      1 // Copyright (c) 2012 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 "ash/drag_drop/drag_drop_controller.h"
      6 
      7 #include "ash/drag_drop/drag_drop_tracker.h"
      8 #include "ash/drag_drop/drag_image_view.h"
      9 #include "ash/shell.h"
     10 #include "ash/wm/coordinate_conversion.h"
     11 #include "base/bind.h"
     12 #include "base/message_loop/message_loop.h"
     13 #include "base/run_loop.h"
     14 #include "ui/aura/client/capture_client.h"
     15 #include "ui/aura/client/drag_drop_delegate.h"
     16 #include "ui/aura/env.h"
     17 #include "ui/aura/root_window.h"
     18 #include "ui/aura/window.h"
     19 #include "ui/aura/window_delegate.h"
     20 #include "ui/base/dragdrop/drag_drop_types.h"
     21 #include "ui/base/dragdrop/os_exchange_data.h"
     22 #include "ui/base/hit_test.h"
     23 #include "ui/events/event.h"
     24 #include "ui/events/event_utils.h"
     25 #include "ui/gfx/animation/linear_animation.h"
     26 #include "ui/gfx/path.h"
     27 #include "ui/gfx/point.h"
     28 #include "ui/gfx/rect.h"
     29 #include "ui/gfx/rect_conversions.h"
     30 #include "ui/views/views_delegate.h"
     31 #include "ui/views/widget/native_widget_aura.h"
     32 
     33 namespace ash {
     34 namespace internal {
     35 
     36 namespace {
     37 // The duration of the drag cancel animation in millisecond.
     38 const int kCancelAnimationDuration = 250;
     39 const int kTouchCancelAnimationDuration = 20;
     40 // The frame rate of the drag cancel animation in hertz.
     41 const int kCancelAnimationFrameRate = 60;
     42 
     43 // For touch initiated dragging, we scale and shift drag image by the following:
     44 static const float kTouchDragImageScale = 1.2f;
     45 static const int kTouchDragImageVerticalOffset = -25;
     46 
     47 // Adjusts the drag image bounds such that the new bounds are scaled by |scale|
     48 // and translated by the |drag_image_offset| and and additional
     49 // |vertical_offset|.
     50 gfx::Rect AdjustDragImageBoundsForScaleAndOffset(
     51     const gfx::Rect& drag_image_bounds,
     52     int vertical_offset,
     53     float scale,
     54     gfx::Vector2d* drag_image_offset) {
     55   gfx::PointF final_origin = drag_image_bounds.origin();
     56   gfx::SizeF final_size = drag_image_bounds.size();
     57   final_size.Scale(scale);
     58   drag_image_offset->set_x(drag_image_offset->x() * scale);
     59   drag_image_offset->set_y(drag_image_offset->y() * scale);
     60   float total_x_offset = drag_image_offset->x();
     61   float total_y_offset = drag_image_offset->y() - vertical_offset;
     62   final_origin.Offset(-total_x_offset, -total_y_offset);
     63   return gfx::ToEnclosingRect(gfx::RectF(final_origin, final_size));
     64 }
     65 
     66 void DispatchGestureEndToWindow(aura::Window* window) {
     67   if (window && window->delegate()) {
     68     ui::GestureEvent gesture_end(
     69         ui::ET_GESTURE_END,
     70         0,
     71         0,
     72         0,
     73         ui::EventTimeForNow(),
     74         ui::GestureEventDetails(ui::ET_GESTURE_END, 0, 0),
     75         0);
     76     window->delegate()->OnGestureEvent(&gesture_end);
     77   }
     78 }
     79 }  // namespace
     80 
     81 class DragDropTrackerDelegate : public aura::WindowDelegate {
     82  public:
     83   explicit DragDropTrackerDelegate(DragDropController* controller)
     84       : drag_drop_controller_(controller) {}
     85   virtual ~DragDropTrackerDelegate() {}
     86 
     87   // Overridden from WindowDelegate:
     88   virtual gfx::Size GetMinimumSize() const OVERRIDE {
     89     return gfx::Size();
     90   }
     91 
     92   virtual gfx::Size GetMaximumSize() const OVERRIDE {
     93     return gfx::Size();
     94   }
     95 
     96   virtual void OnBoundsChanged(const gfx::Rect& old_bounds,
     97                                const gfx::Rect& new_bounds) OVERRIDE {}
     98   virtual gfx::NativeCursor GetCursor(const gfx::Point& point) OVERRIDE {
     99     return gfx::kNullCursor;
    100   }
    101   virtual int GetNonClientComponent(const gfx::Point& point) const OVERRIDE {
    102     return HTCAPTION;
    103   }
    104   virtual bool ShouldDescendIntoChildForEventHandling(
    105       aura::Window* child,
    106       const gfx::Point& location) OVERRIDE {
    107     return true;
    108   }
    109   virtual bool CanFocus() OVERRIDE { return true; }
    110   virtual void OnCaptureLost() OVERRIDE {
    111     if (drag_drop_controller_->IsDragDropInProgress())
    112       drag_drop_controller_->DragCancel();
    113   }
    114   virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
    115   }
    116   virtual void OnDeviceScaleFactorChanged(float device_scale_factor) OVERRIDE {}
    117   virtual void OnWindowDestroying() OVERRIDE {}
    118   virtual void OnWindowDestroyed() OVERRIDE {}
    119   virtual void OnWindowTargetVisibilityChanged(bool visible) OVERRIDE {}
    120   virtual bool HasHitTestMask() const OVERRIDE {
    121     return true;
    122   }
    123   virtual void GetHitTestMask(gfx::Path* mask) const OVERRIDE {
    124     DCHECK(mask->isEmpty());
    125   }
    126   virtual void DidRecreateLayer(ui::Layer* old_layer,
    127                                 ui::Layer* new_layer) OVERRIDE {}
    128 
    129  private:
    130   DragDropController* drag_drop_controller_;
    131 
    132   DISALLOW_COPY_AND_ASSIGN(DragDropTrackerDelegate);
    133 };
    134 
    135 ////////////////////////////////////////////////////////////////////////////////
    136 // DragDropController, public:
    137 
    138 DragDropController::DragDropController()
    139     : drag_data_(NULL),
    140       drag_operation_(0),
    141       drag_window_(NULL),
    142       drag_source_window_(NULL),
    143       should_block_during_drag_drop_(true),
    144       drag_drop_window_delegate_(new DragDropTrackerDelegate(this)),
    145       current_drag_event_source_(ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE),
    146       weak_factory_(this) {
    147   Shell::GetInstance()->PrependPreTargetHandler(this);
    148 }
    149 
    150 DragDropController::~DragDropController() {
    151   Shell::GetInstance()->RemovePreTargetHandler(this);
    152   Cleanup();
    153   if (cancel_animation_)
    154     cancel_animation_->End();
    155   if (drag_image_)
    156     drag_image_.reset();
    157 }
    158 
    159 int DragDropController::StartDragAndDrop(
    160     const ui::OSExchangeData& data,
    161     aura::Window* root_window,
    162     aura::Window* source_window,
    163     const gfx::Point& root_location,
    164     int operation,
    165     ui::DragDropTypes::DragEventSource source) {
    166   if (IsDragDropInProgress())
    167     return 0;
    168 
    169   const ui::OSExchangeData::Provider* provider = &data.provider();
    170   // We do not support touch drag/drop without a drag image.
    171   if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH &&
    172       provider->GetDragImage().size().IsEmpty())
    173     return 0;
    174 
    175   current_drag_event_source_ = source;
    176   DragDropTracker* tracker =
    177       new DragDropTracker(root_window, drag_drop_window_delegate_.get());
    178   if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
    179     // We need to transfer the current gesture sequence and the GR's touch event
    180     // queue to the |drag_drop_tracker_|'s capture window so that when it takes
    181     // capture, it still gets a valid gesture state.
    182     ui::GestureRecognizer::Get()->TransferEventsTo(source_window,
    183         tracker->capture_window());
    184     // We also send a gesture end to the source window so it can clear state.
    185     // TODO(varunjain): Remove this whole block when gesture sequence
    186     // transferring is properly done in the GR (http://crbug.com/160558)
    187     DispatchGestureEndToWindow(source_window);
    188   }
    189   tracker->TakeCapture();
    190   drag_drop_tracker_.reset(tracker);
    191   drag_source_window_ = source_window;
    192   if (drag_source_window_)
    193     drag_source_window_->AddObserver(this);
    194   pending_long_tap_.reset();
    195 
    196   drag_data_ = &data;
    197   drag_operation_ = operation;
    198 
    199   float drag_image_scale = 1;
    200   int drag_image_vertical_offset = 0;
    201   if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
    202     drag_image_scale = kTouchDragImageScale;
    203     drag_image_vertical_offset = kTouchDragImageVerticalOffset;
    204   }
    205   gfx::Point start_location = root_location;
    206   ash::wm::ConvertPointToScreen(root_window, &start_location);
    207   drag_image_final_bounds_for_cancel_animation_ = gfx::Rect(
    208       start_location - provider->GetDragImageOffset(),
    209       provider->GetDragImage().size());
    210   drag_image_.reset(new DragImageView(source_window->GetRootWindow(), source));
    211   drag_image_->SetImage(provider->GetDragImage());
    212   drag_image_offset_ = provider->GetDragImageOffset();
    213   gfx::Rect drag_image_bounds(start_location, drag_image_->GetPreferredSize());
    214   drag_image_bounds = AdjustDragImageBoundsForScaleAndOffset(drag_image_bounds,
    215       drag_image_vertical_offset, drag_image_scale, &drag_image_offset_);
    216   drag_image_->SetBoundsInScreen(drag_image_bounds);
    217   drag_image_->SetWidgetVisible(true);
    218   if (source == ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH) {
    219     drag_image_->SetTouchDragOperationHintPosition(gfx::Point(
    220         drag_image_offset_.x(),
    221         drag_image_offset_.y() + drag_image_vertical_offset));
    222   }
    223 
    224   drag_window_ = NULL;
    225 
    226   // Ends cancel animation if it's in progress.
    227   if (cancel_animation_)
    228     cancel_animation_->End();
    229 
    230   if (should_block_during_drag_drop_) {
    231     base::RunLoop run_loop(aura::Env::GetInstance()->GetDispatcher());
    232     quit_closure_ = run_loop.QuitClosure();
    233     base::MessageLoopForUI* loop = base::MessageLoopForUI::current();
    234     base::MessageLoop::ScopedNestableTaskAllower allow_nested(loop);
    235     run_loop.Run();
    236   }
    237 
    238   if (!cancel_animation_.get() || !cancel_animation_->is_animating() ||
    239       !pending_long_tap_.get()) {
    240     // If drag cancel animation is running, this cleanup is done when the
    241     // animation completes.
    242     if (drag_source_window_)
    243       drag_source_window_->RemoveObserver(this);
    244     drag_source_window_ = NULL;
    245   }
    246 
    247   return drag_operation_;
    248 }
    249 
    250 void DragDropController::DragUpdate(aura::Window* target,
    251                                     const ui::LocatedEvent& event) {
    252   aura::client::DragDropDelegate* delegate = NULL;
    253   int op = ui::DragDropTypes::DRAG_NONE;
    254   if (target != drag_window_) {
    255     if (drag_window_) {
    256       if ((delegate = aura::client::GetDragDropDelegate(drag_window_)))
    257         delegate->OnDragExited();
    258       if (drag_window_ != drag_source_window_)
    259         drag_window_->RemoveObserver(this);
    260     }
    261     drag_window_ = target;
    262     // We are already an observer of |drag_source_window_| so no need to add.
    263     if (drag_window_ != drag_source_window_)
    264       drag_window_->AddObserver(this);
    265     if ((delegate = aura::client::GetDragDropDelegate(drag_window_))) {
    266       ui::DropTargetEvent e(*drag_data_,
    267                             event.location(),
    268                             event.root_location(),
    269                             drag_operation_);
    270       e.set_flags(event.flags());
    271       delegate->OnDragEntered(e);
    272     }
    273   } else {
    274     if ((delegate = aura::client::GetDragDropDelegate(drag_window_))) {
    275       ui::DropTargetEvent e(*drag_data_,
    276                             event.location(),
    277                             event.root_location(),
    278                             drag_operation_);
    279       e.set_flags(event.flags());
    280       op = delegate->OnDragUpdated(e);
    281       gfx::NativeCursor cursor = ui::kCursorNoDrop;
    282       if (op & ui::DragDropTypes::DRAG_COPY)
    283         cursor = ui::kCursorCopy;
    284       else if (op & ui::DragDropTypes::DRAG_LINK)
    285         cursor = ui::kCursorAlias;
    286       else if (op & ui::DragDropTypes::DRAG_MOVE)
    287         cursor = ui::kCursorGrabbing;
    288       ash::Shell::GetInstance()->cursor_manager()->SetCursor(cursor);
    289     }
    290   }
    291 
    292   DCHECK(drag_image_.get());
    293   if (drag_image_->visible()) {
    294     gfx::Point root_location_in_screen = event.root_location();
    295     ash::wm::ConvertPointToScreen(target->GetRootWindow(),
    296                                   &root_location_in_screen);
    297     drag_image_->SetScreenPosition(
    298         root_location_in_screen - drag_image_offset_);
    299     drag_image_->SetTouchDragOperation(op);
    300   }
    301 }
    302 
    303 void DragDropController::Drop(aura::Window* target,
    304                               const ui::LocatedEvent& event) {
    305   ash::Shell::GetInstance()->cursor_manager()->SetCursor(ui::kCursorPointer);
    306   aura::client::DragDropDelegate* delegate = NULL;
    307 
    308   // We must guarantee that a target gets a OnDragEntered before Drop. WebKit
    309   // depends on not getting a Drop without DragEnter. This behavior is
    310   // consistent with drag/drop on other platforms.
    311   if (target != drag_window_)
    312     DragUpdate(target, event);
    313   DCHECK(target == drag_window_);
    314 
    315   if ((delegate = aura::client::GetDragDropDelegate(target))) {
    316     ui::DropTargetEvent e(
    317         *drag_data_, event.location(), event.root_location(), drag_operation_);
    318     e.set_flags(event.flags());
    319     drag_operation_ = delegate->OnPerformDrop(e);
    320     if (drag_operation_ == 0)
    321       StartCanceledAnimation(kCancelAnimationDuration);
    322     else
    323       drag_image_.reset();
    324   } else {
    325     drag_image_.reset();
    326   }
    327 
    328   Cleanup();
    329   if (should_block_during_drag_drop_)
    330     quit_closure_.Run();
    331 }
    332 
    333 void DragDropController::DragCancel() {
    334   DoDragCancel(kCancelAnimationDuration);
    335 }
    336 
    337 bool DragDropController::IsDragDropInProgress() {
    338   return !!drag_drop_tracker_.get();
    339 }
    340 
    341 void DragDropController::OnKeyEvent(ui::KeyEvent* event) {
    342   if (IsDragDropInProgress() && event->key_code() == ui::VKEY_ESCAPE) {
    343     DragCancel();
    344     event->StopPropagation();
    345   }
    346 }
    347 
    348 void DragDropController::OnMouseEvent(ui::MouseEvent* event) {
    349   if (!IsDragDropInProgress())
    350     return;
    351 
    352   // If current drag session was not started by mouse, dont process this mouse
    353   // event, but consume it so it does not interfere with current drag session.
    354   if (current_drag_event_source_ !=
    355       ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE) {
    356     event->StopPropagation();
    357     return;
    358   }
    359 
    360   aura::Window* translated_target = drag_drop_tracker_->GetTarget(*event);
    361   if (!translated_target) {
    362     DragCancel();
    363     event->StopPropagation();
    364     return;
    365   }
    366   scoped_ptr<ui::LocatedEvent> translated_event(
    367       drag_drop_tracker_->ConvertEvent(translated_target, *event));
    368   switch (translated_event->type()) {
    369     case ui::ET_MOUSE_DRAGGED:
    370       DragUpdate(translated_target, *translated_event.get());
    371       break;
    372     case ui::ET_MOUSE_RELEASED:
    373       Drop(translated_target, *translated_event.get());
    374       break;
    375     default:
    376       // We could also reach here because RootWindow may sometimes generate a
    377       // bunch of fake mouse events
    378       // (aura::RootWindow::PostMouseMoveEventAfterWindowChange).
    379       break;
    380   }
    381   event->StopPropagation();
    382 }
    383 
    384 void DragDropController::OnTouchEvent(ui::TouchEvent* event) {
    385   if (!IsDragDropInProgress())
    386     return;
    387 
    388   // If current drag session was not started by touch, dont process this touch
    389   // event, but consume it so it does not interfere with current drag session.
    390   if (current_drag_event_source_ != ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH)
    391     event->StopPropagation();
    392 
    393   if (event->handled())
    394     return;
    395 
    396   if (event->type() == ui::ET_TOUCH_CANCELLED)
    397     DragCancel();
    398 }
    399 
    400 void DragDropController::OnGestureEvent(ui::GestureEvent* event) {
    401   if (!IsDragDropInProgress())
    402     return;
    403 
    404   // No one else should handle gesture events when in drag drop. Note that it is
    405   // not enough to just set ER_HANDLED because the dispatcher only stops
    406   // dispatching when the event has ER_CONSUMED. If we just set ER_HANDLED, the
    407   // event will still be dispatched to other handlers and we depend on
    408   // individual handlers' kindness to not touch events marked ER_HANDLED (not
    409   // all handlers are so kind and may cause bugs like crbug.com/236493).
    410   event->StopPropagation();
    411 
    412   // If current drag session was not started by touch, dont process this event.
    413   if (current_drag_event_source_ != ui::DragDropTypes::DRAG_EVENT_SOURCE_TOUCH)
    414     return;
    415 
    416   // Apply kTouchDragImageVerticalOffset to the location.
    417   ui::GestureEvent touch_offset_event(*event,
    418                                       static_cast<aura::Window*>(NULL),
    419                                       static_cast<aura::Window*>(NULL));
    420   gfx::Point touch_offset_location = touch_offset_event.location();
    421   gfx::Point touch_offset_root_location = touch_offset_event.root_location();
    422   touch_offset_location.Offset(0, kTouchDragImageVerticalOffset);
    423   touch_offset_root_location.Offset(0, kTouchDragImageVerticalOffset);
    424   touch_offset_event.set_location(touch_offset_location);
    425   touch_offset_event.set_root_location(touch_offset_root_location);
    426 
    427   aura::Window* translated_target =
    428       drag_drop_tracker_->GetTarget(touch_offset_event);
    429   if (!translated_target) {
    430     DragCancel();
    431     event->SetHandled();
    432     return;
    433   }
    434   scoped_ptr<ui::LocatedEvent> translated_event(
    435       drag_drop_tracker_->ConvertEvent(translated_target, touch_offset_event));
    436 
    437   switch (event->type()) {
    438     case ui::ET_GESTURE_SCROLL_UPDATE:
    439       DragUpdate(translated_target, *translated_event.get());
    440       break;
    441     case ui::ET_GESTURE_SCROLL_END:
    442       Drop(translated_target, *translated_event.get());
    443       break;
    444     case ui::ET_SCROLL_FLING_START:
    445     case ui::ET_GESTURE_LONG_TAP:
    446       // Ideally we would want to just forward this long tap event to the
    447       // |drag_source_window_|. However, webkit does not accept events while a
    448       // drag drop is still in progress. The drag drop ends only when the nested
    449       // message loop ends. Due to this stupidity, we have to defer forwarding
    450       // the long tap.
    451       pending_long_tap_.reset(
    452           new ui::GestureEvent(*event,
    453               static_cast<aura::Window*>(drag_drop_tracker_->capture_window()),
    454               static_cast<aura::Window*>(drag_source_window_)));
    455       DoDragCancel(kTouchCancelAnimationDuration);
    456       break;
    457     default:
    458       break;
    459   }
    460   event->SetHandled();
    461 }
    462 
    463 void DragDropController::OnWindowDestroyed(aura::Window* window) {
    464   if (drag_window_ == window)
    465     drag_window_ = NULL;
    466   if (drag_source_window_ == window)
    467     drag_source_window_ = NULL;
    468 }
    469 
    470 ////////////////////////////////////////////////////////////////////////////////
    471 // DragDropController, protected:
    472 
    473 gfx::LinearAnimation* DragDropController::CreateCancelAnimation(
    474     int duration,
    475     int frame_rate,
    476     gfx::AnimationDelegate* delegate) {
    477   return new gfx::LinearAnimation(duration, frame_rate, delegate);
    478 }
    479 
    480 ////////////////////////////////////////////////////////////////////////////////
    481 // DragDropController, private:
    482 
    483 void DragDropController::AnimationEnded(const gfx::Animation* animation) {
    484   cancel_animation_.reset();
    485 
    486   // By the time we finish animation, another drag/drop session may have
    487   // started. We do not want to destroy the drag image in that case.
    488   if (!IsDragDropInProgress())
    489     drag_image_.reset();
    490   if (pending_long_tap_) {
    491     // If not in a nested message loop, we can forward the long tap right now.
    492     if (!should_block_during_drag_drop_)
    493       ForwardPendingLongTap();
    494     else {
    495       // See comment about this in OnGestureEvent().
    496       base::MessageLoopForUI::current()->PostTask(
    497           FROM_HERE,
    498           base::Bind(&DragDropController::ForwardPendingLongTap,
    499                      weak_factory_.GetWeakPtr()));
    500     }
    501   }
    502 }
    503 
    504 void DragDropController::DoDragCancel(int drag_cancel_animation_duration_ms) {
    505   ash::Shell::GetInstance()->cursor_manager()->SetCursor(ui::kCursorPointer);
    506 
    507   // |drag_window_| can be NULL if we have just started the drag and have not
    508   // received any DragUpdates, or, if the |drag_window_| gets destroyed during
    509   // a drag/drop.
    510   aura::client::DragDropDelegate* delegate = drag_window_?
    511       aura::client::GetDragDropDelegate(drag_window_) : NULL;
    512   if (delegate)
    513     delegate->OnDragExited();
    514 
    515   Cleanup();
    516   drag_operation_ = 0;
    517   StartCanceledAnimation(drag_cancel_animation_duration_ms);
    518   if (should_block_during_drag_drop_)
    519     quit_closure_.Run();
    520 }
    521 
    522 void DragDropController::AnimationProgressed(const gfx::Animation* animation) {
    523   gfx::Rect current_bounds = animation->CurrentValueBetween(
    524       drag_image_initial_bounds_for_cancel_animation_,
    525       drag_image_final_bounds_for_cancel_animation_);
    526   drag_image_->SetBoundsInScreen(current_bounds);
    527 }
    528 
    529 void DragDropController::AnimationCanceled(const gfx::Animation* animation) {
    530   AnimationEnded(animation);
    531 }
    532 
    533 void DragDropController::StartCanceledAnimation(int animation_duration_ms) {
    534   DCHECK(drag_image_.get());
    535   drag_image_->SetTouchDragOperationHintOff();
    536   drag_image_initial_bounds_for_cancel_animation_ =
    537       drag_image_->GetBoundsInScreen();
    538   cancel_animation_.reset(CreateCancelAnimation(animation_duration_ms,
    539                                                 kCancelAnimationFrameRate,
    540                                                 this));
    541   cancel_animation_->Start();
    542 }
    543 
    544 void DragDropController::ForwardPendingLongTap() {
    545   if (drag_source_window_ && drag_source_window_->delegate()) {
    546     drag_source_window_->delegate()->OnGestureEvent(pending_long_tap_.get());
    547     DispatchGestureEndToWindow(drag_source_window_);
    548   }
    549   pending_long_tap_.reset();
    550   if (drag_source_window_)
    551     drag_source_window_->RemoveObserver(this);
    552   drag_source_window_ = NULL;
    553 }
    554 
    555 void DragDropController::Cleanup() {
    556   if (drag_window_)
    557     drag_window_->RemoveObserver(this);
    558   drag_window_ = NULL;
    559   drag_data_ = NULL;
    560   // Cleanup can be called again while deleting DragDropTracker, so use Pass
    561   // instead of reset to avoid double free.
    562   drag_drop_tracker_.Pass();
    563 }
    564 
    565 }  // namespace internal
    566 }  // namespace ash
    567