/* 
 * Copyright (c) 2009, 2011, 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 <math.h>

#include "grtpp.h"
#include "grts/structs.app.h"

#include "mdc_image_manager.h"
#include "home_screen.h"
#include "base/string_utilities.h"

#include "mforms/button.h"
#include "mforms/imagebox.h"
#include "mforms/menubar.h"

using namespace wb;
using namespace mforms;
using namespace MySQL::Geometry;

#ifdef __APPLE__
#define HOME_NORMAL_FONT "Lucida Grande"
#define HOME_DETAILS_FONT "Lucida Grande"
#define ACTION_LINK_NORMAL_FONT "Tahoma"
#elif _WIN32
#define HOME_NORMAL_FONT "Tahoma"
#define HOME_DETAILS_FONT "Arial"
#define ACTION_LINK_NORMAL_FONT "Tahoma"
#else
#define HOME_NORMAL_FONT "Helvetica"
#define HOME_DETAILS_FONT "Helvetica"
#define ACTION_LINK_NORMAL_FONT "Tahoma"
#endif

#define HOME_NORMAL_FONT_SIZE 13
#define HOME_DETAILS_FONT_SIZE 11
#define HOME_STARTER_FONT_SIZE 11

#define ACTION_LINK_NORMAL_FONT_SIZE 12
#define ACTION_LINK_DETAILS_FONT_SIZE 11

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

// 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 void delete_pattern(cairo_pattern_t* pattern)
{
  if (pattern != NULL)
    cairo_pattern_destroy(pattern);
}

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

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, int width)
{
  cairo_set_source_rgb(cr, 0x37 / 255.0, 0x3d / 255.0, 0x48 / 255.0);
  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, y + 1.5);
    cairo_line_to(cr, x + width, y + 1.5);
    cairo_stroke(cr);
  }
}

//----------------- WorkbenchCentral ---------------------------------------------------------------

WorkbenchCentral::WorkbenchCentral(HomeScreen* owner, grt::GRT* grt)
: DrawBox()
{
  _grt= grt;
  _owner= owner;
  _title_image= Utilities::load_icon("wb_central_title_en.png");
  _main_icon= Utilities::load_icon("wb_central_logo.png");
  _pointer_icon= Utilities::load_icon("news_arrow.png");

  _more_starters_icon= Utilities::load_icon("wb_starter_more.png");
  _more_starters_icon_bounds.size.width = image_width(_more_starters_icon);
  _more_starters_icon_bounds.size.height = image_height(_more_starters_icon);
  _default_starter_icon= Utilities::load_icon("wb_starter_generic_52.png");
  
  _title1_hot= false;
  _starter_context_menu.add_item("Remove Starter from Home Screen", "remove_starter");
  _starter_context_menu.set_handler(boost::bind(&WorkbenchCentral::handle_command, this, _1));
}

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

WorkbenchCentral::~WorkbenchCentral()
{
  delete_surface(_title_image);
  delete_surface(_main_icon);
  delete_surface(_pointer_icon);
  delete_surface(_more_starters_icon);
  delete_surface(_default_starter_icon);

  void clear_starters();
}

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

#define CENTRAL_MIN_STARTER_ICON_SPACING 22 // Minimal distance between two starter icons.
#define CENTRAL_MIN_STARTER_TEXT_SPACING 4  // Minimal distance between starter text labels.
#define CENTRAL_STARTER_MORE_SPACING 16     // Horizontal distance between last starter icon and "more" button.

/**
 * Computes the offset to the next starter entry, which is a bit more involved 
 * due to some constraints we have.
 */
double WorkbenchCentral::get_next_offset(std::vector<StarterEntry>::iterator iterator, bool is_end_of_list)
{
  // The task is to find the offset from the current starter entry left border (given by the iterator)
  // to the next entry's left border, with these rules:
  // 1) Use as few space as possible.
  // 2) Maintain a minimum distance between the entry icons.
  // 3) Maintain a minimum distance between the text labels.
  // 4) The distance from the last starter to the "more" button is different.

  // Start with the minimal distance we need, depending on how much space the labels require.
  // This distance goes from the right icon border to the next starter entry left border.
  double next_left_offset= (is_end_of_list) ? 0 : (iterator + 1)->icon_bounds.left();
  double entry_distance= iterator->bounds.size.width - iterator->icon_bounds.right() +
    CENTRAL_MIN_STARTER_TEXT_SPACING + next_left_offset;
  double icon_distance= (is_end_of_list) ? CENTRAL_STARTER_MORE_SPACING : CENTRAL_MIN_STARTER_ICON_SPACING;
  if (entry_distance < icon_distance)
    entry_distance= icon_distance;

  // Given that partial offset we just computed, we can now determine the full offset.
  return ceil(iterator->icon_bounds.right() + entry_distance - next_left_offset);
}

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

/**
 * Called from the context menu.
 */
void WorkbenchCentral::handle_command(const std::string& command)
{
  if (command == "remove_starter")
    _owner->trigger_callback(ActionRemoveStarter, _last_hit);
}

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

app_StarterRef WorkbenchCentral::starter_from_point(int x, int y)
{
  for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); iterator != _starters.end();
    iterator++)
    if (iterator->bounds.contains(x, y))
      return iterator->starter;

  return app_StarterRef();
}

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

#define TITLE _("Welcome to MySQL Workbench")
#define CHAPTER1_TITLE _("What's New in This Release?")
#define CHAPTER1_DETAILS _("Read about all changes in this MySQL Workbench release.")

#define CENTRAL_STARTER_OFFSET 130       // The distance between header image and starters block.
#define CENTRAL_RIGHT_SPACING 16         // Horizontal distance between right border and starter block.

void WorkbenchCentral::repaint(cairo_t* cr, int areax, int areay, int areaw, int areah)
{
  layout(cr);
  
  // TODO: cache the gradient.
  cairo_pattern_t* gradient = cairo_pattern_create_linear(0, 0, 0, get_height());
  cairo_pattern_add_color_stop_rgb(gradient, 0, 0xff / 255.0, 0xff / 255.0, 0xff / 255.0);
  cairo_pattern_add_color_stop_rgb(gradient, 0.33, 0xff / 255.0, 0xff / 255.0, 0xff / 255.0);
  cairo_pattern_add_color_stop_rgb(gradient, 1, 0xf4 / 255.0, 0xf7 / 255.0, 0xfc / 255.0);
  
  cairo_set_source(cr, gradient);
  cairo_paint(cr);
  
  cairo_pattern_destroy(gradient);

  double width= get_width();
  
  cairo_set_source_surface(cr, _main_icon, 14, 11);
  cairo_paint(cr);

  cairo_set_source_surface(cr, _title_image, _title_bounds.left(), _title_bounds.top());
  cairo_paint(cr);

  cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, HOME_NORMAL_FONT_SIZE);
  text_with_decoration(cr, _chapter1_title_bounds.left(), _chapter1_title_bounds.top(), CHAPTER1_TITLE, 
                       _title1_hot, (int) _chapter1_title_bounds.size.width);

  cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, HOME_DETAILS_FONT_SIZE);
  
  // The second part might not have enough room, so we need to shorten the text eventually.
  std::string text= Utilities::shorten_string(cr, CHAPTER1_DETAILS, width / 2);
  text_with_decoration(cr, _chapter1_details_bounds.left(), _chapter1_details_bounds.top(), text.c_str(),
                       false, 0);

  cairo_set_source_surface(cr, _pointer_icon, _arrow1_bounds.left(), _arrow1_bounds.top());
  cairo_paint(cr);
  
  int starter_top= (get_height() - _starter_area_height) / 2;
  int current_offset= (int) _title_bounds.left() + (int) _title_bounds.size.width + CENTRAL_STARTER_OFFSET;
  int available_width= get_width() - current_offset - image_width(_more_starters_icon) - CENTRAL_RIGHT_SPACING;

  // Starter block.
  if (_starters.size() > 0)
  {
    cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_STARTER_FONT_SIZE);

    // Count how many starters we can show in the available area and compute the remaining space we
    // have to distribute between the buttons.
    int starter_count= 0;
    int total_width= 0;
    for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); iterator != _starters.end(); iterator++)
    {
      double next_offset= get_next_offset(iterator, (iterator + 1) == _starters.end());
      if (total_width + (int) next_offset > available_width)
        break;
      total_width += (int) next_offset;
      starter_count++;
    }

    if (starter_count > 0)
    {
      // Starter buttons are right aligned if they all fit into the available space. Otherwise we
      // distribute the remaining space equally to all starter distances.
      int remaining_space= 0;
      if (starter_count == _starters.size())
        current_offset += available_width - total_width;
      else
        remaining_space= available_width - total_width;

      int extra_offset= 0;

      // Do an integer div and keep the remaining pixel(s) separately to show smooth layout changes when resizing.
      extra_offset= remaining_space / starter_count;
      remaining_space= available_width - extra_offset * starter_count;

      for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); 
        iterator != _starters.end(), starter_count > 0; iterator++)
      {
        // Store final position of the starter for later hit tests.
        iterator->bounds.pos.x = current_offset;
        iterator->bounds.pos.y = starter_top;

        cairo_set_source_surface(cr, iterator->icon, current_offset + iterator->icon_bounds.left(), 
          starter_top + iterator->icon_bounds.top());
        cairo_paint(cr);

        cairo_set_source_rgb(cr, 106 / 255.0, 109 / 255.0, 118 / 255.0);
        if (iterator->title_line1.length() > 0)
        {
          cairo_move_to(cr, current_offset + iterator->title1_location.x, starter_top + iterator->title1_location.y);
          cairo_show_text(cr, iterator->title_line1.c_str());
          cairo_stroke(cr);
        }
        if (iterator->title_line2.length() > 0)
        {
          cairo_move_to(cr, current_offset + iterator->title2_location.x, starter_top + iterator->title2_location.y);
          cairo_show_text(cr, iterator->title_line2.c_str());
          cairo_stroke(cr);
        }

        current_offset += (int) get_next_offset(iterator,(iterator + 1) == _starters.end()) + extra_offset;
        if (remaining_space > 0)
        {
          current_offset++;
          remaining_space--;
        }

        starter_count--;
      }
    }
  }

  // Finally the "more" button. Draw it vertically centered relative to the last starter button
  // (if there is any) or centered over the available height).
  if (_starters.size() > 0)
    starter_top += (int) (_starters.back().icon_bounds.top() +
      (_starters.back().icon_bounds.size.height - _more_starters_icon_bounds.size.height) / 2);
  else
  {
    starter_top= (int) ((get_height() - _more_starters_icon_bounds.size.height) / 2);
    current_offset += available_width;
  }
  cairo_set_source_surface(cr, _more_starters_icon, current_offset, starter_top);
  cairo_paint(cr);

  _more_starters_icon_bounds.pos.x = current_offset;
  _more_starters_icon_bounds.pos.y = starter_top;
}

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

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

  StarterEntry entry= {starter, NULL, "", ""};

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

  // Check if the icon isn't too large.
  if (image_height(entry.icon) > 55 || image_width(entry.icon) > 70)
  {
    delete_surface(entry.icon);
    mforms::Utilities::show_warning(_("Home Screen Starter Error"), _("Home Screen error: starter icon too large (") + icon_name + ")", "OK");
    entry.icon= _default_starter_icon;
  }

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

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

