monomer-1.3.0.0: src/Monomer/Widgets/Singles/DateField.hs
{-|
Module : Monomer.Widgets.Singles.DateField
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Input field for dates types.
Supports the Day type of the <https://hackage.haskell.org/package/time time>
library, but other types can be supported by implementing 'DayConverter'. Maybe
is also supported.
Supports different date formats and separators.
Handles mouse wheel and shift + vertical drag to increase/decrease days.
-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
module Monomer.Widgets.Singles.DateField (
-- * Configuration
DateFieldCfg,
FormattableDate,
DayConverter(..),
DateTextConverter,
-- * Constructors
dateField,
dateField_,
dateFieldV,
dateFieldV_,
dateFormatDelimiter,
dateFormatDDMMYYYY,
dateFormatMMDDYYYY,
dateFormatYYYYMMDD
) where
import Control.Applicative ((<|>))
import Control.Lens ((^.), ALens', _1, _2, _3)
import Control.Monad (join)
import Data.Default
import Data.Either
import Data.Maybe
import Data.Text (Text)
import Data.Time
import Data.Typeable (Typeable, typeOf)
import TextShow
import qualified Data.Attoparsec.Text as A
import qualified Data.Text as T
import Monomer.Core
import Monomer.Core.Combinators
import Monomer.Event.Types
import Monomer.Widgets.Singles.Base.InputField
import qualified Monomer.Lens as L
import qualified Monomer.Widgets.Util.Parser as P
data DateFormat
= FormatDDMMYYYY
| FormatYYYYMMDD
| FormatMMDDYYYY
deriving (Eq, Show)
defaultDateFormat :: DateFormat
defaultDateFormat = FormatDDMMYYYY
defaultDateDelim :: Char
defaultDateDelim = '/'
{-|
Converter to and form the Day type of the time library. To use types other than
Day of said library, this typeclass needs to be implemented.
--}
class (Eq a, Ord a, Show a, Typeable a) => DayConverter a where
convertFromDay :: Day -> a
convertToDay :: a -> Maybe Day
instance DayConverter Day where
convertFromDay = id
convertToDay = Just
-- | Converts a 'Day' instance to and from 'Text'.
class DateTextConverter a where
dateAcceptText :: DateFormat -> Char -> Maybe a -> Maybe a -> Text -> (Bool, Bool, Maybe a)
dateFromText :: DateFormat -> Char -> Text -> Maybe a
dateToText :: DateFormat -> Char -> a -> Text
dateFromDay :: Day -> a
dateToDay :: a -> Maybe Day
instance {-# OVERLAPPABLE #-} DayConverter a => DateTextConverter a where
dateAcceptText format delim minVal maxVal text = result where
accept = acceptTextInput format delim text
parsed = dateFromText format delim text
isValid = isJust parsed && dateInBounds minVal maxVal (fromJust parsed)
fromText
| isValid = parsed
| otherwise = Nothing
result = (accept, isValid, fromText)
dateFromText = dateFromTextSimple
dateToText = dateToTextSimple
dateFromDay = convertFromDay
dateToDay = convertToDay
instance (DayConverter a, DateTextConverter a) => DateTextConverter (Maybe a) where
dateAcceptText format delim minVal maxVal text
| T.strip text == "" = (True, True, Just Nothing)
| otherwise = (accept, isValid, result) where
resp = dateAcceptText format delim (join minVal) (join maxVal) text
(accept, isValid, tmpResult) = resp
result
| isJust tmpResult = Just tmpResult
| otherwise = Nothing
dateFromText format delim = Just . dateFromText format delim
dateToText format delim Nothing = ""
dateToText format delim (Just value) = dateToText format delim value
dateFromDay = Just . dateFromDay
dateToDay Nothing = Nothing
dateToDay (Just value) = dateToDay value
-- | Constraints for date types accepted by dateField.
type FormattableDate a
= (Eq a, Ord a, Show a, DateTextConverter a, Typeable a)
{-|
Configuration options for dateField:
- 'validInput': field indicating if the current input is valid. Useful to show
warnings in the UI, or disable buttons if needed.
- 'resizeOnChange': Whether input causes 'ResizeWidgets' requests.
- 'selectOnFocus': Whether all input should be selected when focus is received.
- 'minValue': Minimum valid date.
- 'maxValue': Maximum valid date.
- 'wheelRate': The rate at which wheel movement affects the date.
- 'dragRate': The rate at which drag movement affects the date.
- 'onFocus': event to raise when focus is received.
- 'onFocusReq': 'WidgetRequest' to generate when focus is received.
- 'onBlur': event to raise when focus is lost.
- 'onBlurReq': 'WidgetRequest' to generate when focus is lost.
- 'onChange': event to raise when the value changes.
- 'onChangeReq': 'WidgetRequest' to generate when the value changes.
- 'dateFormatDelimiter': which text delimiter to separate year, month and day.
- 'dateFormatDDMMYYYY': using the current delimiter, accept DD/MM/YYYY.
- 'dateFormatMMDDYYYY': using the current delimiter, accept MM/DD/YYYY.
- 'dateFormatYYYYMMDD': using the current delimiter, accept YYYY/MM/DD.
-}
data DateFieldCfg s e a = DateFieldCfg {
_dfcCaretWidth :: Maybe Double,
_dfcCaretMs :: Maybe Int,
_dfcValid :: Maybe (WidgetData s Bool),
_dfcValidV :: [Bool -> e],
_dfcDateDelim :: Maybe Char,
_dfcDateFormat :: Maybe DateFormat,
_dfcMinValue :: Maybe a,
_dfcMaxValue :: Maybe a,
_dfcWheelRate :: Maybe Double,
_dfcDragRate :: Maybe Double,
_dfcResizeOnChange :: Maybe Bool,
_dfcSelectOnFocus :: Maybe Bool,
_dfcOnFocusReq :: [Path -> WidgetRequest s e],
_dfcOnBlurReq :: [Path -> WidgetRequest s e],
_dfcOnChangeReq :: [a -> WidgetRequest s e]
}
instance Default (DateFieldCfg s e a) where
def = DateFieldCfg {
_dfcCaretWidth = Nothing,
_dfcCaretMs = Nothing,
_dfcValid = Nothing,
_dfcValidV = [],
_dfcDateDelim = Nothing,
_dfcDateFormat = Nothing,
_dfcMinValue = Nothing,
_dfcMaxValue = Nothing,
_dfcWheelRate = Nothing,
_dfcDragRate = Nothing,
_dfcResizeOnChange = Nothing,
_dfcSelectOnFocus = Nothing,
_dfcOnFocusReq = [],
_dfcOnBlurReq = [],
_dfcOnChangeReq = []
}
instance Semigroup (DateFieldCfg s e a) where
(<>) t1 t2 = DateFieldCfg {
_dfcCaretWidth = _dfcCaretWidth t2 <|> _dfcCaretWidth t1,
_dfcCaretMs = _dfcCaretMs t2 <|> _dfcCaretMs t1,
_dfcValid = _dfcValid t2 <|> _dfcValid t1,
_dfcValidV = _dfcValidV t1 <> _dfcValidV t2,
_dfcDateDelim = _dfcDateDelim t2 <|> _dfcDateDelim t1,
_dfcDateFormat = _dfcDateFormat t2 <|> _dfcDateFormat t1,
_dfcMinValue = _dfcMinValue t2 <|> _dfcMinValue t1,
_dfcMaxValue = _dfcMaxValue t2 <|> _dfcMaxValue t1,
_dfcWheelRate = _dfcWheelRate t2 <|> _dfcWheelRate t1,
_dfcDragRate = _dfcDragRate t2 <|> _dfcDragRate t1,
_dfcResizeOnChange = _dfcResizeOnChange t2 <|> _dfcResizeOnChange t1,
_dfcSelectOnFocus = _dfcSelectOnFocus t2 <|> _dfcSelectOnFocus t1,
_dfcOnFocusReq = _dfcOnFocusReq t1 <> _dfcOnFocusReq t2,
_dfcOnBlurReq = _dfcOnBlurReq t1 <> _dfcOnBlurReq t2,
_dfcOnChangeReq = _dfcOnChangeReq t1 <> _dfcOnChangeReq t2
}
instance Monoid (DateFieldCfg s e a) where
mempty = def
instance CmbCaretWidth (DateFieldCfg s e a) Double where
caretWidth w = def {
_dfcCaretWidth = Just w
}
instance CmbCaretMs (DateFieldCfg s e a) Int where
caretMs ms = def {
_dfcCaretMs = Just ms
}
instance CmbValidInput (DateFieldCfg s e a) s where
validInput field = def {
_dfcValid = Just (WidgetLens field)
}
instance CmbValidInputV (DateFieldCfg s e a) e where
validInputV fn = def {
_dfcValidV = [fn]
}
instance CmbResizeOnChange (DateFieldCfg s e a) where
resizeOnChange_ resize = def {
_dfcResizeOnChange = Just resize
}
instance CmbSelectOnFocus (DateFieldCfg s e a) where
selectOnFocus_ sel = def {
_dfcSelectOnFocus = Just sel
}
instance FormattableDate a => CmbMinValue (DateFieldCfg s e a) a where
minValue len = def {
_dfcMinValue = Just len
}
instance FormattableDate a => CmbMaxValue (DateFieldCfg s e a) a where
maxValue len = def {
_dfcMaxValue = Just len
}
instance CmbWheelRate (DateFieldCfg s e a) Double where
wheelRate rate = def {
_dfcWheelRate = Just rate
}
instance CmbDragRate (DateFieldCfg s e a) Double where
dragRate rate = def {
_dfcDragRate = Just rate
}
instance WidgetEvent e => CmbOnFocus (DateFieldCfg s e a) e Path where
onFocus fn = def {
_dfcOnFocusReq = [RaiseEvent . fn]
}
instance CmbOnFocusReq (DateFieldCfg s e a) s e Path where
onFocusReq req = def {
_dfcOnFocusReq = [req]
}
instance WidgetEvent e => CmbOnBlur (DateFieldCfg s e a) e Path where
onBlur fn = def {
_dfcOnBlurReq = [RaiseEvent . fn]
}
instance CmbOnBlurReq (DateFieldCfg s e a) s e Path where
onBlurReq req = def {
_dfcOnBlurReq = [req]
}
instance WidgetEvent e => CmbOnChange (DateFieldCfg s e a) a e where
onChange fn = def {
_dfcOnChangeReq = [RaiseEvent . fn]
}
instance CmbOnChangeReq (DateFieldCfg s e a) s e a where
onChangeReq req = def {
_dfcOnChangeReq = [req]
}
-- | Which character should be used to delimit dates.
dateFormatDelimiter :: Char -> DateFieldCfg s e a
dateFormatDelimiter delim = def {
_dfcDateDelim = Just delim
}
-- | Date format DD/MM/YYYY, using the appropriate delimiter.
dateFormatDDMMYYYY :: DateFieldCfg s e a
dateFormatDDMMYYYY = def {
_dfcDateFormat = Just FormatDDMMYYYY
}
-- | Date format MM/DD/YYYY, using the appropriate delimiter.
dateFormatMMDDYYYY :: DateFieldCfg s e a
dateFormatMMDDYYYY = def {
_dfcDateFormat = Just FormatMMDDYYYY
}
-- | Date format YYYY/MM/DD, using the appropriate delimiter.
dateFormatYYYYMMDD :: DateFieldCfg s e a
dateFormatYYYYMMDD = def {
_dfcDateFormat = Just FormatYYYYMMDD
}
-- | Creates a date field using the given lens.
dateField
:: (FormattableDate a, WidgetEvent e)
=> ALens' s a -> WidgetNode s e
dateField field = dateField_ field def
-- | Creates a date field using the given lens. Accepts config.
dateField_
:: (FormattableDate a, WidgetEvent e)
=> ALens' s a
-> [DateFieldCfg s e a]
-> WidgetNode s e
dateField_ field configs = widget where
widget = dateFieldD_ (WidgetLens field) configs
-- | Creates a date field using the given value and 'onChange' event handler.
dateFieldV
:: (FormattableDate a, WidgetEvent e)
=> a -> (a -> e) -> WidgetNode s e
dateFieldV value handler = dateFieldV_ value handler def
-- | Creates a date field using the given value and 'onChange' event handler.
-- Accepts config.
dateFieldV_
:: (FormattableDate a, WidgetEvent e)
=> a
-> (a -> e)
-> [DateFieldCfg s e a]
-> WidgetNode s e
dateFieldV_ value handler configs = newNode where
widgetData = WidgetValue value
newConfigs = onChange handler : configs
newNode = dateFieldD_ widgetData newConfigs
-- | Creates a date field providing a 'WidgetData' instance and config.
dateFieldD_
:: (FormattableDate a, WidgetEvent e)
=> WidgetData s a
-> [DateFieldCfg s e a]
-> WidgetNode s e
dateFieldD_ widgetData configs = newNode where
config = mconcat configs
format = fromMaybe defaultDateFormat (_dfcDateFormat config)
delim = fromMaybe defaultDateDelim (_dfcDateDelim config)
minVal = _dfcMinValue config
maxVal = _dfcMaxValue config
initialValue
| isJust minVal = fromJust minVal
| isJust maxVal = fromJust maxVal
| otherwise = dateFromDay (fromGregorian 1970 1 1)
acceptText = dateAcceptText format delim minVal maxVal
acceptInput text = acceptText text ^. _1
validInput text = acceptText text ^. _2
fromText text = acceptText text ^. _3
toText = dateToText format delim
inputConfig = InputFieldCfg {
_ifcPlaceholder = Nothing,
_ifcInitialValue = initialValue,
_ifcValue = widgetData,
_ifcValid = _dfcValid config,
_ifcValidV = _dfcValidV config,
_ifcFromText = fromText,
_ifcToText = toText,
_ifcAcceptInput = acceptInput,
_ifcIsValidInput = validInput,
_ifcDefCursorEnd = True,
_ifcDefWidth = 160,
_ifcCaretWidth = _dfcCaretWidth config,
_ifcCaretMs = _dfcCaretMs config,
_ifcDisplayChar = Nothing,
_ifcResizeOnChange = fromMaybe False (_dfcResizeOnChange config),
_ifcSelectOnFocus = fromMaybe True (_dfcSelectOnFocus config),
_ifcStyle = Just L.dateFieldStyle,
_ifcWheelHandler = Just (handleWheel config),
_ifcDragHandler = Just (handleDrag config),
_ifcDragCursor = Just CursorSizeV,
_ifcOnFocusReq = _dfcOnFocusReq config,
_ifcOnBlurReq = _dfcOnBlurReq config,
_ifcOnChangeReq = _dfcOnChangeReq config
}
wtype = WidgetType ("dateField-" <> showt (typeOf initialValue))
newNode = inputField_ wtype inputConfig
handleWheel
:: FormattableDate a
=> DateFieldCfg s e a
-> InputFieldState a
-> Point
-> Point
-> WheelDirection
-> (Text, Int, Maybe Int)
handleWheel config state point move dir = result where
Point _ dy = move
sign = if dir == WheelNormal then 1 else -1
curValue = _ifsCurrValue state
wheelRate = fromMaybe 1 (_dfcWheelRate config)
result = handleMove config state wheelRate curValue (dy * sign)
handleDrag
:: FormattableDate a
=> DateFieldCfg s e a
-> InputFieldState a
-> Point
-> Point
-> (Text, Int, Maybe Int)
handleDrag config state clickPos currPos = result where
Point _ dy = subPoint clickPos currPos
selValue = _ifsDragSelValue state
dragRate = fromMaybe 1 (_dfcDragRate config)
result = handleMove config state dragRate selValue dy
handleMove
:: FormattableDate a
=> DateFieldCfg s e a
-> InputFieldState a
-> Double
-> a
-> Double
-> (Text, Int, Maybe Int)
handleMove config state rate value dy = result where
format = fromMaybe defaultDateFormat (_dfcDateFormat config)
delim = fromMaybe defaultDateDelim (_dfcDateDelim config)
minVal = _dfcMinValue config
maxVal = _dfcMaxValue config
acceptText = dateAcceptText format delim minVal maxVal
fromText text = acceptText text ^. _3
toText = dateToText format delim
(valid, mParsedVal, parsedVal) = case dateToDay value of
Just val -> (True, mParsedVal, parsedVal) where
tmpValue = addDays (round (dy * rate)) val
mParsedVal = fromText (toText (dateFromDay tmpValue))
parsedVal = fromJust mParsedVal
Nothing -> (False, Nothing, undefined)
newVal
| isJust mParsedVal = parsedVal
| valid && dy > 0 && isJust maxVal = fromJust maxVal
| valid && dy < 0 && isJust minVal = fromJust minVal
| otherwise = _ifsCurrValue state
newText = toText newVal
newPos = _ifsCursorPos state
newSel = _ifsSelStart state
result = (newText, newPos, newSel)
dateFromTextSimple
:: (DayConverter a, FormattableDate a)
=> DateFormat
-> Char
-> Text
-> Maybe a
dateFromTextSimple format delim text = newDate where
compParser = A.char delim *> A.decimal
dateParser = (,,) <$> A.decimal <*> compParser <*> compParser
tmpDate = case A.parseOnly dateParser text of
Left _ -> Nothing
Right (n1, n2, n3)
| format == FormatDDMMYYYY -> fromGregorianValid (fromIntegral n3) n2 n1
| format == FormatMMDDYYYY -> fromGregorianValid (fromIntegral n3) n1 n2
| otherwise -> fromGregorianValid (fromIntegral n1) n2 n3
newDate = tmpDate >>= dateFromDay
dateToTextSimple :: FormattableDate a => DateFormat -> Char -> a -> Text
dateToTextSimple format delim val = result where
converted = dateToDay val
(year, month, day) = toGregorian (fromJust converted)
sep = T.singleton delim
padd num
| num < 10 = "0" <> T.pack (show num)
| otherwise = T.pack (show num)
tday = padd day
tmonth = padd month
tyear = T.pack (show year)
result
| isNothing converted = ""
| format == FormatDDMMYYYY = tday <> sep <> tmonth <> sep <> tyear
| format == FormatMMDDYYYY = tmonth <> sep <> tday <> sep <> tyear
| otherwise = tyear <> sep <> tmonth <> sep <> tday
acceptTextInput :: DateFormat -> Char -> Text -> Bool
acceptTextInput format delim text = isRight (A.parseOnly parser text) where
numP = A.digit *> ""
delimP = A.char delim *> ""
dayP = P.upto 2 numP
monthP = P.upto 2 numP
yearP = P.upto 4 numP
withDelim parser = A.option "" (delimP *> parser)
parsers
| format == FormatDDMMYYYY = [dayP, withDelim monthP, withDelim yearP]
| format == FormatMMDDYYYY = [monthP, withDelim dayP, withDelim yearP]
| otherwise = [yearP, withDelim monthP, withDelim dayP]
parser = P.join parsers <* A.endOfInput
dateInBounds :: (Ord a) => Maybe a -> Maybe a -> a -> Bool
dateInBounds Nothing Nothing _ = True
dateInBounds (Just minVal) Nothing val = val >= minVal
dateInBounds Nothing (Just maxVal) val = val <= maxVal
dateInBounds (Just minVal) (Just maxVal) val = val >= minVal && val <= maxVal