/*----------------------------------------------------------------------------*/
/*                                                                            */
/* Copyright (c) 2013-2014 Rexx Language Association. All rights reserved.    */
/*                                                                            */
/* This program and the accompanying materials are made available under       */
/* the terms of the Common Public License v1.0 which accompanies this         */
/* distribution. A copy is also available at the following address:           */
/* https://www.oorexx.org/license.html                                        */
/*                                                                            */
/* Redistribution and use in source and binary forms, with or                 */
/* without modification, are permitted provided that the following            */
/* conditions are met:                                                        */
/*                                                                            */
/* Redistributions of source code must retain the above copyright             */
/* notice, this list of conditions and the following disclaimer.              */
/* Redistributions in binary form must reproduce the above copyright          */
/* notice, this list of conditions and the following disclaimer in            */
/* the documentation and/or other materials provided with the distribution.   */
/*                                                                            */
/* Neither the name of Rexx Language Association nor the names                */
/* of its contributors may be used to endorse or promote products             */
/* derived from this software without specific prior written permission.      */
/*                                                                            */
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS        */
/* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT          */
/* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS          */
/* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT   */
/* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,      */
/* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED   */
/* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,        */
/* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY     */
/* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING    */
/* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS         */
/* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.               */
/*                                                                            */
/*----------------------------------------------------------------------------*/

/**
 *  This example shows how to add tool tips to a combo box.
 *
 *  When the items in a combo box are longer than the width of the combo box,
 *  they can be difficult for the user to read.  Tool tips are a good solution
 *  to this, but combo boxes do not have built in support for tool tips like
 *  list-views or tree-views.
 *
 *  Adding the support to a combo box is doable, but it takes a little work.
 *  This example shows how it is done.  The example also shows how to custom
 *  position the tool tips.  To see the difference in where the tool tip
 *  positions the tips by default and where this program positions them, the
 *  dialog has 2 radio buttons that allow switching between the default
 *  positioning and the custom positioning.
 *
 *  One approach to adding tool tips to a combo box would be to just have the
 *  tool tips display for the items in the drop down list.  This example goes
 *  beyond that to also show tool tips for the selection field and the drop down
 *  button.
 *
 *  When the drop down is closed, adding tool tips to the selection field and
 *  the button is relatively easy.  But, when the drop down is open, it is hard
 *  because the list box has the mouse captured.  This prevents tool tips from
 *  activating when the mouse goes over the selection field or the button.  What
 *  we do here is create tracking tool tips for the selection field and the
 *  button to use when the drop down is open.  Tracking tool tips need to be
 *  shown and hidden manually, which is what is done in this program.
 */

  sd = locate()
  .application~setDefaults('O', sd'comboBoxToolTip.h', .false)

  dlg = .CitiesOfTheWorldDlg~new(sd'comboBoxToolTip.rc', IDD_MAJOR_CITIES)
  dlg~execute("SHOWTOP", IDI_DLG_OOREXX)

  return 0

::requires "ooDialog.cls"

::class 'CitiesOfTheWorldDlg' subclass RcDialog

/** init()
 *
 * Normally for RcDialog dialogs there is no need for an init() method.  Here,
 * we use the init() method to set up our state varialbes and to connect a
 * combo box event.
 *
 * There is no real need to do it here, it could just as well been done in the
 * initDialog() method.
 */
::method init
  expose currentItem currentText currentOnClient ttEditActive ttButtonActive doCustomPositioning

  forward class (super) continue

  -- These variables are used to help track which tool needs to be shown.
  currentItem     = 0
  currentText     = ''
  currentOnClient = .false
  ttEditActive    = .false
  ttButtonActive  = .false

  doCustomPositioning = .true

  self~connectComboBoxEvent(IDC_COMBOBOX, 'CLOSEUP', onCloseUP)

  self~connectButtonEvent(IDC_RB_DEFAULT, 'CLICKED', doDefault)
  self~connectButtonEvent(IDC_RB_CUSTOM, 'CLICKED', doCustom)


