/*
 * ComboBoxMod.prg
 * Clase TComboBoxMod()
 *
 * Copyright 2020 Ignacio Ortiz de Zúñiga
 * Copyright 2020 https://Xailer.com
 * All rights reserved
 *
 */

#include "Xailer.ch"

#xtranslate IsSpecialChar( <nKey> ) => Chr( <nKey> ) $ ( " .:,;-_¿¡'?=)(/&%$!ºª*Çç¨{}\|@#[]+<>" + Chr( 32 ) )

CLASS xComboBoxMod FROM TEditMod

PUBLISHED:

   PROPERTY oImageList     WRITE METHOD SetImageList AS TImageList ;
                           EDITOR PE_ImageList
   PROPERTY aItems         INIT {} WRITE METHOD SetItems ;
                           EDITOR PE_StringList
   PROPERTY cText          WRITE METHOD SetText ;
                           READ  METHOD GetText ;
                           INIT ""

   PROPERTY nIndex         INIT 0 ;
                           WRITE METHOD SetIndex ;
                           READ  METHOD GetIndex

   PROPERTY nClrDropPane      INIT clWindow        EDITOR PE_Color
   PROPERTY nClrDropText      INIT clWindowText    EDITOR PE_Color
   PROPERTY nClrDropHotPane   INIT clActiveCaption EDITOR PE_Color
   PROPERTY nClrDropHotText   INIT clWindowText    EDITOR PE_Color
   PROPERTY nClrDropSelPane   INIT clHighlight     EDITOR PE_Color
   PROPERTY nClrDropSelText   INIT clWindowText    EDITOR PE_Color

   PROPERTY nMaxLength        INIT 0 WRITE INLINE ::nOrgMaxLen := Value,;
                                                  ::SetMaxLength( Value )

   PROPERTY nDropMargin       INIT 10
   PROPERTY nDropItemHeight   INIT 25
   PROPERTY nDroppedWidth     INIT 0
   PROPERTY nDroppedHeight    INIT 300
   PROPERTY lDropAnimation    INIT .T.
   PROPERTY nStyle            INIT esCOMBO VALUES esCOMBO, esARROW ;
                              WRITE METHOD SetStyle
   PROPERTY lFreeEdit         INIT .F.

   EVENT OnDropCreate( oSender )   // --> lValue
   EVENT OnDropDown( oSender, oDropForm, oControl )   // --> Nil
   EVENT OnCloseUp( oSender )   // --> Nil
   EVENT OnChange( oSender, nIndex, nOldIndex, cText )
   EVENT OnDrawItem( oSender, nItem, cText, nImage, nClrText, nClrPane, hDC, aRect )

PUBLIC:
   PROPERTY Value
   PROPERTY lDropped    INIT .F. READONLY

   METHOD New( oParent )      CONSTRUCTOR
   METHOD Create( oParent )   CONSTRUCTOR

PROTECTED:
   PROPERTY oBitmaps
   PROPERTY cPicture
   PROPERTY lTrimSpaces  INIT .F.

   METHOD SetStyle( Value )
   METHOD SetSelection()

   EVENT OnBtnClick( oSender, nX, nY )

   DATA nTmpColor

RESERVED:
   DATA nOrgMaxLen INIT 0

   METHOD SetImageList( oImgLst )
   METHOD SetText( cText, lFocused, lUpdPict, lWithEvent )
   METHOD GetText( nIndex )
   METHOD SetItems( aItems )
   METHOD SetIndex( nIndex )
   METHOD GetIndex( cText )
   METHOD SearchInEdit( nKey )
   METHOD SearchString( cText )
   METHOD SelectString( cText )
   METHOD SearchNeeded()
   METHOD ValueInList( cText )
   METHOD Dropdown()

   METHOD WMKeyDown( nKey, nFlags, hWnd )
   METHOD WMChar( nKey, nFlags, hWnd )
   METHOD WMLButtonDown( wParam, lParam, hWnd )
   METHOD WMLButtonUp( wParam, lParam, hWnd )
   METHOD WMXailer( nMsg, nPos )
   METHOD WMEraseBkgnd()   INLINE 1

   // Para compatibilidad con clase TComboBox

   METHOD InsertItem( nPos, cItem ) // --> lSuccess
   METHOD AddItem( cItem ) INLINE ::InsertItem( Nil, cItem ) // --> lSuccess
   METHOD DeleteItem( nPos ) // --> lSuccess
   METHOD DeleteItems()    INLINE ::aItems := {}
   METHOD ModifyItem( nPos, cItem ) // --> lSuccess
   METHOD GetCount()       INLINE Len( ::aItems )

