//<copyright>
// 
// Copyright (c) 1994,95
// Institute for Information Processing and Computer Supported New Media (IICM),
// Graz University of Technology, Austria.
// 
//</copyright>

//<file>
//
// Name:        listbox.C
//
// Purpose:     implementation of class ListBox
//
// Created:      7 Mar 94   Michael Pichler
//
// Changed:     10 Jan 95   Michael Pichler
//
//
//</file>


#include "listbox.h"

#include <IV-look/choice.h>
#include <IV-look/button.h>
#include <IV-look/kit.h>
#include <IV-look/stepper.h>

#include <InterViews/event.h>
#include <InterViews/handler.h>
#include <InterViews/layout.h>
#include <InterViews/patch.h>
#include <InterViews/session.h>
#include <InterViews/target.h>
#include <InterViews/window.h>
#include <InterViews/hit.h>

#include <IV-X11/xevent.h>
#include <IV-X11/xwindow.h>

#include <X11/keysym.h>

#include <iostream.h>

declarePtrList(BrowserList,TelltaleState)
// implementation included in InterViews/browser.c


// maxrequisition
// if Glyph g has a higher (natural) requirement in x or y direction,
// maxx and maxy are updated
// (see also compute_tile_request in InterViews/tile.c)

static void maxrequisition (const Glyph* g, float& maxx, float& maxy)
{
  Requisition requisition;
  g->request (requisition);
  const Requirement& xreq = requisition.x_requirement ();
  const Requirement& yreq = requisition.y_requirement ();
  float nat = xreq.natural ();
  if (nat > maxx)
    maxx = nat;
  nat = yreq.natural ();
  if (nat > maxy)
    maxy = nat;
}


/*** ListBox ***/


ListBox::ListBox (
  PolyGlyph* items,
  GlyphIndex selected,
  ListBoxAction* confirm,
  Style* style,
  TelltaleState* tstate
)
: Button (nil, style, tstate ? tstate : new TelltaleState (TelltaleState::is_enabled), nil)
{
  items_ = items;
  selected_ = selected;
  confirm_ = confirm;
  tstate = state ();
  current_ = -1;
  telltales_ = new BrowserList;
  reporteachsel_ = 0;

  margin_ = 5.0;  // might use X-attribute
  toplinebox_ = nil;
  win_ = nil;
  winpatch_ = nil;

  trfleft_ = trfy_ = 0;

  Resource::ref (items);
  Resource::ref (confirm);

  Glyph* selitem = (items && selected >= 0 && selected < items->count ())
                   ? items->component (selected) : 0;

  maxx_ = 10.0;  // minimum width
  maxy_ = 10.0;  // minimum height

  // search list item of maximal size
  if (items)
  { GlyphIndex n = items->count ();
    for (GlyphIndex i = 0;  i < n;  i++)
    {
      Glyph* g = items->component (i);
      maxrequisition (g, maxx_, maxy_);
    }
  }

  WidgetKit& kit = *WidgetKit::instance ();

  body (kit.push_button_look (
    createTopline (selitem),
    tstate
  ));
} // ListBox


ListBox::~ListBox ()
{
  delete win_;
  delete telltales_;

  Resource::unref (items_);
  Resource::unref (confirm_);
}


// selected item
Glyph* ListBox::selectedItem () const
{
  return items_ ? items_->component (selected_) : nil;
}


//select another item (externally)
void ListBox::selectItem (GlyphIndex i)
{
  if (!items_)
    return;
  GlyphIndex n = items_->count ();
  if (i < 0 || i >= n)  // invalid index
    return;

  if (selected_ != i)
  {
    selected_ = i;
    updateTopline (*WidgetKit::instance (), *LayoutKit::instance (), items_->component (selected_));
    redraw ();
  }
} // selectItem


// remove item from item list
void ListBox::removeItem (GlyphIndex i)
{
  if (!items_)
    return;
  GlyphIndex n = items_->count ();
  if (i < 0 || i >= n)  // invalid index
    return;

  items_->remove (i);

  if (i < selected_)  // indices have shift
    selected_--;
  else if (i == selected_)  // removing previously selected item
  {
    if (i == --n)  // last item being removed
      selected_--;
    updateTopline (*WidgetKit::instance (), *LayoutKit::instance (), n ? items_->component (selected_) : nil);
    redraw ();
  }
} // removeItem