void WorkbenchCentral::clear_starters()
{
  for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); iterator != _starters.end(); iterator++)
    if (iterator->icon != _default_starter_icon)
      delete_surface(iterator->icon);
  _starters.clear();
  set_layout_dirty(true);
}

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

#define CENTRAL_LEFT_SPACING 12          // Space between left border and icon.
#define CENTRAL_TOP_SPACING 24           // Vertical space between top border and heading.
#define CENTRAL_ICON_SPACING 18          // Horizontal space between icon and content.
#define CENTRAL_ARROW_OFFSET 2           // Horizontal movement of the left arrow (there is some inset).
#define CENTRAL_HEADLINE_ICON_SPACING 7  // Vertical space between headline and pointer icon.
#define CENTRAL_HEADLINE_SPACING 8       // Vertical distance between headline image and normal text.
#define CENTRAL_CHAPTER_ARROW_SPACING 2  // Space between chapter arrow and chapter title.
#define CENTRAL_LINE_SPACING 2           // Extra space between two lines of normal text.

void WorkbenchCentral::layout(cairo_t* cr)
{
  if (is_layout_dirty())
  {
    cairo_text_extents_t extents;

    set_layout_dirty(false);
    
    double offset= CENTRAL_LEFT_SPACING + image_width(_main_icon) + CENTRAL_ICON_SPACING;

    _title_bounds.pos.x = offset;
    _title_bounds.pos.y = CENTRAL_TOP_SPACING;
    _title_bounds.size.width= image_width(_title_image);
    _title_bounds.size.height= image_height(_title_image);
    
    // Text sizes for chapter text.
    cairo_select_font_face(cr, HOME_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, HOME_NORMAL_FONT_SIZE);
    cairo_text_extents(cr, CHAPTER1_TITLE, &extents);
    _chapter1_title_bounds.size.width= ceil(extents.width);
    _chapter1_title_bounds.size.height= ceil(extents.height);

    cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
    cairo_set_font_size(cr, HOME_DETAILS_FONT_SIZE);
    cairo_text_extents(cr, CHAPTER1_DETAILS, &extents);
    _chapter1_details_bounds.size.width= ceil(extents.width);
    _chapter1_details_bounds.size.height= ceil(extents.height);

    // Chapter 1 arrow, title and details text positioning.
    _chapter1_title_bounds.pos.y = _title_bounds.top() + _title_bounds.height() + _chapter1_title_bounds.height() + CENTRAL_HEADLINE_SPACING;

    _arrow1_bounds.pos.y = _title_bounds.top() + _title_bounds.height() + CENTRAL_HEADLINE_ICON_SPACING;
    _arrow1_bounds.pos.x = _title_bounds.left() + CENTRAL_ARROW_OFFSET;
    _arrow1_bounds.size.width= image_width(_pointer_icon);
    _arrow1_bounds.size.height= image_height(_pointer_icon);
    
    _chapter1_title_bounds.pos.x = _arrow1_bounds.left() + _arrow1_bounds.width() + CENTRAL_CHAPTER_ARROW_SPACING;
    _chapter1_details_bounds.pos.x = _chapter1_title_bounds.left();

    _chapter1_details_bounds.pos.y = _chapter1_title_bounds.top() + _chapter1_title_bounds.height() + CENTRAL_LINE_SPACING;
    
    // Starters.
    _starter_area_height= 0;
    cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, HOME_STARTER_FONT_SIZE);

    cairo_font_extents_t font_extents; // To determine a constant line height for all text independent on content.
    cairo_font_extents(cr, &font_extents);
    double text_height= ceil(font_extents.height);

    // Compute bounding box for each starter entry.
    for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); iterator != _starters.end(); iterator++)
    {
      iterator->bounds.size.width= image_width(iterator->icon);
      iterator->bounds.size.height= image_height(iterator->icon);
      iterator->icon_bounds.size.width= iterator->bounds.size.width;
      iterator->icon_bounds.size.height= iterator->bounds.size.height;

      // If there is no title then we are done already.
      std::string title= iterator->starter->title();
      std::string description = iterator->starter->description();
      if (title.length() == 0)
      {
        iterator->bounds.pos.x = 0;
        iterator->bounds.pos.y = 0;
        iterator->icon_bounds.pos.x = 0;
        iterator->icon_bounds.pos.y = 0;
      }
      else
      {
        // Make sure the title does not mess up our layout. Hence we apply a few rules.
        // 1) The overall width of the starter entry must not exceed twice its icon width.
        // 2) Display at most two lines for the title.
        // 3) The first line should be at least have the icon width but be as small as possible.
        //    The actual width is determined by word wrapping.
        // 4) The rest of the title is displayed in the second line (if anything is left over).
        // 5) If any of the lines is wider than twice the icon width (after word wrapping) then
        //    shorten them and show ellipses at their ends.
        gchar* head= (gchar*) title.c_str();
        const gchar* end;
        g_utf8_validate(head, -1, &end); // Display only the valid part of the string.
        gchar* tail= head;
        double min_width= iterator->icon_bounds.size.width;

        while (true)
        {
          // Get the next word.
          while (tail != end && (*tail != ' '))
            tail= g_utf8_next_char(tail);
          gchar* part= g_strndup(head, tail - head);
          iterator->title_line1= part;
          
          // See which space the part requires. The result of this check is used later again.
          cairo_text_extents(cr, part, &extents);
          g_free(part);
          if ((tail == end) || ceil(extents.width) >= min_width)
            break;

          // Skip over the space char.
          tail= g_utf8_next_char(tail);
        }

        // Keep the last extent for further computation (the title part has already been stored above).
        // However, if the title part is already too large then shorten it.
        if (extents.width > 2 * min_width)
        {
          iterator->title_line1= Utilities::shorten_string(cr, iterator->title_line1, 2 * min_width);
          cairo_text_extents(cr, iterator->title_line1.c_str(), &extents);
        }
        cairo_text_extents_t line1_extents= extents;

        // Get the rest of the string and determine space requirements.
        // Skip over the split space char if we found one.
        if (tail != end)
          tail= g_utf8_next_char(tail);
        gchar* part= g_strndup(tail, end - tail);
        iterator->title_line2= Utilities::shorten_string(cr, part, 2 * min_width);
        cairo_text_extents(cr, iterator->title_line2.c_str(), &extents);
        g_free(part);

        double title_width;
        if (extents.width > line1_extents.width)
          title_width= ceil(extents.width);
        else
          title_width= ceil(line1_extents.width);

        if (title_width > iterator->bounds.size.width)
          iterator->bounds.size.width= title_width;

        if (iterator->title_line1.length() > 0)
        {
          iterator->bounds.size.height += text_height;
          iterator->title1_location.x= floor((iterator->bounds.size.width - ceil(line1_extents.width)) / 2);
          iterator->title1_location.y= image_height(iterator->icon) + text_height;
        }
        else
          iterator->title1_location= Point(0, image_height(iterator->icon));

        if (iterator->title_line2.length() > 0)
        {
          iterator->bounds.size.height += text_height;
          iterator->title2_location.x= floor((iterator->bounds.size.width - ceil(extents.width)) / 2);
          iterator->title2_location.y= iterator->title1_location.y + text_height;
        }

        // Now that we have the overall size move the icon to its final location.
        iterator->icon_bounds.pos.x = ceil((iterator->bounds.size.width - image_height(iterator->icon)) / 2);
      }

      if (iterator->bounds.size.height > _starter_area_height)
        _starter_area_height= (int) iterator->bounds.size.height;
    }
  }
}

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

