/* 
 * Copyright (c) 2009, 2013, 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 "stdafx.h"

#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 "grts/structs.app.h"
#include "grts/structs.db.mgmt.h"

#include "home_screen.h"

#include "workbench/wb_context_names.h"

using namespace wb;

#ifdef __APPLE__
  #define HOME_TITLE_FONT "Helvetica Neue Light"
  #define HOME_NORMAL_FONT "Helvetica Neue Light"
  #define HOME_DETAILS_FONT "Helvetica Neue Light"
  // Info font is only used on Mac.
  #define HOME_INFO_FONT "Baskerville"
#elif defined(_WIN32)
  #define HOME_TITLE_FONT "Segoe UI"
  #define HOME_NORMAL_FONT "Segoe UI"
  #define HOME_NORMAL_FONT_XP "Tahoma"

  #define HOME_DETAILS_FONT "Segoe UI"
#else
  #define HOME_TITLE_FONT "Tahoma"
  #define HOME_NORMAL_FONT "Tahoma"
  #define HOME_DETAILS_FONT "Helvetica"
#endif

#define HOME_TITLE_FONT_SIZE 20
#define HOME_SUBTITLE_FONT_SIZE 13

#define HOME_TILES_TITLE_FONT_SIZE 16
#define HOME_SMALL_INFO_FONT_SIZE 10
#define HOME_DETAILS_FONT_SIZE 12

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

// The following helpers are just temporary. They will be replaced by a cairo context class.
static 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)
    return cairo_image_surface_get_width(image);
  return 0;
}

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

static int image_height(cairo_surface_t* image)
{
  if (image != NULL)
    return 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);
  }
}

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

/**
 * Determines if the given connection is an SSH connection and returns true if so.
 */
static bool is_ssh_connection(const db_mgmt_ConnectionRef &connection)
{
  if (connection.is_valid())
  {
    std::string driver= connection->driver().is_valid() ? connection->driver()->name() : "";
    return (driver == "MysqlNativeSSH");
  }
  return false;
}

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

/**
 * Determines if the given connection is a local connection (i.e. to the current box).
 */
static bool is_local_connection(const db_mgmt_ConnectionRef &connection)
{
  if (connection.is_valid())
  {
    std::string hostname= connection->parameterValues().get_string("hostName");

    if (!is_ssh_connection(connection) && (hostname == "localhost" || hostname.empty() || hostname == "127.0.0.1"))
      return true;
  }
  return false;
}

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

class ConnectionInfoPopup : public mforms::Popup
{
private:
  HomeScreen *_owner;

  base::Rect _free_area;
  int _info_width;
  db_mgmt_ConnectionRef _connection;
  db_mgmt_ServerInstanceRef _instance; // Might be invalid.
  base::Rect _button1_rect;
  base::Rect _button2_rect;
  base::Rect _button3_rect;
  base::Rect _button4_rect;
  base::Rect _close_button_rect;

  cairo_surface_t* _close_icon;

public:
  ConnectionInfoPopup(HomeScreen *owner, const db_mgmt_ConnectionRef connection,
    const db_mgmt_ServerInstanceRef instance, base::Rect host_bounds, base::Rect free_area, int info_width)
    : Popup(mforms::PopupPlain)
  {
    _owner = owner;
    _connection = connection;
    _instance = instance;

    _close_icon = mforms::Utilities::load_icon("wb_close.png");

    // Host bounds is the overall size the popup should cover.
    // The free area is a hole in that overall area which should not be covered to avoid darkening it.
    _free_area = free_area;
    _info_width = info_width;
    set_size((int)host_bounds.width(), (int)host_bounds.height());
    show((int)host_bounds.left(), (int)host_bounds.top());
  }

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

  ~ConnectionInfoPopup()
  {
    delete_surface(_close_icon);
  }

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

#define POPUP_HEIGHT 240
#define POPUP_TIP_HEIGHT 14
#define POPUP_LR_PADDING 53 // Left and right padding.
#define POPUP_TB_PADDING 24 // Top and bottom padding.
#define POPUP_BUTTON_MIN_WIDTH 88
#define POPUP_BUTTON_HEIGHT 24
#define POPUP_BUTTON_SPACING 19 // Horizontal space between adjacent buttons.
#define POPUP_BUTTON_PADDING 11 // Horizontal space between button border and text.

  /**
   * Draws a button with the given text at the given position. The button width depends on the
   * text. Font face and size are set already.
   * Result is the actual button bounds rectangle we can use for hit tests later.
   */
  base::Rect draw_button(cairo_t *cr, base::Point position, std::string text, bool right_aligned = false)
  {
    cairo_text_extents_t extents;
    cairo_text_extents(cr, text.c_str(), &extents);

    base::Rect button_rect = base::Rect(position.x, position.y,
      extents.width + 2 * POPUP_BUTTON_PADDING, POPUP_BUTTON_HEIGHT);
    if (button_rect.width() < POPUP_BUTTON_MIN_WIDTH)
      button_rect.size.width = POPUP_BUTTON_MIN_WIDTH;

    if (right_aligned)
      button_rect.pos.x -= button_rect.width();

    button_rect.use_inter_pixel = true;
    cairo_rectangle(cr, button_rect.left(), button_rect.top(), button_rect.width(), button_rect.height());
    cairo_set_source_rgb(cr, 0xF6 / 255.0, 0xF6 / 255.0, 0xF6 / 255.0);
    cairo_stroke(cr);

    double x = (int)(button_rect.left() + (button_rect.width() - extents.width) / 2.0);
    double y = (int)(button_rect.bottom() - (button_rect.height() - extents.height) / 2.0);
    cairo_set_source_rgb(cr, 0xF3 / 255.0, 0xF3 / 255.0, 0xF3 / 255.0);
    cairo_move_to(cr, x, y);
    cairo_show_text(cr, text.c_str());
    cairo_stroke(cr);

    return button_rect;
  }

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

  void repaint(cairo_t *cr, int x, int y, int w, int h)
  {
    cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING);
    
