//<copyright>
// 
// Copyright (c) 1994,95,96
// 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
//
// $Id: listbox.C,v 1.9 1996/05/17 11:33:15 bmarsch Exp $
//
// $Log: listbox.C,v $
// Revision 1.9  1996/05/17 11:33:15  bmarsch
// Bugfix: undraw must call undraw of base class Button
//
// Revision 1.8  1996/02/21 13:32:53  bmarsch
// Removed warning
//
// Revision 1.7  1996/02/15 17:17:31  bmarsch
// Includes from Dispatch/ were moved to hyperg/Dispatch/
//
// Revision 1.6  1996/02/02 13:54:50  bmarsch
// Bugfix in selectItem(): before selecting new item make
// current selection inactive
//
// Revision 1.5  1996/01/22 12:46:07  bmarsch
// Use ActiveHandler::unmap_help_window() to unmap help window
//
// Revision 1.4  1996/01/19 17:16:13  bmarsch
// Unmap help window (if there's one) before showing popup
//
// Revision 1.3  1996/01/19 12:55:56  bmarsch
// Bugfix in disabling whole button
//
// Revision 1.2  1996/01/18 17:56:54  bmarsch
// Implemented disabling/enabling of items
//
//</file>


#include "listbox.h"

#include <hyperg/Dispatch/dispatcher.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;
  reporteachsel_ = 0;
  disabled_ = false;

  // create and ref all TelltaleState's
  GlyphIndex n = items ? items->count() : 0;
  telltales_ = new BrowserList(n);
  for (GlyphIndex i = 0; i < n; i++) {
    TelltaleState* t = new TelltaleState (
      (i == selected_) ? TelltaleState::is_enabled_active : TelltaleState::is_enabled
    );
    t->ref();
    telltales_->append (t);  // ref/unref of telltale states done by choice item
 }

  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_;
  if (telltales_) {
    while (telltales_->count()) {
      Resource::unref(telltales_->item(0));
      telltales_->remove(0);
    }
    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;
  if (i < 0 || i >= items_->count ())  // invalid index
    return;

  if (!telltales_->item(i)->test(TelltaleState::is_enabled))
    return;   // item is disabled

  if (selected_ != i)
  {
    // make currently selected item inactive
    if (selected_ >= 0 && selected_ < telltales_->count())
      telltales_->item(selected_)->set(TelltaleState::is_active, false);
    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);

  // remove TelltaleState
  Resource::unref(telltales_->item(i));
  telltales_->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

void ListBox::enabled(GlyphIndex i, boolean enabled)
{
  if (!items_)
    return;

  GlyphIndex n = items_->count();
  // range check
  if (i < 0 || i >= n)
    return;

  if (enabled) {
    // on enabling if it's the only enabled item, reenable whole button
    boolean no_enabled = true;
    for (int j = 0; j < n; j++)
      if (telltales_->item(j)->test(TelltaleState::is_enabled))
        no_enabled = false;

    if (no_enabled && disabled_) {
      state()->set(TelltaleState::is_enabled, true);
      selected_ = i;
      disabled_ = false;
    }
  }
  else {
    // on disabling clear active flag
    telltales_->item(i)->set(TelltaleState::is_active, false);
    // if selected item is disabled change selection
    if (i == selected_ && !enabled) {
      do {
        selected_++;
        if (selected_ >= n) selected_ = 0;
        if (selected_ == i) {
          // there's no enabled item, so disable whole button
          if (state()->test(TelltaleState::is_enabled)) {
            state()->set(TelltaleState::is_enabled, false);
            disabled_ = true;
          }
          break;
        }
      } while (!telltales_->item(selected_)->test(TelltaleState::is_enabled));
    }
  }

  // set telltalestate
  telltales_->item(i)->set(TelltaleState::is_enabled, enabled);

  // redraw
  updateTopline (*WidgetKit::instance (), *LayoutKit::instance (),
                 n ? items_->component (selected_) : nil);
  redraw ();
}

void ListBox::enableAll(GlyphIndex selected)
{
  for (int i = 0; i < telltales_->count(); i++)
    telltales_->item(i)->set(TelltaleState::is_enabled, true);

  if (disabled_) {
    state()->set(TelltaleState::is_enabled, true);
    disabled_ = false;
  }

  selectItem(selected);
}

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


// 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))
  {
    unmap_help_window();
    s->set (TelltaleState::is_active, true);
    runpopup ();
    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 ()
{
  Button::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;

  // set telltale state
  if (selected_ >=0 && selected_ < telltales_->count())
    telltales_->item(selected_)->set(TelltaleState::is_active, true);

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

  // 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_ = nil;

  if (current_ >= 0 && current_ < items_->count ()) {
    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);

      int savecurrent = current_;

      if (key == ' ' || key == '\r' || keysym == XK_KP_Enter)
        return 1;  // when mapped, use space/return to confirm
      else if (keysym == XK_Down) {
        if (items_) {
          boolean restore = true;
          while (current_ < items_->count () - 1) {
            int c = current_ + 1;
            if (setCurItem (c)) {
              restore = false;
              break;
            }
            current_ = c;
          }
          if (restore)
            setCurItem(savecurrent);
        }
      }
      else if (keysym == XK_Up) {
        if (items_) {
          boolean restore = true;
          while (current_ > 0) {
            int c = current_ - 1;
            if (setCurItem (c)) {
              restore = false;
              break;
            }
            current_ = c;
          }
          if (restore)
            setCurItem(savecurrent);
        }
      }
      else if (keysym == XK_Home) {
        if (items_) {
          current_ = -1;
          while (current_ < items_->count() - 1) {
            int c = current_ + 1;
            if (setCurItem (c)) break;
            current_ = c;
          }
        }
      }
      else if (keysym == XK_End) {
        if (items_) {
          current_ = items_->count();
          while (current_ > 0) {
            int c = current_ - 1;
            if (setCurItem(c)) break;
            current_ = c;
          }
        }
      }
      else if (key == '\x1b' || keysym == XK_Cancel) {
        current_ = -1;
        return 1;
      }
    }
    break;

    default:
      break;
  }

  return 0;

} // boxevent


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

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

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

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

  winpatch_->redraw ();
  return ret;
}


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

  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
    );
    itembox->append(kit.menu_item_look(component, telltales_->item(i)));
  }

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