flaw-editor/Flaw/Editor/UI/FileDialog.hs

Summary

Maintainability
Test Coverage
{-|
Module: Flaw.Editor.UI.FileDialog
Description: Dialog allowing user to select a file.
License: MIT
-}

{-# LANGUAGE OverloadedStrings, ViewPatterns #-}

module Flaw.Editor.UI.FileDialog
  ( FileDialogService(..)
  , newFileDialogService
  , FileDialogConfig(..)
  , runFileDialog
  ) where

import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import qualified Data.Text as T
import Numeric
import System.Directory
import System.FilePath
import qualified System.IO as IO

import Flaw.Flow
import Flaw.Math
import Flaw.UI
import Flaw.UI.Button
import Flaw.UI.Frame
import Flaw.UI.Label
import Flaw.UI.ListBox
import Flaw.UI.Metrics
import Flaw.UI.Panel
import Flaw.UI.VisualElement

-- | File dialog service.
-- Preferable to be shared between dialog invocations.
data FileDialogService = FileDialogService
  { fileDialogServiceMetrics :: !Metrics
  , fileDialogServiceParentPanel :: !Panel
  , fileDialogServiceFlow :: !Flow
  , fileDialogServiceDirVar :: {-# UNPACK #-} !(TVar T.Text)
  }

newFileDialogService :: Metrics -> Panel -> Flow -> STM FileDialogService
newFileDialogService metrics parentPanel flow = do
  dirVar <- newTVar ""
  return FileDialogService
    { fileDialogServiceMetrics = metrics
    , fileDialogServiceParentPanel = parentPanel
    , fileDialogServiceFlow = flow
    , fileDialogServiceDirVar = dirVar
    }

data Entry = FileEntry
  {
  -- | Full disk path.
    fileEntryPath :: !T.Text
  -- | Name to display.
  , fileEntryName :: !T.Text
  -- | Size in bytes.
  , fileEntrySize :: !Integer
  }

data FileDialogConfig = FileDialogConfig
  { fileDialogConfigTitle :: !T.Text
  , fileDialogConfigInitialPath :: !(Maybe T.Text)
  }

runFileDialog :: FileDialogService -> FileDialogConfig -> (Maybe T.Text -> STM ()) -> STM ()
runFileDialog FileDialogService
  { fileDialogServiceMetrics = metrics@Metrics
    { metricsGap = gap
    , metricsBigGap = bigGap
    , metricsFrameClient = frameClient
    , metricsButtonSize = buttonSize@(Vec2 buttonWidth buttonHeight)
    , metricsEditBoxHeight = editBoxHeight
    , metricsLabelSize = Vec2 labelWidth _labelHeight
    , metricsListBoxColumnHeaderHeight = columnHeaderHeight
    , metricsListBoxItemHeight = itemHeight
    }
  , fileDialogServiceParentPanel = parentPanel
  , fileDialogServiceFlow = flow
  , fileDialogServiceDirVar = dirVar
  } FileDialogConfig
  { fileDialogConfigTitle = title
  , fileDialogConfigInitialPath = maybeInitialPath
  } handler = do
  panel <- newPanel True
  frame <- newFrame panel metrics
  setText frame title

  frameChild <- addFreeChild parentPanel frame
  setSelfFreeChild frame parentPanel frameChild True

  dirPathLabel <- newTextLabel
  dirPathLabelVE <- newVisualElement dirPathLabel
  dirPathLabelVEChild <- addFreeChild panel dirPathLabelVE

  let fileNameSortKeyFunc = T.toCaseFold . fileEntryName
  listBox <- do
    nameColumnDesc <- newListBoxTextColumnDesc "name" (labelWidth * 2) fileNameSortKeyFunc fileEntryName
    sizeColumnDesc <- newListBoxTextColumnDesc "size" labelWidth fileEntrySize (T.pack . showSize . fileEntrySize)
    newListBox metrics [nameColumnDesc, sizeColumnDesc]
  reorderListBox listBox fileNameSortKeyFunc
  listBoxChild <- addFreeChild panel listBox

  okButtonLabel <- newLabel LabelStyleButton
  okButton <- newButton okButtonLabel
  layoutElement okButton buttonSize
  okButtonChild <- addFreeChild panel okButton
  cancelButton <- newLabeledButton "cancel"
  layoutElement cancelButton buttonSize
  cancelButtonChild <- addFreeChild panel cancelButton

  setLayoutHandler panel $ \(Vec2 sx sy) -> do
    placeFreeChild panel dirPathLabelVEChild $ Vec2 bigGap bigGap
    layoutElement dirPathLabelVE $ Vec2 (sx - bigGap * 2) editBoxHeight
    placeFreeChild panel listBoxChild $ Vec2 bigGap (bigGap + editBoxHeight + gap)
    layoutElement listBox $ Vec2 (sx - bigGap * 2) (sy - bigGap * 2 - gap * 2 - editBoxHeight - buttonHeight)
    placeFreeChild panel okButtonChild $ Vec2 (sx - bigGap - buttonWidth * 2 - gap) (sy - bigGap - buttonHeight)
    placeFreeChild panel cancelButtonChild $ Vec2 (sx - bigGap - buttonWidth) (sy - bigGap - buttonHeight)

  layoutElement frame $ xy__ frameClient + zw__ frameClient
    + Vec2 ((bigGap * 2 + buttonWidth * 2 + gap) * 2) (bigGap * 2 + columnHeaderHeight + itemHeight * 15 + buttonHeight + gap)

  setChangeHandler listBox $ do
    entries <- getListBoxSelectedValues listBox
    setText okButtonLabel $ case entries of
      [FileEntry
        { fileEntrySize = size
        }] -> if size >= 0 then "choose" else "open"
      _ -> ""

  let
    openDirectory unnormalizedPath = do
      path <- canonicalizePath $ T.unpack unnormalizedPath
      directoryContents <- filter (/= ".") <$> getDirectoryContents path
      entries <- forM directoryContents $ \entry -> do
        let entryPath = path </> entry
        isDir <- doesDirectoryExist entryPath
        size <- if isDir then return (-1) else handle (\SomeException {} -> return (-1)) $ IO.withFile entryPath IO.ReadMode IO.hFileSize
        return FileEntry
          { fileEntryPath = T.pack entryPath
          , fileEntryName = T.pack entry
          , fileEntrySize = size
          }
      atomically $ do
        setText dirPathLabel $ T.pack path
        clearListBox listBox
        forM_ entries $ addListBoxItem listBox

  setActionHandler okButton $ do
    entries <- getListBoxSelectedValues listBox
    case entries of
      [e] -> do
        let
          FileEntry
            { fileEntryPath = path
            } = e
        asyncRunInFlow flow $ do
          isDir <- doesDirectoryExist $ T.unpack path
          if isDir then openDirectory path
          else atomically $ do
            removeFreeChild parentPanel frameChild
            -- save path to file for later use
            writeTVar dirVar $ T.pack $ takeDirectory $ T.unpack path
            handler $ Just path
      _ -> return ()
  setActionHandler cancelButton $ do
    removeFreeChild parentPanel frameChild
    handler Nothing

  setDefaultElement panel okButton
  setButtonDefault okButton
  setCancelElement panel cancelButton
  setButtonCancel cancelButton

  asyncRunInFlow flow . openDirectory =<< case maybeInitialPath of
    Just (T.pack . takeDirectory . T.unpack -> initialDir) -> do
      writeTVar dirVar initialDir
      return initialDir
    Nothing -> readTVar dirVar

  focusFreeChild parentPanel frameChild

showSize :: Integer -> String
showSize size
  | size < 0                  = ""
  | size < 1024               = shows size " b"
  | size < 1024 * 1024        = showFFloat (Just 1) (fromIntegral size / 1024 :: Float) " Kb"
  | size < 1024 * 1024 * 1024 = showFFloat (Just 1) (fromIntegral size / (1024 * 1024) :: Float) " Mb"
  | otherwise                 = showFFloat (Just 1) (fromIntegral size / (1024 * 1024 * 1024) :: Float) " Gb"