    base::Rect bounds = get_content_rect();
    cairo_set_line_width(cr, 1);
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width(), bounds.height());

    // Exclude free area by specifying it counterclockwise.
    cairo_move_to(cr, _free_area.left(), _free_area.top());
    cairo_rel_line_to(cr, 0, _free_area.height());
    cairo_rel_line_to(cr, _free_area.width(), 0);
    cairo_rel_line_to(cr, 0, -_free_area.height());
    cairo_close_path(cr);

    cairo_set_source_rgba(cr, 0, 0, 0, 0.5);
    cairo_fill(cr);

    // Determine which side of the free area we can show the popup. We use the lower part as long
    // as there is enough room.
    base::Point tip = base::Point((int)_free_area.xcenter(), 0);
    base::Rect content_bounds = bounds;
    content_bounds.pos.x += POPUP_LR_PADDING;
    content_bounds.size.width = _info_width - 2 *  POPUP_LR_PADDING;
    double right = (int)bounds.left() + _info_width;
    double top;
    if (bounds.bottom() - _free_area.bottom() >= POPUP_HEIGHT + POPUP_TIP_HEIGHT + 2)
    {
      // Below the free area.
      tip.y += (int)_free_area.bottom() + 2;
      top = tip.y + POPUP_TIP_HEIGHT;
      cairo_move_to(cr, bounds.left(), top);
      cairo_line_to(cr, tip.x - POPUP_TIP_HEIGHT, top);
      cairo_line_to(cr, tip.x, tip.y);
      cairo_line_to(cr, tip.x + POPUP_TIP_HEIGHT, top);
      cairo_line_to(cr, right, top);
      cairo_line_to(cr, right, top + POPUP_HEIGHT);
      cairo_line_to(cr, bounds.left(), top + POPUP_HEIGHT);

      cairo_set_source_rgba(cr, 0x1d / 255.0, 0x1d / 255.0, 0x1d / 255.0, 1);
      cairo_fill(cr);

      content_bounds.pos.y = tip.y + POPUP_TIP_HEIGHT;
    }
    else
    {
      // Above the free area.
      tip.y += _free_area.top() - 2;
      top = tip.y - POPUP_TIP_HEIGHT - POPUP_HEIGHT;
      cairo_move_to(cr, bounds.left(), top);
      cairo_line_to(cr, right, top);
      cairo_line_to(cr, right, tip.y - POPUP_TIP_HEIGHT);
      cairo_line_to(cr, tip.x + POPUP_TIP_HEIGHT, tip.y - POPUP_TIP_HEIGHT);
      cairo_line_to(cr, tip.x, tip.y);
      cairo_line_to(cr, tip.x - POPUP_TIP_HEIGHT, tip.y - POPUP_TIP_HEIGHT);
      cairo_line_to(cr, bounds.left(), tip.y - POPUP_TIP_HEIGHT);

      cairo_set_source_rgb(cr, 0x1d / 255.0, 0x1d / 255.0, 0x1d / 255.0);
      cairo_fill(cr);

      content_bounds.pos.y = tip.y - POPUP_TIP_HEIGHT - POPUP_HEIGHT;
    }
    
    content_bounds.pos.y += POPUP_TB_PADDING;
    content_bounds.size.height = POPUP_HEIGHT - 2 * POPUP_TB_PADDING;

    // The title.
    cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_TITLE_FONT_SIZE);
    cairo_set_source_rgb(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0);
    cairo_move_to(cr, content_bounds.left(), content_bounds.top() + 16);
    cairo_show_text(cr, _connection->name().c_str());
    cairo_stroke(cr);

    // All the various info.
    cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_DETAILS_FONT_SIZE);
    print_details_text(cr, content_bounds);

    // Buttons at the bottom.
    base::Point position = base::Point(content_bounds.left(), content_bounds.bottom() - POPUP_BUTTON_HEIGHT);
    _button1_rect = draw_button(cr, position, _("Edit Connection..."));

    grt::DictRef serverInfo;
    if (_instance.is_valid())
      serverInfo =_instance->serverInfo();

    bool pending = !serverInfo.is_valid() || serverInfo.get_int("setupPending") == 1;
    if (!pending && !is_local_connection(_connection) && serverInfo.get_int("remoteAdmin") == 0 &&
      serverInfo.get_int("windowsAdmin") == 0)
      pending = true;

    if (pending)
    {
      position.x += _button1_rect.width() + POPUP_BUTTON_SPACING;
      if (is_local_connection(_connection))
        _button2_rect = draw_button(cr, position, _("Configure Local Management..."));
      else
        _button2_rect = draw_button(cr, position, _("Configure Remote Management..."));
    }
    else
      _button2_rect = base::Rect();

    /*
    position.x += _button2_rect.width() + POPUP_BUTTON_SPACING;
    _button3_rect = draw_button(cr, position, _("Add to Favorites"));
    */
    // The last button is right-aligned.
    position.x = right - POPUP_LR_PADDING;
    _button4_rect = draw_button(cr, position, _("Connect"), true);

    // Finally the close button.
    _close_button_rect = base::Rect(right - image_width(_close_icon) - 10, top + 10, 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);
  }

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

  /**
   * Prints a single details line with name left aligned in the given bounds and info right aligned.
   * The height value in the given bounds is not set and is ignored. The position is as used by
   * cairo for text output (lower-left corner).
   */
  void print_info_line(cairo_t *cr, base::Rect bounds, std::string name, std::string info)
  {
    if (info.empty())
      info = _("<unknown>");

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

    cairo_move_to(cr, bounds.left(), bounds.top());
    cairo_show_text(cr, name.c_str());

    cairo_move_to(cr, bounds.right() - extents.width, bounds.top());
    cairo_show_text(cr, info.c_str());

    cairo_stroke(cr);
  }

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

#define DETAILS_TOP_OFFSET   44
#define DETAILS_LINE_HEIGHT  18
#define DETAILS_LINE_WIDTH  340

  /**
   * Prints all info details for the current connection. Font face, size and color must be set up
   * already.
   */
  void print_details_text(cairo_t *cr, base::Rect bounds)
  {
    // Connection info first.
    base::Rect line_bounds = bounds;
    line_bounds.pos.y += DETAILS_TOP_OFFSET;

    // Use POPUP_LR_PADDIND as space between the two columns.
    line_bounds.size.width = (bounds.width() - POPUP_LR_PADDING) / 2;

    grt::DictRef parameter_values = _connection->parameterValues();
    grt::DictRef server_info;
    if (_instance.is_valid())
      server_info = _instance->serverInfo();

    std::string server_version = parameter_values.get_string("serverVersion");
    if (server_version.empty() && server_info.is_valid())
      server_version = server_info.get_string("serverVersion");

    print_info_line(cr, line_bounds, _("MySQL Version"), server_version);
    line_bounds.pos.y += DETAILS_LINE_HEIGHT;
    time_t time = parameter_values.get_int("lastConnected");
    if (time == 0)
      print_info_line(cr, line_bounds, _("Last connected"), "");
    else
    {
      struct tm * ptm = localtime(&time);
      char buffer[32];
      strftime(buffer, 32, "%d %B %Y %H:%M", ptm);
      print_info_line(cr, line_bounds, _("Last connected"), buffer);
    }
    line_bounds.pos.y += DETAILS_LINE_HEIGHT;

    std::string sshHost = parameter_values.get_string("sshHost");
    if (!sshHost.empty())
    {
      std::string sshUser = parameter_values.get_string("sshUserName");
      print_info_line(cr, line_bounds, _("Using Tunnel"), sshUser + "@" + sshHost);
    }

    line_bounds.pos.y += DETAILS_LINE_HEIGHT;
    std::string user_name = parameter_values.get_string("userName");
    print_info_line(cr, line_bounds, _("User Account"), user_name);
    line_bounds.pos.y += DETAILS_LINE_HEIGHT;

    std::string password_stored = _("<not stored>");
    std::string password;
    if (mforms::Utilities::find_password(_connection->hostIdentifier(), user_name, password))
    {
      password = "";
      password_stored = _("<stored>");
    }
    print_info_line(cr, line_bounds, _("Password"), password_stored);
    line_bounds.pos.y += DETAILS_LINE_HEIGHT;
    print_info_line(cr, line_bounds, _("Network Address"), parameter_values.get_string("hostName"));
    line_bounds.pos.y += DETAILS_LINE_HEIGHT;
    int port = parameter_values.get_int("port");
    print_info_line(cr, line_bounds, _("TCP/IP Port"), base::strfmt("%i", port));

    // Instance info next.
    line_bounds = bounds;
    line_bounds.pos.x += (bounds.width() + POPUP_LR_PADDING) / 2;
    line_bounds.pos.y += DETAILS_TOP_OFFSET;
    line_bounds.size.width = (bounds.width() - POPUP_LR_PADDING) / 2;

    // Make sure the entire right part does not extend beyond the available horizontal space.
    if (line_bounds.right() > bounds.right())
      line_bounds.pos.x -= bounds.right() - line_bounds.right();

    grt::DictRef serverInfo;
    if (_instance.is_valid())
      serverInfo =_instance->serverInfo();

    bool pending = !serverInfo.is_valid() || serverInfo.get_int("setupPending") == 1;
    if (!pending && !is_local_connection(_connection) && serverInfo.get_int("remoteAdmin") == 0 &&
      serverInfo.get_int("windowsAdmin") == 0)
      pending = true;

    if (pending)
    {
      if (is_local_connection(_connection))
        print_info_line(cr, line_bounds, _("Local management not set up"), " ");
      else
        print_info_line(cr, line_bounds, _("Remote management not set up"), " ");
    }
    else
    {
      if (is_local_connection(_connection))
      {
        print_info_line(cr, line_bounds, _("Local management"), "Enabled");
        line_bounds.pos.y += 6 * DETAILS_LINE_HEIGHT; // Same layout as for remote mgm. So config file is at bottom.
        print_info_line(cr, line_bounds, _("Config Path"), serverInfo.get_string("sys.config.path"));
      }
      else
      {
        grt::DictRef loginInfo = _instance->loginInfo();
        bool windowsAdmin = serverInfo.get_int("windowsAdmin", 0) == 1;

        std::string os = serverInfo.get_string("serverOS");
        if (os.empty()) // If there's no OS set (yet) then use the generic system identifier (which is not that specific, but better than nothing).
          os = serverInfo.get_string("sys.system");
        if (os.empty() && windowsAdmin)
          os = "Windows";
        print_info_line(cr, line_bounds, _("Operating System"), os);
        line_bounds.pos.y += DETAILS_LINE_HEIGHT;

        if (windowsAdmin)
        {
          print_info_line(cr, line_bounds, _("Remote management via"), "WMI");
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;

          std::string host_name = loginInfo.get_string("wmi.hostName");
          print_info_line(cr, line_bounds, _("Target Server"), loginInfo.get_string("wmi.hostName"));
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;
          print_info_line(cr, line_bounds, _("WMI user"), loginInfo.get_string("wmi.userName"));
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;

          std::string password_key = "wmi@" + host_name;
          user_name = loginInfo.get_string("wmi.userName");
          if (mforms::Utilities::find_password(password_key, user_name, password))
          {
            password = "";
            password_stored = _("<stored>");
          }
          else
            password_stored = _("<not stored>");
          print_info_line(cr, line_bounds, _("WMI Password"), user_name);
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;

          line_bounds.pos.y += DETAILS_LINE_HEIGHT; // Empty line by design. Separated for easier extension.
          print_info_line(cr, line_bounds, _("Config Path"), serverInfo.get_string("sys.config.path"));
        }
        else
        {
          print_info_line(cr, line_bounds, _("Remote management via"), "SSH");
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;

          line_bounds.pos.y += DETAILS_LINE_HEIGHT; // Empty line by design. Separated for easier extension.

          std::string host_name = loginInfo.get_string("ssh.hostName");
          print_info_line(cr, line_bounds, _("SSH Target"), host_name);
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;
          print_info_line(cr, line_bounds, _("SSH User"), loginInfo.get_string("ssh.userName"));
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;

          std::string security = (loginInfo.get_int("ssh.useKey", 0) != 0) ? _("Public Key") : _("Password ") + password_stored;
          print_info_line(cr, line_bounds, _("SSH Security"), security);
          line_bounds.pos.y += DETAILS_LINE_HEIGHT;
          print_info_line(cr, line_bounds, _("SSH Port"), loginInfo.get_string("ssh.port", "22"));
        }
      }
    }
  }

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

  void mouse_up(int button, int x, int y)
  {
    if (button == 0)
    {
      // We are going to destroy ourselves when starting an action, so we have to cache
      // values we need after destruction. The self destruction is also the reason why we
      // use mouse_up instead of mouse_click.
      HomeScreen *owner = _owner;
      db_mgmt_ConnectionRef connection = _connection;

      if (_button1_rect.contains(x, y))
      {
        set_modal_result(1); // Just a dummy value to close ourselves.
        owner->handle_context_menu(connection, "manage_connections");
      }
      else 
        if (_button2_rect.contains(x, y))
        {
          set_modal_result(1);
          owner->trigger_callback(ActionSetupRemoteManagement, connection);
        }
        else 
          if (_button3_rect.contains(x, y))
          {
            set_modal_result(1);
            owner->handle_context_menu(connection, "");
          }
          else 
            if (_button4_rect.contains(x, y))
            {
              set_modal_result(1);
              owner->handle_context_menu(connection, "open_connection");
            }
            else
              if (_close_button_rect.contains(x, y))
                set_modal_result(1);
    }
  }
};