void WorkbenchCentral::mouse_click(int button, int x, int y)
{
  switch (button)
  {
    case 0: // left button
      {
        HomeScreenAction action= action_from_point(x, y);
        if (action != ActionNone)
          _owner->trigger_callback(action, _last_hit);
      }
      break;
    case 1: // right button
      {
        // Find the index of the starter at the given position.
        int i= 0;
        _last_hit= grt::ValueRef();
        for (std::vector<StarterEntry>::iterator iterator= _starters.begin(); iterator != _starters.end();
          iterator++, i++)
          if (iterator->bounds.contains(x, y))
          {
            _last_hit= grt::IntegerRef(i);
            break;
          }

        if (_last_hit.is_valid())
          _starter_context_menu.popup_at(this, x, y);
      }
      break;
  }
}

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

/**
 * Determines an action from the given coordinates and sets the hit test result if a valid action 
 * could be found (is one is needed).
 */
HomeScreenAction WorkbenchCentral::action_from_point(int x, int y)
{
  _last_hit= grt::ValueRef(); // Invalid hit result.
  if (_chapter1_title_bounds.contains_flipped(x, y))
    return ActionWhatsNew;
  else
    if (_more_starters_icon_bounds.contains(x, y))
    {
      grt::DictRef dict= grt::DictRef(_grt);

      // Compute a hot spot just below the "more" button, horizontally centered.
      int x = (int)((_more_starters_icon_bounds.left() + _more_starters_icon_bounds.right()) / 2);
      int y = (int)(_more_starters_icon_bounds.bottom() + 17);
      client_to_screen(x, y);
      dict.gset("left", x);
      dict.gset("top", y);
      _last_hit= dict;
      return ActionStarterPopup;
    }
    else
      if (_hot_starter.is_valid())
      {
        _last_hit= _hot_starter;
        return ActionStarter;
      }

  return ActionNone;
}

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

void WorkbenchCentral::mouse_leave()
{
  bool repaint_needed= _title1_hot || _hot_starter.is_valid();
  _title1_hot= false;
  _hot_starter= app_StarterRef();
  if (repaint_needed)
    set_needs_repaint();
}

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

void WorkbenchCentral::mouse_move(int x, int y)
{
  bool repaint_needed= false;
  if (_chapter1_title_bounds.contains_flipped(x, y))
  {
    repaint_needed= !_title1_hot;
    _title1_hot= true;
  }
  else
  {
    repaint_needed= _title1_hot;
    _title1_hot= false;
  }

  app_StarterRef starter= starter_from_point(x, y);
  if (starter != _hot_starter)
  {
    _hot_starter= starter;
    repaint_needed= true;
  }

  if (repaint_needed)
    set_needs_repaint();
}

//----------------- ActionLink ---------------------------------------------------------------------

ActionLink::ActionLink(cairo_surface_t *icon, const std::string& title, const std::string& description,
                       const grt::ValueRef &object, HomeScreenAction action, bool enabled, ActionLinkType type)
{
  _icon= icon ? cairo_surface_reference(icon) : NULL;
  _title= title;
  _description= description;
  _last_text_width= 0;
  _action= action;
  _object= object;
  _enabled= enabled;
  _type= type;
  _parent = NULL;
}

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

ActionLink::~ActionLink()
{
  if (_icon)
    delete_surface(_icon);
}

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

#define ACTION_LINK_PADDING 2      // Left and right padding.
#define ACTION_LINK_ICON_SPACING 4 // Horizontal distance between icon and text.
#define ACTION_LINK_TEXT_SPACING 6 // Vertical distance between title and description.

void ActionLink::paint(cairo_t* cr, Rect bounds, bool hot, bool active)
{
  this->_bounds= bounds;
  cairo_save(cr);
  
  double icon_width= image_width(_icon);
  double icon_height= image_height(_icon);

  double new_width= bounds.size.width - 2 * ACTION_LINK_PADDING - icon_width - ACTION_LINK_ICON_SPACING;
  
  if (new_width != _last_text_width)
  {
    _last_text_width= new_width;
    layout(cr);
  }
  
  // Fill a blue background if the item is active.
  if (active && _enabled)
  {
    cairo_set_source_rgb(cr, 0x5a / 255.0, 0x85 / 255.0, 0xdc / 255.0);
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.size.width, bounds.size.height);
    cairo_fill(cr);
  }

  double child_padding = (_type == GroupedLink) ? icon_width : 0;
  cairo_set_source_surface(cr, _icon, bounds.left() + ACTION_LINK_PADDING + child_padding, bounds.top() + (int) ((bounds.height() - icon_height) / 2));
  if (_enabled)
    cairo_paint(cr);
  else
    cairo_paint_with_alpha(cr, 0.25);

  int text_offset= (int) (bounds.height() - _text_height) / 2;
  cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, ACTION_LINK_NORMAL_FONT_SIZE);

  if (!_enabled)
    cairo_set_source_rgb(cr, 0.75, 0.75, 0.75);
  else
    if (active)
      cairo_set_source_rgb(cr, 1, 1, 1);
    else
      if (hot)
        cairo_set_source_rgb(cr, 90 / 255.0, 147 / 255.0, 220 / 255.0);
      else
        cairo_set_source_rgb(cr, 0x37 / 255.0, 0x3d / 255.0, 0x48 / 255.0);
  double offset= bounds.left() + ACTION_LINK_PADDING + icon_width + ACTION_LINK_ICON_SPACING + child_padding;
  
  cairo_move_to(cr, offset, bounds.top()  + _title_offset + text_offset);
  cairo_show_text(cr, _shortened_title.c_str());
  cairo_stroke(cr);

  cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, ACTION_LINK_DETAILS_FONT_SIZE);
  
  if (!_enabled)
    cairo_set_source_rgb(cr, 0.75, 0.75, 0.75);
  else
    if (active)
      cairo_set_source_rgb(cr, 1, 1, 1);
    else
      cairo_set_source_rgb(cr, 0xc6 / 255.0, 0xc6 / 255.0, 0xc6 / 255.0);
  
  cairo_move_to(cr, offset, bounds.top()  + _description_offset + text_offset);
  cairo_show_text(cr, _shortened_description.c_str());
  cairo_stroke(cr);

  // Hot style implementation is currently done manually (draw a line).
  // TODO: replace this with font decoration once pango is incorporated.
  if (hot && _enabled)
  {
    cairo_set_line_width(cr, 1);
    cairo_set_source_rgb(cr, 90 / 255.0, 147 / 255.0, 220 / 255.0);
    cairo_move_to(cr, offset, bounds.top() + _title_offset + text_offset + 1.5);
    cairo_line_to(cr, offset + _title_width, bounds.top() + _title_offset + text_offset + 1.5);
    cairo_stroke(cr);
  }
  
  cairo_restore(cr);
}

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

void ActionLink::layout(cairo_t* cr)
{
  // Re-compute shortened title and its position.
  cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, ACTION_LINK_NORMAL_FONT_SIZE);
  
  std::string caption = _title;
  
  if (_parent != NULL)
  {
    size_t position = _title.find("/");
    if (position  != std::string::npos)
      caption = _title.substr(position + 1);
  }
  
  _shortened_title= Utilities::shorten_string(cr, caption, _last_text_width);

  cairo_text_extents_t title_extents;
  cairo_text_extents(cr, _shortened_title.c_str(), &title_extents);
  _title_offset= (int) -title_extents.y_bearing;
  _title_width= title_extents.width;

  // Same for the description.
  cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, ACTION_LINK_DETAILS_FONT_SIZE);
  _shortened_description= Utilities::shorten_string(cr, _description, _last_text_width);

  cairo_text_extents_t description_extents;
  cairo_text_extents(cr, _shortened_description.c_str(), &description_extents);
  _description_offset= _title_offset - (int) description_extents.y_bearing + ACTION_LINK_TEXT_SPACING;

  // Determine overall text height. This is used to center the text during paint.
  _text_height= (int) ceil(title_extents.height + description_extents.height + ACTION_LINK_TEXT_SPACING);
}

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

bool ActionLink::contains(double x, double y)
{
  return _bounds.contains(x, y);
}

//---------------------------- ActionLinkGroup -----------------------------------------------------

ActionLinkGroup::ActionLinkGroup(cairo_surface_t *icon, const std::string& title, const std::string& description,
                const grt::ValueRef &object, HomeScreenAction action, bool enabled, ActionLinkGroupState state):
ActionLink(NULL, title, description, object, action, enabled, GroupLink)
{
  _state = state;
  _collapsed_icon = Utilities::load_icon("list_group_closed.png");
  _expanded_icon = Utilities::load_icon("list_group_open.png");
}

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

void ActionLinkGroup::add_link(cairo_surface_t *icon, const std::string& title,
                             const std::string& description, const grt::ValueRef &object, HomeScreenAction action, bool enabled)
{
  ActionLink* link= new ActionLink(icon, title, description, object, action, enabled, GroupedLink);
  link->set_parent(this);
  _links.push_back(link);
}

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