ENDCLASS

//--------------------------------------------------------------------------

METHOD New( oParent ) CLASS XComboBoxMod

   ::Super:New( oParent )
   ::FoImageList := TImageList():Create( Self )

RETURN Self

//--------------------------------------------------------------------------

METHOD Create( oParent ) CLASS XComboBoxMod

   UPDATE ::oParent TO oParent

   ::Super:Create()

   IF Empty( ::oImageList )
      ::FoImageList := TImageList():Create( Self )
   ENDIF

   ::OnBtnClick := {|o,x,y| ::Dropdown() }

RETURN Self

//--------------------------------------------------------------------------

METHOD SetStyle( Value ) CLASS xComboBoxMod

   IF Value != esCOMBO .AND. Value != esARROW
      Value := esCOMBO
   ENDIF

   ::FnStyle := Value

RETURN Value

//--------------------------------------------------------------------------

METHOD SetSelection() CLASS xComboBoxMod

   IF ::lAutoSelect != asSELECTNONE
      ::SetSel( 0, -1 )
   ELSE
      ::SetSel( 0, 0 )
   ENDIF

RETURN NIL

//--------------------------------------------------------------------------

METHOD SetImageList( oImgLst ) CLASS XComboBoxMod

   IF ::FoImageList != Nil .AND. ::FoImageList:oParent == Self
      ::FoImageList:Destroy()
   ENDIF

   ::FoImageList := oImgLst

RETURN oImgLst

//--------------------------------------------------------------------------

METHOD SetText( cText, lFocused, lUpdPict, lWithEvent ) CLASS xComboBoxMod

   LOCAL nOld
   LOCAL lRet, lChg

   DEFAULT cText TO ""

   nOld := ::nIndex
   lChg := !( cText == ::cText )
   lRet := ::SetValue( cText, lFocused, lUpdPict, lWithEvent )

   IF lChg
      ::OnChange( ::nIndex, nOld, cText )
   ENDIF

RETURN lRet

//--------------------------------------------------------------------------

METHOD GetText( nIndex ) CLASS xComboBoxMod

   LOCAL cText

   IF PCount() == 0 .OR. ( (nIndex == 0 ) .AND. ::lFreeEdit )
      RETURN ::Super:GetText()
   ENDIF

   DEFAULT nIndex TO 0

   IF nIndex > 0 .AND. nIndex <= Len( ::aItems )
      cText := ::aItems[ nIndex ]
   ELSE
      cText := ""
   ENDIF

RETURN cText

//--------------------------------------------------------------------------

METHOD SetItems( aItems ) CLASS xComboBoxMod

   LOCAL nMaxLength := ::nMaxLength

   ::FaItems := aItems

   IF nMaxLength != -1
      nMaxLength := 0
      AEval( aItems, {|v| nMaxLength := Max( nMaxLength, Len( v ) ) } )
      ::nMaxLength := Max( ::nOrgMaxLen, nMaxLength )
   ENDIF

   IF !Empty( aItems ) .AND. ::lFreeEdit
      ::nIndex := 0
   ENDIF

RETURN nil

//--------------------------------------------------------------------------

METHOD SetIndex( nIndex ) CLASS XComboBoxMod

   IF nIndex >= 0 .AND. nIndex <= Len( ::aItems )
      ::FnIndex := nIndex
      ::SetText( IIF( nIndex == 0, "", ::aItems[ nIndex ] ) )
      ::SetSelection()
   ENDIF