//----------------- ConnectionsSection -------------------------------------------------------------

#define CONNECTIONS_LEFT_PADDING  50
#define CONNECTIONS_RIGHT_PADDING 50 // The tile spacing right to the last tile in the row does not belong to this padding.
#define CONNECTIONS_TOP_PADDING   75 // The vertical offset of the first visible shortcut entry.
#define CONNECTIONS_SPACING        8 // Vertical/horizontal space between entries.

#define CONNECTIONS_TILE_WIDTH   233
#define CONNECTIONS_TILE_HEIGHT   91

struct ConnectionEntry
{
  db_mgmt_ConnectionRef connection;

  std::string title;
  std::string description;
  std::string user;
  std::string schema;
  bool compute_strings; // True after creation to indicate the need to compute the final display strings.

  // For filtering we need the full strings.
  std::string search_title;
  std::string search_description;
  std::string search_user;
  std::string search_schema;

  std::vector<ConnectionEntry> children;
};

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

  cairo_surface_t* _folder_icon;
  cairo_surface_t* _mouse_over_icon;
  cairo_surface_t* _mouse_over2_icon;
  cairo_surface_t* _network_icon;
  cairo_surface_t* _page_down_icon;
  cairo_surface_t* _page_up_icon;
  cairo_surface_t* _plus_icon;
  cairo_surface_t* _sakila_icon;
  cairo_surface_t* _schema_icon;
  cairo_surface_t* _user_icon;
  cairo_surface_t* _manage_icon;

  base::Color _tile_bk_color1;
  base::Color _tile_bk_color2;
  base::Color _folder_tile_bk_color;
  base::Color _back_tile_bk_color;

  base::Color _tile_bk_color1_hl;
  base::Color _tile_bk_color2_hl;
  base::Color _folder_tile_bk_color_hl;
  base::Color _back_tile_bk_color_hl;

  int _page_start;        // Index into the list where root display starts.
  int _page_start_backup; // Copy of the current page start when we go into a folder (for restauration).
  int _active_folder;     // The index of the folder entry that is currently active.
  int _tiles_per_page;

  typedef std::vector<ConnectionEntry> ConnectionVector;
  typedef ConnectionVector::iterator ConnectionIterator;
  ConnectionVector _connections;
  ConnectionVector _filtered_connections;
  bool _filtered;

  mforms::Menu *_context_menu;
  mforms::Menu *_folder_context_menu;

  int _hot_entry;      // The index of the connection entry under the mouse.
  int _entry_for_menu; // The index that was hot when the context menu was opened.
  bool _show_details;  // If there's a hot connection this indicates if we just show the hot state or the connection details.

  base::Rect _add_button_rect;
  base::Rect _manage_button_rect;
  base::Rect _page_up_button_rect;
  base::Rect _page_down_button_rect;
  base::Rect _info_button_rect;

  ConnectionInfoPopup *_info_popup;

  mforms::Box _search_box;
  mforms::TextEntry _search_text;

