/* 
 * Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "base/string_utilities.h"
#include "base/file_utilities.h"

#include "mforms/popup.h"
#include "mforms/menu.h"
#include "mforms/menubar.h"
#include "mforms/utilities.h"
#include "mforms/drawbox.h"
#include "mforms/textentry.h"
#include "mforms/imagebox.h"

#include "home_screen.h"
#include "home_screen_connections.h"

#include "workbench/wb_context_names.h"

using namespace wb;
//--------------------------------------------------------------------------------------------------

// The following helpers are just temporary. They will be replaced by a cairo context class.
inline void delete_surface(cairo_surface_t* surface)
{
  if (surface != NULL)
    cairo_surface_destroy(surface);
}

//--------------------------------------------------------------------------------------------------

static int image_width(cairo_surface_t* image)
{
  if (image != NULL)
  {
    if (mforms::Utilities::is_hidpi_icon(image) && mforms::App::get()->backing_scale_factor() > 1.0)
      return (int)(cairo_image_surface_get_width(image) / mforms::App::get()->backing_scale_factor());
    else
      return (int)cairo_image_surface_get_width(image);
  }
  return 0;
}

//--------------------------------------------------------------------------------------------------

static int image_height(cairo_surface_t* image)
{
  if (image != NULL)
  {
    if (mforms::Utilities::is_hidpi_icon(image) && mforms::App::get()->backing_scale_factor() > 1.0)
      return (int)(cairo_image_surface_get_height(image) / mforms::App::get()->backing_scale_factor());
    else
      return (int)cairo_image_surface_get_height(image);
  }
  return 0;
}

//--------------------------------------------------------------------------------------------------

/**
 * Helper to draw text with a hot decoration.
 */
void text_with_decoration(cairo_t* cr, double x, double y, const char* text, bool hot, double width)
{
  cairo_move_to(cr, x, y);
  cairo_show_text(cr, text);
  cairo_stroke(cr);
  
  // TODO: replace this with font decoration once pango is incorporated.
  if (hot)
  {
    cairo_set_line_width(cr, 1);
    cairo_move_to(cr, x, (int)y + 2.5);
    cairo_line_to(cr, x + width, (int)y + 2.5);
    cairo_stroke(cr);
  }
}

//----------------- DocumentsSection ---------------------------------------------------------------

struct DocumentEntry: mforms::Accessible
{
  grt::StringRef path;
  time_t timestamp; // Last accessed as timestamp for sorting.

  std::string title;
  std::string title_shorted;
  std::string folder;
  std::string folder_shorted;
  std::string schemas;
  std::string schemas_shorted;
  std::string last_accessed;
  std::string size;

  base::Rect bounds;
  bool is_model;

  bool operator < (const DocumentEntry &other) const
  {
    return other.timestamp < timestamp; // Sort from newest do oldest.
  }

  //------ Accessibility Methods -----
  virtual std::string get_acc_name() { return title; }
  virtual std::string get_acc_description() 
  { 
    return base::strfmt("schemas:%s;last_accessed:%s;size:%s", schemas.c_str(), last_accessed.c_str(), size.c_str()); 
  }

  virtual Accessible::Role get_acc_role() { return Accessible::ListItem;}
  virtual base::Rect get_acc_bounds() { return bounds;}
  virtual std::string get_acc_default_action() { return "Open Model";}
};

class DocumentsSection: public mforms::DrawBox
{
private:
  HomeScreen *_owner;

  cairo_surface_t* _model_icon;
  cairo_surface_t* _sql_icon;
  cairo_surface_t* _page_down_icon;
  cairo_surface_t* _page_up_icon;
  cairo_surface_t* _plus_icon;
  cairo_surface_t* _schema_icon;
  cairo_surface_t* _time_icon;
  cairo_surface_t* _folder_icon;
  cairo_surface_t* _size_icon;
  cairo_surface_t* _close_icon;
  cairo_surface_t* _open_icon;
  cairo_surface_t* _action_icon;
  float _backing_scale_when_icons_loaded;

  ssize_t _page_start;
  ssize_t _entries_per_page;
  ssize_t _entries_per_row;

  bool _show_selection_message; // Additional info to let the user a connection (when opening a script).
  base::Rect _message_close_button_rect;

  typedef std::vector<DocumentEntry>::iterator DocumentIterator;
  std::vector<DocumentEntry> _documents;
  std::vector<DocumentEntry> _filtered_documents;

  mforms::Menu *_model_context_menu;
  mforms::Menu *_model_action_menu;

  ssize_t _hot_entry;
  ssize_t _active_entry;
  enum DisplayMode {Nothing, ModelsOnly, ScriptsOnly, Mixed} _display_mode;

  boost::function <bool (int, int)> _accessible_click_handler;

  HomeAccessibleButton _add_button;
  HomeAccessibleButton _open_button;
  HomeAccessibleButton _action_button;
  HomeAccessibleButton _page_up_button;
  HomeAccessibleButton _page_down_button;

  base::Rect _close_button_rect;
  base::Rect _use_default_button_rect;

  DisplayMode _hot_heading;
  base::Rect _model_heading_rect;
  base::Rect _sql_heading_rect;
  base::Rect _mixed_heading_rect;
public:

  DocumentsSection(HomeScreen *owner)
  {
    _owner = owner;
    _page_start = 0;
    _model_context_menu = NULL;
    _model_action_menu = NULL;
    _hot_entry = -1;
    _active_entry = -1;
    _display_mode = ModelsOnly;
    _hot_heading = Nothing;
    _entries_per_page = 0;
    _entries_per_row = 0;
    _show_selection_message = false;
    _backing_scale_when_icons_loaded = 0.0;

    load_icons();

    _add_button.name = "Add Model";
    _add_button.default_action = "Create New Model";
    _add_button.default_handler = _accessible_click_handler;

    _open_button.name = "Open Model";
    _open_button.default_action = "Open Existing Model";
    _open_button.default_handler = _accessible_click_handler;

    _action_button.name = "Create Model Options";
    _action_button.default_action = "Open Create Model Options Menu";
    _action_button.default_handler = _accessible_click_handler;

    _page_up_button.name = "Page Up";
    _page_up_button.default_action = "Move Model Pages Up";
    _page_up_button.default_handler = _accessible_click_handler;
    
    _page_down_button.name = "Page Down";
    _page_down_button.default_action = "Move Model Pages Down";
    _page_down_button.default_handler = _accessible_click_handler;
  }

  //------------------------------------------------------------------------------------------------

  ~DocumentsSection()
  {
    if (_model_context_menu != NULL)
      _model_context_menu->release();

    delete_surface(_page_down_icon);
    delete_surface(_page_up_icon);
    delete_surface(_plus_icon);
    delete_surface(_model_icon);
    delete_surface(_sql_icon);
    delete_surface(_schema_icon);
    delete_surface(_time_icon);
    delete_surface(_folder_icon);
    delete_surface(_size_icon);
    delete_surface(_close_icon);
    delete_surface(_open_icon);
    delete_surface(_action_icon);
  }

  //------------------------------------------------------------------------------------------------

#define DOCUMENTS_LEFT_PADDING     40
#define DOCUMENTS_RIGHT_PADDING    40
#define DOCUMENTS_TOP_PADDING      64
#define DOCUMENTS_VERTICAL_SPACING 26

#define DOCUMENTS_ENTRY_WIDTH     250 // No spacing horizontally.
#define DOCUMENTS_ENTRY_HEIGHT     60
#define DOCUMENTS_HEADING_SPACING  10 // Spacing between a heading part and a separator.
#define DOCUMENTS_TOP_BASELINE     40 // Vertical space from top border to title base line.

