/*=========================================================================

  Module:    vtkKWThumbWheel.cxx

  Copyright (c) Kitware, Inc.
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkKWThumbWheel.h"

#include "vtkKWApplication.h"
#include "vtkKWEntry.h"
#include "vtkKWLabel.h"
#include "vtkKWPushButton.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkKWTopLevel.h"

#include <vtksys/stl/string>

#define VTK_KW_TW_BORDER_SIZE      2
#define VTK_KW_TW_MIN_SIZE_NOTCHES 2
#define VTK_KW_TW_MIN_WIDTH        7
#define VTK_KW_TW_MIN_HEIGHT       7
#define VTK_KW_TW_NL_REFRESH_RATE  50
#define VTK_KW_TW_INDICATOR_SIZE   5

#define VTK_KW_TW_CLAMP_UCHAR_MACRO(var) if (var < 0) {var = 0;} else if (var > 255) {var = 255;}

// ---------------------------------------------------------------------------
vtkStandardNewMacro( vtkKWThumbWheel );
vtkCxxRevisionMacro(vtkKWThumbWheel, "1.41");

// ---------------------------------------------------------------------------
/* 
 * This part was generated by ImageConvert from image:
 *    arrow.png (zlib, base64)
 */
#define image_arrow_width         4
#define image_arrow_height        7
#define image_arrow_pixel_size    4
#define image_arrow_length 40

static unsigned char image_arrow[] = 
  "eNpjYGD4z4AK/jOgiv1HE/uPB+PSDwcAlQUP8Q==";

// ---------------------------------------------------------------------------
vtkKWThumbWheel::vtkKWThumbWheel()
{
  this->Value                      = 0;
  this->Resolution                 = 1;
  this->NonLinearMaximumMultiplier = 20;
  this->ThumbWheelWidth            = 80;
  this->ThumbWheelHeight           = 16;
  this->ResizeThumbWheel           = 1;
  this->SizeOfNotches              = 4;
  this->LinearThreshold            = 0.05;
  this->MinimumValue               = 0.0;
  this->ClampMinimumValue          = 0;
  this->MaximumValue               = 0.0;
  this->ClampMaximumValue          = 0;

  this->InteractionModes[0]        = vtkKWThumbWheel::InteractionModeLinearMotion;
  this->InteractionModes[1]        = vtkKWThumbWheel::InteractionModeNonLinearMotion;
  this->InteractionModes[2]        = vtkKWThumbWheel::InteractionModeToggleCenterIndicator;

  this->ThumbWheelPositionIndicatorColor[0] = 0.91;
  this->ThumbWheelPositionIndicatorColor[1] = 0.41;
  this->ThumbWheelPositionIndicatorColor[2] = 0.22;

  this->DisplayLabel               = 0;
  this->DisplayEntry               = 0;
  this->DisplayEntryAndLabelOnTop  = 1;
  this->DisplayThumbWheelPositionIndicator = 1;
  this->DisplayThumbWheelCenterIndicator = 0;
  this->PopupMode                  = 0;
  this->ExpandEntry                = 0;

  this->Command         = NULL;
  this->StartCommand    = NULL;
  this->EndCommand      = NULL;
  this->EntryCommand    = NULL;

  this->ThumbWheel      = vtkKWLabel::New();
  this->Entry           = NULL;
  this->Label           = NULL;
  this->TopLevel        = NULL;
  this->PopupPushButton = NULL;

  this->State           = vtkKWThumbWheel::Idle;

  this->ThumbWheelShift = 0.0;
  this->InInvokeCommand = 0;
}