/** initDialog()
 *
 *  This is a busy initDialog().  We need to set up and configure the combo box
 *  and create and configure the tool tips.
 */
::method initDialog
  expose comboBox cbEdit cbListBox lbRect editRect buttonRect ttEditTrack ttButtonTrack itemHeight

  comboBox = self~newComboBox(IDC_COMBOBOX)

  -- The tool tips are slightly higher than the default height of the combo box
  -- items.  So, when we position the tool tip over an item, it does not quite
  -- fit.  Making the item height slightly bigger makes things look a lot
  -- better.
  --
  -- If the item height is to be changed, be sure to do it before the items
  -- are inserted.  Otherwise when we get the client rect for the list box, its
  -- vertical height will be incorrect.
  itemHeight = comboBox~getItemHeight
  itemHeight += 2
  comboBox~setItemHeight(itemHeight)

  entries = self~getComboItems
  do e over entries
    comboBox~add(e)
  end

  comboBox~setCue("Select a city")
  comboBox~setMinVisible(10)

  d = comboBox~getComboBoxInfo
  cbEdit    = d~textObj
  cbListBox = d~listBoxObj

  self~calcRects(d)

  self~newRadioButton(IDC_RB_CUSTOM)~check

  -- Create a tracking tool tip for the selection field of the combo box.  For a
  -- drop-down list combo box this is actually the combo box itself.  We still
  -- use cbEdit, even though we know cbEdit and comboBox are the same.
  --
  -- 1.) This tool tip is used for the selection field when the dropdown is open
  ttEditTrack = self~createToolTip(IDC_TT_EDIT_TRACK)

  toolInfo = .ToolInfo~new(self, cbEdit, '', 'ABSOLUTE TRACK IDISHWND', .Rect~new)
  ttEditTrack~addToolEx(toolInfo)

  self~connectToolTipEvent(IDC_TT_EDIT_TRACK, 'NEEDTEXT', onEditTrackNeedText)
  self~connectToolTipEvent(IDC_TT_EDIT_TRACK, 'SHOW', onEditTrackShow)

  -- Create a tracking tool tip for the button portion of the combo box.
  --
  -- 2.) This tool tip is used for the button when the dropdown is open.
  ttButtonTrack = self~createToolTip(IDC_TT_BUTTON_TRACK)

  text = 'Press the button to close the drop down'
  toolInfo = .ToolInfo~new(self, comboBox, text, 'ABSOLUTE TRACK IDISHWND', .Rect~new)
  ttButtonTrack~addToolEx(toolInfo)

  self~connectToolTipEvent(IDC_TT_BUTTON_TRACK, 'SHOW', onButtonTrackShow)

  -- Create a tool tip for the combo box itself.
  --
  -- 3.) This tool tip is used for the selection and button components when the
  -- dropdown is closed.
  ttComboBox = self~createToolTip(IDC_TT_CB_MAIN)

  toolInfo = .ToolInfo~new(comboBox, IDC_TI_EDITRECT, '', 'TRANSPARENT', editRect, IDC_TI_EDITRECT)
  ret = ttComboBox~addToolEx(toolInfo)

  text = 'Press the button to open the drop down'
  toolInfo = .ToolInfo~new(comboBox, IDC_TI_BUTTONRECT, text, 'TRANSPARENT', buttonRect, IDC_TI_BUTTONRECT)
  ttComboBox~addToolEx(toolInfo)

  ttComboBox~manageAtypicalTool(comboBox, .array~of('NEEDTEXT', 'SHOW'), .array~of('ONCBNEEDTEXT', 'ONCBSHOW'))

  -- Create tool tip for the items of the combo box.
  --
  -- 4.) This tool tip is for the individual items when the dropdown is open.
  ttList = self~createToolTip(IDC_TT_CB_LISTBOX)

  toolInfo = .ToolInfo~new(cbListBox, IDC_TI_LISTBOX, '', 'TRANSPARENT', lbRect)
  ttList~addToolEx(toolInfo)

  -- Now start the monitoring process for the list box tool.
  ttList~manageAtypicalTool(cbListBox, .array~of('RELAY', 'NEEDTEXT', 'SHOW'))