void ActionLinkGroup::add_link(ActionLink* link)
{
  link->set_type(GroupedLink);
  link->set_parent(this);
  _links.push_back(link);
}

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

int ActionLinkGroup::get_item_position(ActionLink* link)
{
  std::vector<ActionLink*>::iterator location = std::find(_links.begin(), _links.end(), link);
  int index = (location == _links.end()) ? -1 : location - _links.begin();
  return index;
}

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

void ActionLinkGroup::layout(cairo_t* cr)
{
  // Re-compute shortened title and its position.
  cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, ACTION_LINK_NORMAL_FONT_SIZE);
  
  std::string group_count = base::strfmt("  (%ld)", _links.size());
  
  cairo_text_extents_t count_extents;
  cairo_text_extents(cr, group_count.c_str(), &count_extents);
  _count_width= count_extents.width;

  _shortened_title= Utilities::shorten_string(cr, _title, _last_text_width - _count_width);

  cairo_text_extents_t title_extents;
  cairo_text_extents(cr, _shortened_title.c_str(), &title_extents);
  _title_offset= (int) -title_extents.y_bearing;
  _title_width= title_extents.width;
  
  _text_height= (int) ceil(title_extents.height);
}

void ActionLinkGroup::paint(cairo_t* cr, Rect bounds, bool hot, bool active)
{
  this->_bounds= bounds;
  cairo_save(cr);
  
  // Sets the corresponding icon.
  _icon = (_state == Expanded) ? _expanded_icon : _collapsed_icon;
  
  double icon_width= image_width(_icon);
  double icon_height= image_height(_icon);
  
  // Updates the icon/bar bounds
  _arrow_bounds.pos.x = bounds.pos.x;
  _arrow_bounds.pos.y = bounds.pos.y;
  _arrow_bounds.size.width = icon_width + ACTION_LINK_ICON_SPACING;
  _arrow_bounds.size.height = bounds.size.height;

  _bar_bounds.pos.x = bounds.pos.x + icon_width;
  _bar_bounds.pos.y = bounds.pos.y;
  _bar_bounds.size.width = bounds.size.width - (ACTION_LINK_ICON_SPACING + icon_width);
  _bar_bounds.size.height = bounds.size.height;
  
  double new_width= bounds.size.width - 2 * ACTION_LINK_PADDING - icon_width - ACTION_LINK_ICON_SPACING;
  
  if (new_width != _last_text_width)
  {
    _last_text_width= new_width;
    layout(cr);
  }
  
  // Fill a blue background if the item is active, gray otherwise.
  if (active && _enabled)
    cairo_set_source_rgb(cr, 0x5a / 255.0, 0x85 / 255.0, 0xdc / 255.0);
  else
    cairo_set_source_rgb(cr, 0xe6 / 255.0, 0xea / 255.0, 0xef / 255.0);

  cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.size.width, bounds.size.height);
  cairo_fill(cr);
  
  cairo_set_source_surface(cr, _icon, bounds.left() + ACTION_LINK_PADDING, bounds.top() + (int) ((bounds.height() - icon_height) / 2));
  if (_enabled)
    cairo_paint(cr);
  else
    cairo_paint_with_alpha(cr, 0.25);
  
  int text_offset= (int) (bounds.height() - _text_height) / 2;
  cairo_select_font_face(cr, ACTION_LINK_NORMAL_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, ACTION_LINK_NORMAL_FONT_SIZE);
  
  if (!_enabled)
    cairo_set_source_rgb(cr, 0.75, 0.75, 0.75);
  else
    if (active)
      cairo_set_source_rgb(cr, 1, 1, 1);
    else
      if (hot)
        cairo_set_source_rgb(cr, 90 / 255.0, 147 / 255.0, 220 / 255.0);
      else
        cairo_set_source_rgb(cr, 0x37 / 255.0, 0x3d / 255.0, 0x48 / 255.0);

  double offset= bounds.left() + ACTION_LINK_PADDING + icon_width + ACTION_LINK_ICON_SPACING;
  
  cairo_move_to(cr, offset, bounds.top()  + _title_offset + text_offset);
  cairo_show_text(cr, _shortened_title.c_str());
  cairo_stroke(cr);
  
  cairo_select_font_face(cr, HOME_DETAILS_FONT, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, ACTION_LINK_DETAILS_FONT_SIZE);
  
  if (!_enabled)
    cairo_set_source_rgb(cr, 0.75, 0.75, 0.75);
  else
    if (active)
      cairo_set_source_rgb(cr, 1, 1, 1);
    else
      cairo_set_source_rgb(cr, 0xc6 / 255.0, 0xc6 / 255.0, 0xc6 / 255.0);
  
  std::string group_count = base::strfmt("  (%ld)", _links.size());

  cairo_move_to(cr, offset + _title_width, bounds.top()  + _title_offset + text_offset);
  cairo_show_text(cr, group_count.c_str());
  cairo_stroke(cr);
  
  
  // Hot style implementation is currently done manually (draw a line).
  // TODO: replace this with font decoration once pango is incorporated.
  if (hot && _enabled)
  {
    cairo_set_line_width(cr, 1);
    cairo_set_source_rgb(cr, 90 / 255.0, 147 / 255.0, 220 / 255.0);
    cairo_move_to(cr, offset, bounds.top() + _title_offset + text_offset + 1.5);
    cairo_line_to(cr, offset + _title_width, bounds.top() + _title_offset + text_offset + 1.5);
    cairo_stroke(cr);
  }
  
  _icon = NULL;
  
  cairo_restore(cr);
}

void ActionLinkGroup::mouse_click(int button, int x, int y)
{
  if (button == MouseButtonLeft && _arrow_bounds.contains(x,y))
    toggle_state();
}

void ActionLinkGroup::mouse_double_click(int button, int x, int y)
{
  if (button == MouseButtonLeft && _bar_bounds.contains(x,y))
    toggle_state();
}


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

void ActionLinkGroup::toggle_state()
{
  if (_state == Expanded)
    _state = Collapsed;
  else
    _state = Expanded;
  
  _toggled(_title);
}

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

ActionLinkGroup::~ActionLinkGroup()
{
  delete_surface(_expanded_icon);
  delete_surface(_collapsed_icon);
}

//-------------------------- WorkspaceGradient -----------------------------------------------------

void WorkspaceGradient::add_stop_color(double position, double r, double g, double b, double a)
{
  StopColor entry= {position, r, g, b, a};
  _values.push_back(entry);
}

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

void WorkspaceGradient::add_stop_color(double position, int color)
{
  int red = (color >> 16) & 0xff;
  int green = (color >> 8) & 0xff;
  int blue = color & 0xff;
  StopColor entry= {position, red / 255.0, green / 255.0, blue / 255.0, 1};
  _values.push_back(entry);
}

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

cairo_pattern_t* WorkspaceGradient::create_pattern(int height)
{
  cairo_pattern_t* result = cairo_pattern_create_linear(0, 0, 0, height);
  for (std::vector<StopColor>::const_iterator iterator= _values.begin(); iterator != _values.end(); iterator++)
    cairo_pattern_add_color_stop_rgba(result, 
                                      (*iterator).position,
                                      (*iterator).red,
                                      (*iterator).green,
                                      (*iterator).blue,
                                      (*iterator).alpha);
  return result;
}

//-------------------------- ActionLinkBox ---------------------------------------------------------

ActionLinkBox::ActionLinkBox(HomeScreen* owner, const std::string& header_image, WorkspaceGradient* gradient,
                             int top_spacing, int bottom_spacing, int left_spacing, int right_spacing)
{
  _owner= owner;
  _layout_width= 10;
  _layout_height= 10;
  _background= NULL;
  _gradient= gradient; // Weak reference, we don't own it.
  _image= Utilities::load_icon(header_image);
  _hot_link= NULL;
  _selected_link= NULL;
  _selected_index= -1;
  _top_spacing= top_spacing;
  _bottom_spacing= bottom_spacing;
  _back_red= 1;
  _back_green= 1;
  _back_blue= 1;
  _left_spacing= left_spacing;
  _right_spacing= right_spacing;
  _can_select= false;
  _single_click= true;
  _context_menu= NULL;
  _group_context_menu= NULL;
  _handle_groups= false;
}

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

ActionLinkBox::~ActionLinkBox()
{
  set_destroying();
  clear();
  delete_surface(_image);
  delete_pattern(_background);
  
  if (_context_menu)
    delete _context_menu;
  
  if (_group_context_menu)
    delete _group_context_menu;
}

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

void ActionLinkBox::add_link(cairo_surface_t *icon, const std::string& title,
                             const std::string& description, const grt::ValueRef &object, HomeScreenAction action, bool enabled)
{
  ActionLink* link= new ActionLink(icon, title, description, object, action, enabled);
  add_link(link);
}

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

void ActionLinkBox::add_link(const std::string &icon_name, const std::string& title,
                             const std::string& description, const grt::ValueRef &object, HomeScreenAction action, bool enabled)
{
  cairo_surface_t *icon = Utilities::load_icon(icon_name);
  ActionLink* link= new ActionLink(icon, title, description, object, action, enabled);
  delete_surface(icon);
  add_link(link);
}

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

void ActionLinkBox::remove_link(ActionLink* link)
{
  _links.erase(std::find(_links.begin(), _links.end(), link));
}

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