// allocate must notice a canvas transformation (11950110)

void ListBox::allocate (Canvas* c, const Allocation& a, Extension& e)
{
  Button::allocate (c, a, e);
  c->transformer ().transform (a.left (), a.y (), trfleft_, trfy_);
//cerr << "allocate: (" << a.left () << ", " << a.y () << ") was transformed to (" << trfleft_ << ", " << trfy_ << ")." << endl;
}


// move, press, release
// these functions of Button had to be redefined to achive the desired
// behavior of an option button

void ListBox::move (const Event& e)
{
  // ignore moves while popup is mapped/running
  if (!win_)
    Button::move (e);
}


void ListBox::press (const Event&)
{
  TelltaleState* s = state ();
  if (s->test (TelltaleState::is_enabled))
  {
    s->set (TelltaleState::is_active, true);
    // e.grab (handler ());
    runpopup ();
    // e.ungrab (handler ());
    s->set (TelltaleState::is_active, false);
  }
}


void ListBox::release (const Event&)
{
  // nothing more to do
  // e.ungrab (handler ());
}


// undraw
// unmap pop up window when parent window gets iconified

void ListBox::undraw ()
{
  MonoGlyph::undraw ();
  delete win_;
  win_ = 0;
  current_ = -1;
}


void ListBox::runpopup ()
{
  if (!items_ || !items_->count ())  // no items - no selection possible
    return;

  if (win_)  // map only once
    return;

  current_ = selected_;  // any reason against???
  win_ = new PopupWindow (winpatch_ = new Patch (createBox ()));
  moved_ = 0;

  Canvas* can = canvas ();
  const Window* pwin = can->window ();  // parent window
  const Allocation& a = allocation ();

  // for window placement *always* use left and bottom of parent window and add
  // desired coordinates of allocation (and set proper alignment)
  // win_->place (pwin->left () + a.left () + 1.0, pwin->bottom () + a.y ());
  win_->place (pwin->left () + trfleft_ + 1.0,
               pwin->bottom () + trfy_);  // middle of button label (vcenter in top line)
  win_->align (0, 1 - (selected_ + 0.5)/items_->count ());  // assuming components of equal size

  win_->map ();

  // process events
  Session* s = Session::instance ();
  Event e;
  for (;;)
  {
    s->read (e);
    if (e.grabber ())
      e.handle ();
    if (boxevent (e))
      break;
    if (s->done ())  // this happens when the application window is deleted
      break;
    if (!win_)
      break;
  } // read events

  // unmap popup box
  delete win_;
  win_ = 0;

  if (current_ >= 0 && current_ < items_->count () && (reporteachsel_ || current_ != selected_))
  {
    selected_ = current_;
    updateTopline (*WidgetKit::instance (), *LayoutKit::instance (), items_->component (current_));
    redraw ();
    if (confirm_)
      confirm_->changed (this);  // selected_ is set right
  }
  current_ = -1;

} // runpopup


// boxevent
// handle event while pop up box is mapped (only called by runpopup)
// return non zero when it should be unmapped

// note that when the original click that caused popup mapping occurs
// inside the popup window InterViews generates two motion events (can
// be seen as bug) therefore the window stays on screen only when the
// user clicks on the small bar of the option button

int ListBox::boxevent (Event& e)
{
  switch (e.type ())
  {
    case Event::motion:
      updateCurrent (e);
      moved_ = 1;
    break;

    case Event::down:
      updateCurrent (e);
    return 1;  // quit

    case Event::up:
      if (moved_)  // release of dragging
      { updateCurrent (e);
        return 1;  // quit
      }
    break;

    case Event::key:
    {
      unsigned long keysym = e.keysym ();
      unsigned char key = (unsigned char) (keysym & 0xff);

      if (key == ' ' || key == '\r' || keysym == XK_KP_Enter)
        return 1;  // when mapped, use space/return to confirm
      else if (keysym == XK_Down)
      { if (items_ && current_ < items_->count () - 1)
          setCurItem (current_ + 1);
      }
      else if (keysym == XK_Up)
      { if (items_ && current_ > 0)
          setCurItem (current_ - 1);
      }
      else if (keysym == XK_Home)
        setCurItem (0);
      else if (keysym == XK_End)
        setCurItem (items_ ? items_->count () - 1 : -1);
      else if (key == '\x1b' || keysym == XK_Cancel)
      { current_ = -1;
        return 1;
      }
    }
    break;
  }

  return 0;

} // boxevent