public:

  ConnectionsSection(HomeScreen *owner)
  : _search_box(true), _search_text(mforms::SmallSearchEntry)
  {
    _owner = owner;
    _page_start = 0;
    _context_menu = NULL;
    _folder_context_menu = NULL;
    _hot_entry = -1;
    _entry_for_menu = -1;
    _show_details = false;
    _active_folder = -1;
    _filtered = false;

    _folder_icon = mforms::Utilities::load_icon("wb_tile_folder.png");
    _mouse_over_icon = mforms::Utilities::load_icon("wb_tile_mouseover.png");
    _mouse_over2_icon = mforms::Utilities::load_icon("wb_tile_mouseover_2.png");
    _network_icon = mforms::Utilities::load_icon("wb_tile_network.png");
    _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");
    _sakila_icon = mforms::Utilities::load_icon("wb_tile_sakila.png");
    _schema_icon = mforms::Utilities::load_icon("wb_tile_schema.png");
    _user_icon = mforms::Utilities::load_icon("wb_tile_user.png");
    _manage_icon = mforms::Utilities::load_icon("wb_tile_manage.png");

#ifdef __APPLE__
    _tile_bk_color1 = base::Color::parse("#1e1e1e");
    _tile_bk_color1_hl = base::Color::parse("#3f3f3f");
#else
    _tile_bk_color1 = base::Color::parse("#666666");
    _tile_bk_color1_hl = base::Color::parse("#838383");
#endif

    _tile_bk_color2 = base::Color::parse("#868686");
    _tile_bk_color2_hl = base::Color::parse("#9b9b9b");

#ifdef __APPLE__
    _folder_tile_bk_color = base::Color::parse("#3477a6");
    _folder_tile_bk_color_hl = base::Color::parse("#4699b8");
#else
    _folder_tile_bk_color = base::Color::parse("#178ec5");
    _folder_tile_bk_color_hl = base::Color::parse("#63a6c5");
#endif

    _back_tile_bk_color = base::Color::parse("#d9532c");
    _back_tile_bk_color_hl = base::Color::parse("#d97457");

    _info_popup = NULL;

    _search_box.set_name("search-box");
    _search_box.set_spacing(5);
    _search_text.set_size(150, -1);

    _search_text.set_front_color("#FFFFFF");
    _search_text.set_placeholder_color("#A0A0A0");
    _search_text.set_back_color("#474747");
    
#ifdef _WIN32
    _search_text.set_bordered(false);
    _search_text.set_size(-1, 18);
    _search_text.set_font(HOME_NORMAL_FONT" 10");
    _search_box.set_size(-1, 18);
#else
    _search_box.set_padding(8, 1, 8, 5);
#endif

#ifdef _WIN32
    mforms::ImageBox *image = mforms::manage(new mforms::ImageBox, false);
    image->set_image("search_sidebar.png");
    image->set_image_align(mforms::MiddleCenter);
    _search_box.add(image, false, false);
#endif

    _search_text.set_placeholder_text("Filter connections");
    _search_box.add(&_search_text, true, true);
    scoped_connect(_search_text.signal_changed(), boost::bind(&ConnectionsSection::on_search_text_changed, this));
    scoped_connect(_search_text.signal_action(), boost::bind(&ConnectionsSection::on_search_text_action, this, _1));
    add(&_search_box, mforms::TopRight);
    set_padding(0, 30, CONNECTIONS_RIGHT_PADDING, 0);
  }

  ~ConnectionsSection()
  {
    if (_context_menu != NULL)
      _context_menu->release();
    if (_folder_context_menu != NULL)
      _folder_context_menu->release();

    delete_surface(_folder_icon);
    delete_surface(_mouse_over_icon);
    delete_surface(_mouse_over2_icon);
    delete_surface(_network_icon);
    delete_surface(_page_down_icon);
    delete_surface(_page_up_icon);
    delete_surface(_plus_icon);
    delete_surface(_sakila_icon);
    delete_surface(_schema_icon);
    delete_surface(_user_icon);
    delete_surface(_manage_icon);

    if (_info_popup != NULL)
      delete _info_popup;
  }

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

  void focus_search_box()
  {
    _search_text.focus();
  }

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

  void on_search_text_changed()
  {
    std::string filter = _search_text.get_string_value();
    _filtered_connections.clear();

    _filtered = !filter.empty();
    if (_filtered)
    {
      ConnectionVector current_connections = (_active_folder < 0) ? _connections : _connections[_active_folder].children;
      for (ConnectionIterator iterator = current_connections.begin(); iterator != current_connections.end(); ++iterator)
      {
        // Always keep the first entry if we are in a folder. It's not filtered.
        if (_active_folder >= 0 && iterator == current_connections.begin())
          _filtered_connections.push_back(*iterator);
        else
          if (base::contains_string(iterator->search_title, filter, false) ||
              base::contains_string(iterator->search_description, filter, false) ||
              base::contains_string(iterator->search_user, filter, false) ||
              base::contains_string(iterator->search_schema, filter, false))
            _filtered_connections.push_back(*iterator);
      }
    }
    set_needs_repaint();
  }

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

  void on_search_text_action(mforms::TextEntryAction action)
  {
    if (action == mforms::EntryActivate)
    {
      if (_active_folder > -1)
      {
        // Within a folder.
        switch (_filtered_connections.size())
        {
        case 1: // Just the back tile. Return to top level.
          _page_start = _page_start_backup;
          _active_folder = -1;
          _filtered = false;
          _search_text.set_value("");
          set_needs_repaint();
          break;

        case 2: // Exactly one entry matched the filter. Activate it.
          _owner->trigger_callback(ActionOpenConnectionFromList, _filtered_connections[1].connection);
          break;
        }
      }
      else
      {
        if (_filtered_connections.size() == 1)
        {
          // If only one entry is visible through filtering activate it. I.e. for a group show its content
          // and for a connection open it.
          if (_filtered_connections[0].children.size() > 1)
          {
            _page_start_backup = _page_start;
            _page_start = 0;

            // Loop through the unfiltered list to find the index of the group we are about to open.
            _active_folder = -1; // Just a defensive action. Should never play a role.
            for (size_t i = 0; i < _connections.size(); ++i)
            {
              if (_connections[i].title == _filtered_connections[0].title)
              {
                _active_folder = i;
                break;
              }
            }
            _filtered = false;
            _search_text.set_value("");
            set_needs_repaint();
          }
          else
            _owner->trigger_callback(ActionOpenConnectionFromList, _filtered_connections[0].connection);
        }
      }
    }
  }

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

  /**
   * Init some stuff we cannot do in the c-tor.
   */
  void init()
  {
    _search_box.set_back_color(get_back_color());
  }

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

  int entry_from_point(int x, int y, bool &in_details_area)
  {
    in_details_area = false;

    int width = get_width();
    if (x < CONNECTIONS_LEFT_PADDING || x > (width - CONNECTIONS_RIGHT_PADDING) ||
      y < CONNECTIONS_TOP_PADDING)
      return -1; // Outside the tiles area.

    x -= CONNECTIONS_LEFT_PADDING;
    if ((x % (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING)) > CONNECTIONS_TILE_WIDTH)
      return -1; // Within the horizontal spacing between two tiles.

    y -= CONNECTIONS_TOP_PADDING;
    if ((y % (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING)) > CONNECTIONS_TILE_HEIGHT)
      return -1; // Within the vertical spacing between two tiles.

    width -= CONNECTIONS_LEFT_PADDING + CONNECTIONS_RIGHT_PADDING;
    int tiles_per_row = width / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
    if (x >= tiles_per_row * (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING))
      return -1; // After the last tile in a row.

    int height = get_height() - CONNECTIONS_TOP_PADDING;
    int column = x / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
    int row = y / (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING);

    int row_bottom = row * (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING) + CONNECTIONS_TILE_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 = _connections.size();
    if (_filtered)
      count = _filtered_connections.size(); // For both, main list or folder.
    else
      if (_active_folder > -1)
        count = _connections[_active_folder].children.size();
    size_t index = _page_start + row * tiles_per_row + column;
    if (index < count)
    {
      in_details_area = (x % (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING)) > 3 * CONNECTIONS_TILE_WIDTH / 4.0;
      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, double x, double y, cairo_surface_t *icon,
    const std::string &text, double alpha)
  {
    cairo_set_source_surface(cr, icon, floor(x), floor(y));
    cairo_paint_with_alpha(cr, alpha);

    x += image_width(icon) + 3;

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

#ifdef __APPLE__
    cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, 0.6 * alpha);
#else
    cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, alpha);
#endif

    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);
  }

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

  /**
   * Separate tile drawing for the special back tile (to return from a folder).
   */
  void draw_back_tile(cairo_t *cr, base::Rect bounds, bool hot)
  {
    base::Color current_color = hot ? _back_tile_bk_color_hl : _back_tile_bk_color;
    cairo_fill(cr);

#ifdef __APPLE__
    cairo_new_sub_path(cr);

    double radius = 8;
    double degrees = M_PI / 180.0;
    bounds.use_inter_pixel = false;
    cairo_arc(cr, bounds.left() + bounds.width() - radius, bounds.top() + radius, radius, -90 * degrees, 0 * degrees);
    cairo_arc(cr, bounds.left() + bounds.width() - radius, bounds.top() + bounds.height() - radius, radius, 0 * degrees, 90 * degrees);
    cairo_arc(cr, bounds.left() + radius, bounds.top() + bounds.height() - radius, radius, 90 * degrees, 180 * degrees);
    cairo_arc(cr, bounds.left() + radius, bounds.top() + radius, radius, 180 * degrees, 270 * degrees);
    cairo_close_path(cr);
    cairo_set_source_rgba(cr, current_color.red, current_color.green, current_color.blue, 1);
    cairo_fill(cr);

    // Border.
    bounds.use_inter_pixel = true;
    cairo_arc(cr, -2 + bounds.right() - radius, 1 + bounds.top() + radius, radius, -90 * degrees, 0 * degrees);
    cairo_arc(cr, -2 + bounds.right() - radius, -2 + bounds.bottom() - radius, radius, 0 * degrees, 90 * degrees);
    cairo_arc(cr, 1 + bounds.left() + radius, -2 + bounds.bottom() - radius, radius, 90 * degrees, 180 * degrees);
    cairo_arc(cr, 1 + bounds.left() + radius, 1 + bounds.top() + radius, radius, 180 * degrees, 270 * degrees);
    cairo_close_path(cr);
    cairo_set_source_rgba(cr, 1, 1, 1, 0.19);
    cairo_set_line_width(cr, 3);
    cairo_stroke(cr);
#else
    bounds.use_inter_pixel = false;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width(), bounds.height());
    cairo_set_source_rgb(cr, current_color.red, current_color.green, current_color.blue);
    cairo_fill(cr);

    // Border.
    bounds.use_inter_pixel = true;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width() - 1, bounds.height() - 1);
    cairo_set_source_rgba(cr, 1, 1, 1, 0.125);
    cairo_set_line_width(cr, 1);
    cairo_stroke(cr);