/** onRelay()
 *
 * The onRelay() event handler is invoked by the monitor that the
 * manageAtypicalTool() method sets up.  It is invoked for every mouse message
 * that is sent to the list box control.
 *
 * The monitor first informs us of the mouse message.  This gives us a chance to
 * examine the message *before* the tool tip sees it.
 *
 * This event handler here is the key to how this whole program works.
 *
 * All the rest of the code in the program is simply busy work, keeping track of
 * what state we are in.  In this method we determine where the mouse is and
 * then determine how we have to configure all the tool tips we are using.
 *
 * We need to know, or to take into account, some facts about the combo box.  1
 * fact is that when the list box is dropped down, it captures the mouse.  All
 * mouse messages are then sent to the list box until it releases the capture
 * when it closes up.  This means that the pos argument can be anywhere on the
 * screen.
 *
 * Fact number 2 is that the combo box will always tell us the index of the item
 * that is *closest* to the mouse position.  So, even if the mouse is on the
 * other side of the screen, the combo box will still return a valid item index.
 * It will be what ever item is closest to that mouse position, even if it is
 * 500 pixels away.
 *
 * We need to track what item the mouse is over, and whether the mouse is over
 * the client area of the combo box.  Every time the location changes from over
 * the clien to not over the client, and every time the item the mouse is over
 * changes, the tool tips need to be updated.
 *
 * To handle the selection field and the button, we treat them a pseudo list box
 * item.  When the mouse is over either of them, it will not be over the list
 * box client area.  We *pretend* that it is by changing the value in the
 * directory object returned by the hit test info method.  We do a similar thing
 * for the item index.  We *pretend* that the selection field has an item index
 * of 0 and that the button has an item index of -1.
 *
 * If we are, newly, over the selection field or button, we need to manually
 * activate the tracking tool tip and send it the mouse position.  In screen
 * coordinates, not client coordinates.  Other code in this program is
 * responsible for hiding the tracking tool tips at the proper time.
 *
 * The monitor will pass all mouse messages on to the tool tip after we return
 * from this method.  This allows the tool tip to 'do its thing.'
 */
::method onRelay unguarded
    expose currentItem currentText currentOnClient editOffsetRect buttonOffsetRect -
           ttEditTrack ttEditActive cbEdit ttButtonTrack ttButtonActive comboBox
    use arg toolTip, pos, mouseMsg, listBox

    -- Determine which, if any, item the mouse is over.
    d = .directory~new
    ret = listBox~hitTestInfo(pos, d)

    -- Treat the edit and button rects as though they are in the client area of
    -- the list box and assign them a, fake, item index.
    if pos~inRect(editOffsetRect) then do
      d~inClientArea = .true
      d~itemIndex = 0
    end
    else if pos~inRect(buttonOffsetRect) then do
      d~inClientArea = .true
      d~itemIndex = -1
    end

    -- Save the old item and location and sent the current item and location to
    -- where the mouse is at this moment.
    oldItem         = currentItem
    oldOnClient     = currentOnClient
    currentItem     = d~itemIndex
    currentOnClient = d~inClientArea

    -- If mouse is over a new item or location, hide the ToolTip.  Set our
    -- current text to the text for the item we are now over.  If we are not
    -- on the client area, set the current text to the empty string.
    if oldItem \== currentItem | oldOnClient \== currentOnClient then do
        if currentOnClient then do
            if d~itemIndex == -1 then do
                p = pos~copy
                listBox~client2Screen(p)
                ttButtonTrack~trackActivate(self, comboBox, .true)
                ttButtonActive        = .true
                turnTTEditActiveOff   = .true
                turnTTButtonActiveOff = .false
                ttEditTrack~trackPosition(p)
            end
            else if d~itemIndex == 0 then do
                p = pos~copy
                listBox~client2Screen(p)
                ttEditTrack~trackActivate(self, cbEdit, .true)
                ttEditActive = .true
                turnTTEditActiveOff   = .false
                turnTTButtonActiveOff = .true
                ttEditTrack~trackPosition(p)
            end
            else do
                turnTTButtonActiveOff = .true
                turnTTEditActiveOff   = .true
                currentText = listBox~getText(d~itemIndex)
            end
        end
        else do
            turnTTButtonActiveOff = .true
            turnTTEditActiveOff    = .true
            currentText = ''
        end

        -- Each time we have a change, we close the current tool tip.  The tool
        -- tips are now configured for the *new* item the mouse is over.  Which
        -- may be not over an item at all, of course.  When we return from this
        -- handler, the mouse message will be relayed on to the tool tip, and it
        -- will do its tool tip thing.
        toolTip~pop

        if turnTTEditActiveOff, ttEditActive then do
            ttEditTrack~trackActivate(self, cbEdit, .false)
            ttEditActive = .false
        end

        if turnTTButtonActiveOff, ttButtonActive then do
            ttButtonTrack~trackActivate(self, comboBox, .false)
            ttButtonActive = .false
        end
    end

    return 0