// setCurItem
// set current_ (currently marked item) to new item number

void ListBox::setCurItem (GlyphIndex newindex)
{
  if (newindex == current_)
    return;

  // cerr << '[' << current_ << '>' << newindex << ']';
  // remove current selection
  if (current_ < 0)
    current_ = selected_;
  if (current_ >= 0)
    telltales_->item (current_)->set (TelltaleState::is_active, false);

  if (newindex >= 0 && newindex < telltales_->count ())
  {
    telltales_->item (newindex)->set (TelltaleState::is_active, true);
    current_ = newindex;
  }
  else
    current_ = -1;

  winpatch_->redraw ();
}


// updateCurrent
// set current_ to the index of item hit, update highlight (if any)
// if no item gets hit remove selection

void ListBox::updateCurrent (const Event& e)
{
  if (!winpatch_ || !winpatch_->canvas ())
    return;

  if (e.window () == win_)  // event occured in popup window
  {
    Hit hit (&e);
    winpatch_->pick (winpatch_->canvas (), winpatch_->allocation (), 0, hit);

    if (hit.any ())
      setCurItem (hit.index (0));
  }
  else  // other window: cancel selection
    setCurItem (-1);

} // updateCurrent


// createTopline
// top line (static part of list box) shows current selection
// and a down arrow

Glyph* ListBox::createTopline (Glyph* selitem)
{
  WidgetKit& kit = *WidgetKit::instance ();
  const LayoutKit& layout = *LayoutKit::instance ();

  float xsize = maxx_;  // incl. border
  float ysize = maxy_;

  toplinebox_ = layout.hbox (
    layout.vcenter (layout.hglue ()),  // dummy entry for selected item
    layout.vcenter (
      kit.outset_frame (layout.fixed (nil, 12, 2))
    )
  );

  // calculate how big toplinebox_ is for biggest item
  updateTopline (kit, layout, layout.fixed (nil, xsize, ysize));
  maxrequisition (toplinebox_, xsize, ysize);

  Glyph* body = layout.fixed (toplinebox_, xsize, ysize);

  updateTopline (kit, layout, selitem);

  return new Target (body, TargetPrimitiveHit);  // allow click anywhere
} // createTopline


// updateTopline
// show new selected item
// (caller should do patch_->redraw ())

void ListBox::updateTopline (WidgetKit&, const LayoutKit& layout, Glyph* selitem)
{
  if (!selitem)  // margin gets confused on empty body
    selitem = layout.hspace (0);

  if (toplinebox_)
    toplinebox_->replace (
      0,  // index
      layout.vcenter (
        layout.hmargin (selitem, margin_, fil, 0.0, margin_, fil, 0.0)
      )
    );
} // updateTopline


// createBox
// box glyph to be put into pop up window
// all items are put into a vbox, current item is highlighted
// assumes items_ to be non nil

Glyph* ListBox::createBox ()
{
  WidgetKit& kit = *WidgetKit::instance ();
  const LayoutKit& layout = *LayoutKit::instance ();
  telltales_->remove_all ();  // clear list

  PolyGlyph* itembox = layout.vbox ();

  GlyphIndex n = items_->count ();
  for (GlyphIndex i = 0;  i < n;  i++)
  {
    Glyph* comp = layout.fixed (items_->component (i), maxx_, maxy_);  // for later removal of items

    Glyph* component = new Target (  // because margin is not pickable
      layout.hmargin (comp, margin_, 0.0, 0.0, margin_, 0.0, 0.0),
      TargetPrimitiveHit
    );
    TelltaleState* t = new TelltaleState (
      (i == selected_) ? TelltaleState::is_enabled_active : TelltaleState::is_enabled
    );
    itembox->append (new ChoiceItem (t, component, kit.bright_inset_frame (component)));
    telltales_->append (t);  // ref/unref of telltale states done by choice item
  }

  return kit.outset_frame (itembox);
} // createBox