RETURN ::FnIndex

//--------------------------------------------------------------------------

METHOD GetIndex( cText ) CLASS xComboBoxMod

   LOCAL nAt := 0

   DEFAULT cText TO ::cText

   IF Valtype( cText ) == "C"
      nAt := AScan( ::aItems, {|v| Upper( v ) == Upper( cText ) } )
   ENDIF

   IF PCount() == 0
      ::FnIndex := nAt
   ENDIF

RETURN nAt

//--------------------------------------------------------------------------

METHOD SearchInEdit( nKey ) CLASS xComboBoxMod

   LOCAL cSeek
   LOCAL nStart, nEnd
   LOCAL lFound, lKey, lPut

   cSeek := ::cText
   lKey  := !Empty( nKey )
   lPut  := .F.

   IF !::lFreeEdit
      ::GetSel( @nStart, @nEnd )
      nStart ++
      nEnd ++
      IF nEnd > nStart
         cSeek := Left( cSeek, nStart - 1 ) + ;
                  IIF( lKey, Chr( nKey ), "" ) + ;
                  SubStr( cSeek, nEnd )
         lPut := .T.
      ELSE
         cSeek := Left( cSeek, nStart - 1 )
      ENDIF
   ENDIF

   IF lKey .AND. !lPut
      cSeek += Chr( nKey )
   ENDIF

   lFound := ::SelectString( cSeek )

   IF !lFound .AND. lKey
      Beep( 150,100 )
   ENDIF

RETURN lFound

//--------------------------------------------------------------------------

METHOD SearchString( cText ) CLASS xComboBoxMod

   LOCAL nAt
   LOCAL lExact

   IF ValType( cText ) != "C"
      RETURN 0
   ENDIF

   lExact := Set( _SET_EXACT, .F. )
   nAt := AScan( ::FaItems, {|v| Upper( v ) = Upper( cText )} )
   Set( _SET_EXACT, lExact )

RETURN nAt

//--------------------------------------------------------------------------

METHOD SelectString( cText ) CLASS xComboBoxMod

   LOCAL nAt

   nAt := ::SearchString( cText )

   IF nAt > 0
      ::nIndex := nAt
      IF !::lFreeEdit
         ::SetSel( Len( cText ), Len( cText ) )
      ENDIF
   ENDIF

RETURN ( nAt > 0 )

//--------------------------------------------------------------------------

METHOD ValueInList( cText ) CLASS xComboBoxMod

   DEFAULT cText TO ::cText

RETURN ( Ascan( ::FaItems, {|v| v == cText } ) > 0 )

//--------------------------------------------------------------------------

METHOD SearchNeeded() CLASS xComboBoxMod

   IF ::lFreeEdit
      RETURN !Empty( ::cText ) .AND. !::ValueInList()
   ENDIF

RETURN .F.

//--------------------------------------------------------------------------