#endif

    // Title string.
    double x = bounds.left() + 10;
    double y = bounds.top() + 27;
    cairo_set_font_size(cr, HOME_TILES_TITLE_FONT_SIZE);
    cairo_set_source_rgb(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0);

    cairo_move_to(cr, x, y);
    cairo_show_text(cr, _("< back"));
    cairo_stroke(cr);
  }

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

  void draw_tile(cairo_t *cr, base::Rect bounds, ConnectionEntry &entry,
    bool second_color, bool hot, double alpha)
  {
    base::Color current_color;
    if (entry.children.size() > 0)
      current_color = hot ? _folder_tile_bk_color_hl : _folder_tile_bk_color;
    else
    {
#ifndef __APPLE__
      if (second_color)
        current_color = hot ? _tile_bk_color2_hl : _tile_bk_color2;
      else
#endif
        // No checker board for Mac.
        current_color = hot ? _tile_bk_color1_hl : _tile_bk_color1;
    }

#ifdef __APPLE__
    cairo_new_sub_path(cr);

    double radius = 8;
    double degrees = M_PI / 180.0;
    bounds.use_inter_pixel = false;
    cairo_arc(cr, bounds.left() + bounds.width() - radius, bounds.top() + radius, radius, -90 * degrees, 0 * degrees);
    cairo_arc(cr, bounds.left() + bounds.width() - radius, bounds.top() + bounds.height() - radius, radius, 0 * degrees, 90 * degrees);
    cairo_arc(cr, bounds.left() + radius, bounds.top() + bounds.height() - radius, radius, 90 * degrees, 180 * degrees);
    cairo_arc(cr, bounds.left() + radius, bounds.top() + radius, radius, 180 * degrees, 270 * degrees);
    cairo_close_path(cr);
    cairo_set_source_rgba(cr, current_color.red, current_color.green, current_color.blue, alpha);
    cairo_fill(cr);

    // Border.
    bounds.use_inter_pixel = true;
    cairo_arc(cr, -2 + bounds.right() - radius, 1 + bounds.top() + radius, radius, -90 * degrees, 0 * degrees);
    cairo_arc(cr, -2 + bounds.right() - radius, -2 + bounds.bottom() - radius, radius, 0 * degrees, 90 * degrees);
    cairo_arc(cr, 1 + bounds.left() + radius, -2 + bounds.bottom() - radius, radius, 90 * degrees, 180 * degrees);
    cairo_arc(cr, 1 + bounds.left() + radius, 1 + bounds.top() + radius, radius, 180 * degrees, 270 * degrees);
    cairo_close_path(cr);
    cairo_set_source_rgba(cr, 1, 1, 1, 0.19 * alpha);
    cairo_set_line_width(cr, 3);
    cairo_stroke(cr);

    float image_alpha = 0.3;
#else
    bounds.use_inter_pixel = false;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width(), bounds.height());
    cairo_set_source_rgba(cr, current_color.red, current_color.green, current_color.blue, alpha);
    cairo_fill(cr);

    // Border.
    bounds.use_inter_pixel = true;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width() - 1, bounds.height() - 1);
    cairo_set_source_rgba(cr, 1, 1, 1, 0.125 * alpha);
    cairo_set_line_width(cr, 1);
    cairo_stroke(cr);

    float image_alpha = 1;
#endif

    // Background icon.
    bounds.use_inter_pixel = false;
    cairo_surface_t *back_icon = entry.children.size() > 0 ? _folder_icon : _sakila_icon;
    double x = bounds.left() + bounds.width() - image_width(back_icon);
    double y = bounds.top() + bounds.height() - image_height(back_icon);
    cairo_set_source_surface(cr, back_icon, x, y);
    cairo_paint_with_alpha(cr, image_alpha * alpha);

    cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, alpha);

    if (hot && _show_details && entry.children.size() == 0)
    {
#ifdef __APPLE__
      // On OS X we show the usual italic small i letter instead of the peeling corner.
      cairo_select_font_face(cr, HOME_INFO_FONT, CAIRO_FONT_SLANT_ITALIC, CAIRO_FONT_WEIGHT_BOLD);
      cairo_set_font_size(cr, HOME_TILES_TITLE_FONT_SIZE);

      _info_button_rect = base::Rect(bounds.right() - 15, bounds.bottom() - 10, 10, 10);
      cairo_move_to(cr, _info_button_rect.left(), _info_button_rect.top());
      cairo_show_text(cr, "i");
      cairo_stroke(cr);

#else
      cairo_surface_t *overlay = _mouse_over_icon;
      x = bounds.left() + bounds.width() - image_width(overlay);
      cairo_set_source_surface(cr, overlay, x, bounds.top());
      cairo_paint_with_alpha(cr, alpha);

      cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, alpha);