void ActionLinkBox::add_link(ActionLink* link)
{
  std::vector<ActionLink*>::iterator position = _links.end();
  
  if (_handle_groups)
  {
    if (link->get_type() == GroupLink)
    {
      ActionLinkGroup *link_group = dynamic_cast<ActionLinkGroup*> (link);
      scoped_connect(link_group->toggled(),boost::bind(&ActionLinkBox::toggle_group, this, _1)); 
    }
    else
    {
      int group_separator_position = link->get_title().find("/");
     
      // When it's a grouped link it's needed to find the position where
      // it should be inserted so it is displayed inside the group on the list
      if (group_separator_position != std::string::npos)
      {
        std::string group_name = link->get_title().substr(0, group_separator_position + 1);
        for (std::vector<ActionLink*>::iterator end = _links.end(),
            link_index = _links.begin(); link_index != end; link_index++)
        {
          if((*link_index)->get_title().compare(0, group_separator_position + 1, group_name) == 0 || link->get_parent() == (*link_index))
            position = link_index;
        }
        
        if (position != _links.end())
          position++;
      }
    }
  }
  
  // Inserts the link on the corresponding position
  _links.insert(position, link);
  set_layout_dirty(true);
}

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

void ActionLinkBox::clear()
{
  for (std::vector<ActionLink*>::iterator iterator= _links.begin(); iterator != _links.end(); iterator++)
    delete *iterator;
  _links.clear();

  if (!is_destroying())
    set_layout_dirty(true);
}

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

void ActionLinkBox::set_back_color(double red, double green, double blue)
{
  _back_red= red;
  _back_green= green;
  _back_blue= blue;
  set_needs_repaint();
}

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

void ActionLinkBox::set_back_color(int color)
{
  _back_red= ((color >> 16) & 0xff) / 255.0;
  _back_green= ((color >> 8) & 0xff) / 255.0;
  _back_blue= (color & 0xff) / 255.0;
  set_needs_repaint();
}

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

void ActionLinkBox::can_select(bool selectable)
{
  _can_select= selectable;
  if (!_can_select && _selected_link != NULL)
  {
    _selected_link= NULL;
    _selected_index= -1;
    set_needs_repaint();
  }
}

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

void ActionLinkBox::single_click(bool value)
{
  _single_click= value;
}

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

void ActionLinkBox::select(grt::ValueRef item)
{
  ActionLink* selected = NULL;
  
  if (item.is_valid() )
  {
    switch (item.type())
    {
      case grt::StringType:
      case grt::ObjectType:
        for(std::vector<ActionLink*>::iterator end = _links.end(), 
            index = _links.begin(); index != end && !selected; index++)
        {
          if ((*index)->get_object() == item)
            selected = *index;
        }
      
        set_selected(selected);
        break;
      case grt::IntegerType:
        int index = grt::IntegerRef::extract_from(item);

        if (index >= 0 && index < (int)_links.size())
          set_selected(_links[index]);
        else
          set_selected(NULL);
      
        break;
    }
  }
  else
    set_selected(NULL);
}

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

ActionLink* ActionLinkBox::selected()
{
  return _selected_link;
}

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

void ActionLinkBox::set_context_menu(Menu* menu, bool group_menu)
{
  if (group_menu)
    _group_context_menu= menu;
  else
    _context_menu = menu;
}

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

void ActionLinkBox::set_context_menu_enabled(std::string action, bool enabled, bool group_menu)
{
  if (group_menu)
    _group_context_menu->set_item_enabled(_group_context_menu->get_item_index(action), enabled);
  else
    _context_menu->set_item_enabled(_context_menu->get_item_index(action), enabled);
}


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

#define ACTION_LINK_HEIGHT 39       // The height of a single action link.
#define GROUP_ACTION_LINK_HEIGHT 20 // The height of a group single action link.
#define ACTION_LINK_SPACING 0       // Vertical distance between two action links.
#define BOX_IMAGE_SPACING 5         // The vertical distance between header image and first action link.

void ActionLinkBox::repaint(cairo_t *cr, int areax, int areay, int areaw, int areah)
{
  layout();
  
  double width= get_width();
  double height= get_height();
  
  if (_background != NULL)
  {
    cairo_set_source(cr, _background);
    cairo_rectangle(cr, 0, 0, width, height);
    cairo_fill(cr);
  }
  else
  {
    cairo_set_source_rgb(cr, _back_red, _back_green, _back_blue);
    cairo_paint(cr);
  }

  Rect link_bounds(_left_spacing, _top_spacing, width - _left_spacing - _right_spacing, ACTION_LINK_HEIGHT);
  
  if (_image != NULL)
  {
    cairo_set_source_surface(cr, _image, link_bounds.left(), _top_spacing);
    cairo_paint(cr);

    link_bounds.pos.y += BOX_IMAGE_SPACING + image_height(_image);;
  }
  for (std::vector<ActionLink*>::const_iterator iterator= _links.begin(); iterator != _links.end(); iterator++)
  {
    link_bounds.size.height = ((*iterator)->get_type() == GroupLink) ? GROUP_ACTION_LINK_HEIGHT : ACTION_LINK_HEIGHT;
    (*iterator)->paint(cr, link_bounds, *iterator == _hot_link, (*iterator == _selected_link) && _can_select);
    link_bounds.pos.y += link_bounds.size.height + ACTION_LINK_SPACING;
  }
}

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

void ActionLinkBox::mouse_enter()
{
}

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

void ActionLinkBox::mouse_leave()
{
  // Hot link markup is only used for single click lists (to indicate the item can activated).
  // Hence when single click actions are disabled we also don't want hot items.
  if (_single_click && _hot_link != NULL)
  {
    _hot_link= NULL;
    set_needs_repaint();
  }
}

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

void ActionLinkBox::mouse_move(int x, int y)
{
  // See comment about single clicks and hot items in mouse_leave.
  if (_single_click)
  {
    ActionLink* link= action_link_from_point(x, y);
    if (link != _hot_link)
    {
      _hot_link= link;
      set_needs_repaint();
    }
  }
}

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

void ActionLinkBox::mouse_down(int button, int x, int y)
{
  if (button == MouseButtonLeft || button == MouseButtonRight)
  {
    ActionLink* link= action_link_from_point(x, y);
    set_selected(link);
  }
}

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

void ActionLinkBox::mouse_up(int button, int x, int y)
{
}

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

void ActionLinkBox::mouse_click(int button, int x, int y)
{
  _last_mouse_button = (mforms::MouseButton)button;
  
  // Passes the call to the action link
  ActionLink* link= action_link_from_point(x, y);

  switch (button)
  {
    case MouseButtonLeft:
    {
      if (link)
        link->mouse_click(button, x, y);
      
      if (_single_click)
      {
        if (link == _selected_link)
        {
          for (int index= 0; index < (int) _links.size(); index++)
            if (_links[index] == _selected_link)
            {
              _owner->trigger_callback(link->get_action(), link->get_object());
              return;
            }
        }
      }
      break;
    } 
    case MouseButtonRight:
      if (_selected_link != NULL)
      {
        if (link->get_type() == GroupLink)
        {
          if (_group_context_menu != NULL)
            _group_context_menu->popup_at(this, x + 5, y + 5);
        }
        else if (_context_menu != NULL)
          _context_menu->popup_at(this, x + 5, y + 5);
      }
      break;
  }
}

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

void ActionLinkBox::mouse_double_click(int button, int x, int y)
{
  ActionLink* link= action_link_from_point(x, y);
      
  switch (button)
  {
    case MouseButtonLeft:
    {
      // Passes the call to the action link
      if (link)
        link->mouse_double_click(button, x, y);

      if (!_single_click)
      {
        if (link == _selected_link)
        {
          // TODO: does this make sense? Can a link (pointer) appear more than once in the list?
          for (int index= 0; index < (int) _links.size(); index++)
            if (_links[index] == _selected_link)
            {
              _owner->trigger_callback(link->get_action(), link->get_object());
              return;
            }
        }
      }
      break;
    }
    case MouseButtonRight:
    {
      // If this right double click came from a !right + right sequence handle it as single right click.
      if (_last_mouse_button != mforms::MouseButtonRight && (_selected_link != NULL))
      {
        if (link->get_type() == GroupLink)
        {
          if (_group_context_menu != NULL)
            _group_context_menu->popup_at(this, x + 5, y + 5);
        }
        else if (_context_menu != NULL)
          _context_menu->popup_at(this, x + 5, y + 5);
      }

    }
  }
  _last_mouse_button = mforms::MouseButtonNone;
}

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

void ActionLinkBox::get_layout_size(int* w, int* h)
{
  layout();
  
  *w= _layout_width;
  *h= _layout_height;
}

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

void ActionLinkBox::set_selected(ActionLink* link)
{
  int old_selected_index = _selected_index;
  bool signal_change = false;
  
  if (_selected_link != link)
  {
    _selected_link = link;
    set_needs_repaint();
    signal_change = true;
  }
  
  // The selection index may change without a change in the selection
  // when moving items up and down
  _selected_index = find_selected_index();
  
  if (old_selected_index != _selected_index)
    signal_change=true;
  
  if (signal_change)
    selection_changed();
}

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