/** onNeedText()
 *
 * The event handler for the NEEDTEXT event.  This method is invoked by the
 * monitor for the list box tool when that tool is requesting the text it should
 * display.
 *
 * We set the text to the text of the current item the mouse is over.
 */
::method onNeedText unguarded
    expose currentItem currentText
    use arg toolTip, listBox, info

    info~text = currentText

    -- Return false so that the ToolTip does not store the text.  We want it to
    -- always ask us for the text because the mouse could be over any item.
    return .false


/** onShow()
 *
 * The event handler for the SHOW event.  This method is invoked by the monitor
 * for the list box tool when that tool is notifiying the list box that it is
 * about to show the tip.  This gives us the chance to do a custom positioning
 * of the tip.
 *
 * If we are not doing custom positioning we just return false.  False tells the
 * tool tip that it should position the tip itself.
 *
 * If we are doing the positioning, we set the postion we want and return true.
 * True tells the tool tip that we already positioned the tip and it should not
 * change its postion.
 *
 * To get the position we determine which item the mouse is over, in list box
 * client coordinates and convert the client coordinates to screen coordinates.
 * The position we want is the top left corner of the list box item.  But, the
 * tool tip tips are slightly taller than a list box item, so we shift the
 * position up 3 pixels, which gives us a position that does not obscure either
 * the item above or the item below.
 */
::method onShow unguarded
    expose currentItem comboBox lbRect itemHeight doCustomPositioning
    use arg toolTip, listBox, toolID

    if \ doCustomPositioning then return .false

    if currentItem > 0 then do
        topIndex = comboBox~getFirstVisible
        r = lbRect~copy
        r~top = (currentItem - topIndex) * itemHeight
        r~top -= 3
        listBox~client2screen(r)

        toolTip~setWindowPos(0, r~left, r~top, 0, 0, "NOACTIVATE NOSIZE")
        return .true
    end

    return .false


/** onCBNeetText()
 *
 * The event handler for the NEEDTEXT event.  This method is invoked when the
 * combo box tool tip is requesting the text for the tip.
 *
 * The combo box tool tip is a normal tool tip, there is no manageAtypicalTool
 * monitor for this tool tip.  Recall, that we set this tool tip up with 2
 * tools.  One for the rectangle of the selection field and one for the
 * rectangle of the button.  We use the tool ID to detemine which tool we are
 * invoked for.  If it is the selection field, we return the selected item text.
 * If it is the button, we return the button tip.
 *
 * We can only be invoked when the drop down is closed.
 */
::method onCBNeedText unguarded
    use arg toolTip, comboBox, info

    if info~toolID == .constDir[IDC_TI_EDITRECT] then do
        info~text = comboBox~selected
    end
    else if info~toolID == .constDir[IDC_TI_BUTTONRECT] then do
        info~text = 'Press the button to open the drop down'
    end
    else do
        -- Can not happen
        info~text = ''
        return .true
    end

    return .false