#endif
    }

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

    // Title string.
    x = (int)bounds.left() + 10.5; // Left offset from the border to caption, user and network icon.
    y = bounds.top() + 27; // Distance from top to the caption base line.

    if (entry.compute_strings)
    {
      // On first render compute the actual string to show. We only need to do this once
      // as neither the available space changes nor is the entry manipulated.
      double available_width = bounds.width() - 21;
      entry.title = mforms::Utilities::shorten_string(cr, entry.title, available_width);
    }

    cairo_move_to(cr, x, y);
    cairo_show_text(cr, entry.title.c_str());
    cairo_stroke(cr);

    cairo_set_font_size(cr, HOME_SMALL_INFO_FONT_SIZE);
    if (entry.children.size() > 0)
    {
#ifdef __APPLE__
      cairo_set_source_rgba(cr, 0xF9 / 255.0, 0xF9 / 255.0, 0xF9 / 255.0, 0.6 * alpha);
#endif
      
      std::string info = base::strfmt(_("%ld Connections"), entry.children.size() - 1);
      y = bounds.top() + 55;
      cairo_move_to(cr, x, y);
      cairo_show_text(cr, info.c_str());
      cairo_stroke(cr);
    }
    else
    {
      if (entry.compute_strings)
      {
        double available_width = bounds.width() - 24 - image_width(_network_icon);
        entry.description = mforms::Utilities::shorten_string(cr, entry.description, available_width);

        available_width = bounds.center().x - x - image_width(_user_icon) - 6; // -6 is the spacing between text and icons.
        entry.user = mforms::Utilities::shorten_string(cr, entry.user, available_width);

        entry.schema = mforms::Utilities::shorten_string(cr, entry.schema, available_width);
      }

      y = bounds.top() + 56 - image_height(_user_icon);
      draw_icon_with_text(cr, x, y, _user_icon, entry.user, alpha);

      y = bounds.top() + 74 - image_height(_network_icon);
      draw_icon_with_text(cr, x, y, _network_icon, entry.description, alpha);

      y = bounds.top() + 56 - image_height(_schema_icon);
      draw_icon_with_text(cr, bounds.center().x, y, _schema_icon, entry.schema.empty() ? _("n/a") : entry.schema, alpha);
    }

    entry.compute_strings = false;
  }

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

  void draw_paging_part(cairo_t *cr, int current_page, int pages)
  {
    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_rect = base::Rect(0, 0, image_width(_page_down_icon), image_height(_page_down_icon));
    double y = get_height() - _page_down_button_rect.width() - 6;
    double x = get_width() - extents.width - 8;
    double icon_x = x + ceil((extents.width - _page_down_button_rect.width()) / 2.0) + 1;
    _page_down_button_rect.pos = base::Point(icon_x, y);

    cairo_set_source_surface(cr, _page_down_icon, icon_x, y);
    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_rect = base::Rect();
    }
    else
      cairo_paint(cr);

    y -= 6;

    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_rect = base::Rect(icon_x, 0, image_width(_page_up_icon), image_height(_page_up_icon));
    y -= extents.height + 6 + _page_up_button_rect.height();
    _page_up_button_rect.pos.y = y;

    cairo_set_source_surface(cr, _page_up_icon, icon_x, y);
    if (current_page == 1)
    {
      cairo_paint_with_alpha(cr, 0.5);
      _page_up_button_rect = base::Rect();
    }
    else
      cairo_paint_with_alpha(cr, 1);
  }

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

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

    int tiles_per_row = width / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);

    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);

    cairo_set_source_rgb(cr, 0xf3 / 255.0, 0xf3 / 255.0, 0xf3 / 255.0);
    cairo_move_to(cr, CONNECTIONS_LEFT_PADDING, 45);

    ConnectionVector *connections;
    std::string title = _("MySQL Connections");
    if (_active_folder > -1)
    {
      title += " / " + _connections[_active_folder].title;
      connections = &_connections[_active_folder].children;
    }
    else
      connections = &_connections;

    if (_filtered)
      connections = &_filtered_connections;

    cairo_show_text(cr, title.c_str());
    cairo_stroke(cr);

    // The + button after the title.
    cairo_text_extents_t extents;
    cairo_text_extents(cr, title.c_str(), &extents);
    double text_width = ceil(extents.width);

    _add_button_rect = base::Rect(CONNECTIONS_LEFT_PADDING + text_width + 10, 45 - image_height(_plus_icon),
      image_width(_plus_icon), image_height(_plus_icon));
    cairo_set_source_surface(cr, _plus_icon, _add_button_rect.left(), _add_button_rect.top());
    cairo_paint(cr);

    _manage_button_rect = base::Rect(_add_button_rect.right() + 10, 45 - image_height(_manage_icon),
      image_width(_manage_icon), image_height(_manage_icon));
    cairo_set_source_surface(cr, _manage_icon, _manage_button_rect.left(), _manage_button_rect.top());
    cairo_paint(cr);

    int row = 0;
    base::Rect bounds(0, CONNECTIONS_TOP_PADDING, CONNECTIONS_TILE_WIDTH, CONNECTIONS_TILE_HEIGHT);
    bool done = false;
    while (!done)
    {
      bounds.pos.x = CONNECTIONS_LEFT_PADDING;
      double alpha = bounds.bottom() > height ? 0.25 : 1;
      for (int column = 0; column < tiles_per_row; column++)
      {
        size_t index = _page_start + row * tiles_per_row + column;
        if (index >= connections->size())
        {
          done = true;
          break;
        }
        else
        {
          int draw_position = (row % 2) + column;
          if (index == 0 && _active_folder > -1)
            draw_back_tile(cr, bounds, index == _hot_entry);
          else
            draw_tile(cr, bounds, (*connections)[index], (draw_position % 2) != 0, index == _hot_entry, alpha);
        }
        bounds.pos.x += CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING;
      }

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

    // See if we need to draw the paging indicator.
    height -= CONNECTIONS_TOP_PADDING;
    int rows_per_page = height / (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING);
    if (rows_per_page < 1)
      rows_per_page = 1;
    int rows = (int)ceil(connections->size() / (float)tiles_per_row);
    _tiles_per_page = tiles_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)tiles_per_row);
      int current_page = (int)ceil(current_row / (float)rows_per_page);
      draw_paging_part(cr, current_page, pages);
    }
    else
    {
      _page_up_button_rect = base::Rect();
      _page_down_button_rect = base::Rect();
      _page_start = 0; // Size increased to cover the full content.
    }
  }

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

  void add_connection(const db_mgmt_ConnectionRef &connection, const std::string &title,
    const std::string &description, const std::string &user, const std::string &schema)
  {
    ConnectionEntry entry = {connection, title, description, user, schema, true, title, description, user, schema};
    
    std::string::size_type slash_position = title.find("/");
    if (slash_position != std::string::npos)
    {
      // A child entry.
      std::string parent_name = title.substr(0, slash_position);
      entry.title = title.substr(slash_position + 1);
      entry.search_title = entry.title;
      bool found_parent = false;
      for (ConnectionIterator iterator = _connections.begin(); iterator != _connections.end(); iterator++)
      {
        if (iterator->children.size() > 0 && iterator->title == parent_name)
        {
          found_parent = true;
          iterator->children.push_back(entry);
          break;
        }
      }

      if (!found_parent)
      {
        ConnectionEntry parent = {db_mgmt_ConnectionRef(), parent_name, "", "", "", true, parent_name};
        ConnectionEntry back_entry = {db_mgmt_ConnectionRef(), "< back", "", "", ""};
        parent.children.push_back(back_entry);
        parent.children.push_back(entry);
        _connections.push_back(parent);
      }
    }
    else
      _connections.push_back(entry);
    set_layout_dirty(true);
  }

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

  void clear_connections()
  {
    _entry_for_menu = -1;
    _active_folder = -1;
    _connections.clear();
    _filtered = false;
    _filtered_connections.clear();
    _search_text.set_value("");

    set_layout_dirty(true);
  }

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

  void mouse_double_click(int button, int x, int y)
  {
    switch (button)
    {
      case 0: // left button
      {
        // In order to allow quick clicking for page flipping we also handle double clicks for this.
        if (_page_up_button_rect.contains(x, y))
        {
          _page_start -= _tiles_per_page;
          if (_page_start < 0)
            _page_start = 0;
          set_needs_repaint();
          return;
        }

        if (_page_down_button_rect.contains(x, y))
        {
          _page_start += _tiles_per_page;
          set_needs_repaint();
          return;
        }

        break;
      }
    }
  }

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

  void mouse_click(int button, int x, int y)
  {
    switch (button)
    {
      case 0: // left button
        {
          if (_add_button_rect.contains(x, y))
          {
            _owner->trigger_callback(ActionNewConnection, grt::ValueRef());
            return;
          }

          if (_manage_button_rect.contains(x, y))
          {
            _owner->trigger_callback(ActionManageConnections, grt::ValueRef());
            return;
          }

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

          if (_page_down_button_rect.contains(x, y))
          {
            _page_start += _tiles_per_page;
            set_needs_repaint();
            return;
          }

          if (_hot_entry > -1)
          {
            if (_active_folder > -1 && _hot_entry == 0)
            {
              // Returning to root list.
              _page_start = _page_start_backup;
              _active_folder = -1;
              _filtered = false;
              _search_text.set_value("");
              set_needs_repaint();
              return;
            }

            bool is_folder = false;
            if (_filtered)
              is_folder = _filtered_connections[_hot_entry].children.size() > 1;
            else
            {
              if (_active_folder > -1)
                is_folder = _connections[_active_folder].children[_hot_entry].children.size() > 1;
              else
                is_folder = _connections[_hot_entry].children.size() > 1;
            }

#ifdef __APPLE__
            bool show_info = _info_button_rect.contains_flipped(x, y);
#else
            bool show_info = _show_details;
#endif

            if (!is_folder && show_info)
            {
              show_info_popup();
              return;
            }

            if (is_folder)
            {
              // Drilling into a folder.
              _page_start_backup = _page_start;
              _page_start = 0;
              _active_folder = _hot_entry;
              _filtered = false;
              _search_text.set_value("");
              set_needs_repaint();
              return;
            }

            // Anything else.
            if (_filtered)
              _owner->trigger_callback(ActionOpenConnectionFromList,
                                       _filtered_connections[_hot_entry].connection
                                       );
            else
              _owner->trigger_callback(ActionOpenConnectionFromList,
                                       (_active_folder == -1) ? _connections[_hot_entry].connection :
                                       _connections[_active_folder].children[_hot_entry].connection
                                       );
          }
        }
        break;
      case 1: // right button
        {
          if (_hot_entry > -1)
          {
            if (_active_folder > -1)
            {
              // There can't be any folder, as we don't support folder nesting.
              if (_context_menu != NULL && _hot_entry > 0)
              {
                _entry_for_menu = _hot_entry;
                _context_menu->popup_at(this, x, y);
              }
            }
            else
            {
              bool is_folder;
              if (_filtered)
                is_folder = _filtered_connections[_hot_entry].children.size() > 0;
              else
                is_folder = _connections[_hot_entry].children.size() > 0;
              if (is_folder)
              {
                if (_folder_context_menu != NULL)
                {
                  _entry_for_menu = _hot_entry;
                  _folder_context_menu->popup_at(this, x, y);
                }
              }
              else
                if (_context_menu != NULL)
                {
                  _entry_for_menu = _hot_entry;
                  _context_menu->popup_at(this, x, y);
                }
            }
          }
        }
        break;
    }
  }

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

  void mouse_leave()
  {
    // Ignore mouse leaves if we are showing the info popup. We want the entry to stay hot.
    if (_info_popup != NULL)
      return;

    if (_hot_entry > -1)
    {
      _hot_entry = -1;
      _show_details = false;
      set_needs_repaint();
    }
  }

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

  void mouse_move(int x, int y)
  {
    bool in_details_area;
    int entry = entry_from_point(x, y, in_details_area);
    if (entry != _hot_entry || _show_details != in_details_area)
    {
      _hot_entry = entry;
#ifndef __APPLE__
      if (_hot_entry > - 1)
        _show_details = in_details_area;
#else
      _show_details = true;
#endif
      set_needs_repaint();
    }
  }

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

  void set_context_menu(mforms::Menu *menu, bool forFolder)
  {
    if (forFolder)
    {
      if (_folder_context_menu != NULL)
        _folder_context_menu->release();
      _folder_context_menu = menu;
      if (_folder_context_menu != NULL)
      {
        _folder_context_menu->retain();
        menu->set_handler(boost::bind(&ConnectionsSection::handle_folder_command, this, _1));
      }
    }
    else
    {
      if (_context_menu != NULL)
        _context_menu->release();
      _context_menu = menu;
      if (_context_menu != NULL)
      {
        _context_menu->retain();
        menu->set_handler(boost::bind(&ConnectionsSection::handle_command, this, _1));
      }
    }

    if (menu != NULL)
      scoped_connect(menu->signal_will_show(), boost::bind(&ConnectionsSection::menu_open, this));
  }

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

  void handle_command(const std::string &command)
  {
    grt::ValueRef item;
    if (_active_folder > -1)
    {
      if (command == "delete_connection_all")
      {
        // We only want to delete all connections in the active group. This is the same as
        // removing the group entirely, since the group is formed by connections in it.
        _entry_for_menu = _active_folder;
        handle_folder_command("delete_connection_group");
        return;
      }
      else
      {
        if (_filtered)
          item = _filtered_connections[_entry_for_menu].connection;
        else
          item = _connections[_active_folder].children[_entry_for_menu].connection;
      }
    }
    else
    {
      if (_filtered)
        item = _filtered_connections[_entry_for_menu].connection;
      else
        item = _connections[_entry_for_menu].connection;
    }
    _owner->handle_context_menu(item, command);
    _entry_for_menu = -1;
  }

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

  void handle_folder_command(const std::string &command)
  {
    grt::ValueRef item;

    // We have to pass on a valid connection (for the group name).
    // All child items have the same group name (except the dummy entry for the back tile).
    if (_filtered)
      item = grt::StringRef(_filtered_connections[_entry_for_menu].title);
    else
      item = grt::StringRef(_connections[_entry_for_menu].title);
    _owner->handle_context_menu(item, command);
    _entry_for_menu = -1;
  }

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

  void menu_open()
  {
    int first_index = _active_folder > -1 ? 1 : 0;
    int last_index;

    if (_filtered)
      last_index = _filtered_connections.size() - 1;
    else
      last_index = _active_folder > -1 ? _connections[_active_folder].children.size() - 1 : _connections.size() - 1;
    if (_context_menu != NULL)
    {
      _context_menu->set_item_enabled(_context_menu->get_item_index("move_connection_up"), _entry_for_menu > first_index);
      _context_menu->set_item_enabled(_context_menu->get_item_index("move_connection_down"), _entry_for_menu < last_index);
    }
    if (_folder_context_menu != NULL)
    {
      _folder_context_menu->set_item_enabled(_folder_context_menu->get_item_index("move_connection_up"), _entry_for_menu > first_index);
      _folder_context_menu->set_item_enabled(_folder_context_menu->get_item_index("move_connection_down"), _entry_for_menu < last_index);
    }
  }

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

  /**
   * Displays the info popup for the hot entry and enters a quasi-modal-state.
   */
  void show_info_popup()
  {
    if (_info_popup != NULL || _parent == NULL)
      return;

    // We have checked in the hit test already that we are on a valid connection object.
    int x = _parent->get_x();
    int y = _parent->get_y();
    _parent->client_to_screen(x, y);

    // Stretch the popup window over all 3 sections, but keep the info area in our direct parent's bounds
    base::Rect host_bounds = base::Rect(x, y, _parent->get_parent()->get_width(), _parent->get_parent()->get_height());

    int width = get_width();
    width -= CONNECTIONS_LEFT_PADDING + CONNECTIONS_RIGHT_PADDING;
    int tiles_per_row = width / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);

    int top_entry = _hot_entry - _page_start;
    int row = top_entry / tiles_per_row;
    int column = top_entry % tiles_per_row;
    x = CONNECTIONS_LEFT_PADDING + column * (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
    y = CONNECTIONS_TOP_PADDING + row * (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING);
    base::Rect item_bounds = base::Rect(x, y, CONNECTIONS_TILE_WIDTH, CONNECTIONS_TILE_HEIGHT);
    db_mgmt_ConnectionRef connection;

    if (_filtered)
      connection = _filtered_connections[_hot_entry].connection;
    else
      connection = _active_folder > -1 ?
        _connections[_active_folder].children[_hot_entry].connection :
        _connections[_hot_entry].connection;

    db_mgmt_ServerInstanceRef instance;
    grt::ListRef<db_mgmt_ServerInstance> instances = _owner->rdbms()->storedInstances();
    for (grt::ListRef<db_mgmt_ServerInstance>::const_iterator iterator = instances.begin();
      iterator != instances.end(); iterator++)
    {
      if ((*iterator)->connection() == connection)
      {
        instance = *iterator;
        break;
      }
    }

    int info_width =  _parent->get_width();
    if (info_width < 735)
      info_width = (int)host_bounds.width();
    _info_popup = mforms::manage(new ConnectionInfoPopup(_owner, connection, instance, host_bounds, item_bounds, info_width));
    scoped_connect(_info_popup->on_close(), boost::bind(&ConnectionsSection::popup_closed, this));
  }

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

  void hide_info_popup()
  {
    if (_info_popup != NULL)
    {
      _hot_entry = -1;
      _show_details = false;

      _info_popup->release();
      _info_popup = NULL;

      set_needs_repaint();
    }
  }

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

  void popup_closed()
  {
    hide_info_popup();
  }

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

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

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

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

  std::string title;
  std::string title_shorted;
  std::string schemas;
  std::string schemas_shorted;
  std::string last_accessed;
  std::string size;
  bool is_model;

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

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* _size_icon;
  cairo_surface_t* _close_icon;
  cairo_surface_t* _open_icon;
  cairo_surface_t* _action_icon;

  int _page_start;
  int _entries_per_page;
  int _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;

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

  base::Rect _add_button_rect;
  base::Rect _open_button_rect;
  base::Rect _action_button_rect;
  base::Rect _close_button_rect;
  base::Rect _use_default_button_rect;
  base::Rect _page_up_button_rect;
  base::Rect _page_down_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;

    _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");
    _model_icon = mforms::Utilities::load_icon("wb_doc_model.png");
    _sql_icon = mforms::Utilities::load_icon("wb_doc_sql.png");
    _schema_icon = mforms::Utilities::load_icon("wb_tile_schema.png");
    _time_icon = mforms::Utilities::load_icon("wb_tile_time.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");
  }

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

  ~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(_size_icon);
    delete_surface(_close_icon);
    delete_surface(_open_icon);
    delete_surface(_action_icon);
  }

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

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