void ActionLinkBox::layout()
{
  if (is_layout_dirty())
  {
    set_layout_dirty(false);

    _layout_height= _top_spacing;
    _layout_width= _left_spacing + _right_spacing;

    if (_links.size() > 0)
      _layout_height += _links.size() * ACTION_LINK_HEIGHT + (_links.size() - 1) * ACTION_LINK_SPACING;
    if (_image != NULL)
    {
      _layout_height += BOX_IMAGE_SPACING + image_height(_image);
      _layout_width += image_width(_image);
    }

    if (_layout_height < ACTION_LINK_HEIGHT)
      _layout_height= ACTION_LINK_HEIGHT;
    _layout_height += _bottom_spacing;

    delete_pattern(_background);
    if (_gradient != NULL)
      _background = _gradient->create_pattern(_layout_height);
    else
      _background = NULL;
  }  
}

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

ActionLink* ActionLinkBox::action_link_from_point(double x, double y)
{
  if (x >= 0 && x < get_width() && y >= 0 && y <= get_height())
  {
    for (std::vector<ActionLink*>::const_iterator iterator= _links.begin(); iterator != _links.end(); iterator++)
      if ((*iterator)->contains(x, y) && (*iterator)->enabled())
        return *iterator;
  }
  return NULL;
}

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

void ActionLinkBox::selection_changed()
{
  _signal_changed();
}

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

int ActionLinkBox::find_selected_index()
{
  std::vector<ActionLink*>::iterator location = std::find(_links.begin(), _links.end(), _selected_link);
  int index = (location == _links.end()) ? -1 : location - _links.begin();
  return index;
}

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

void ActionLinkBox::toggle_group(const std::string &group_name)
{
  _group_toggled(group_name);
  set_needs_repaint();
}

//----------------- GradientBox --------------------------------------------------------------------

GradientBox::GradientBox(WorkspaceGradient* gradient)
{
  _gradient= gradient;
  _background= NULL;
  _back_red= 1;
  _back_green= 1;
  _back_blue= 1;
}

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

GradientBox::~GradientBox()
{
  delete_pattern(_background);
}

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

void GradientBox::set_back_color(double red, double green, double blue)
{
  _back_red= red;
  _back_green= green;
  _back_blue= blue;
  set_needs_repaint();
}

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

void GradientBox::set_back_color(int color)
{
  _back_red= ((color >> 16) & 0xff) / 255.0;
  _back_green= ((color >> 8) & 0xff) / 255.0;
  _back_blue= (color & 0xff) / 255.0;
  set_needs_repaint();
}

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

void GradientBox::repaint(cairo_t *cr, int areax, int areay, int areaw, int areah)
{
  if (_background == NULL)
  {
    if (_gradient != NULL)
      _background= _gradient->create_pattern(get_height());
  }
  if (_background != NULL)
  {
    cairo_set_source(cr, _background);
    cairo_rectangle(cr, areax, areay, areaw, areah);
    cairo_fill(cr);
  }
  else
  {
    cairo_set_source_rgb(cr, _back_red, _back_green, _back_blue);
    cairo_paint(cr);
  }
}

//----------------- ActionList ---------------------------------------------------------------------

ActionList::ActionList(HomeScreen* owner, int left_spacing, int right_spacing, WorkspaceGradient* spacer_gradient,
                       const std::string& icon_name, HomeScreenAction action, bool handle_groups)
: Box(true), _panel(mforms::ScrollPanelDrawBackground), _panel_border(false)
{
  _owner= owner;
  _action= action;
  _icon = Utilities::load_icon(icon_name);

  _left_spacer= new GradientBox(spacer_gradient);
  _left_spacer->set_size(left_spacing, -1);
  _left_spacer->set_name("left spacer"); // Debug
  add(_left_spacer, false, true);

  _link_box= new ActionLinkBox(_owner, "", NULL, 0, 0, 0, 0);
  _link_box->set_back_color(1, 1, 1);
  _link_box->can_select(true);
  _link_box->single_click(false);
  _link_box->set_back_color(0xeef1f5);
  _link_box->set_name("LinkBox");
  scoped_connect(_link_box->group_toggled(), boost::bind(&ActionList::toggle_group, this, _1)); 

#if !defined(_WIN32) && !defined(__APPLE__)
  _link_box->set_size(-1, -1); 
#endif
  _panel.add(_link_box);
  _panel.set_back_color("#eef1f5");
  _panel.set_visible_scrollers(true, false);
  _panel.set_name("scroll panel"); // Debug

  _panel_border.set_back_color("#93accb");
  _panel_border.set_padding(1);
  _panel_border.add(&_panel, true, true);
  _panel_border.set_name("scroll panel border"); // Debug
  add(&_panel_border, true, true);

  _right_spacer= new GradientBox(spacer_gradient);
  _right_spacer->set_size(right_spacing, -1);
  _right_spacer->set_name("right spacer"); // Debug
  add(_right_spacer, false, true);

  set_name("action list"); // Debug
  
  _handle_groups = handle_groups;
  
  _link_box->set_handle_groups(_handle_groups);
}

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

ActionList::~ActionList()
{
  delete_surface(_icon);
  delete _link_box;
  delete _left_spacer;
  delete _right_spacer;
}

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

void ActionList::set_spacer_back_color(int color)
{
  _left_spacer->set_back_color(color);
  _right_spacer->set_back_color(color);
}

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

void ActionList::add_entry(const std::string& title, const std::string& description, const grt::ValueRef &object,
                           cairo_surface_t *icon)
{
//  bool is_group = false;
  bool group_expanded = true;
  ActionLinkGroup *link_group=NULL;
  ActionLink* link=NULL;

  // When handling groups it may be needed to add not only the entry but
  // an entry for the group if it doesn't yet exist
  if (_handle_groups )
  {
    std::string group_name("");
    size_t position = title.find("/");
    
    if (position != std::string::npos )
      group_name = title.substr( 0, position );
    
    if ( group_name.length() > 0 )
    {
      // The shown/view state is either set(to a default value) or retrieved
      if (_link_group_state.count(group_name) == 0)
        _link_group_state[group_name] = true;
      else
        group_expanded = _link_group_state[group_name];
      
      // The link group is created or retrieved
      if (_link_groups.count(group_name) == 0)
      {
        link_group = new ActionLinkGroup(icon ? icon : _icon, group_name, "", grt::StringRef(group_name), ActionNone, true, group_expanded ? Expanded : Collapsed);
        _link_groups[group_name] = link_group;
        _link_box->add_link(link_group);
      }
      else
        link_group = _link_groups[group_name];
    }
  }
  

  link = new ActionLink(icon ? icon : _icon, title, description, object, _action, true);
  if (link_group)
    link_group->add_link(link);
  
  
  if (!link_group || group_expanded)
    _link_box->add_link(link);
  
  _link_box->set_needs_repaint();
}

//--------------------------------------------------------------------------------------------------
void ActionList::toggle_group(const std::string &group_name)
{
  if (_link_group_state.count(group_name) > 0 )
  {
    _link_group_state[group_name] = !_link_group_state[group_name];
    
    ActionLinkGroup* link_group = dynamic_cast<ActionLinkGroup*> (_link_groups[group_name]);
    
    bool expanded = _link_group_state[group_name];
    for (std::vector<ActionLink*>::iterator end = link_group->get_links()->end(),
         link = link_group->get_links()->begin(); link != end; link++)
    {
      if (expanded)
        _link_box->add_link(*link);
      else
        _link_box->remove_link(*link);
    }
  }
  
  refresh();
}

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

void ActionList::set_collapsed_groups(std::string collapsed_groups)
{
  std::vector<std::string> groups = base::split(collapsed_groups, "/");
  
  for(std::vector<std::string>::iterator end = groups.end(),
      group = groups.begin(); group != groups.end(); group++)
  {
    _link_group_state[*group] = false;
  }
}
      
//--------------------------------------------------------------------------------------------------

std::string ActionList::get_collapsed_groups()
{
  std::string collapsed_data = "";
  
  for ( std::map<std::string, bool>::iterator end = _link_group_state.end(),
       group_state = _link_group_state.begin() ; group_state != end; group_state++ )
  {
    if (!(*group_state).second)
    {
      if (collapsed_data == "")
        collapsed_data = (*group_state).first;
      else
      {
        collapsed_data += "/";
        collapsed_data += (*group_state).first;
      }
    }
  }
  
  return collapsed_data;
}
//--------------------------------------------------------------------------------------------------

void ActionList::select(grt::ValueRef item)
{
  _link_box->select(item);
}

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

ActionLink* ActionList::selected()
{
  return _link_box->selected();
}

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

void ActionList::clear()
{
  _link_box->clear();
  _link_groups.clear();
}

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

void ActionList::refresh()
{
  _link_box->set_layout_dirty(true);
  _link_box->set_needs_repaint();
  _panel.relayout();
}

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

void ActionList::set_context_menu(mforms::Menu* menu, bool group_menu)
{
  _link_box->set_context_menu(menu, group_menu);
}

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

bool ActionList::is_first_selected()
{
  bool ret_val = false;
  
  if (selected() != NULL)
  {
    if (_handle_groups && selected()->get_type() == GroupedLink)
    {
      ActionLinkGroup* link_group = dynamic_cast<ActionLinkGroup*>(selected()->get_parent());
      ret_val = link_group->get_item_position(selected()) == 0;
    }
    else
      ret_val = selected_index() == 0;
  }
  
  return ret_val;
}

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

