text: //////////////////////////////////////////////////////////////////////////////
  //
  // Copyright (c) 2004-2021 musikcube team
  //
  // All rights reserved.
  //
  // Redistribution and use in source and binary forms, with or without
  // modification, are permitted provided that the following conditions are met:
  //
  //    * Redistributions of source code must retain the above copyright notice,
  //      this list of conditions and the following disclaimer.
  //
  //    * Redistributions in binary form must reproduce the above copyright
  //      notice, this list of conditions and the following disclaimer in the
  //      documentation and/or other materials provided with the distribution.
  //
  //    * Neither the name of the author nor the names of other contributors may
  //      be used to endorse or promote products derived from this software
  //      without specific prior written permission.
  //
  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  // POSSIBILITY OF SUCH DAMAGE.
  //
  //////////////////////////////////////////////////////////////////////////////
  
  #include <stdafx.h>
  #include <algorithm>
  #include <functional>
  #include <cursespp/ListOverlay.h>
  #include <cursespp/Scrollbar.h>
  #include <cursespp/Colors.h>
  #include <cursespp/Screen.h>
  #include <cursespp/Text.h>
  
  using namespace cursespp;
  
  #define VERTICAL_PADDING 2
  #define DEFAULT_WIDTH 26
  
  /* a little custom type that allows us to draw a scrollbar
  ourself, outside of the list window frame. */
  class ListOverlay::CustomListWindow : public ListWindow {
      public:
          using Callback = std::function<void()>;
  
          CustomListWindow(Callback decorator, Callback adapterChanged)
          : ListWindow() {
              this->decorator = decorator;
              this->adapterChanged = adapterChanged;
          }
  
          virtual ~CustomListWindow() {
          }
  
          virtual void OnAdapterChanged() {
              if (adapterChanged) { adapterChanged(); };
              ListWindow::OnAdapterChanged();
          }
  
          void Reset() {
              decorator = adapterChanged = Callback();
          }
  
          size_t EntryCount() {
              return this->GetScrollAdapter().GetEntryCount();
          }
  
      protected:
          virtual void DecorateFrame() {
              if (decorator) { decorator(); }
          }
  
      private:
          Callback decorator, adapterChanged;
  };
  
  ListOverlay::ListOverlay() {
      this->SetFrameVisible(true);
      this->SetFrameColor(Color::OverlayFrame);
      this->SetContentColor(Color::OverlayContent);
  
      this->autoDismiss = true;
  
      this->width = this->height = 0;
      this->setWidth = this->setWidthPercent = 0;
  
      auto decorator = [this] {
          if (this->ScrollbarVisible()) {
              Scrollbar::Draw(this->listWindow.get(), this->scrollbar.get());
          }
      };
  
      auto adapterChanged = [this] { this->Layout(); };
  
      this->scrollbar.reset(new Window());
      this->scrollbar->SetFrameVisible(false);
      this->scrollbar->SetContentColor(Color::OverlayContent);
  
      this->listWindow.reset(new CustomListWindow(decorator, adapterChanged));
      this->listWindow->SetContentColor(Color::OverlayContent);
      this->listWindow->SetFocusedContentColor(Color::OverlayContent);
      this->listWindow->SetFrameVisible(false);
      this->listWindow->EntryActivated.connect(this, &ListOverlay::OnListEntryActivated);
  
      this->listWindow->SetFocusOrder(0);
      this->AddWindow(this->scrollbar);
      this->AddWindow(this->listWindow);
  }
  
  ListOverlay::~ListOverlay() {
      this->listWindow->Reset();
  }
  
  void ListOverlay::Layout() {
      this->RecalculateSize();
  
      if (this->width > 0 && this->height > 0) {
          this->MoveAndResize(
              this->x,
              this->y,
              this->width,
              this->height);
  
          bool scrollbar = this->ScrollbarVisible();
          auto contentWidth = this->GetContentWidth() - 2; /* subtract padding */
          auto contentHeight = this->height - 4; /* top and bottom padding + title */
          auto startX = 1; /* L padding */
          auto startY = 2; /* below the title, plus an extra space */
          auto listWidth = scrollbar ? contentWidth - 2 : contentWidth;
  
          this->listWindow->MoveAndResize(startX, startY, listWidth, contentHeight);
  
          auto index = this->listWindow->GetSelectedIndex();
          if (!this->listWindow->IsEntryVisible(index)) {
              this->listWindow->ScrollTo(index);
          }
  
          if (scrollbar) {
              this->scrollbar->Show();
              this->scrollbar->MoveAndResize(contentWidth, startY, 1, contentHeight);
          }
          else {
              this->scrollbar->Hide();
          }
  
          this->UpdateContents();
      }
  }
  
  bool ListOverlay::ScrollbarVisible() {
      auto contentHeight = this->height - 4; /* top and bottom padding + title */
      return (int) this->listWindow->EntryCount() > contentHeight;
  }
  
  ListOverlay& ListOverlay::SetTitle(const std::string& title) {
      this->title = title;
      this->RecalculateSize();
      this->Layout();
      this->Invalidate();
      return *this;
  }
  
  ListOverlay& ListOverlay::SetWidth(int width) {
      this->setWidth = width;
      this->setWidthPercent = 0;
  
      if (this->IsVisible()) {
          this->Layout();
      }
  
      return *this;
  }
  
  ListOverlay& ListOverlay::SetWidthPercent(int percent) {
      this->setWidthPercent = percent;
      this->setWidth = 0;
  
      if (this->IsVisible()) {
          this->Layout();
      }
  
      return *this;
  }
  
  ListOverlay& ListOverlay::SetAdapter(IScrollAdapterPtr adapter) {
      if (this->adapter != adapter) {
          this->adapter = adapter;
          this->listWindow->SetAdapter(adapter);
      }
  
      return *this;
  }
  
  ListOverlay& ListOverlay::SetAutoDismiss(bool autoDismiss) {
      this->autoDismiss = autoDismiss;
      return *this;
  }
  
  ListOverlay& ListOverlay::SetSelectedIndex(size_t index) {
      this->listWindow->SetSelectedIndex(index);
  
      if (!this->listWindow->IsEntryVisible(index)) {
          this->listWindow->ScrollTo(index);
      }
  
      return *this;
  }
  
  ListOverlay& ListOverlay::SetItemSelectedCallback(ItemSelectedCallback cb) {
      this->itemSelectedCallback = cb;
      return *this;
  }
  
  ListOverlay& ListOverlay::SetDeleteKeyCallback(DeleteKeyCallback cb) {
      this->deleteKeyCallback = cb;
      return *this;
  }
  
  ListOverlay& ListOverlay::SetDismissedCallback(DismissedCallback cb) {
      this->dismissedCallback = cb;
      return *this;
  }
  
  ListOverlay& ListOverlay::SetKeyInterceptorCallback(KeyInterceptorCallback cb) {
      this->keyInterceptorCallback = cb;
      return *this;
  }
  
  void ListOverlay::OnListEntryActivated(cursespp::ListWindow* sender, size_t index) {
      if (itemSelectedCallback) {
          itemSelectedCallback(this, this->adapter, index);
      }
      if (this->autoDismiss) {
          this->Dismiss();
      }
  }
  
  size_t ListOverlay::GetSelectedIndex() {
      return this->listWindow->GetSelectedIndex();
  }
  
  bool ListOverlay::KeyPress(const std::string& key) {
      if (keyInterceptorCallback && keyInterceptorCallback(this, key)) {
          return true;
      }
      else if (key == "^[") { /* esc closes */
          this->Dismiss();
          return true;
      }
      else if (key == " ") { /* space bar also toggles activation */
          this->OnListEntryActivated(
              this->listWindow.get(),
              this->listWindow->GetSelectedIndex());
          return true;
      }
      else if (key == "KEY_BACKSPACE" || key == "KEY_DC") {
          if (deleteKeyCallback) {
              deleteKeyCallback(
                  this,
                  this->adapter,
                  listWindow->GetSelectedIndex());
  
              return true;
          }
      }
  
      return LayoutBase::KeyPress(key);
  }
  
  void ListOverlay::OnVisibilityChanged(bool visible) {
      LayoutBase::OnVisibilityChanged(visible);
      if (visible) {
          this->UpdateContents();
      }
  }
  
  void ListOverlay::OnDismissed() {
      if (this->dismissedCallback) {
          this->dismissedCallback(this);
      }
  }
  
  void ListOverlay::RecalculateSize() {
      if (this->setWidthPercent > 0) {
          int cx = Screen::GetWidth();
          this->width = (int)((this->setWidthPercent / 100.0f) * cx);
      }
      else {
          this->width = this->setWidth > 0 ? this->setWidth : DEFAULT_WIDTH;
      }
  
      this->height = 4; /* top frame + text + space + bottom frame */
      const int maxHeight = Screen::GetHeight() - 4;
  
      if (this->adapter) {
          this->height = std::min(
              maxHeight,
              (int) (4 + this->adapter->GetEntryCount()));
      }
  
      /* constrain to app bounds */
      this->height = std::max(0, std::min(Screen::GetHeight() - 4, this->height));
      this->width = std::max(0, std::min(Screen::GetWidth() - 4, this->width));
  
      this->y = VERTICAL_PADDING;
      this->x = (Screen::GetWidth() / 2) - (this->width / 2);
  }
  
  void ListOverlay::UpdateContents() {
      if (!this->IsVisible() || this->width <= 0 || this->height <= 0) {
          return;
      }
  
      WINDOW* c = this->GetContent();
  
      const int currentX = 1;
      int currentY = 0;
  
      if (this->title.size()) {
          wmove(c, currentY, currentX);
          wattron(c, A_BOLD);
          checked_wprintw(c, text::Align(this->title, text::AlignCenter, this->width - 4).c_str());
          wattroff(c, A_BOLD);
          currentY += 2;
      }
  }
  
  void ListOverlay::RefreshAdapter() {
      this->listWindow->OnAdapterChanged();
  }
  