/** onCBShow()
 *
 * The event handler for the SHOW event.  This method is invoked when the
 * combo box tool tip is about to show the tip.  This gives us a chance to do
 * custom positiong.  If we are not doing custom positioning, we simple return
 * false, telling the tool tip to position the tip itself.
 *
 * The combo box tool tip is a normal tool tip, there is no manageAtypicalTool
 * monitor for this tool tip.  Recall, that we set this tool tip up with 2
 * tools.  One for the rectangle of the selection field and one for the
 * rectangle of the button.  We use the tool ID to detemine which tool we are
 * invoked for.  If it is the selection field, we use the postion of the
 * selection field rectangle, convert it to screen coordinates and position the
 * tip at the top left corner.
 *
 * If it is the button, we do the same thing, but position the tip immediately
 * to right of the button.
 *
 * We can only be invoked when the drop down is closed.
 */
::method onCBShow unguarded
    expose editRect buttonRect doCustomPositioning
    use arg toolTip, comboBox, toolID

    if \ doCustomPositioning then return .false

    if toolID == .constDir[IDC_TI_EDITRECT] then do
        r = editRect~copy

        comboBox~client2screen(r)
        toolTip~setWindowPos(0, r~left, r~top, 0, 0, "NOACTIVATE NOSIZE NOZORDER")
        return .true
    end
    else if toolID == .constDir[IDC_TI_BUTTONRECT] then do
        r = buttonRect~copy

        comboBox~client2screen(r)
        toolTip~setWindowPos(0, r~right, r~top, 0, 0, "NOACTIVATE NOSIZE NOZORDER")
        return .true
    end

    return .false


/** onEditTrackNeedText()
 *
 * This is the event handler for the NEEDTEXT event.  It is invoked when the
 * edit tracking tool tip needs the text to display for the selection field.
 *
 * We simply send it the text of the currently selected item.
 */
::method onEditTrackNeedText unguarded
    use arg comboBox, toolTip, info

    info~text = comboBox~selected
    return .false


/** onEditTrackShow
 *
 * This is the event handler for the SHOW event.  It is invoked when the edit
 * tracking tool tip is about to show the tip.  It gives us the chance to do
 * custom positioning of the tip.  If we are not doing custom positioning of the
 * tip we simply return false to tell the tool tip to position the tip itself.
 *
 * If we are positioing the tip, we place it directly over the selection field.
 */
::method onEditTrackShow unguarded
    expose editRect doCustomPositioning
    use arg comboBox, toolTip

    if \ doCustomPositioning then return .false

    r = editRect~copy

    comboBox~client2screen(r)
    toolTip~setWindowPos(0, r~left, r~top, 0, 0, "NOACTIVATE NOSIZE NOZORDER")
    return .true



/** onButtonTrackShow
 *
 * This is the event handler for the SHOW event.  It is invoked when the button
 * tracking tool tip is about to show the tip.  It gives us the chance to do
 * custom positioning of the tip.  If we are not doing custom positioning of the
 * tip we simply return false to tell the tool tip to position the tip itself.
 *
 * If we are positioing the tip, we place it directly to the right of the
 * button.
 */
::method onButtonTrackShow unguarded
    expose buttonRect doCustomPositioning
    use arg comboBox, toolTip

    if \ doCustomPositioning then return .false

    r = buttonRect~copy

    comboBox~client2screen(r)
    toolTip~setWindowPos(0, r~right, r~top, 0, 0, "NOACTIVATE NOSIZE NOZORDER")
    return .true


/** onCloseUp()
 *
 * Event handler for the CLOSEUP event.  This method is invoked when the drop
 * down list is closed.
 *
 * We have 2 tracking tool tips in this program.  They must be manually shown
 * and hidden.  If one or the other is visble and the drop down is closed, it
 * will remain on the screen.  So, here we hide the tool tip if it is currently
 * visible.
 */
::method onCloseup unguarded
    expose ttEditTrack ttEditActive cbEdit ttButtonTrack ttButtonActive comboBox

    if ttEditActive then do
        ttEditTrack~trackActivate(self, cbEdit, .false)
        ttEditActive = .false
    end

    if ttButtonActive then do
        ttButtonTrack~trackActivate(self, comboBox, .false)
        ttButtonActive = .false
    end

    return 0