bool ActionList::is_last_selected()
{
  bool ret_val = false;
  
  if (selected() != NULL)
  {
    ActionLinkType type = selected()->get_type();

    if (_handle_groups && type != NormalLink)
    {
      ActionLinkGroup* link_group = NULL;
      
      if (selected()->get_type() == GroupedLink)
      {
        link_group = dynamic_cast<ActionLinkGroup*>(selected()->get_parent());
        ret_val = link_group->get_item_position(selected()) == link_group->get_item_count() - 1;
      }
      else
      {
        link_group = dynamic_cast<ActionLinkGroup*>(selected());

        // When it is a group, if expanded will not be the last item in the list
        // but may be the last item on the main list ( discarding the childs )
        if (_link_group_state[link_group->get_title()])
          ret_val = selected_index() == link_count() - ( link_group->get_item_count() + 1 );
        else
          ret_val = selected_index() == link_count() - 1;
      }
    }
    else
      ret_val = selected_index() == link_count() - 1;
  }
  
  return ret_val;
}

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

void ActionList::set_context_menu_enabled(std::string action, bool enabled, bool group_menu)
{
  _link_box->set_context_menu_enabled(action, enabled, group_menu);
}


//----------------- Workspace ----------------------------------------------------------------------

#define COLUMN_TOP_SPACE 18            // Vertical distance between top border and content.
#define COLUMN_BOTTOM_SPACE 18         // Vertical distance between bottom border and content.
#define COLUMN_TOP_LIST_SPACING 7      // Vertical distance between top action list and list box.
#define COLUMN_BOTTOM_LIST_SPACING 18  // Vertical distance between bottom action list and list box.
#define COLUMN_LIST_LEFT_SPACING 46    // Space between left column border and action list.
#define COLUMN_LIST_RIGHT_SPACING 42   // Ditto for right border.
#define COLUMN_LEFT_LINK_SPACING 14    // Horizontal distance between left border and action links.
#define COLUMN_RIGHT_LINK_SPACING 14   // Ditto for right border.

Workspace::Workspace(HomeScreen* owner)
: Box(true), _left_column(false), _middle_column(false), _right_column(false)
{
  _owner= owner;

  suspend_layout();
  set_back_color("#f4f7fc");

  _top_gradient= new WorkspaceGradient();
  _top_gradient->add_stop_color(0, 0xffffff);
  _top_gradient->add_stop_color(0.4, 0xf4f7fc);
  _top_gradient->add_stop_color(0.5, 0xdde6f3);
  _top_gradient->add_stop_color(0.7, 0xf4f7fc);
  _top_gradient->add_stop_color(1, 0xf4f7fc);
  
  _separator_gradient= new WorkspaceGradient();
  _separator_gradient->add_stop_color(0, 0xf4f7fc);
  _separator_gradient->add_stop_color(0.44, 0xf4f7fc);
  _separator_gradient->add_stop_color(1, 0xe5ebf5);
  
  // SQL column data.
  _sql_top = new ActionLinkBox(_owner, "home_header_sql.png", _top_gradient, COLUMN_TOP_SPACE, COLUMN_TOP_LIST_SPACING,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _sql_top->add_link("action_query.png", "Open Connection to Start Querying",
                     "Or click a DB connection to open the SQL Editor.", grt::ValueRef(),
                     ActionOpenConnection, true);
  
  _sql_bottom= new ActionLinkBox(_owner, "", NULL, COLUMN_BOTTOM_LIST_SPACING, COLUMN_BOTTOM_SPACE,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _sql_bottom->add_link("action_new_connection.png", "New Connection", 
                        "Add a new database connection for querying.", grt::ValueRef(),
                        ActionNewConnection, true);
  _sql_bottom->add_link("action_edit_table_data.png", "Edit Table Data",
                        "Select a connection and schema table to edit.", grt::ValueRef(),
                        ActionEditTable, true);
  _sql_bottom->add_link("action_edit_sql_script.png", "Edit SQL Script",
                        "Open an existing SQL Script file for editing.", grt::ValueRef(),
                        ActionEditSQLScript, true);
  _sql_bottom->add_link("action_manage_connection.png", "Manage Connections",
                        "Modify connection settings or add connections.", grt::ValueRef(),
                        ActionManageConnections, true);
  _sql_bottom->set_back_color(0xf4f7fc);
  
  _connections= new ActionList(_owner, COLUMN_LIST_LEFT_SPACING, COLUMN_LIST_RIGHT_SPACING, NULL,
    "db_connection_34x34.png", ActionOpenConnectionFromList, true);
  _connections->set_spacer_back_color(0xf4f7fc);
  scoped_connect(_connections->signal_changed(),boost::bind(&Workspace::connection_selection_changed, this));
  _left_column.set_back_color("#f4f7fc");
  _left_column.add(_sql_top, false, true);
  _left_column.add(_connections, true, true);
  _left_column.add_end(_sql_bottom, false, true);

  // EER column data.
  _eer_top= new ActionLinkBox(_owner, "home_header_eer.png", _top_gradient, COLUMN_TOP_SPACE, COLUMN_TOP_LIST_SPACING,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _eer_top->add_link("action_eer_open.png", "Open Existing EER Model",
                     "Or select a model to open or click here to browse.", grt::ValueRef(),
                     ActionOpenEERModel, true);
  
  _eer_bottom= new ActionLinkBox(_owner, "", NULL, COLUMN_BOTTOM_LIST_SPACING, COLUMN_BOTTOM_SPACE,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _eer_bottom->set_back_color(0xe5ebf5);
  _eer_bottom->add_link("action_new_eer.png", "Create New EER Model",
                        "Create a new EER Model from scratch.", grt::ValueRef(),
                        ActionNewEERModel, true);
  _eer_bottom->add_link("action_eer_rev_eng_db.png", "Create EER Model From Existing Database",
                        "Create by connecting and reverse engineering.", grt::ValueRef(),
                        ActionNewModelFromDB, true);
  _eer_bottom->add_link("action_eer_rev_eng_file.png", "Create EER Model From SQL Script",
                        "Import an existing SQL file.", grt::ValueRef(),
                        ActionNewModelFromScript, true);
  
  _models= new ActionList(_owner, COLUMN_LIST_LEFT_SPACING, COLUMN_LIST_RIGHT_SPACING, _separator_gradient,
    "db_model_34x34.png", ActionOpenEERModelFromList);

  _middle_column.set_back_color("#f4f7fc");
  _middle_column.add(_eer_top, false, true);
  _middle_column.add(_models, true, true);
  _middle_column.add_end(_eer_bottom, false, true);

  // Admin column data.
  _admin_top= new ActionLinkBox(_owner, "home_header_admin.png", _top_gradient, COLUMN_TOP_SPACE, COLUMN_TOP_LIST_SPACING,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _admin_top->add_link("action_admin.png", "Server Administration",
                       "Or click to manage a database server instance.", grt::ValueRef(),
                       ActionServerAdministration, true);
  
  _admin_bottom= new ActionLinkBox(_owner, "", NULL, COLUMN_BOTTOM_LIST_SPACING, COLUMN_BOTTOM_SPACE,
    COLUMN_LEFT_LINK_SPACING, COLUMN_RIGHT_LINK_SPACING);
  _admin_bottom->add_link("action_new_server.png", "New Server Instance",
                          "Register a new server instance to manage.", grt::ValueRef(),
                          ActionNewServerInstance, true);
  _admin_bottom->add_link("action_import_export.png", "Manage Import / Export",
                          "Create a dump file or restore data from a file.", grt::ValueRef(),
                          ActionDumpRestore, true);
  _admin_bottom->add_link("action_security.png", "Manage Security",
                          "Manage user accounts and assign privileges.", grt::ValueRef(),
                          ActionManageSecurity, true);
  _admin_bottom->add_link("action_manage_servers.png", "Manage Server Instances",
                          "Add, delete and update server instance settings.", grt::ValueRef(),
                          ActionManageServerInstances, true);
  _admin_bottom->set_back_color(0xf4f7fc);
  
  _server_instances= new ActionList(_owner, COLUMN_LIST_LEFT_SPACING, COLUMN_LIST_RIGHT_SPACING, NULL,
    "db_server_34x34.png", ActionManageInstanceFromList, true);
  _server_instances->set_spacer_back_color(0xf4f7fc);
  scoped_connect(_server_instances->signal_changed(),boost::bind(&Workspace::server_selection_changed, this) );
  _right_column.set_back_color("#f4f7fc");
  _right_column.add(_admin_top, false, true);
  _right_column.add(_server_instances, true, true);
  _right_column.add_end(_admin_bottom, false, true);
  
  add(&_left_column, true, true);
  add(&_middle_column, true, true);
  add(&_right_column, true, true);
  
  resume_layout();
}

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

Workspace::~Workspace()
{
  delete _sql_top;
  delete _sql_bottom;
  delete _eer_top;
  delete _eer_bottom;
  delete _admin_top;
  delete _admin_bottom;
  delete _top_gradient;
  delete _separator_gradient;
  
  delete _connections;
  delete _models;
  delete _server_instances;
}

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

void Workspace::add_list_entry(HomeScreenListType list, const std::string& title, 
                               const std::string& description, const grt::ValueRef &object,
                               const std::string &icon_name)
{
  cairo_surface_t *icon = icon_name.empty() ? 0 : mdc::ImageManager::get_instance()->get_image(icon_name);

  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->add_entry(title, description, object, icon);
      break;
    case HomeScreenModellingList:
      _models->add_entry(title, description, object, icon);
      break;
    case HomeScreenAdministrationList:
      _server_instances->add_entry(title, description, object, icon);
      break;
  }
}

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

void Workspace::select_list_entry(HomeScreenListType list, grt::ValueRef item)
{
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->select(item);
      break;
    case HomeScreenModellingList:
      _models->select(item);
      break;
    case HomeScreenAdministrationList:
      _server_instances->select(item);
      break;
  }  
}

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

void Workspace::clear_list(HomeScreenListType list)
{
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->clear();
      break;
    case HomeScreenModellingList:
      _models->clear();
      break;
    case HomeScreenAdministrationList:
      _server_instances->clear();
      break;
  }
}

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

void Workspace::refresh_list(HomeScreenListType list)
{
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->refresh();
      break;
    case HomeScreenModellingList:
      _models->refresh();
      break;
    case HomeScreenAdministrationList:
      _server_instances->refresh();
      break;
  }
}

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

void Workspace::toggle_list_group(HomeScreenListType list, const std::string &group_name)
{
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->toggle_group(group_name);
      break;
    case HomeScreenAdministrationList:
      _server_instances->toggle_group(group_name);
      break;
  }  
}

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