#define DOCUMENTS_ENTRY_WIDTH     183 // 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.

  int 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)
  {
    cairo_set_source_surface(cr, icon, x, y);
    cairo_paint(cr);

    x += image_width(icon) + 3;

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

    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)
  {
    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_rect = base::Rect(0, 0, image_width(_page_down_icon), image_height(_page_down_icon));
    double y = get_height() - _page_down_button_rect.width() - 6;
    double x = get_width() - extents.width - 8;
    double icon_x = x + ceil((extents.width - _page_down_button_rect.width()) / 2.0) + 1;
    _page_down_button_rect.pos = base::Point(icon_x, y);

    cairo_set_source_surface(cr, _page_down_icon, icon_x, y);
    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_rect = base::Rect();
    }
    else
      cairo_paint(cr);

    y -= 6;

    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_rect = base::Rect(icon_x, 0, image_width(_page_up_icon), image_height(_page_up_icon));
    y -= extents.height + 6 + _page_up_button_rect.height();
    _page_up_button_rect.pos.y = y;

    cairo_set_source_surface(cr, _page_up_icon, icon_x, y);
    if (current_page == 1)
    {
      cairo_paint_with_alpha(cr, 0.5);
      _page_up_button_rect = base::Rect();
    }
    else
      cairo_paint_with_alpha(cr, 1);
  }

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

  void draw_entry(cairo_t *cr, base::Rect bounds, const DocumentEntry &entry, bool hot)
  {
    cairo_set_source_surface(cr, _model_icon, bounds.left(), bounds.top());
    cairo_paint(cr);

    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)(bounds.left() + image_width(_model_icon) + 10);
    int y = (int)bounds.top() + 18;
    double width = 0;
    if (hot)
    {
      cairo_text_extents_t extents;
      cairo_text_extents(cr, entry.title_shorted.c_str(), &extents);
      width = ceil(extents.width);
    }
    text_with_decoration(cr, x, y, entry.title_shorted.c_str(), hot, width);

    cairo_set_font_size(cr, HOME_SMALL_INFO_FONT_SIZE);

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

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

  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

  void draw_selection_message(cairo_t *cr)
  {
    // Attach the message to the current active entry as this is what is used when
    // a connection is opened.
    int column = (_active_entry - _page_start) % _entries_per_row;
    int 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);
    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_rect = base::Rect(heading_left, DOCUMENTS_TOP_BASELINE - image_height(_plus_icon),
        image_width(_plus_icon), image_height(_plus_icon));

      _open_button_rect = base::Rect(_add_button_rect.right() + 10, DOCUMENTS_TOP_BASELINE - image_height(_open_icon),
        image_width(_open_icon), image_height(_open_icon));

      _action_button_rect = base::Rect(_open_button_rect.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->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 repaint(cairo_t *cr, int areax, int areay, int areaw, int areah)
  {
    int width = get_width();
    int height = get_height();

    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;

    // Heading for switching display mode. Draw heading hot only when we support more sections.
    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());

    cairo_set_source_surface(cr, _plus_icon, _add_button_rect.left(), _add_button_rect.top());
    cairo_paint(cr);

    cairo_set_source_surface(cr, _open_icon, _open_button_rect.left(), _open_button_rect.top());
    cairo_paint(cr);

    cairo_set_source_surface(cr, _action_icon, _action_button_rect.left(), _action_button_rect.top());
    cairo_paint(cr);

    /* Disabled for now.
    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());

    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!
    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);
*/
    
    int row = 0;
    base::Rect bounds(0, DOCUMENTS_TOP_PADDING, DOCUMENTS_ENTRY_WIDTH, DOCUMENTS_ENTRY_HEIGHT);
    bool done = false;
    while (!done)
    {
      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
          draw_entry(cr, bounds, _filtered_documents[index], _hot_entry == index);
        bounds.pos.x += DOCUMENTS_ENTRY_WIDTH;
      }

      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);
    }
    else
    {
      _page_up_button_rect = base::Rect();
      _page_down_button_rect = base::Rect();
      _page_start = 0; // Size increased to cover the full content.
    }

    if (_show_selection_message)
      draw_selection_message(cr);
  }

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

  void add_document(const grt::StringRef &path, const time_t &time, const std::string schemas,
    long file_size)
  {
    DocumentEntry entry = {path, time, "", "", schemas};
    entry.title = base::strip_extension(base::basename(path));
    if (entry.title.empty())
      entry.title = "???";
    entry.is_model = base::tolower(base::extension(path)) == ".mwb";

    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);
  }

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

  void mouse_double_click(int button, int x, int y)
  {
    // Similar handling like for single mouse down.
    switch (button)
    {
      case 0: // left button
      {
        if (_add_button_rect.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;
        }

        if (_open_button_rect.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;
        }

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

        if (_page_up_button_rect.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;
        }

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

        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;
        }

        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;
        }

        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;
        }

        // 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);
        }
      }
        break;
      case 1: // right button
      {
        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);
        }
      }
        break;
    }
  }

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

  void mouse_click(int button, int x, int y)
  {
    switch (button)
    {
      case 0: // left button
        {
          if (_show_selection_message && _close_button_rect.contains(x, y))
          {
            _owner->cancel_script_loading();
            return;
          }

          if (_add_button_rect.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;
          }

          if (_open_button_rect.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;
          }

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

          if (_page_up_button_rect.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;
          }

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

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

          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;
          }

          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;
          }

          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;
          }

          // 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);
          }
        }
        break;
      case 1: // right button
        {
          _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);
          }
        }
        break;
    }
  }

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

  void mouse_leave()
  {
    if (_hot_heading != Nothing || _hot_entry > -1)
    {
      _hot_heading = Nothing;
      _hot_entry = -1;
      set_needs_repaint();
    }
  }

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

  void mouse_move(int x, int y)
  {
    int entry = entry_from_point(x, y);
    if (entry != _hot_entry)
    {
      _hot_entry = entry;
      set_needs_repaint();
    }

    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;
        set_needs_repaint();
      }
    }
  }

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

  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();
  }
};

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