/** doDefault()
 *
 * The event handler for the 'Tool Tip positions the tool tips' radio button
 * click event.  We just need to flip our flag.
 */
::method doDefault unguarded
    expose doCustomPositioning
    doCustomPositioning = .false
    return 0


/** doCustom()
 *
 * The event handler for the 'Application positions the tool tips' radio button
 * click event.  We just need to flip our flag.
 */
::method doCustom unguarded
    expose doCustomPositioning
    doCustomPositioning = .true
    return 0


/** getComboItems()
 *
 * Simple convenience method, returns an array of the items we will add to the
 * combo box.
 */
::method getComboItems private

  entries = .array~of("New York, United States of America, North America",     -
                      "Johannesburg, South Africa, Africa",                    -
                      "Cape Town, South Africa, Africa",                       -
                      "Saint Petersburg, Russia, Europe",                      -
                      "Mexico City, Mexico, North America",                    -
                      "Los Angles, United States of America, North America",   -
                      "Casablanca, Morocco, Africa",                           -
                      "Pyongyang, North Korea, Asia",                          -
                      "New Taipei, Republic of China (Taiwan), Asia",          -
                      "Addis Ababa, Ethiopia, Africa",                         -
                      "Moscow, Russia, Europe",                                -
                      "Yokohama, Japan, Asia",                                 -
                      "Chongqing, China, Asia",                                -
                      "Berlin, Germany, Europe",                               -
                      "Rio de Janeiro, Brazil, South America",                 -
                      "Guangzhou, China, Asia",                                -
                      "São Paulo, Brazil, South America",                      -
                      "Karachi, Pakistan, Asia",                               -
                      "Istanbul, Turkey, Europe / Asia",                       -
                     )

  return entries


/** calcRects()
 *
 *  To properly set the tool info for our tool tips, and to properly position
 *  the tool tips when we do custom positioning, we need to calculate and save
 *  a number of rectangles.
 *
 *  We start off with the client rectangles of the combo box, the drop down list
 *  box, the selection field, and the button.  The client rects within the combo
 *  box do not include the borders of the controls.  We want to adjust those
 *  rects so that they do include the borders.
 *
 *  The other problem is that when the list box is dropped down, the list box
 *  has the mouse captured, so all mouse messages go to the list box and all
 *  mouse positions are relative to the client area of the list box.  So, we
 *  need to calculate a rectangle for the selection field and the button that is
 *  in client coordinates of the list box.
 *
 *  The 'd' argument passed in here is the .Directory object returned from
 *  getComboBoxInfo().  This directory object contains both the selection field
 *  client rect and the button client rect.
 */
::method calcRects private
    expose comboBox cbEdit cbListBox lbRect editRect buttonRect editOffsetRect buttonOffsetRect
    use arg d

    -- The button rect, edit rect, and lb rect do not include borders.  We
    -- basically want to adjust them to the combo box width and height.
    cbRect = comboBox~clientRect

    lbRect     = cbListBox~clientRect
    editRect   = d~textRect
    buttonRect = d~buttonRect

    lbRect~left  = cbRect~left
    lbRect~right = cbRect~right

    -- The top and bottom coordinates of both the text and button rects have to
    -- be in relationship to the client rect of the list box.  I.e. the
    -- bottoms are at the 0 coordinate of the list box client area and the tops
    -- are at 0 - the height of the combo box.
    height = cbRect~bottom - cbRect~top

    editRect~left   = cbRect~left
    editRect~top    = cbRect~top
    editRect~right  = buttonRect~left
    editRect~bottom = cbRect~bottom

    -- Button rect left stays the same.
    buttonRect~top    = cbRect~top
    buttonRect~right  = cbRect~right
    buttonRect~bottom = cbRect~bottom

    editOffsetRect = editRect~copy
    editOffsetRect~top -= height
    editOffSetRect~bottom -= height

    buttonOffsetRect = buttonRect~copy
    buttonOffsetRect~top -= height
    buttonOffsetRect~bottom -= height