static Menu *mforms_menu_from_menu_item_list(const bec::MenuItemList &menu_items)
{
  Menu *menu = new Menu();
  menu->add_items_from_list(menu_items);
  return menu;
}

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

void Workspace::set_context_menu_items(HomeScreenListType list, const bec::MenuItemList &menu_items, bool group_menu)
{
  mforms::Menu* menu = mforms_menu_from_menu_item_list(menu_items);
  menu->set_handler(boost::bind(&Workspace::handle_command, this, _1, list));
  
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->set_context_menu(menu, group_menu);
      break;
    case HomeScreenModellingList:
      _models->set_context_menu(menu, group_menu);
      break;
    case HomeScreenAdministrationList:
      _server_instances->set_context_menu(menu, group_menu);
      break;
  }
}

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

void Workspace::set_collapsed_group_data(HomeScreenListType list, std::string group_data)
{
  switch (list)
  {
    case HomeScreenSQLDevList:
      _connections->set_collapsed_groups(group_data);
      break;
    case HomeScreenAdministrationList:
      _server_instances->set_collapsed_groups(group_data);
      break;
  }
}

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

std::string Workspace::get_collapsed_group_data(HomeScreenListType list)
{
  std::string group_data="";
  
  switch (list)
  {
    case HomeScreenSQLDevList:
      group_data = _connections->get_collapsed_groups();
      break;
    case HomeScreenAdministrationList:
      group_data = _server_instances->get_collapsed_groups();
      break;
  }
  
  return group_data;
}

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

void Workspace::handle_command(const std::string& command, HomeScreenListType list)
{
  grt::ValueRef object;
  
  switch (list)
  {
    case HomeScreenSQLDevList:
      object= _connections->selected()->get_object();
      break;
    case HomeScreenModellingList:
      object= _models->selected()->get_object();
      break;
    case HomeScreenAdministrationList:
      object = _server_instances->selected()->get_object();
      break;
  }      

  _owner->handle_context_menu(list, object, command);
}

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

void Workspace::connection_selection_changed()
{
  _connections->set_context_menu_enabled("move_connection_up", !_connections->is_first_selected());
  _connections->set_context_menu_enabled("move_connection_down", !_connections->is_last_selected());
  _connections->set_context_menu_enabled("move_connection_up", !_connections->is_first_selected(), true);
  _connections->set_context_menu_enabled("move_connection_down", !_connections->is_last_selected(), true);
}

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

void Workspace::server_selection_changed()
{
  _server_instances->set_context_menu_enabled("move_instance_up", !_server_instances->is_first_selected());
  _server_instances->set_context_menu_enabled("move_instance_down", !_server_instances->is_last_selected());
  _server_instances->set_context_menu_enabled("move_instance_up", !_server_instances->is_first_selected(), true);
  _server_instances->set_context_menu_enabled("move_instance_down", !_server_instances->is_last_selected(), true);
}

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

#include "workbench/wb_command_ui.h"

HomeScreen::HomeScreen(CommandUI *cmdui, grt::GRT *grt)
: AppView(false, "home", true), _workbench_central_section(true, "Workbench Central", true),
_workspace_section(false, "Workspace"), _callback(NULL), _user_data(0), _auto_save_warning(0)
{  
  // TODO: cannot set a minimum size here as this doesn't work on OS X (box layouter is buggy).
  //set_size(1024, 671); // Minimum size for this entire page.
  _central_content= new WorkbenchCentral(this, grt);
  _central_content->set_size(-1, 112);
  _workbench_central_section.set_content(_central_content);
  add(&_workbench_central_section, false, true);

  //_workspace_content.set_size(10, 10);
  _workspace_content= new Workspace(this);
  _workspace_content->set_name("workspace content");
  _workspace_section.set_content(_workspace_content);
  _workspace_section.set_name("workspace section");
  add(&_workspace_section, true, true);
  
  _menubar = mforms::manage(cmdui->create_menubar_for_context(WB_CONTEXT_HOME_GLOBAL));
}

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

HomeScreen::~HomeScreen()
{
  delete _auto_save_warning;
  delete _central_content;
  delete _workspace_content;
}

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

void HomeScreen::set_wb_central_expanded(bool flag)
{
  _workbench_central_section.set_expanded(flag);
}

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

bool HomeScreen::get_wb_central_expanded()
{
  return _workbench_central_section.get_expanded();
}

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

void HomeScreen::show_autosave_warning(int model, int sqleditor)
{
  if (model == 0 && sqleditor == 0)
  {
    if (_auto_save_warning)
    {
      remove(_auto_save_warning);
      delete _auto_save_warning;
      _auto_save_warning= 0;
    }
  }
  else if (!_auto_save_warning)
  {
    Box *box = _auto_save_warning = manage(new Box(true));
    box->set_padding(12);
    box->set_spacing(12);
    
    ImageBox *icon = manage(new ImageBox());
    icon->set_image("warning_icon.png");
    box->add(icon, false, true);
    
    Box *vb = manage(new Box(false));
    vb->set_padding(8);
    Label *label = manage(new Label());
    label->set_style(BoldStyle);
    label->set_color("#dd0000");
    label->set_text(_("Orphaned document changes detected"));
    vb->add(label, true, true);
    if (model > 0 && sqleditor == 0)
      label = manage(new Label(_("Unsaved changes in a model from a previous run of Workbench were detected.\n"
      "To restore them open the corresponding document and save it. Otherwise these changes will be lost.")));
    else if (model == 0 && sqleditor > 0)
      label = manage(new Label(_("Unsaved changes of an SQL Editor workspace were found.\n"
      "To restore them, open the corresponding SQL connection and save the SQL data. Otherwise these changes will be lost.")));
    else
      label = manage(new Label(_("There are unsaved changes in SQL Editors and models from a previous run of Workbench.\n"
      "To restore them, open the corresponding document and SQL connections and save the documents.")));
    label->set_style(SmallStyle);
    vb->add(label, false, true);
    box->add(vb, true, true);
    
    Button *button = manage(new Button());
    button->set_text(_("Dismiss"));
    box->add_end(button, false, true);
    box->set_back_color("#eeeef7");
    add(box, false, true);
    
    UIForm::scoped_connect(button->signal_clicked(),boost::bind(&Box::remove, this, box));
  }
}

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

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 (_callback != NULL)
    (*_callback)(action, object, _user_data);
}

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

void HomeScreen::set_context_menu_items(HomeScreenListType list, const bec::MenuItemList &menu_items, bool group_menu)
{
  _workspace_content->set_context_menu_items(list, menu_items, group_menu);
}

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

void HomeScreen::set_collapsed_group_data(HomeScreenListType list, std::string group_data)
{
  _workspace_content->set_collapsed_group_data(list, group_data);
}

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

std::string HomeScreen::get_collapsed_group_data(HomeScreenListType list)
{
  return _workspace_content->get_collapsed_group_data(list);
}

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

/**
 * Adds a new entry to the specified list on the home screen.
 */
void HomeScreen::add_list_entry(HomeScreenListType list, const std::string& title,
                                const std::string& description, const grt::ValueRef &object,
                                const std::string& icon_name)
{
  if (list == HomeScreenStarters)
    _central_content->add_starter(icon_name, object);
  else
    _workspace_content->add_list_entry(list, title, description, object, icon_name);
}

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

void HomeScreen::select_list_entry(HomeScreenListType list, grt::ValueRef item)
{
  _workspace_content->select_list_entry(list, item);
}

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

void HomeScreen::refresh_list(HomeScreenListType list)
{
  _workspace_content->refresh_list(list);
}

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

void HomeScreen::clear_list(HomeScreenListType list)
{
  if (list == HomeScreenStarters)
  {
    _central_content->clear_starters();
    _central_content->set_needs_repaint();
  }
  else
    _workspace_content->clear_list(list);
}

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

void HomeScreen::toggle_list_group(HomeScreenListType list, const std::string &group_name)
{
  _workspace_content->toggle_list_group(list, group_name);
}

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