struct ShortcutEntry
{
  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.
};

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;

  int _page_start;
  int _shortcuts_per_page;
  cairo_surface_t *_page_down_icon;
  cairo_surface_t *_page_up_icon;
  base::Rect _page_up_button_rect;
  base::Rect _page_down_button_rect;

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");
    _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;
    /* 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)
  {
    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_rect = base::Rect(0, 0, image_width(_page_down_icon), image_height(_page_down_icon));
    double y = get_height() - _page_down_button_rect.width() - 6;
    double x = get_width() - extents.width - 8;
    double icon_x = x + ceil((extents.width - _page_down_button_rect.width()) / 2.0) + 1;
    _page_down_button_rect.pos = base::Point(icon_x, y);

    cairo_set_source_surface(cr, _page_down_icon, icon_x, y);
    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_rect = base::Rect();
    }
    else
      cairo_paint(cr);

    y -= 6;

    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_rect = base::Rect(icon_x, 0, image_width(_page_up_icon), image_height(_page_up_icon));
    y -= extents.height + 6 + _page_up_button_rect.height();
    _page_up_button_rect.pos.y = y;

    cairo_set_source_surface(cr, _page_up_icon, icon_x, y);
    if (current_page == 1)
    {
      cairo_paint_with_alpha(cr, 0.5);
      _page_up_button_rect = base::Rect();
    }
    else
      cairo_paint_with_alpha(cr, 1);
  }

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

#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);

    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++)
      {
        double alpha = (yoffset + SHORTCUTS_ROW_HEIGHT) > height ? 0.25 : 1;
        cairo_set_source_surface(cr, iterator->icon, SHORTCUTS_LEFT_PADDING, yoffset);
        cairo_paint_with_alpha(cr, alpha);

        if (!iterator->title.empty())
        {
          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);
      }
      else
      {
        _page_up_button_rect = base::Rect();
        _page_down_button_rect = base::Rect();
        _page_start = 0; // Size increased to cover the full content.
      }
    }
  }

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

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

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

    size_t row = y / (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING);
    int height = get_height() - SHORTCUTS_TOP_PADDING;
    int row_bottom = row * (SHORTCUTS_ROW_HEIGHT + SHORTCUTS_SPACING) + SHORTCUTS_ROW_HEIGHT;
    if (row_bottom > height)
      return app_StarterRef(); // 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 (_shortcuts.begin() + row)->shortcut;

    return app_StarterRef();
  }

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

  /**
   * 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 = {shortcut, NULL, "", base::Rect()};

    // See if we can load the icon. If not use the placeholder.
    entry.icon = mforms::Utilities::load_icon(icon_name);
    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;
      }
    }
  }

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

  void mouse_double_click(int button, int x, int y)
  {
    mouse_click(button, x, y); // Handle both the same way. Important especially for fast scrolling.
  }

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

  void mouse_click(int button, int x, int y)
  {
    switch (button)
    {
      case 0: // left button
        {
          if (_page_up_button_rect.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;
          }

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

          if (_hot_shortcut.is_valid())
            _owner->trigger_callback(ActionShortcut, _hot_shortcut);
        }
        break;
      case 1: // right button
        {
          if (_hot_shortcut.is_valid())
          {
            _active_shortcut = _hot_shortcut;
            _shortcut_context_menu.popup_at(this, x, y);
          }
        }
        break;
    }
  }

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

  void mouse_leave()
  {
    if (_hot_shortcut.is_valid())
    {
      _hot_shortcut = app_StarterRef();
      set_needs_repaint();
    }
  }

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

  void mouse_move(int x, int y)
  {
    app_StarterRef shortcut = shortcut_from_point(x, y);
    if (shortcut != _hot_shortcut)
    {
      _hot_shortcut = shortcut;
      set_needs_repaint();
    }
  }

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

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

//----------------- 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 *left_part = mforms::manage(new mforms::Box(false));
  _connection_section = new ConnectionsSection(this);
#ifdef __APPLE__
  _connection_section->set_back_color("#323232");
#else
  _connection_section->set_back_color("#1d1d1d");
#endif

  _connection_section->init();
  left_part->add(_connection_section, true, true);

  _document_section = new DocumentsSection(this);
#ifdef __APPLE__
  _document_section->set_back_color("#343434");
#else
  _document_section->set_back_color("#242424");
#endif

  _document_section->set_size(-1, 236);
  left_part->add(_document_section, false, true);

  add(left_part, true, true);

  _shortcut_section = new ShortcutSection(this);
#ifdef __APPLE__
  _shortcut_section->set_back_color("#373737");
#else
  _shortcut_section->set_back_color("#303030");
#endif

  _shortcut_section->set_size(300, -1);
  add(_shortcut_section, false, true);
  
  _menubar = mforms::manage(cmdui->create_menubar_for_context(WB_CONTEXT_HOME_GLOBAL));
  //_toolbar = mforms::manage(cmdui->create_toolbar(""));

  Box::scoped_connect(signal_resized(), boost::bind(&HomeScreen::on_resize, this));
}

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

HomeScreen::~HomeScreen()
{
  delete _shortcut_section;
  delete _connection_section;
  delete _document_section;
}

//--------------------------------------------------------------------------------------------------
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()
{
  _connection_section->clear_connections();
}

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

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::set_connection_menu(mforms::Menu *menu, bool forFolder)
{
  _connection_section->set_context_menu(menu, forFolder);
}

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

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_document_menu(mforms::Menu *menu, bool forModels)
{
  _document_section->set_context_menu(menu, forModels);
}

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

void HomeScreen::set_action_document_menu( mforms::Menu *menu, bool forModels )
{
  _document_section->set_action_context_menu(menu, forModels);
}

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

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();
}

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