METHOD DropDown() CLASS xComboBoxMod

   LOCAL oDrop
   LOCAL aItems
   LOCAL cItem, cText
   LOCAL lRet

   IF !::lDropped .AND. Len( ::aItems ) > 0
      lRet := ::OnDropCreate()

      IF ::SearchNeeded()
         cText  := AllTrim( Upper( ::cText ) )
         aItems := {}
         FOR EACH cItem IN ::aItems
            IF cText $ cItem
               AAdd( aItems, cItem )
            ENDIF
         NEXT
         IF Empty( aItems )
            aItems := ::aItems
         ENDIF
      ELSE
         aItems := ::aItems
      ENDIF

      IF lRet == NIL .OR. lRet
         WITH OBJECT oDrop := TDropControl():New( Self )
            :Create()
            WITH OBJECT :oControl := TListboxMod():New( oDrop )
               :oImageList   := ::oImageList
               :aItems       := aItems
               :nBorderStyle := bvNONE
               :cText        := ::cText
               :nClrPane     := ::nClrDropPane
               :nClrText     := ::nClrDropText
               :nClrHotPane  := ::nClrDropHotPane
               :nClrHotText  := ::nClrDropHotText
               :nClrSelPane  := ::nClrDropSelPane
               :nClrSelText  := ::nClrDropSelText
               :nMargin      := ::nDropMargin
               :nItemHeight  := ::nDropItemHeight
               :oFont        := ::oFont:Clone()
               :lShortCuts   := .t.
               :lHtmlText    := .f.
               IF ::EventAssigned( "OnDrawItem" )
                  :OnDrawItem  := {|o,id,txt,img,clt,clp,hdc,aRec| ;
                                    ::OnDrawItem( id,@txt,@img,@clt,@clp,hdc,aRec ) }
               ENDIF
               :Create()
            END WITH

            :OnSelect   := {|oForm, oControl| ::SetText( oControl:cText ), ::SetSelection() }
            :OnReturn   := {|| ::lDropped := .F., ::OnCloseUp(), ::Refresh() }
            :nHeight    := Min( ::nDroppedHeight, ::nDropItemHeight * Len( aItems ) )
            :nWidth     := IIF( ::nDroppedWidth = 0, ::nWidth, ::nDroppedWidth )
            :lAnimation := ::lDropAnimation
            :lAside     := ( ::nStyle == esARROW )

            ::OnDropDown( oDrop, oDrop:oControl )

            :Show()
         END WITH

         ::lDropped := .T.
         ::Refresh()
      ENDIF
   ENDIF

RETURN NIL

//--------------------------------------------------------------------------

METHOD WMLButtonDown( wParam, lParam, hWnd ) CLASS xComboBoxMod

   IF !::lFreeEdit .AND. !::lDropped .AND. !::lReadOnly
      ::nTmpColor := ::nClrPaneFocus
      ::nClrPaneFocus:= clDarkGray
   ENDIF

RETURN ::Super:WMLButtonDown( wParam, lParam, hWnd )

//--------------------------------------------------------------------------

METHOD WMLButtonUp( wParam, lParam, hWnd ) CLASS xComboBoxMod

// IOZ: Comentado porque se complica al hacer click en el control
// cuando el dropdown está activo ya que se vuelve a disparar. TOFIX

   IF !::lFreeEdit .AND. !::lDropped .AND. !::lReadOnly
      ::nClrPaneFocus:= ::nTmpColor
//      ::Dropdown()
   ENDIF

RETURN ::Super:WMLButtonUp( wParam, lParam, hWnd )

//------------------------------------------------------------------------------