  size_t entry_from_point(int x, int y)
  {
    int width = get_width();
    if (x < DOCUMENTS_LEFT_PADDING || x > (width - DOCUMENTS_RIGHT_PADDING) ||
      y < DOCUMENTS_TOP_PADDING)
      return -1; // Outside the entries area.

    x -= DOCUMENTS_LEFT_PADDING;

    y -= DOCUMENTS_TOP_PADDING;
    if ((y % (DOCUMENTS_ENTRY_HEIGHT + DOCUMENTS_VERTICAL_SPACING)) > DOCUMENTS_ENTRY_HEIGHT)
      return -1; // Within the vertical spacing between two entries.

    width -= DOCUMENTS_LEFT_PADDING + DOCUMENTS_RIGHT_PADDING;
    _entries_per_row = width / DOCUMENTS_ENTRY_WIDTH;
    if (x >= _entries_per_row * DOCUMENTS_ENTRY_WIDTH)
      return -1; // After the last entry in a row.

    int height = get_height() - DOCUMENTS_TOP_PADDING;
    int column = x / DOCUMENTS_ENTRY_WIDTH;
    int row = y / (DOCUMENTS_ENTRY_HEIGHT + DOCUMENTS_VERTICAL_SPACING);

    int row_bottom = row * (DOCUMENTS_ENTRY_HEIGHT + DOCUMENTS_VERTICAL_SPACING) + DOCUMENTS_ENTRY_HEIGHT;
    if (row_bottom > height)
      return -1; // The last visible row is dimmed if not fully visible. So take it out from hit tests too.

    size_t count = _filtered_documents.size();
    size_t index = _page_start + row * _entries_per_row + column;
    if (index < count)
      return index;

    return -1;
  }

  //------------------------------------------------------------------------------------------------

  /**
   * Draws and icon followed by the given text. The given position is that of the upper left corner
   * of the image.
   */
  void draw_icon_with_text(cairo_t *cr, int x, int y, cairo_surface_t *icon,
    const std::string &text, bool high_contrast)
  {
    mforms::Utilities::paint_icon(cr, icon, x, y);
    x += image_width(icon) + 3;

    cairo_text_extents_t extents;
    cairo_text_extents(cr, text.c_str(), &extents);

    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, 0.5);
    cairo_move_to(cr, x , (int)(y + image_height(icon) / 2.0 + extents.height / 2.0));
    cairo_show_text(cr, text.c_str());
    cairo_stroke(cr);
  }

  //------------------------------------------------------------------------------------------------

  void draw_paging_part(cairo_t *cr, int current_page, int pages, bool high_contrast)
  {
    cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);

    std::string page_string = base::strfmt("%d/%d", ++current_page, pages);
    cairo_text_extents_t extents;
    cairo_text_extents(cr, page_string.c_str(), &extents);

    _page_down_button.bounds = base::Rect(0, 0, image_width(_page_down_icon), image_height(_page_down_icon));
    double y = get_height() - _page_down_button.bounds.width() - 6;
    double x = get_width() - extents.width - 8;
    double icon_x = x + ceil((extents.width - _page_down_button.bounds.width()) / 2.0) + 1;
    _page_down_button.bounds.pos = base::Point(icon_x, y);

    cairo_set_source_surface(cr, _page_down_icon, icon_x, y);
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_XOR);

    if (current_page == pages)
    {
      // If we are on the last page then dim the page down button and remove the button
      // rectangle used for hit tests (so the user can't click it).
      cairo_paint_with_alpha(cr, 0.5);
      _page_down_button.bounds = base::Rect();
    }
    else
      cairo_paint(cr);

    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

    y -= 6;

    double component = 0x5E / 255.0;
    if (high_contrast)
      component = 0;

    cairo_set_source_rgb(cr, component, component, component);
    cairo_move_to(cr, x, y);
    cairo_show_text(cr, page_string.c_str());
    cairo_stroke(cr);

    _page_up_button.bounds = base::Rect(icon_x, 0, image_width(_page_up_icon), image_height(_page_up_icon));
    y -= extents.height + 6 + _page_up_button.bounds.height();
    _page_up_button.bounds.pos.y = y;

    cairo_set_source_surface(cr, _page_up_icon, icon_x, y);
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_XOR);

    if (current_page == 1)
    {
      cairo_paint_with_alpha(cr, 0.5);
      _page_up_button.bounds = base::Rect();
    }
    else
      cairo_paint_with_alpha(cr, 1);

    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
  }

  //------------------------------------------------------------------------------------------------

  void draw_entry(cairo_t *cr, const DocumentEntry &entry, bool hot, bool high_contrast)
  {
    mforms::Utilities::paint_icon(cr, _model_icon, entry.bounds.left(), entry.bounds.top() + 26);

    int icon_width, icon_height;
    mforms::Utilities::get_icon_size(_model_icon, icon_width, icon_height);

    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgb(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0);
    cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);
    int x = (int)entry.bounds.left();
    int y = (int)entry.bounds.top() + 18;
    if (hot)
    {
      double width = 0;
      cairo_text_extents_t extents;
      cairo_text_extents(cr, entry.title.c_str(), &extents);
      width = ceil(extents.width);

      cairo_save(cr);
      if (high_contrast)
        cairo_set_source_rgb(cr, 1, 1, 1);
      else
        cairo_set_source_rgb(cr, 0, 0, 0);
      text_with_decoration(cr, x-1, y, entry.title.c_str(), true, width);
      text_with_decoration(cr, x+1, y, entry.title.c_str(), true, width);
      text_with_decoration(cr, x, y-1, entry.title.c_str(), true, width);
      text_with_decoration(cr, x, y+1, entry.title.c_str(), true, width);
      cairo_restore(cr);

      text_with_decoration(cr, x, y, entry.title.c_str(), true, width);
    }
    else
      text_with_decoration(cr, x, y, entry.title_shorted.c_str(), false, 0);

    x += icon_width + 10;

    cairo_set_font_size(cr, HOME_SMALL_INFO_FONT_SIZE);

    draw_icon_with_text(cr, x, (int)entry.bounds.top() + 26, _folder_icon,
                        entry.folder_shorted, high_contrast);
    if (entry.is_model)
      draw_icon_with_text(cr, x, (int)entry.bounds.top() + 40, _schema_icon,
      entry.schemas.empty() ? "--" : entry.schemas_shorted, high_contrast);
    else
      draw_icon_with_text(cr, x, (int)entry.bounds.top() + 40, _size_icon,
        entry.size.empty() ? "--" : entry.size, high_contrast);
    draw_icon_with_text(cr, x, (int)entry.bounds.top() + 54, _time_icon, entry.last_accessed,
      high_contrast);
  }

  //------------------------------------------------------------------------------------------------

  void update_filtered_documents()
  {
    _filtered_documents.clear();
    _filtered_documents.reserve(_documents.size());
    switch (_display_mode)
    {
    case ModelsOnly:
      {
        // std::copy_if is C++11 only, so we do it manually.
        for (DocumentIterator source = _documents.begin(); source != _documents.end(); source++)
        {
          if (source->is_model)
            _filtered_documents.push_back(*source);
        }
        break;
      }

    case ScriptsOnly:
      {
        for (DocumentIterator source = _documents.begin(); source != _documents.end(); source++)
        {
          if (!source->is_model)
            _filtered_documents.push_back(*source);
        }
        break;
      }

    default: // Mixed mode. All types are shown.
      _filtered_documents = _documents;
    }
  }

  //------------------------------------------------------------------------------------------------

#define MESSAGE_WIDTH 200
#define MESSAGE_HEIGHT 75