// ---------------------------------------------------------------------------
vtkKWThumbWheel::~vtkKWThumbWheel()
{
  if (this->Command)
    {
    delete [] this->Command;
    this->Command = NULL;
    }

  if (this->StartCommand)
    {
    delete [] this->StartCommand;
    this->StartCommand = NULL;
    }

  if (this->EndCommand)
    {
    delete [] this->EndCommand;
    this->EndCommand = NULL;
    }

  if (this->EntryCommand)
    {
    delete [] this->EntryCommand;
    this->EntryCommand = NULL;
    }

  if (this->ThumbWheel)
    {
    this->ThumbWheel->Delete();
    this->ThumbWheel = NULL;
    }

  if (this->Entry)
    {
    this->Entry->Delete();
    this->Entry = NULL;
    }

  if (this->Label)
    {
    this->Label->Delete();
    this->Label = NULL;
    }

  if (this->TopLevel)
    {
    this->TopLevel->Delete();
    this->TopLevel = NULL;
    }

  if (this->PopupPushButton)
    {
    this->PopupPushButton->Delete();
    this->PopupPushButton = NULL;
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::Create(vtkKWApplication *app)
{
  // Check if already created

  if (this->IsCreated())
    {
    vtkErrorMacro(<< this->GetClassName() << " already created");
    return;
    }

  // Call the superclass to create the whole widget

  this->Superclass::Create(app);

  // If we need the scale to popup, create the top level window accordingly
  // and its push button

  if (this->PopupMode)
    {
    this->TopLevel = vtkKWTopLevel::New();
    this->TopLevel->Create(app);
    this->TopLevel->SetBackgroundColor(0.0, 0.0, 0.0);
    this->TopLevel->SetBorderWidth(2);
    this->TopLevel->SetReliefToFlat();
    this->TopLevel->HideDecorationOn();
    this->TopLevel->Withdraw();
    this->TopLevel->SetMasterWindow(this);

    this->PopupPushButton = vtkKWPushButton::New();
    this->PopupPushButton->SetParent(this);
    this->PopupPushButton->Create(app);
    this->PopupPushButton->SetPadX(0);
    this->PopupPushButton->SetPadY(0);

    this->PopupPushButton->SetImageToPixels(image_arrow, 
                                            image_arrow_width, 
                                            image_arrow_height, 
                                            image_arrow_pixel_size,
                                            image_arrow_length);

    this->ThumbWheel->SetParent(this->TopLevel);
    }
  else
    {
    this->ThumbWheel->SetParent(this);
    }

  // Create the scale

  this->ThumbWheel->Create(app);
  this->ThumbWheel->SetBorderWidth(VTK_KW_TW_BORDER_SIZE);
  this->ThumbWheel->SetHighlightThickness(0);
  this->ThumbWheel->SetReliefToSunken();

  this->UpdateThumbWheelImage();
  this->Bind();
  this->PackWidget();

  // Update enable state

  this->UpdateEnableState();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetValue(double arg)
{
  if (this->ClampMinimumValue && arg < this->MinimumValue)
    {
    arg = this->MinimumValue;
    }
  if (this->ClampMaximumValue && arg > this->MaximumValue)
    {
    arg = this->MaximumValue;
    }

  if (this->Value == arg)
    {
    // Pass the value to the entry to keep it in sync with the thumb wheel
    this->RefreshValue();
    return;
    }

  this->Value = arg;
  this->Modified();

  this->RefreshValue();
  
  this->InvokeCommand();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::RefreshValue()
{
  if (this->Entry && this->Entry->IsCreated())
    {
    this->Entry->SetValueAsDouble(this->Value);
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetResolution(double arg)
{
  if (this->Resolution == arg)
    {
    return;
    }

  this->Resolution = arg;
  this->Modified();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetThumbWheelWidth(int arg)
{
  if (this->ThumbWheelWidth == arg)
    {
    return;
    }

  this->ThumbWheelWidth = 
    (arg < VTK_KW_TW_MIN_WIDTH ? VTK_KW_TW_MIN_WIDTH : arg);
  this->Modified();

  this->UpdateThumbWheelImage();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetThumbWheelHeight(int arg)
{
  if (this->ThumbWheelHeight == arg)
    {
    return;
    }

  this->ThumbWheelHeight = 
    (arg < VTK_KW_TW_MIN_HEIGHT ? VTK_KW_TW_MIN_HEIGHT : arg);
  this->Modified();

  this->UpdateThumbWheelImage();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetResizeThumbWheel(int arg)
{
  if (this->ResizeThumbWheel == arg)
    {
    return;
    }

  this->ResizeThumbWheel = arg;
  this->Modified();

  this->Bind();
  this->PackWidget();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetDisplayThumbWheelCenterIndicator(int arg)
{
  if (this->DisplayThumbWheelCenterIndicator == arg)
    {
    return;
    }

  this->DisplayThumbWheelCenterIndicator = arg;
  this->Modified();

  this->UpdateThumbWheelImage();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::ToggleDisplayThumbWheelCenterIndicator()
{
  if (this->DisplayThumbWheelCenterIndicator)
    {
    this->DisplayThumbWheelCenterIndicatorOff();
    }
  else
    {
    this->DisplayThumbWheelCenterIndicatorOn();
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::ResizeThumbWheelCallback()
{
  const char *res = 
    this->Script("concat [winfo width %s] [winfo height %s]",
                 this->ThumbWheel->GetWidgetName(),
                 this->ThumbWheel->GetWidgetName());
  
  int tw, th;
  sscanf(res, "%d %d", &tw, &th);

  // Remove the border size (-bd)

  tw -= VTK_KW_TW_BORDER_SIZE * 2;
  th -= VTK_KW_TW_BORDER_SIZE * 2;
  
  if (this->ThumbWheelWidth == tw && this->ThumbWheelHeight == th)
    {
    return;
    }

  this->ThumbWheelWidth =
    (tw < VTK_KW_TW_MIN_WIDTH ? VTK_KW_TW_MIN_WIDTH : tw);
  this->ThumbWheelHeight =
    (th < VTK_KW_TW_MIN_HEIGHT ? VTK_KW_TW_MIN_HEIGHT : th);

  this->UpdateThumbWheelImage();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetSizeOfNotches(double arg)
{
  if (this->SizeOfNotches == arg)
    {
    return;
    }

  this->SizeOfNotches = 
    (arg < VTK_KW_TW_MIN_SIZE_NOTCHES ? VTK_KW_TW_MIN_SIZE_NOTCHES : arg);
  this->Modified();

  this->UpdateThumbWheelImage();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::CreateEntry()
{
  if (this->Entry && this->Entry->IsCreated())
    {
    return;
    }

  this->Entry = vtkKWEntry::New();
  this->Entry->SetParent(this);
  this->Entry->Create(this->GetApplication());
  this->Entry->SetWidth(7);
  this->PropagateEnableState(this->Entry);
  this->Entry->SetValueAsDouble(this->GetValue());
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetDisplayEntry(int arg)
{
  if (this->DisplayEntry == arg)
    {
    return;
    }

  this->DisplayEntry = arg;
  this->Modified();

  if (this->DisplayEntry && !this->Entry)
    {
    this->CreateEntry();
    }

  this->Bind();
  this->PackWidget();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::CreateLabel()
{
  if (this->Label && this->Label->IsCreated())
    {
    return;
    }

  this->Label = vtkKWLabel::New();
  this->Label->SetParent(this);
  this->Label->Create(this->GetApplication());
  this->PropagateEnableState(this->Label);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetDisplayLabel(int arg)
{
  if (this->DisplayLabel == arg)
    {
    return;
    }

  this->DisplayLabel = arg;
  this->Modified();

  if (this->DisplayLabel && !this->Label)
    {
    this->CreateLabel();
    }

  this->Bind();
  this->PackWidget();
}

// ---------------------------------------------------------------------------
vtkKWLabel* vtkKWThumbWheel::GetLabel()
{
  if (!this->Label)
    {
    this->CreateLabel();
    }

  return this->Label;
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetDisplayEntryAndLabelOnTop(int arg)
{
  if (this->DisplayEntryAndLabelOnTop == arg)
    {
    return;
    }

  this->DisplayEntryAndLabelOnTop = arg;
  this->Modified();

  this->PackWidget();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetExpandEntry(int arg)
{
  if (this->ExpandEntry == arg)
    {
    return;
    }

  this->ExpandEntry = arg;
  this->Modified();

  this->PackWidget();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetInteractionMode(int mode, int arg)
{
  if (mode < 0 || mode > 2 || this->InteractionModes[mode] == arg)
    {
    return;
    }

  this->InteractionModes[mode] = arg;
  if (this->InteractionModes[mode] < vtkKWThumbWheel::InteractionModeNone)
    {
    this->InteractionModes[mode] = vtkKWThumbWheel::InteractionModeNone;
    }
  else if (this->InteractionModes[mode] > vtkKWThumbWheel::InteractionModeToggleCenterIndicator)
    {
    this->InteractionModes[mode] = vtkKWThumbWheel::InteractionModeToggleCenterIndicator;
    }
  this->Modified();

  this->Bind();
}

// ---------------------------------------------------------------------------
int vtkKWThumbWheel::GetInteractionMode(int mode)
{
  if (mode < 0 || mode > 2)
    {
    return vtkKWThumbWheel::InteractionModeNone;
    }
  return InteractionModes[mode];
}

//----------------------------------------------------------------------------
char *vtkKWThumbWheel::GetInteractionModeAsString(int mode)
{
  if (mode < 0 || mode > 2)
    {
    return (char *)"Invalid mode";
    }

  switch (this->InteractionModes[mode])
    {
    case vtkKWThumbWheel::InteractionModeNone:
      return (char *)"None";
      break;
    case vtkKWThumbWheel::InteractionModeLinearMotion:
      return (char *)"Linear";
      break;
    case vtkKWThumbWheel::InteractionModeNonLinearMotion:
      return (char *)"NonLinear";
      break;
    case vtkKWThumbWheel::InteractionModeToggleCenterIndicator:
      return (char *)"ToggleCenterIndicator";
      break;
    default:
      return (char *)"Unknown";
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::PackWidget()
{
  if (this->ThumbWheel && this->ThumbWheel->IsCreated())
    {
    if (this->DisplayEntryAndLabelOnTop && !this->PopupMode)
      {
      this->Script("pack %s -side bottom -fill x -expand %d -pady 0 -padx 0",
                   this->ThumbWheel->GetWidgetName(),
                   this->ResizeThumbWheel);
      }
    else
      {
      this->Script("pack %s -side left -fill x -expand %d -pady 0 -padx 0",
                   this->ThumbWheel->GetWidgetName(),
                   this->ResizeThumbWheel);
      }
    }

  if (this->Label && this->Label->IsCreated())
    {
    this->Script("pack forget %s", this->Label->GetWidgetName());
    if (this->DisplayLabel)
      {
      if (this->DisplayEntryAndLabelOnTop || this->PopupMode)
        {
        this->Script("pack %s -side left -padx 0 -fill y", 
                     this->Label->GetWidgetName());
        }
      else
        {
        this->Script("pack %s -side left -padx 0 -fill y -before %s", 
                     this->Label->GetWidgetName(), 
                     this->ThumbWheel->GetWidgetName());
        }
      }
    }

  if (this->Entry && this->Entry->IsCreated())
    {
    this->Script("pack forget %s", this->Entry->GetWidgetName());
    if (this->DisplayEntry)
      {
      if (this->PopupMode)
        {
        this->Script("pack %s -side left -padx 0 %s", 
                     this->Entry->GetWidgetName(),
                     (this->ExpandEntry ? "-fill both -expand t" : "-fill y"));
        }
      else
        {
        if (this->DisplayEntryAndLabelOnTop)
          {
          this->Script("pack %s -side right -padx 0 -fill y", 
                       this->Entry->GetWidgetName());
          }
        else
          {
          this->Script("pack %s -side right -padx 0 -fill y -after %s", 
                       this->Entry->GetWidgetName(), 
                       this->ThumbWheel->GetWidgetName());
          }
        }
      }
    }

  if (this->PopupMode && 
      this->PopupPushButton && this->PopupPushButton->IsCreated())
    {
    this->Script("pack forget %s", this->PopupPushButton->GetWidgetName());
    if (this->DisplayLabel || this->DisplayEntry)
      {
      this->Script("pack %s -side left -padx 1 -fill y -ipadx 1 -after %s", 
                   this->PopupPushButton->GetWidgetName(),
                   (this->Entry ? this->Entry->GetWidgetName() 
                                : this->Label->GetWidgetName()));
      }
    else
      {
      this->Script("pack %s -side left -padx 1 -fill y -ipadx 1", 
                   this->PopupPushButton->GetWidgetName());
      }
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::Bind()
{
  if (this->ThumbWheel && this->ThumbWheel->IsCreated())
    {
    this->ThumbWheel->SetBinding(
      "<ButtonRelease>", this, "StopMotionCallback");

    // Interaction modes

    char button_event[20], motion_event[20];

    int i;
    for (i = 0 ; i < 3; i++)
      {
      int b = i + 1;

      sprintf(button_event, "<Button-%d>", b);
      sprintf(motion_event, "<B%d-Motion>", b);

      switch (this->InteractionModes[i])
        {
        case vtkKWThumbWheel::InteractionModeLinearMotion:
          this->ThumbWheel->SetBinding(
            button_event, this, "StartLinearMotionCallback");
          this->ThumbWheel->SetBinding(
            motion_event, this, "PerformLinearMotionCallback");
          break;
        case vtkKWThumbWheel::InteractionModeNonLinearMotion:
          this->ThumbWheel->SetBinding(
            button_event, this, "StartNonLinearMotionCallback");
          this->ThumbWheel->RemoveBinding(
            motion_event);
          break;
        case vtkKWThumbWheel::InteractionModeToggleCenterIndicator:
          this->ThumbWheel->SetBinding(
            button_event, this, "ToggleDisplayThumbWheelCenterIndicator");
          this->ThumbWheel->RemoveBinding(
            motion_event);
          break;
        default:
          this->ThumbWheel->RemoveBinding(
            button_event);
          this->ThumbWheel->RemoveBinding(
            motion_event);
        }
      }

    // Auto resize

    if (this->ResizeThumbWheel)
      {
      this->ThumbWheel->SetBinding(
        "<Configure>", this, "ResizeThumbWheelCallback");
      }
    else
      {
      this->ThumbWheel->RemoveBinding("<Configure>");
      }

    // If in popup mode and the mouse is leaving the top level window, 
    // then withdraw it, unless the user is interacting with the wheel.

    if (this->PopupMode &&
        this->TopLevel && this->TopLevel->IsCreated())
      {
      this->TopLevel->SetBinding("<Leave>", this, "WithdrawPopupCallback");

      vtksys_stl::string callback;

      int j;
      for (j = 0 ; j < 3; j++)
        {
        sprintf(button_event, "<Button-%d>", j + 1);
        this->ThumbWheel->AddBinding(
          button_event, this->TopLevel, "RemoveBinding <Leave>");
        }

      callback = "SetBinding ";
      callback += " <Leave> ";
      callback += this->GetTclName();
      callback += " WithdrawPopupCallback";

      this->ThumbWheel->AddBinding(
        "<ButtonRelease>", this->TopLevel, callback.c_str());
      }
    }

  if (this->Entry && this->Entry->IsCreated())
    {
    this->Entry->SetBinding("<Return>", this, "EntryValueCallback");
    this->Entry->SetBinding("<FocusOut>", this, "EntryValueCallback");
    }

  if (this->PopupMode && 
      this->PopupPushButton && this->PopupPushButton->IsCreated())
    {
    this->PopupPushButton->SetBinding(
      "<ButtonPress>", this, "DisplayPopupCallback");
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::UnBind()
{
  if (this->ThumbWheel && this->ThumbWheel->IsCreated())
    {
    this->ThumbWheel->RemoveBinding("<ButtonPress>");
    this->ThumbWheel->RemoveBinding("<ButtonRelease>");

    char button_event[20], motion_event[20];

    int i;
    for (i = 0 ; i < 3; i++)
      {
      sprintf(button_event, "<Button-%d>", i + 1);
      sprintf(motion_event, "<B%d-Motion>", i + 1);

      this->ThumbWheel->RemoveBinding(button_event);
      this->ThumbWheel->RemoveBinding(motion_event);
      }

    this->ThumbWheel->RemoveBinding("<Configure>");
    }

  if (this->Entry && this->Entry->IsCreated())
    {
    this->Entry->RemoveBinding("<Return>");
    this->Entry->RemoveBinding("<FocusOut>");
    }

  if (this->PopupMode && 
      this->PopupPushButton && this->PopupPushButton->IsCreated())
    {
    this->PopupPushButton->RemoveBinding("<ButtonPress>");
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::DisplayPopupCallback()
{
  if (!this->PopupMode ||
      !this->PopupPushButton || !this->PopupPushButton->IsCreated() ||
      !this->TopLevel || !this->TopLevel->IsCreated() ||
      !this->ThumbWheel || !this->ThumbWheel->IsCreated())
    {
    return;
    }

  // Get the position of the mouse, the position and size of the push button,
  // the size of the scale.

  const char *res = 
    this->Script("concat "
                 " [winfo pointerx %s] [winfo pointery %s]" 
                 " [winfo rooty %s] [winfo height %s]"
                 " [winfo width %s] [winfo height %s]",
                 this->GetWidgetName(), this->GetWidgetName(),
                 this->PopupPushButton->GetWidgetName(), 
                 this->PopupPushButton->GetWidgetName(),
                 this->ThumbWheel->GetWidgetName(),
                 this->ThumbWheel->GetWidgetName());
  
  int x, y, py, ph, tw, th;
  sscanf(res, 
         "%d %d %d %d %d %d", 
         &x, &y, &py, &ph, &tw, &th);
 
  // Place the scale so that the slider is coincident with the x mouse position
  // and just below the push button
  
  x -= tw / 2;

  if (py <= y && y <= (py + ph -1))
    {
    y = py + ph - 3;
    }
  else
    {
    y -= th / 2;
    }

  this->TopLevel->SetPosition(x, y);
  this->Script("update");
  this->TopLevel->DeIconify();
  this->TopLevel->Raise();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::WithdrawPopupCallback()
{
  if (!this->PopupMode ||
      !this->TopLevel || !this->TopLevel->IsCreated() ||
      !this->ThumbWheel || !this->ThumbWheel->IsCreated())
    {
    return;
    }

  // Withdraw the popup

  this->TopLevel->Withdraw();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::EntryValueCallback()
{
  double value = this->Entry->GetValueAsDouble();
  double old_value = this->GetValue();
  this->SetValue(value);

  if (value != old_value)
    {
    this->InvokeEntryCommand();
    }
}

// ---------------------------------------------------------------------------
// Return normalized mouse position relative to the thumbwheel.
// i.e. if the mouse is "inside" the thumbwheel, the range is [0.0, 1.0]
// (< 0.0 if position < lower limit , > 1.0 if position > upper limit)
double vtkKWThumbWheel::GetMousePositionInThumbWheel()
{
  const char *res = 
    this->Script("concat [winfo pointerx %s] [winfo rootx %s]",
                 this->GetWidgetName(), this->ThumbWheel->GetWidgetName());
  
  int x, tx;
  sscanf(res, "%d %d", &x, &tx);

  return (double)(x - tx - VTK_KW_TW_BORDER_SIZE) / 
    (double)(this->ThumbWheelWidth - 1);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::StartLinearMotionCallback()
{
  if (this->State == vtkKWThumbWheel::InMotion)
    {
    this->StopMotionCallback();
    }

  this->State = vtkKWThumbWheel::InMotion;

  // Save current state (mouse position, thumbwheel shift, value)

  this->StartLinearMotionState.MousePosition = 
    this->GetMousePositionInThumbWheel();
  this->StartLinearMotionState.ThumbWheelShift = this->ThumbWheelShift;
  this->StartLinearMotionState.Value = this->Value;
  this->StartLinearMotionState.InPerform = 0;

  // If the position indicator has to be shown, drawing the wheel
  
  if (this->DisplayThumbWheelPositionIndicator)
    {
    this->UpdateThumbWheelImage(this->StartLinearMotionState.MousePosition);
    }

  this->InvokeStartCommand();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::PerformLinearMotionCallback()
{
  if (this->State != vtkKWThumbWheel::InMotion ||
      this->StartLinearMotionState.InPerform)
    {
    return;
    }

  this->StartLinearMotionState.InPerform = 1;

  // pos: mouse position relative to the wheel range [0.0, 1.0]
  // distance: distance between current pos and starting pos

  double pos = this->GetMousePositionInThumbWheel();
  double distance = pos - this->StartLinearMotionState.MousePosition;

  double new_value = 
    this->StartLinearMotionState.Value + 
    (distance / this->LinearThreshold) * this->Resolution;

  // Update thumbwheel aspect

  this->ThumbWheelShift = 
    this->StartLinearMotionState.ThumbWheelShift + distance;
  this->UpdateThumbWheelImage(pos);

  // If the resolution implies an integer, round the value

  if ((double)((int)this->Resolution) == this->Resolution)
    {
    this->SetValue((int)(new_value));
    }
  else
    {
    this->SetValue(new_value);
    }

  this->StartLinearMotionState.InPerform = 0;
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::StartNonLinearMotionCallback()
{
  if (this->State == vtkKWThumbWheel::InMotion)
    {
    this->StopMotionCallback();
    }

  this->State = vtkKWThumbWheel::InMotion;

  // Save current state (value)

  this->StartNonLinearMotionState.Value = this->Value;
  this->StartNonLinearMotionState.Increment = 0.0;
  this->StartNonLinearMotionState.InPerform = 0;

  this->InvokeStartCommand();

  // Now perform the motion immediately

  this->PerformNonLinearMotionCallback();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::PerformNonLinearMotionCallback()
{
  if (this->State != vtkKWThumbWheel::InMotion ||
      this->StartNonLinearMotionState.InPerform)

    {
    return;
    }

  this->StartNonLinearMotionState.InPerform = 1;

  // Get current mouse position, compute "speed", update increment

  // pos:   mouse position relative to the wheel range [0.0, 1.0], clamped
  // xn:    mouse position value mapped to [-1.0, 1.0], i.e. cos()
  // angle: angle corresponding  to that xn
  // yn:    sin(angle), i.e. [0.0, 1.0]

  double pos = this->GetMousePositionInThumbWheel();
  if (pos < 0.0)
    {
    pos = 0.0;
    }
  else if (pos > 1.0)
    {
    pos = 1.0;
    }
  double xn = pos * 2.0 - 1.0;
  double yn = sin(acos(xn));
  double direction = (xn < 0.0 ? -1.0 : 1.0);

  // Compute the non-linear increment corresponding to that position, and
  // add it to the increment buffer which will be added to the value (can
  // not update value directly since it can be rounded/clamped by SetValue)

  double inc = 
    ((1.0 - yn) * (this->NonLinearMaximumMultiplier - 0.0) + 0.0) * this->Resolution;
  this->StartNonLinearMotionState.Increment += inc * direction;
  double new_value = this->StartNonLinearMotionState.Value + 
    this->StartNonLinearMotionState.Increment;

  // Update thumbwheel aspect (accelerates depending on the position)

  double shift = (0.0704321 + (1.0 - yn) * 0.00543) * direction;
  this->ThumbWheelShift += shift;
  this->UpdateThumbWheelImage(pos);

  // If the resolution implies an integer, round the value

  if ((double)((int)this->Resolution) == this->Resolution)
    {
    this->SetValue((int)(new_value));
    }
  else
    {
    this->SetValue(new_value);
    }
  
  // Check if we are still in motion later (asynchronously)

  this->Script("after %d {catch {%s PerformNonLinearMotionCallback}}", 
               VTK_KW_TW_NL_REFRESH_RATE,
               this->GetTclName());

  this->StartNonLinearMotionState.InPerform = 0;
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::StopMotionCallback()
{
  this->State = vtkKWThumbWheel::Idle;

  // If the position indicator was shown, hide it by redrawing the wheel
  
  if (this->DisplayThumbWheelPositionIndicator)
    {
    this->UpdateThumbWheelImage();
    }

  this->InvokeEndCommand();
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::InvokeCommand()
{
  if (!this->Command || !*this->Command || this->InInvokeCommand)
    {
    return;
    }

  this->InInvokeCommand = 1;

  this->Script("eval %s", this->Command);

  this->InInvokeCommand = 0;
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::InvokeStartCommand()
{
  if (this->StartCommand && *this->StartCommand && this->IsCreated())
    {
    this->Script("eval %s", this->StartCommand);
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::InvokeEndCommand()
{
  if (this->EndCommand && *this->EndCommand && this->IsCreated())
    {
    this->Script("eval %s", this->EndCommand);
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::InvokeEntryCommand()
{
  if (this->EntryCommand && *this->EntryCommand && this->IsCreated())
    {
    this->Script("eval %s",this->EntryCommand);
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetCommand(vtkObject* Object, 
                                 const char *CommandString)
{
  this->SetObjectMethodCommand(&this->Command, Object, CommandString);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetStartCommand(vtkObject* Object, 
                                      const char * MethodAndArgString)
{
  this->SetObjectMethodCommand(
    &this->StartCommand, Object, MethodAndArgString);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetEndCommand(vtkObject* Object, 
                                    const char * MethodAndArgString)
{
  this->SetObjectMethodCommand(
    &this->EndCommand, Object, MethodAndArgString);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetEntryCommand(vtkObject* Object, 
                                      const char * MethodAndArgString)
{
  this->SetObjectMethodCommand(
    &this->EntryCommand, Object, MethodAndArgString);
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::SetBalloonHelpString(const char *string)
{
  // Interaction modes

  if (string)
    {
    ostrstream modes;
    modes << string << " (";

    int i;
    for (i = 0 ; i < 3; i++)
      {
      if (this->InteractionModes[i] == vtkKWThumbWheel::InteractionModeNone)
        {
        continue;
        }
      switch (i)
        {
        case 0:
          modes << "left";
          break;
        case 1:
          modes << "middle";
          break;
        case 2:
          modes << "right";
          break;
        }

      modes << " button: ";

      switch (this->InteractionModes[i])
        {
        case vtkKWThumbWheel::InteractionModeLinearMotion:
          modes << "linear";
          break;
        case vtkKWThumbWheel::InteractionModeNonLinearMotion:
          modes << "non-linear";
          break;
        case vtkKWThumbWheel::InteractionModeToggleCenterIndicator:
          modes << "toggle center indicator";
          break;
        default:
          modes << "unknown";
        }
    
      if ((i == 0 && (this->InteractionModes[1] != vtkKWThumbWheel::InteractionModeNone ||
                      this->InteractionModes[2] != vtkKWThumbWheel::InteractionModeNone)) ||
          (i == 1 && this->InteractionModes[2] != vtkKWThumbWheel::InteractionModeNone))
        {
        modes << ", ";
        }
      }

    modes << ")" << ends;
    this->ThumbWheel->SetBalloonHelpString(modes.str());
    modes.rdbuf()->freeze(0);
    }
  else
    {
    this->ThumbWheel->SetBalloonHelpString(string);
    }

  if (this->Entry)
    {
    this->Entry->SetBalloonHelpString(string);
    }

  if (this->Label)
    {
    this->Label->SetBalloonHelpString(string);
    }

  if (this->PopupMode && this->PopupPushButton)
    {
    if (string)
      {
      ostrstream temp;
      temp << string << " (press this button to display a thumbwheel)" << ends;
      this->PopupPushButton->SetBalloonHelpString(temp.str());
      temp.rdbuf()->freeze(0);
      }
    else
      {
      this->PopupPushButton->SetBalloonHelpString(string);
      }
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::UpdateEnableState()
{
  this->Superclass::UpdateEnableState();

  this->PropagateEnableState(this->Entry);
  this->PropagateEnableState(this->Label);
  this->PropagateEnableState(this->ThumbWheel);
  this->PropagateEnableState(this->TopLevel);
  this->PropagateEnableState(this->PopupPushButton);

  if (this->GetEnabled())
    {
    this->Bind();
    }
  else
    {
    this->UnBind();
    }
}

// ---------------------------------------------------------------------------
void vtkKWThumbWheel::UpdateThumbWheelImage(double pos)
{
  // Show position indicator ? Compute range

  int posx_start = 0, posx_end = 0;
  double indicator_hsv[3];
  if (this->DisplayThumbWheelPositionIndicator && 
      this->State == vtkKWThumbWheel::InMotion)
    {
    int posx = (int)(pos * (double)(this->ThumbWheelWidth - 1));
    posx_start = posx - (int)floor((double)(VTK_KW_TW_INDICATOR_SIZE - 1) * 0.5);
    posx_end = posx + (int)floor((double)(VTK_KW_TW_INDICATOR_SIZE) * 0.5);
    if (posx_start < 0)
      {
      posx_end -= posx_start;
      posx_start = 0;
      }
    if (posx_end > (this->ThumbWheelWidth - 1))
      {
      posx_start -= (posx_end - (this->ThumbWheelWidth - 1));
      posx_end = this->ThumbWheelWidth - 1;
      }
    vtkMath::RGBToHSV(this->ThumbWheelPositionIndicatorColor, indicator_hsv);
    }

  double width2 = 0.5 * (double)(this->ThumbWheelWidth - 1);

  // gray_global_shift: added to gray values (used to darken or lighten aspect)
  // gray_relief_shift: added to gray values to provide the "notch" relief/shadow

  int gray_global_shift = -20;
  int gray_relief_shift = 35;

  int img_pixel_size = 3;
  int img_row_size = img_pixel_size * this->ThumbWheelWidth;
  int img_buffer_size = img_row_size * this->ThumbWheelHeight;

  // notch_size: size of a notch given the number of notches in a half-wheel

  double notch_size = 
    1.0 / ((double)this->ThumbWheelWidth / (double)this->SizeOfNotches);
  
  int last_notch = 0;
  int relief_flag = 0;

  // Allocate buffer for the whole wheel

  unsigned char *img_buffer = new unsigned char[img_buffer_size];

    // img_ptr_s0: 1st row = gray - shadow * 2 (i.e. dark shadow)
    // img_ptr_s1: 2nd row = gray - shadow     (i.e. ligh shadow)
    // img_ptr:    3rd row = gray              (i.e. normal aspect)

  unsigned char *img_ptr_s0 = img_buffer;
  unsigned char *img_ptr_s1 = img_ptr_s0 + img_row_size;
  unsigned char *img_ptr    = img_ptr_s1 + img_row_size;

  int x;
  for (x = 0; x < this->ThumbWheelWidth; x++)
    {
    // xn:     x value mapped to [-1.0, 1.0], i.e. cos()
    // angle:  angle corresponding  to that xn
    // yn:     sin(angle), i.e. [0.0, 1.0]
    // anglen: normalized angle (+ shift), i.e. mapped to [0.0, 1.0]
    // notch:  index of the current notch given the current angle

    double xn = -1.0 + (double)x / width2;
    double angle = acos(xn);
    double yn = sin(angle);
    double anglen = fmod(angle / 3.14159265358979 + this->ThumbWheelShift, 1.0);
    int notch = (int)(floor(anglen / notch_size));

    // gray:    the gray value for the current pixel in the notch
    // gray_s0: gray + dark shadow
    // gray_s1: gray + light shadow

    int gray = (int)(255.0 * yn + gray_global_shift);

    // If the current x position is a transition from one notch to
    // the other, add some "relief" by first (relief_flag == 0) casting 
    // a shadow (i.e. gray - gray_relief_shift), then (relief_flag == 1)
    // casting a highlight (i.e. gray + gray_relief_shift)

    if (x == 0)
      {
      last_notch = notch;
      }
    else if (notch != last_notch) 
      {
      if (relief_flag == 0)
        {
        gray -= gray_relief_shift;
        relief_flag = 1;
        } 
      else 
        {
        gray += gray_relief_shift;
        relief_flag = 0;
        last_notch = notch;
        }
      }

    // Now the dark and light shadow for the first and second rows of the wheel

    int gray_s0 = gray - gray_relief_shift * 2;
    int gray_s1 = gray - gray_relief_shift;

      // Clamp and assign

    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray);
    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray_s0);
    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray_s1);

    if (this->DisplayThumbWheelPositionIndicator && 
        this->State == vtkKWThumbWheel::InMotion &&
        x >= posx_start && x <= posx_end)
      {
      double r, g, b;

      vtkMath::HSVToRGB(indicator_hsv[0], indicator_hsv[1], (double)gray / 255.0,
                        &r, &g, &b);
      *img_ptr++ = (int)(r * 255.0);
      *img_ptr++ = (int)(g * 255.0);
      *img_ptr++ = (int)(b * 255.0);

      vtkMath::HSVToRGB(indicator_hsv[0], indicator_hsv[1], (double)gray_s0/255.0,
                        &r, &g, &b);
      *img_ptr_s0++ = (int)(r * 255.0);
      *img_ptr_s0++ = (int)(g * 255.0);
      *img_ptr_s0++ = (int)(b * 255.0);

      vtkMath::HSVToRGB(indicator_hsv[0], indicator_hsv[1], (double)gray_s1/255.0,
                        &r, &g, &b);
      *img_ptr_s1++ = (int)(r * 255.0);
      *img_ptr_s1++ = (int)(g * 255.0);
      *img_ptr_s1++ = (int)(b * 255.0);

      }
    else
      {
      *img_ptr++ = gray; *img_ptr++ = gray; *img_ptr++ = gray;

      *img_ptr_s0++ = gray_s0; *img_ptr_s0++ = gray_s0; *img_ptr_s0++ = gray_s0;

      *img_ptr_s1++ = gray_s1; *img_ptr_s1++ = gray_s1; *img_ptr_s1++ = gray_s1;
      }
    }

  // Copy the 1st row to the last row, then 2nd row to last-1 row, so
  // that we got symetrical vertical shadow

  memcpy(img_buffer + img_buffer_size - img_row_size, 
         img_ptr_s0 - img_row_size, img_row_size);

  memcpy(img_buffer + img_buffer_size - img_row_size * 2,
         img_ptr_s1 - img_row_size, img_row_size);

  // Copy the 3rd row to the remaining rows (the body of the wheel)

  int y;
  for (y = 3; y < this->ThumbWheelHeight - 2; y++)
    {
    memcpy(img_ptr, img_ptr_s1, img_row_size);
    img_ptr += img_row_size;
    }

  // Now add a center indicator

  #define VTK_KW_TW_INDICATOR_WIDTH 7
  #define VTK_KW_TW_INDICATOR_HEIGHT 6

  static int indicator[VTK_KW_TW_INDICATOR_WIDTH*VTK_KW_TW_INDICATOR_HEIGHT] = {
    -1, -1, -1,  0, -1, -1, -1,
    -1, -1,  0,  3,  0, -1, -1,
    -1,  0,  3,  2,  1,  0, -1,
     0,  3,  2,  2,  2,  1,  0,
     0,  1,  1,  1,  1,  1,  0,
     0,  0,  0,  0,  0,  0,  0};

  if (this->DisplayThumbWheelCenterIndicator)
    {
    int gray[4];
    gray[2] = 240 + gray_global_shift;
    gray[3] = (int)(gray[2] + 1.7 * gray_relief_shift);
    gray[1] = (int)(gray[2] - 1.7 * gray_relief_shift);
    gray[0] = (int)(gray[2] - 3.4 * gray_relief_shift);

    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray[0]);
    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray[1]);
    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray[2]);
    VTK_KW_TW_CLAMP_UCHAR_MACRO(gray[3]);

    int show_up_indicator = 
      (this->ThumbWheelHeight >= VTK_KW_TW_INDICATOR_HEIGHT * 2 + 2);

    int distance_to_indicator = (int)floor(
      0.5 * (this->ThumbWheelWidth-1-VTK_KW_TW_INDICATOR_WIDTH))*img_pixel_size;

    img_ptr = img_buffer + img_buffer_size 
      - img_row_size * VTK_KW_TW_INDICATOR_HEIGHT + distance_to_indicator;

    int *indicator_ptr = indicator;
    
    unsigned char *img_ptr2 = 0;
    if (show_up_indicator)
      {
      img_ptr2 = img_buffer 
        + img_row_size * (VTK_KW_TW_INDICATOR_HEIGHT-1) + distance_to_indicator;
      }

    for (y = 0; y < VTK_KW_TW_INDICATOR_HEIGHT; y++)
      {
      if (show_up_indicator)
        {
        for (x = 0; x < VTK_KW_TW_INDICATOR_WIDTH; x++)
          {
          if (*indicator_ptr >= 0)
            {
            img_ptr[0] = img_ptr[1] = img_ptr[2] =
              img_ptr2[0] = img_ptr2[1] = img_ptr2[2] = gray[*indicator_ptr];
            }
          img_ptr += img_pixel_size;
          img_ptr2 += img_pixel_size;
          indicator_ptr++;
          }
        img_ptr += img_row_size - VTK_KW_TW_INDICATOR_WIDTH * img_pixel_size;
        img_ptr2 += -img_row_size - VTK_KW_TW_INDICATOR_WIDTH * img_pixel_size;
        }
      else
        {
        for (x = 0; x < VTK_KW_TW_INDICATOR_WIDTH; x++)
          {
          if (*indicator_ptr >= 0)
            {
            img_ptr[0] = img_ptr[1] = img_ptr[2] = gray[*indicator_ptr];
            }
          img_ptr += img_pixel_size;
          indicator_ptr++;
          }
        img_ptr += img_row_size - VTK_KW_TW_INDICATOR_WIDTH * img_pixel_size;
        }
      }
    }

  // Update the Tk photo

  this->ThumbWheel->SetImageToPixels(img_buffer,
                                     this->ThumbWheelWidth, 
                                     this->ThumbWheelHeight, 
                                     img_pixel_size);

  delete [] img_buffer;
}

//----------------------------------------------------------------------------
void vtkKWThumbWheel::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os,indent);
  os << indent << "Value: " << this->Value << endl;
  os << indent << "MinimumValue: " << this->MinimumValue << endl;
  os << indent << "MaximumValue: " << this->MaximumValue << endl;
  os << indent << "ClampMinimumValue: " 
     << (this->ClampMinimumValue ? "On" : "Off") << endl;
  os << indent << "ClampMaximumValue: " 
     << (this->ClampMaximumValue ? "On" : "Off") << endl;
  os << indent << "Resolution: " << this->Resolution << endl;
  os << indent << "NonLinearMaximumMultiplier: " << this->NonLinearMaximumMultiplier << endl;
  os << indent << "ThumbWheelWidth: " << this->ThumbWheelWidth << endl;
  os << indent << "ThumbWheelHeight: " << this->ThumbWheelHeight << endl;
  os << indent << "SizeOfNotches: " << this->SizeOfNotches << endl;
  os << indent << "LinearThreshold: " << this->LinearThreshold << endl;
  os << indent << "ThumbWheelPositionIndicatorColor: (" 
     << this->ThumbWheelPositionIndicatorColor[0] << ", "
     << this->ThumbWheelPositionIndicatorColor[1] << ", "
     << this->ThumbWheelPositionIndicatorColor[2] << ")\n";
  os << indent << "ResizeThumbWheel: " 
     << (this->ResizeThumbWheel ? "On" : "Off") << endl;
  int i;
  for (i = 0; i < 3; i++)
    {
    os << indent << "InteractionMode[" << i << "]: " 
       << this->GetInteractionModeAsString(i) << endl;
    }
  os << indent << "DisplayLabel: " 
     << (this->DisplayLabel ? "On" : "Off") << endl;
  os << indent << "DisplayEntry: " 
     << (this->DisplayEntry ? "On" : "Off") << endl;
  os << indent << "DisplayEntryAndLabelOnTop: " 
     << (this->DisplayEntryAndLabelOnTop ? "On" : "Off") << endl;
  os << indent << "DisplayThumbWheelPositionIndicator: " 
     << (this->DisplayThumbWheelPositionIndicator ? "On" : "Off") << endl;
  os << indent << "DisplayThumbWheelCenterIndicator: " 
     << (this->DisplayThumbWheelCenterIndicator ? "On" : "Off") << endl;
  os << indent << "PupupMode: " 
     << (this->PopupMode ? "On" : "Off") << endl;
  os << indent << "ExpandEntry: " 
     << (this->ExpandEntry ? "On" : "Off") << endl;
  os << indent << "Label: " << this->Label << endl;
  os << indent << "Entry: " << this->Entry << endl;
  os << indent << "PopupPushButton: " << this->PopupPushButton << endl;
}