METHOD WMKeyDown( nKey, nLParam, hWnd ) CLASS XComboBoxMod

   LOCAL aStdKeys, aCtrKeys
   LOCAL nPage, nIndex, nFor, nLen
   LOCAL lRet, lCtrl

   IF ::lLocked // Datacontrol locked
      aStdKeys := { VK_RETURN }
      aCtrKeys := { Asc( "C" ), Asc( "E") }
      lCtrl    := lGetKeyState( VK_CONTROL )
      IF AScan( aStdKeys, nKey ) > 0 .OR. ( lCtrl .AND. AScan( aCtrKeys, nKey ) > 0 )
         RETURN ::Super:WMKeyDown( nKey, nLParam, hWnd )
      ELSE
         ::oForm:SendMsg( WM_CHANGEUISTATE, MakeLong( UIS_CLEAR, UISF_HIDEFOCUS ) )
         RETURN 0
      ENDIF
   ENDIF

   nLen   := Len( ::aItems )
   nPage  := Int( ::nDroppedHeight / ::nDropItemHeight )
   nIndex := ::nIndex

   SWITCH nKey
   CASE VK_F4
      IF !::lReadOnly .AND. !lGetKeyState( VK_CONTROL )
         ::Dropdown()
         RETURN 0
      ENDIF
      EXIT
   CASE VK_DOWN
      IF !::lReadOnly
         IF ::SearchNeeded()
            IF !::SearchInEdit()
               ::Dropdown()
            ENDIF
         ELSEIF ::nIndex < nLen
            ::nIndex ++
         ENDIF
         RETURN 0
      ENDIF
      EXIT
   CASE VK_UP
      IF !::lReadOnly
         IF ::SearchNeeded()
            ::SearchInEdit()
         ELSEIF ::nIndex > 1
            ::nIndex --
         ENDIF
         RETURN 0
      ENDIF
      EXIT
   CASE VK_NEXT
      IF !::lReadOnly
         IF ::SearchNeeded()
            ::SearchInEdit()
         ELSEIF !::lFreeEdit .AND. ( ::nIndex != nLen )
            nLen :=  Min( nLen, ::FnIndex + nPage )
            ::SetIndex( nLen )
         ENDIF
         RETURN 0
      ENDIF
      EXIT
   CASE VK_PRIOR
      IF !::lReadOnly
         IF ::SearchNeeded()
            ::SearchInEdit()
         ELSEIF !::lFreeEdit .AND. ( ::nIndex != 1 )
            nLen := Max( 1, ::FnIndex - nPage )
            ::SetIndex( nLen )
         ENDIF
         RETURN 0
      ENDIF
      EXIT
   END SWITCH

RETURN ::Super:WMKeyDown( nKey, nLParam, hWnd )

//------------------------------------------------------------------------------

METHOD WMChar( nKey, nFlags, hWnd ) CLASS xComboBoxMod

   LOCAL nLen, nPos

   IF ::lLocked // Datacontrol locked
      IF AScan( { VK_RETURN, VK_TAB }, nKey ) > 0
         RETURN ::Super:WMChar( nKey, nFlags, hWnd )
      ELSE
         RETURN 0
      ENDIF
   ENDIF

   IF !::lFreeEdit
      IF IsCharAlphaNumeric( nKey ) .OR. IsSpecialChar( nKey )
         ::SearchInEdit( nKey )
         RETURN 0
      ENDIF
   ENDIF

RETURN ::Super:WMChar( nKey, nFlags, hWnd )

//------------------------------------------------------------------------------

METHOD WMXailer( nMsg, nPos ) CLASS xComboBoxMod

   ::Super:WMXailer( nMsg, nPos )

   IF ( nMsg == WM_PASTE .OR. nMsg == WM_CUT ) .AND. !::lFreeEdit
      IF !::SearchInEdit()
         ::cText := ""
         Beep( 150,100 )
      ENDIF
   ENDIF

RETURN nil

//------------------------------------------------------------------------------

METHOD InsertItem( nPos, cItem ) CLASS XComboBoxMod

   DEFAULT cItem  TO "Combobox item"

   IF nPos == Nil .OR. nPos > ::GetCount()
      AAdd( ::FaItems, cItem )
   ELSE
      AAdd( ::FaItems, Nil )
      AIns( ::FaItems, nPos )
      ::FaItems[ nPos ] := cItem
   ENDIF

RETURN .T.

//------------------------------------------------------------------------------

METHOD DeleteItem( nPos ) CLASS XComboBoxMod

   LOCAL lRet := .T.

   DEFAULT nPos TO ::nIndex

   IF nPos <= 0 .OR. nPos > Len( ::FaItems )
      RETURN .F.
   ENDIF

   ADel( ::FaItems, nPos )
   ASize( ::FaItems, Len( ::FaItems ) - 1 )

RETURN lRet

//------------------------------------------------------------------------------

METHOD ModifyItem( nPos, cItem ) CLASS XComboBoxMod

   DEFAULT nPos   TO ::nIndex, ;
           cItem  TO "ComboBox item"

   IF nPos < 1 .OR. nPos > Len( ::FaItems )
      RETURN .F.
   ENDIF

   ::FaItems[ nPos ] := cItem

RETURN .T.

//------------------------------------------------------------------------------