#define POPUP_TIP_HEIGHT 14

  void draw_selection_message(cairo_t *cr, bool high_contrast)
  {
    // Attach the message to the current active entry as this is what is used when
    // a connection is opened.
    ssize_t column = (_active_entry - _page_start) % _entries_per_row;
    ssize_t row = (_active_entry - _page_start) / _entries_per_row;
    int hotspot_x = (int)(DOCUMENTS_LEFT_PADDING + (column + 0.5) * DOCUMENTS_ENTRY_WIDTH);
    int hotspot_y = (int)(DOCUMENTS_TOP_PADDING + (row + 1) * DOCUMENTS_ENTRY_HEIGHT);
    base::Rect message_rect = base::Rect(hotspot_x - MESSAGE_WIDTH / 2, hotspot_y + POPUP_TIP_HEIGHT,
                                         MESSAGE_WIDTH, MESSAGE_HEIGHT);
    if (message_rect.pos.x < 10)
      message_rect.pos.x = 10;
    if (message_rect.right() > get_width() - 10)
      message_rect.pos.x = get_width() - message_rect.width() - 10;

    bool flipped = false;
    if (message_rect.bottom() > get_height() - 10)
    {
      flipped = true;
      message_rect.pos.y -= MESSAGE_HEIGHT + 2 * POPUP_TIP_HEIGHT + DOCUMENTS_ENTRY_HEIGHT - 10;
    }

    cairo_set_source_rgba(cr, 0, 0, 0, 0.9);
    cairo_rectangle(cr, message_rect.left(), message_rect.top(), MESSAGE_WIDTH, MESSAGE_HEIGHT);
    cairo_move_to(cr, message_rect.left(), message_rect.top());
    if (flipped)
    {
      cairo_rel_line_to(cr, MESSAGE_WIDTH, 0);
      cairo_rel_line_to(cr, 0, MESSAGE_HEIGHT);
      cairo_line_to(cr, hotspot_x + POPUP_TIP_HEIGHT, message_rect.bottom());
      cairo_rel_line_to(cr, -POPUP_TIP_HEIGHT, POPUP_TIP_HEIGHT);
      cairo_rel_line_to(cr, -POPUP_TIP_HEIGHT, -POPUP_TIP_HEIGHT);
      cairo_line_to(cr, message_rect.left(), message_rect.bottom());
    }
    else
    {
      cairo_line_to(cr, hotspot_x - POPUP_TIP_HEIGHT, message_rect.top());
      cairo_rel_line_to(cr, POPUP_TIP_HEIGHT, -POPUP_TIP_HEIGHT);
      cairo_rel_line_to(cr, POPUP_TIP_HEIGHT, POPUP_TIP_HEIGHT);
      cairo_line_to(cr, message_rect.right(), message_rect.top());
      cairo_rel_line_to(cr, 0, MESSAGE_HEIGHT);
      cairo_rel_line_to(cr, -MESSAGE_WIDTH, 0);
    }

    cairo_fill(cr);

    cairo_set_font_size(cr, HOME_DETAILS_FONT_SIZE);
    cairo_font_extents_t extents;
    cairo_font_extents(cr, &extents);

    int y = (int)(message_rect.top() + extents.height + 4);

    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgb(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0);
    cairo_move_to(cr, message_rect.left() + 10, y);
    cairo_show_text(cr, _("Please select a connection"));

    y += (int)ceil(extents.height);
    cairo_move_to(cr, message_rect.left() + 10, y);
    cairo_show_text(cr, _("to open this script with."));

    std::string use_default = _("Use Default");
    cairo_text_extents_t text_extents;
    cairo_text_extents(cr, use_default.c_str(), &text_extents);
    int x = (int)(message_rect.left() + (MESSAGE_WIDTH - text_extents.width) / 2);
    y = (int) message_rect.bottom() - 15;
    cairo_move_to(cr, x, y);
    cairo_show_text(cr, use_default.c_str());
    _use_default_button_rect = base::Rect(x - 7.5, y - ceil(text_extents.height) - 5.5, ceil(text_extents.width) + 16, ceil(text_extents.height) + 12);
    cairo_rectangle(cr, _use_default_button_rect.left(), _use_default_button_rect.top(),
                    _use_default_button_rect.width(), _use_default_button_rect.height());
    cairo_stroke(cr);

    _close_button_rect = base::Rect(message_rect.right() - image_width(_close_icon) - 4, message_rect.top() + 6,
                                    image_width(_close_icon), image_height(_close_icon));
    cairo_set_source_surface(cr, _close_icon, _close_button_rect.left(), _close_button_rect.top());
    cairo_paint(cr);
  }

  //------------------------------------------------------------------------------------------------

  void layout(cairo_t *cr)
  {
    if (is_layout_dirty())
    {
      set_layout_dirty(false);

      cairo_text_extents_t extents;

      // Keep in mind text rectangles are flipped (top is actually the base line of the text).
      double heading_left = DOCUMENTS_LEFT_PADDING;
      cairo_text_extents(cr, _("Models"), &extents);
      double text_width = ceil(extents.width);
      _model_heading_rect = base::Rect(heading_left, DOCUMENTS_TOP_BASELINE, text_width, ceil(extents.height));

      // Models (+) ...
      heading_left += text_width + DOCUMENTS_HEADING_SPACING;
      _add_button.bounds = base::Rect(heading_left, DOCUMENTS_TOP_BASELINE - image_height(_plus_icon),
        image_width(_plus_icon), image_height(_plus_icon));

      _open_button.bounds = base::Rect(_add_button.bounds.right() + 10, DOCUMENTS_TOP_BASELINE - image_height(_open_icon),
        image_width(_open_icon), image_height(_open_icon));

      _action_button.bounds = base::Rect(_open_button.bounds.right() + 10, DOCUMENTS_TOP_BASELINE - image_height(_action_icon),
        image_width(_action_icon), image_height(_action_icon));

      /* Disabled for now.
      // (+) | ...
      heading_left += 2 * DOCUMENTS_HEADING_SPACING + image_width(_plus_icon) + 1;
      cairo_text_extents(cr, _("SQL Scripts"), &extents);
      text_width = ceil(extents.width);
      _sql_heading_rect = base::Rect(heading_left, DOCUMENTS_TOP_BASELINE, text_width, ceil(extents.height));

      // SQL Scripts | ...
      heading_left += text_width + 2 * DOCUMENTS_HEADING_SPACING + 1;
      cairo_text_extents(cr, _("Recent Documents"), &extents);
      text_width = ceil(extents.width);
      _mixed_heading_rect = base::Rect(heading_left, DOCUMENTS_TOP_BASELINE, text_width, ceil(extents.height));
*/

      // Compute the shorted strings.
      cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);

      int model_icon_width = image_width(_model_icon);
      int sql_icon_width = image_width(_sql_icon);
      for (std::vector<DocumentEntry>::iterator iterator = _documents.begin();
        iterator != _documents.end(); iterator++)
      {
        double details_width = DOCUMENTS_ENTRY_WIDTH - 10 - (iterator->is_model ? model_icon_width : sql_icon_width);
        if (iterator->title_shorted.empty() && !iterator->title.empty())
          iterator->title_shorted = mforms::Utilities::shorten_string(cr, iterator->title, details_width);

        if (iterator->folder_shorted.empty() && !iterator->folder.empty())
        {
          // shorten the string while reversed, so that we truncate the beginning of the string instead of the end
          gchar *rev = g_utf8_strreverse(iterator->folder.data(), (gssize)iterator->folder.size());
          iterator->folder_shorted = mforms::Utilities::shorten_string(cr, rev, details_width);
          if (iterator->folder_shorted.compare(rev) != 0) // string was shortened
          {
            g_free(rev);
            iterator->folder_shorted = iterator->folder_shorted.substr(0, iterator->folder_shorted.size()-3); // strip the ...
            rev = g_utf8_strreverse(iterator->folder_shorted.data(), (gssize)iterator->folder_shorted.size());
            iterator->folder_shorted = std::string("...") + rev;
            g_free(rev);
          }
          else
          {
            g_free(rev);
            iterator->folder_shorted = iterator->folder;
          }
        }

        if (iterator->schemas_shorted.empty() && !iterator->schemas.empty())
          iterator->schemas_shorted = mforms::Utilities::shorten_string(cr, iterator->schemas, details_width - 10 - image_width(_schema_icon));
      }

      update_filtered_documents();
    }
  }

  //------------------------------------------------------------------------------------------------


  void load_icons()
  {
    if (_backing_scale_when_icons_loaded != mforms::App::get()->backing_scale_factor())
    {
      // reload icons if the backing scale changed
      if (_backing_scale_when_icons_loaded != 0)
      {
        delete_surface(_model_icon);
        delete_surface(_schema_icon);
        delete_surface(_time_icon);
        delete_surface(_folder_icon);
      }
      _model_icon = mforms::Utilities::load_icon("wb_doc_model.png", true);
      _schema_icon = mforms::Utilities::load_icon("wb_tile_schema.png", true);
      _time_icon = mforms::Utilities::load_icon("wb_tile_time.png", true);
      _folder_icon = mforms::Utilities::load_icon("wb_tile_folder_mini.png", true);

      if (_backing_scale_when_icons_loaded == 0)
      {
        _page_down_icon = mforms::Utilities::load_icon("wb_tile_page-down.png");
        _page_up_icon = mforms::Utilities::load_icon("wb_tile_page-up.png");
        _plus_icon = mforms::Utilities::load_icon("wb_tile_plus.png");
        _sql_icon = mforms::Utilities::load_icon("wb_doc_sql.png");
        _size_icon = mforms::Utilities::load_icon("wb_tile_number.png");
        _close_icon = mforms::Utilities::load_icon("wb_close.png");
        _open_icon = mforms::Utilities::load_icon("wb_tile_open.png");
        _action_icon = mforms::Utilities::load_icon("wb_tile_more.png");
      }

      _backing_scale_when_icons_loaded = mforms::App::get()->backing_scale_factor();
    }
  }

  //------------------------------------------------------------------------------------------------

  void repaint(cairo_t *cr, int areax, int areay, int areaw, int areah)
  {
    int width = get_width();
    int height = get_height();

    load_icons();

    cairo_set_line_width(cr, 1);
    cairo_select_font_face(cr, HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_TITLE_FONT_SIZE);

    layout(cr);

#ifdef __APPLE__
    // On Mac we draw a radial background gradient as if the background is lit by a single light source.
    cairo_pattern_t *pattern = cairo_pattern_create_radial(width / 2.0, -10, 10,
                                                       width / 2.0, -10, 0.6 * width);
    cairo_pattern_add_color_stop_rgba(pattern, 0, 1, 1, 1, 0.05);
    cairo_pattern_add_color_stop_rgba(pattern, 1, 1, 1, 1, 0);
    cairo_set_source(cr, pattern);
    cairo_rectangle(cr, 0, 0, width, height);
    cairo_fill(cr);
    cairo_pattern_destroy(pattern);
#endif

    width -= DOCUMENTS_LEFT_PADDING + DOCUMENTS_RIGHT_PADDING;
    cairo_set_font_size(cr, HOME_TITLE_FONT_SIZE);
    int entries_per_row = width / DOCUMENTS_ENTRY_WIDTH;

    bool high_contrast = base::Color::is_high_contrast_scheme();
    // Heading for switching display mode. Draw heading hot only when we support more sections.
    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgba(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0, _display_mode == ModelsOnly ? 1 : 0.2);
    text_with_decoration(cr, _model_heading_rect.left(), _model_heading_rect.top(), _("Models"),
      false /*_hot_heading == ModelsOnly*/, _model_heading_rect.width());

    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_XOR);

    cairo_set_source_surface(cr, _plus_icon, _add_button.bounds.left(), _add_button.bounds.top());
    cairo_paint(cr);

    cairo_set_source_surface(cr, _open_icon, _open_button.bounds.left(), _open_button.bounds.top());
    cairo_paint(cr);

    cairo_set_source_surface(cr, _action_icon, _action_button.bounds.left(), _action_button.bounds.top());
    cairo_paint(cr);

    /* Disabled for now.
    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgba(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0, _display_mode == ScriptsOnly ? 1 : 0.2);
    text_with_decoration(cr, _sql_heading_rect.left(), _sql_heading_rect.top(), _("SQL Scripts"),
      _hot_heading == ScriptsOnly, _sql_heading_rect.width());

    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgba(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0, _display_mode == Mixed ? 1 : 0.2);
    text_with_decoration(cr, _mixed_heading_rect.left(), _mixed_heading_rect.top(), _("Recent Documents"),
      _hot_heading == Mixed, _mixed_heading_rect.width());

    // Finally the separator lines. Text rects are flipped!
    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgba(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0, 0.2);
    cairo_move_to(cr, _sql_heading_rect.left() - DOCUMENTS_HEADING_SPACING + 0.5, _sql_heading_rect.top() - _sql_heading_rect.height() + 3);
    cairo_line_to(cr, _sql_heading_rect.left() - DOCUMENTS_HEADING_SPACING + 0.5, _sql_heading_rect.top() + 3);
    cairo_move_to(cr, _mixed_heading_rect.left() - DOCUMENTS_HEADING_SPACING + 0.5, _mixed_heading_rect.top() - _sql_heading_rect.height() + 3);
    cairo_line_to(cr, _mixed_heading_rect.left() - DOCUMENTS_HEADING_SPACING + 0.5, _mixed_heading_rect.top() + 3);
    cairo_stroke(cr);
*/
    
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

    int row = 0;
    base::Rect bounds(0, DOCUMENTS_TOP_PADDING, DOCUMENTS_ENTRY_WIDTH, DOCUMENTS_ENTRY_HEIGHT);
    bool done = false;
    while (!done)
    {
      bool draw_hot_entry = false;
      bounds.pos.x = DOCUMENTS_LEFT_PADDING;
      for (int column = 0; column < entries_per_row; column++)
      {
        size_t index = _page_start + row * entries_per_row + column;
        if (index >= _filtered_documents.size())
        {
          done = true;
          break;
        }
        else
        {
          _filtered_documents[index].bounds = bounds;
          if ((size_t)_hot_entry == index)
            draw_hot_entry = true;
          else
            draw_entry(cr, _filtered_documents[index], (size_t)_hot_entry == index, high_contrast);
        }
        bounds.pos.x += DOCUMENTS_ENTRY_WIDTH;
      }
      if (draw_hot_entry)
        draw_entry(cr, _filtered_documents[_hot_entry], true, high_contrast);

      row++;
      bounds.pos.y += DOCUMENTS_ENTRY_HEIGHT + DOCUMENTS_VERTICAL_SPACING;
      if (bounds.top() >= height)
        done = true;
    }

    // See if we need to draw the paging indicator.
    height -= DOCUMENTS_TOP_PADDING;
    int rows_per_page = height / (DOCUMENTS_ENTRY_HEIGHT + DOCUMENTS_VERTICAL_SPACING);
    if (rows_per_page < 1)
      rows_per_page = 1;
    int rows = (int)ceil(_filtered_documents.size() / (float)entries_per_row);
    _entries_per_page = entries_per_row * rows_per_page;
    int pages = (int)ceil(rows / (float)rows_per_page);
    if (pages > 1)
    {
      int current_row = (int)ceil(_page_start / (float)entries_per_row);
      int current_page = (int)ceil(current_row / (float)rows_per_page);
      draw_paging_part(cr, current_page, pages, high_contrast);
    }
    else
    {
      _page_up_button.bounds = base::Rect();
      _page_down_button.bounds = base::Rect();
      _page_start = 0; // Size increased to cover the full content.
    }

    if (_show_selection_message)
      draw_selection_message(cr, high_contrast);
  }

  //------------------------------------------------------------------------------------------------

  void add_document(const grt::StringRef &path, const time_t &time, const std::string schemas,
    long file_size)
  {
    DocumentEntry entry;
    entry.path = path;
    entry.timestamp = time;
    entry.schemas = schemas;

    entry.title = base::strip_extension(base::basename(path));
    if (entry.title.empty())
      entry.title = "???";
    entry.is_model = base::ends_with(path, ".mwb") || base::ends_with(path, ".mwbd");
    entry.folder = base::dirname(path);

    if (time > 0)
    {
      struct tm * ptm = localtime(&time);
      char buffer[32];
      strftime(buffer, 32, "%d %b %y, %H:%M", ptm);
      entry.last_accessed = buffer;
    }
    if (file_size == 0)
      entry.size = "--";
    else
    {
      // Format file size in human readable format. 1000 bytes per K on OSX, otherwise 1024.
#ifdef __APPLE__
      double unit_size = 1000;
#else
      double unit_size = 1024;
#endif
      int i = 0;
      double size = file_size;
      const char* units[] = {"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
      while (size > unit_size) {
        size /= unit_size;
        i++;
      }
      entry.size = base::strfmt("%.*f %s", i, size, units[i]);
    }
    _documents.push_back(entry);
    set_layout_dirty(true);
  }

  //------------------------------------------------------------------------------------------------

  void clear_documents()
  {
    _documents.clear();
    set_layout_dirty(true);
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_double_click(mforms::MouseButton button, int x, int y)
  {
    // Similar handling like for single mouse down.
    switch (button)
    {
      case mforms::MouseButtonLeft:
      {
        if (_add_button.bounds.contains(x, y))
        {
          if (_display_mode != ModelsOnly)
          {
            _display_mode = ModelsOnly;
            _page_start = 0;
            update_filtered_documents();
            set_needs_repaint();
          }

          _owner->trigger_callback(ActionNewEERModel, grt::ValueRef());
          return true;
        }

        if (_open_button.bounds.contains(x, y))
        {
          if (_display_mode != ModelsOnly)
          {
            _display_mode = ModelsOnly;
            _page_start = 0;
            update_filtered_documents();
            set_needs_repaint();
          }

          _owner->trigger_callback(ActionOpenEERModel, grt::ValueRef());
          return true;
        }

        if (_action_button.bounds.contains(x, y))
        {
          if (_display_mode == ModelsOnly && _model_action_menu != NULL)
          {
            _model_action_menu->popup_at(this, x, y);
            return true;
          }
        }

        if (_page_up_button.bounds.contains(x, y))
        {
          // Page up clicked. Doesn't happen if we are on the first page already.
          _page_start -= _entries_per_page;
          if (_page_start < 0)
            _page_start = 0;
          set_needs_repaint();
          return true;
        }

        if (_page_down_button.bounds.contains(x, y))
        {
          _page_start += _entries_per_page;
          set_needs_repaint();
          return true;
        }

        if (_model_heading_rect.contains_flipped(x, y))
        {
          if (_display_mode != ModelsOnly)
          {
            _display_mode = ModelsOnly;
            _page_start = 0;
            update_filtered_documents();
            set_needs_repaint();
          }
          return true;
        }

        if (_sql_heading_rect.contains_flipped(x, y))
        {
          if (_display_mode != ScriptsOnly)
          {
            _display_mode = ScriptsOnly;
            _page_start = 0;
            update_filtered_documents();
            set_needs_repaint();
          }
          return true;
        }

        if (_mixed_heading_rect.contains_flipped(x, y))
        {
          if (_display_mode != Mixed)
          {
            _display_mode = Mixed;
            _page_start = 0;
            update_filtered_documents();
            set_needs_repaint();
          }
          return true;
        }

        // Anything else.
        _active_entry = entry_from_point(x, y);
        if (_active_entry > -1)
        {
          if (_filtered_documents[_active_entry].is_model)
            _owner->trigger_callback(ActionOpenEERModelFromList, _filtered_documents[_active_entry].path);
          else
            _owner->trigger_callback(ActionEditSQLScript, _filtered_documents[_active_entry].path);
          return true;
        }
      }
      break;

      case mforms::MouseButtonRight:
      {
        if (_display_mode == ModelsOnly)
        {
          _active_entry = entry_from_point(x, y);
          if (_active_entry > -1 && _model_context_menu != NULL)
          {
            _model_context_menu->popup_at(this, x, y);
            return true;
          }
        }

        break;

      default:
        break;
      }
    }

    return false;
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_click(mforms::MouseButton button, int x, int y)
  {
    switch (button)
    {
      case mforms::MouseButtonLeft:
        {
          if (_show_selection_message && _close_button_rect.contains(x, y))
          {
            _owner->cancel_script_loading();
            return true;
          }

          if (_add_button.bounds.contains(x, y))
          {
            if (_display_mode != ModelsOnly)
            {
              _display_mode = ModelsOnly;
              _page_start = 0;
              update_filtered_documents();
              set_needs_repaint();
            }

            _owner->trigger_callback(ActionNewEERModel, grt::ValueRef());
            return true;
          }

          if (_open_button.bounds.contains(x, y))
          {
            if (_display_mode != ModelsOnly)
            {
              _display_mode = ModelsOnly;
              _page_start = 0;
              update_filtered_documents();
              set_needs_repaint();
            }

            _owner->trigger_callback(ActionOpenEERModel, grt::ValueRef());
            return true;
          }

          if (_action_button.bounds.contains(x, y))
          {
            if (_display_mode == ModelsOnly && _model_action_menu != NULL)
              _model_action_menu->popup_at(this, x, y);
          }

          if (_page_up_button.bounds.contains(x, y))
          {
            _owner->cancel_script_loading();

            // Page up clicked. Doesn't happen if we are on the first page already.
            _page_start -= _entries_per_page;
            if (_page_start < 0)
              _page_start = 0;
            set_needs_repaint();
            return true;
          }

          if (_page_down_button.bounds.contains(x, y))
          {
            _owner->cancel_script_loading();

            _page_start += _entries_per_page;
            set_needs_repaint();
            return true;
          }

          if (_model_heading_rect.contains_flipped(x, y))
          {
            _owner->cancel_script_loading();

            if (_display_mode != ModelsOnly)
            {
              _display_mode = ModelsOnly;
              _page_start = 0;
              update_filtered_documents();
              set_needs_repaint();
            }
            return true;
          }

          if (_sql_heading_rect.contains_flipped(x, y))
          {
            if (_display_mode != ScriptsOnly)
            {
              _display_mode = ScriptsOnly;
              _page_start = 0;
              update_filtered_documents();
              set_needs_repaint();
            }
            return true;
          }

          if (_mixed_heading_rect.contains_flipped(x, y))
          {
            _owner->cancel_script_loading();

            if (_display_mode != Mixed)
            {
              _display_mode = Mixed;
              _page_start = 0;
              update_filtered_documents();
              set_needs_repaint();
            }
            return true;
          }

          // Anything else.
          _active_entry = entry_from_point(x, y);
          if (_active_entry > -1)
          {
            _owner->cancel_script_loading();

            if (_filtered_documents[_active_entry].is_model)
              _owner->trigger_callback(ActionOpenEERModelFromList, _filtered_documents[_active_entry].path);
            else
              _owner->trigger_callback(ActionEditSQLScript, _filtered_documents[_active_entry].path);

            return true;
          }
        }
        break;

      case mforms::MouseButtonRight:
        {
          _owner->cancel_script_loading();

          if (_display_mode == ModelsOnly)
          {
            _active_entry = entry_from_point(x, y);
            if (_active_entry > -1 && _model_context_menu != NULL)
            {
              _model_context_menu->popup_at(this, x, y);
              return true;
            }
          }
        }
        break;

      default:
        break;
    }

    return false;
  }

  //--------------------------------------------------------------------------------------------------

  bool mouse_leave()
  {
    if (_hot_heading != Nothing || _hot_entry > -1)
    {
      _hot_heading = Nothing;
      _hot_entry = -1;
      set_needs_repaint();
      return true;
    }
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_move(mforms::MouseButton button, int x, int y)
  {
    bool result = false;
    ssize_t entry = entry_from_point(x, y);
    if (entry != _hot_entry)
    {
      _hot_entry = entry;
      result = true;
    }

    if (entry == -1)
    {
      DisplayMode mode;
      // No document hit, but perhaps one of the titles.
      if (_model_heading_rect.contains_flipped(x, y))
        mode = ModelsOnly;
      else
        if (_sql_heading_rect.contains_flipped(x, y))
          mode = ScriptsOnly;
        else
          if (_mixed_heading_rect.contains_flipped(x, y))
            mode = Mixed;
          else
            mode = Nothing;

      if (mode != _hot_heading)
      {
        _hot_heading = mode;
        result = true;
      }
    }

    if (result)
      set_needs_repaint();

    return result;
  }

  //--------------------------------------------------------------------------------------------------

  void set_context_menu(mforms::Menu *menu, bool forModels)
  {
    if (forModels)
    {
      if (_model_context_menu != NULL)
        _model_context_menu->release();
      _model_context_menu = menu;
      if (_model_context_menu != NULL)
        _model_context_menu->retain();
      
      menu->set_handler(boost::bind(&DocumentsSection::handle_command, this, _1));
    }
  }

  //------------------------------------------------------------------------------------------------

  void set_action_context_menu(mforms::Menu *menu, bool forModels)
  {
    if (forModels)
    {
      if (_model_action_menu != NULL)
        _model_action_menu->release();
      _model_action_menu = menu;
      if (_model_context_menu != NULL)
        _model_action_menu->retain();

      menu->set_handler(boost::bind(&DocumentsSection::handle_command, this, _1));
    }
  }

  //------------------------------------------------------------------------------------------------

  void handle_command(const std::string &command)
  {
    if (_active_entry > -1)
      _owner->handle_context_menu(_filtered_documents[_active_entry].path, command);
    else
      _owner->handle_context_menu(grt::ValueRef(), command);
    _active_entry = -1;
  }

  //------------------------------------------------------------------------------------------------

  void show_connection_select_message()
  {
    _show_selection_message = true;
    set_needs_repaint();
  }

  //------------------------------------------------------------------------------------------------

  void hide_connection_select_message()
  {
    _show_selection_message = false;
    set_needs_repaint();
  }

  //------------------------------------------------------------------------------------------------
  
  void cancel_operation()
  {
    _owner->cancel_script_loading();
  }

  //------------------------------------------------------------------------------------------------

  virtual int get_acc_child_count()
  { 
    // Initial value due to the add/open/create EER Model icons
    int ret_val = 3;
    ret_val += (int)_filtered_documents.size();

    // Adds a child for each paging icon if shown
    if (_page_up_button.bounds.width())
      ret_val += 2;

    return ret_val;
  }

  //------------------------------------------------------------------------------------------------
  
  virtual Accessible* get_acc_child(int index)
  { 
    mforms::Accessible* accessible = NULL;
    switch(index)
    {
      case 0:
        break;
      case 1:
        break;
      case 2:
        break;
      default:
        {
          index -=3;

          if (index < (int) _filtered_documents.size())
            accessible = &_filtered_documents[index];
          else
          {
            index -= (int)_filtered_documents.size();
            accessible = index ? &_page_down_button : &_page_up_button;
          }
        }
    }

    return accessible;
  }

  //------------------------------------------------------------------------------------------------
  
  virtual Accessible::Role get_acc_role()
  { 
    return Accessible::List;
  }

  //------------------------------------------------------------------------------------------------
  
  virtual mforms::Accessible* hit_test(int x, int y)
  { 
    mforms::Accessible* accessible = NULL;

    if (_add_button.bounds.contains(x, y))
      accessible = &_add_button;
    else if (_open_button.bounds.contains(x, y))
      accessible = &_open_button;
    else if (_action_button.bounds.contains(x, y))
      accessible = &_action_button;
    else if (_page_up_button.bounds.contains(x, y))
      accessible = &_page_up_button;
    else if (_page_down_button.bounds.contains(x, y))
      accessible = &_page_down_button;
    else
    {
      ssize_t entry = entry_from_point(x, y);
    
      if (entry != -1)
        accessible = &_filtered_documents[entry];
    }

    return accessible;
  }
};

//----------------- ShortcutSection ----------------------------------------------------------------

struct ShortcutEntry : mforms::Accessible
{
  app_StarterRef shortcut;

  cairo_surface_t *icon;
  std::string title;       // Shorted title, depending on available space.
  base::Rect title_bounds; // Relative bounds of the title text.
  base::Rect acc_bounds;   // Bounds to be used for accessibility

  // ------ Accesibility Methods -----
  virtual std::string get_acc_name() { return title; }
  virtual Accessible::Role get_acc_role() { return Accessible::ListItem; }
  virtual base::Rect get_acc_bounds() { return acc_bounds; }
  virtual std::string get_acc_default_action() { return "Open Item"; }
};

class ShortcutSection: public mforms::DrawBox
{
private:
  HomeScreen *_owner;
  cairo_surface_t* _default_shortcut_icon;

  std::vector<ShortcutEntry> _shortcuts;
  typedef std::vector<ShortcutEntry>::iterator ShortcutIterator;

  app_StarterRef _hot_shortcut;
  app_StarterRef _active_shortcut; // For the context menu.
  mforms::Menu _shortcut_context_menu;

  boost::function <bool (int, int)> _accessible_click_handler;

  HomeAccessibleButton _page_up_button;
  HomeAccessibleButton _page_down_button;

  ssize_t _page_start;
  ssize_t _shortcuts_per_page;
  cairo_surface_t *_page_down_icon;
  cairo_surface_t *_page_up_icon;

public:
  ShortcutSection(HomeScreen *owner)
  {
    _owner = owner;
    _hot_shortcut = app_StarterRef();
    _active_shortcut = app_StarterRef();
    _default_shortcut_icon = mforms::Utilities::load_icon("wb_starter_generic_52.png", true);
    _page_down_icon = mforms::Utilities::load_icon("wb_tile_page-down.png");
    _page_up_icon = mforms::Utilities::load_icon("wb_tile_page-up.png");

    _page_start = 0;

    _accessible_click_handler = boost::bind(&ShortcutSection::mouse_click, this,
      mforms::MouseButtonLeft, _1, _2);

    _page_up_button.name = "Page Up";
    _page_up_button.default_action = "Move Shortcut Pages Up";
    _page_up_button.default_handler = _accessible_click_handler;
    
    _page_down_button.name = "Page Down";
    _page_down_button.default_action = "Move Shortcut Pages Down";
    _page_down_button.default_handler = _accessible_click_handler;

    /* Disabled for now until we can add new shortcuts.
    _shortcut_context_menu.add_item(_("Remove Shortcut"), "remove_shortcut");
    _shortcut_context_menu.set_handler(boost::bind(&ShortcutSection::handle_command, this, _1));
    _*/
  }

  ~ShortcutSection()
  {
    delete_surface(_default_shortcut_icon);
    delete_surface(_page_down_icon);
    delete_surface(_page_up_icon);

    clear_shortcuts();
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Called from the context menu.
   */
  void handle_command(const std::string &command)
  {
    if (command == "remove_shortcut")
      _owner->trigger_callback(ActionRemoveShortcut, _active_shortcut);
    _active_shortcut = app_StarterRef();
  }

  //--------------------------------------------------------------------------------------------------

  void draw_paging_part(cairo_t *cr, int current_page, int pages, bool high_contrast)
  {
    cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);

    std::string page_string = base::strfmt("%d/%d", ++current_page, pages);
    cairo_text_extents_t extents;
    cairo_text_extents(cr, page_string.c_str(), &extents);

    _page_down_button.bounds = base::Rect(0, 0, image_width(_page_down_icon), image_height(_page_down_icon));
    double y = get_height() - _page_down_button.bounds.width() - 6;
    double x = get_width() - extents.width - 8;
    double icon_x = x + ceil((extents.width - _page_down_button.bounds.width()) / 2.0) + 1;
    _page_down_button.bounds.pos = base::Point(icon_x, y);

    cairo_set_source_surface(cr, _page_down_icon, icon_x, y);
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_XOR);

    if (current_page == pages)
    {
      // If we are on the last page then dim the page down button and remove the button
      // rectangle used for hit tests (so the user can't click it).
      cairo_paint_with_alpha(cr, 0.5);
      _page_down_button.bounds = base::Rect();
    }
    else
      cairo_paint(cr);
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

    y -= 6;

    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgb(cr, 0x5E / 255.0, 0x5E / 255.0, 0x5E / 255.0);
    cairo_move_to(cr, x, y);
    cairo_show_text(cr, page_string.c_str());
    cairo_stroke(cr);

    _page_up_button.bounds = base::Rect(icon_x, 0, image_width(_page_up_icon), image_height(_page_up_icon));
    y -= extents.height + 6 + _page_up_button.bounds.height();
    _page_up_button.bounds.pos.y = y;

    cairo_set_source_surface(cr, _page_up_icon, icon_x, y);
    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_XOR);

    if (current_page == 1)
    {
      cairo_paint_with_alpha(cr, 0.5);
      _page_up_button.bounds = base::Rect();
    }
    else
      cairo_paint_with_alpha(cr, 1);

    if (high_contrast)
      cairo_set_operator(cr, CAIRO_OPERATOR_OVER);

  }

  //------------------------------------------------------------------------------------------------

#define SHORTCUTS_LEFT_PADDING  55
#define SHORTCUTS_TOP_PADDING   75 // The vertical offset of the first shortcut entry.
#define SHORTCUTS_RIGHT_PADDING 25
#define SHORTCUTS_ROW_HEIGHT    50
#define SHORTCUTS_SPACING       18 // Vertical space between entries.

  void repaint(cairo_t *cr, int areax, int areay, int areaw, int areah)
  {
    layout(cr);
  
    int height = get_height();

    cairo_select_font_face(cr, HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_TITLE_FONT_SIZE);

    bool high_contrast = base::Color::is_high_contrast_scheme();
    if (high_contrast)
      cairo_set_source_rgb(cr, 0, 0, 0);
    else
      cairo_set_source_rgb(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0);
    cairo_move_to(cr, SHORTCUTS_LEFT_PADDING, 45);
    cairo_show_text(cr, _("Shortcuts"));
    cairo_stroke(cr);

    // Shortcuts block.
    int yoffset = SHORTCUTS_TOP_PADDING;
    if (_shortcuts.size() > 0 && yoffset < height)
    {
      cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
      cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);

      for (ShortcutIterator iterator = _shortcuts.begin() + _page_start; iterator != _shortcuts.end();
        iterator++)
      {
        float alpha = (yoffset + SHORTCUTS_ROW_HEIGHT) > height ? 0.25f : 1;

        iterator->acc_bounds.pos.x = SHORTCUTS_LEFT_PADDING;
        iterator->acc_bounds.pos.y = yoffset;
        iterator->acc_bounds.size.width = get_width() - (SHORTCUTS_LEFT_PADDING + SHORTCUTS_RIGHT_PADDING);
        iterator->acc_bounds.size.height = SHORTCUTS_ROW_HEIGHT;

        mforms::Utilities::paint_icon(cr, iterator->icon, SHORTCUTS_LEFT_PADDING, yoffset, alpha);

        if (!iterator->title.empty())
        {
          if (high_contrast)
            cairo_set_source_rgba(cr, 0, 0, 0, alpha);
          else
            cairo_set_source_rgba(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0, alpha);
          text_with_decoration(cr, iterator->title_bounds.left(), iterator->title_bounds.top() + yoffset,
            iterator->title.c_str(), iterator->shortcut == _hot_shortcut, iterator->title_bounds.width());
        }

        yoffset += SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING;
        if (yoffset >= height)
          break;
      }

      // See if we need to draw the paging indicator.
      height -= SHORTCUTS_TOP_PADDING;
      _shortcuts_per_page = height / (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING);
      if (_shortcuts_per_page < 1)
        _shortcuts_per_page = 1;
      int pages = (int)ceil(_shortcuts.size() / (float)_shortcuts_per_page);
      if (pages > 1)
      {
        int current_page = (int)ceil(_page_start / (float)_shortcuts_per_page);
        draw_paging_part(cr, current_page, pages, high_contrast);
      }
      else
      {
        _page_up_button.bounds = base::Rect();
        _page_down_button.bounds = base::Rect();
        _page_start = 0; // Size increased to cover the full content.
      }
    }
  }

  //--------------------------------------------------------------------------------------------------

  int shortcut_from_point(int x, int y)
  {
    if (x < SHORTCUTS_LEFT_PADDING || y < SHORTCUTS_TOP_PADDING || x > get_width() - SHORTCUTS_RIGHT_PADDING)
      return -1;

    y -= SHORTCUTS_TOP_PADDING;
    int point_in_row = y % (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING);
    if (point_in_row >= SHORTCUTS_ROW_HEIGHT)
      return -1; // In the spacing between entries.

    size_t row = y / (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING);
    size_t height = get_height() - SHORTCUTS_TOP_PADDING;
    size_t row_bottom = row * (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING) + SHORTCUTS_ROW_HEIGHT;
    if (row_bottom > height)
      return -1; // The last shortcut is dimmed if it goes over the bottom border.
                               // Take it out from the hit test too.

    row += _page_start;
    if (row < _shortcuts.size())
      return (int)row;

    return -1;
  }

  //--------------------------------------------------------------------------------------------------

  /**
   * Adds a new shortcut entry to the internal list. The function performs some sanity checks.
   */
  void add_shortcut(const std::string& icon_name, const grt::ValueRef& object)
  {
    app_StarterRef shortcut = app_StarterRef::cast_from(object);

    ShortcutEntry entry;
    
    entry.shortcut = shortcut;

    // See if we can load the icon. If not use the placeholder.
    entry.icon = mforms::Utilities::load_icon(icon_name, true);
    if (entry.icon == NULL)
      entry.icon = _default_shortcut_icon;

    _shortcuts.push_back(entry);
    set_layout_dirty(true);
  }

  //--------------------------------------------------------------------------------------------------

  void clear_shortcuts()
  {
    for (ShortcutIterator iterator= _shortcuts.begin(); iterator != _shortcuts.end(); iterator++)
      if (iterator->icon != _default_shortcut_icon)
        delete_surface(iterator->icon);
    _shortcuts.clear();
    set_layout_dirty(true);
  }

  //--------------------------------------------------------------------------------------------------

  void layout(cairo_t* cr)
  {
    if (is_layout_dirty())
    {
      set_layout_dirty(false);

      double icon_xoffset = SHORTCUTS_LEFT_PADDING;
      double text_xoffset = icon_xoffset + 60;

      double yoffset = SHORTCUTS_TOP_PADDING;

      double text_width = get_width() - text_xoffset - SHORTCUTS_RIGHT_PADDING;

      cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
      cairo_set_font_size(cr, HOME_SUBTITLE_FONT_SIZE);

      cairo_font_extents_t font_extents;
      cairo_font_extents(cr, &font_extents);
      double text_height = ceil(font_extents.height);

      // Compute bounding box for each shortcut entry.
      for (ShortcutIterator iterator = _shortcuts.begin(); iterator != _shortcuts.end(); iterator++)
      {
        int icon_height = image_height(iterator->icon);

        std::string title = iterator->shortcut->title();
        if (!title.empty())
        {
          iterator->title_bounds.pos.x = text_xoffset;

          // Text position is the lower-left corner.
          iterator->title_bounds.pos.y = icon_height / 4 + text_height / 2;
          iterator->title_bounds.size.height = text_height;

          cairo_text_extents_t extents;
          iterator->title = mforms::Utilities::shorten_string(cr, title, text_width);
          cairo_text_extents(cr, iterator->title.c_str(), &extents);
          iterator->title_bounds.size.width = extents.width;
        }

        yoffset += SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING;
      }
    }
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_double_click(mforms::MouseButton button, int x, int y)
  {
    return mouse_click(button, x, y); // Handle both the same way. Important especially for fast scrolling.
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_click(mforms::MouseButton button, int x, int y)
  {
    switch (button)
    {
      case mforms::MouseButtonLeft:
        {
          if (_page_up_button.bounds.contains(x, y))
          {
            // Page up clicked. Doesn't happen if we are on the first page already.
            _page_start -= _shortcuts_per_page;
            if (_page_start < 0)
              _page_start = 0;
            set_needs_repaint();
            return true;
          }

          if (_page_down_button.bounds.contains(x, y))
          {
            _page_start += _shortcuts_per_page;
            set_needs_repaint();
            return true;
          }

          if (_hot_shortcut.is_valid())
            _owner->trigger_callback(ActionShortcut, _hot_shortcut);
        }
        break;

      case mforms::MouseButtonRight:
        {
          if (_hot_shortcut.is_valid())
          {
            _active_shortcut = _hot_shortcut;
            _shortcut_context_menu.popup_at(this, x, y);
            return true;
          }
        }
        break;

      default:
        break;
    }
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  bool mouse_leave()
  {
    if (_hot_shortcut.is_valid())
    {
      _hot_shortcut = app_StarterRef();
      set_needs_repaint();
      return true;
    }
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  virtual bool mouse_move(mforms::MouseButton button, int x, int y)
  {
    app_StarterRef shortcut;
    int row = shortcut_from_point(x, y);
    if (row > -1)
      shortcut = _shortcuts[row].shortcut;
    if (shortcut != _hot_shortcut)
    {
      _hot_shortcut = shortcut;
      set_needs_repaint();
      return true;
    }
    return false;
  }

  //--------------------------------------------------------------------------------------------------

  void cancel_operation()
  {
    _owner->cancel_script_loading();
  }

  //------------------------------------------------------------------------------------------------
  
  virtual int get_acc_child_count()
  { 
    int ret_val = 0;

    ret_val += (int)_shortcuts.size();

    // Adds 2 for the paging buttons if shown
    if (_page_up_button.bounds.width())
      ret_val += 2;

    return ret_val;
  }

  //------------------------------------------------------------------------------------------------
  
  virtual Accessible* get_acc_child(int index)
  { 
    mforms::Accessible* accessible = NULL;

    if (index < (int)_shortcuts.size())
      accessible = &_shortcuts[index];
    else
    {
      index -= (int)_shortcuts.size();
      accessible = index ? &_page_down_button : &_page_up_button;
    }

    return accessible;
  }

  //------------------------------------------------------------------------------------------------
  
  virtual Accessible::Role get_acc_role()
  { 
    return Accessible::List;
  }

  virtual mforms::Accessible* hit_test(int x, int y)
  { 
    mforms::Accessible* accessible = NULL;

    if (_page_up_button.bounds.contains(x, y))
      accessible = &_page_up_button;
    else if (_page_down_button.bounds.contains(x, y))
      accessible = &_page_down_button;
    else
    {
      int row = shortcut_from_point(x, y);
      if (row != -1)
        accessible = &_shortcuts[row];
    }

    return accessible;
  }

};

//----------------- HomeScreen ---------------------------------------------------------------------

#include "workbench/wb_command_ui.h"

HomeScreen::HomeScreen(CommandUI *cmdui, db_mgmt_ManagementRef rdbms)
  : AppView(true, "home", true)
{
  _rdbms = rdbms;

  _callback = NULL;
  _user_data = NULL;

  mforms::Box *top_part = mforms::manage(new mforms::Box(false));
  _connection_section = new wb::ConnectionsSection(this);
  _connection_section->set_name("Home Connections Section");

  top_part->add(_connection_section, true, true);

  _document_section = new DocumentsSection(this);
  _document_section->set_name("Home Models Section");
  _document_section->set_size(-1, 236);
  top_part->add(_document_section, false, true);

  add(top_part, true, true);

  _shortcut_section = new ShortcutSection(this);
  _shortcut_section->set_name("Home Shortcuts Section");
  _shortcut_section->set_size(300, -1);
  add(_shortcut_section, false, true);
  
  set_menubar(mforms::manage(cmdui->create_menubar_for_context(WB_CONTEXT_HOME_GLOBAL)));
  //_toolbar = mforms::manage(cmdui->create_toolbar(""));

  update_colors();

  Box::scoped_connect(signal_resized(), boost::bind(&HomeScreen::on_resize, this));
  base::NotificationCenter::get()->add_observer(this, "GNColorsChanged");
}

//--------------------------------------------------------------------------------------------------

HomeScreen::~HomeScreen()
{
  base::NotificationCenter::get()->remove_observer(this);
  clear_subviews(); // Remove our sections or the View d-tor will try to release them.

  delete _shortcut_section;
  delete _connection_section;
  delete _document_section;
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::update_colors()
{
#ifdef __APPLE__
  _connection_section->set_back_color("#323232");
  _document_section->set_back_color("#343434");
  _shortcut_section->set_back_color("#373737");
#else
  bool high_contrast = base::Color::is_high_contrast_scheme();

  _connection_section->set_back_color(high_contrast ? "#f0f0f0" : "#1d1d1d");
  _document_section->set_back_color(high_contrast ? "#f8f8f8" : "#242424");
  _shortcut_section->set_back_color(high_contrast ? "#ffffff" : "#303030");
#endif
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::set_callback(home_screen_action_callback callback, void* user_data)
{
  _callback= callback;
  _user_data= user_data;
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::trigger_callback(HomeScreenAction action, const grt::ValueRef &object)
{
  if (action == ActionEditSQLScript)
  {
    // Editing a script takes 2 steps. The first one happens here, as we store the request and ask the user for
    // a connection to open with that script.
    _pending_script = grt::StringRef::cast_from(object);
    _document_section->show_connection_select_message();
    return;
  }
  else
    _document_section->hide_connection_select_message();

  if (action == ActionOpenConnectionFromList)
  {
    // The second step if we are opening an SQL script. If no SQL is selected we open the connection as usual.
    if (!_pending_script.empty()&& _callback != NULL)
    {
      grt::DictRef dict;
      dict["connection"] = object;
      dict["script"] = grt::StringRef(_pending_script);
      (*_callback)(ActionEditSQLScript, dict, _user_data);
    }
  }

  if (_callback != NULL)
    (*_callback)(action, object, _user_data);
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::cancel_script_loading()
{
  _pending_script = "";
  _document_section->hide_connection_select_message();
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::clear_shortcuts()
{
  _shortcut_section->clear_shortcuts();
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::add_shortcut(const grt::ValueRef& object, const std::string& icon_name)
{
  _shortcut_section->add_shortcut(icon_name, object);
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::clear_connections(bool clear_state)
{
  _connection_section->clear_connections(clear_state);
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::add_connection(db_mgmt_ConnectionRef connection, const std::string &title,
  const std::string &description, const std::string &user, const std::string &schema)
{
  _connection_section->add_connection(connection, title, description, user, schema);
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::add_document(const grt::StringRef& path, const time_t &time,
  const std::string schemas, long file_size)
{
  _document_section->add_document(path, time, schemas, file_size);
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::clear_documents()
{
  _document_section->clear_documents();
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::set_menu(mforms::Menu *menu, HomeScreenMenuType type)
{
  switch (type)
  {
    case HomeMenuConnection:
    case HomeMenuConnectionGroup:
    case HomeMenuConnectionFabric:
    case HomeMenuConnectionGeneric:
      _connection_section->set_context_menu(menu, type);
      break;

    case HomeMenuDocumentModelAction:
      _document_section->set_action_context_menu(menu, true);
      break;

    case HomeMenuDocumentModel:
      _document_section->set_context_menu(menu, true);
      break;

    case HomeMenuDocumentSQLAction:
      _document_section->set_action_context_menu(menu, false);
      break;

    case HomeMenuDocumentSQL:
      _document_section->set_context_menu(menu, false);
      break;
  }
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::on_resize()
{
  // Resize changes the layout so if there is pending script loading the popup is likely misplaced.
  if (!_pending_script.empty())
    cancel_script_loading();
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::setup_done()
{
  _connection_section->focus_search_box();
}

//--------------------------------------------------------------------------------------------------

void HomeScreen::handle_notification(const std::string &name, void *sender, base::NotificationInfo &info)
{
  if (name == "GNColorsChanged")
  {
    update_colors();
  }
}

//--------------------------------------------------------------------------------------------------
