qtah-examples-0.9.0: src/Graphics/UI/Qtah/Example/Notepad.hs
-- This file is part of Qtah.
--
-- Copyright 2015-2024 The Qtah Authors.
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Lesser General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
{-# LANGUAGE ScopedTypeVariables #-}
-- | A notepad based on the Qt notepad example.
module Graphics.UI.Qtah.Example.Notepad (run) where
import Control.Monad (forM_, unless, when)
import Data.Bits ((.|.))
import Data.Functor (void)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import qualified Graphics.UI.Qtah.Core.QCoreApplication as QCoreApplication
import Graphics.UI.Qtah.Event
import Graphics.UI.Qtah.Gui.QCloseEvent (QCloseEvent)
import Graphics.UI.Qtah.Signal (connect_)
import qualified Graphics.UI.Qtah.Core.QEvent as QEvent
import qualified Graphics.UI.Qtah.Gui.QTextDocument as QTextDocument
import Graphics.UI.Qtah.Gui.QTextDocument (
modificationChangedSignal,
)
import qualified Graphics.UI.Qtah.Widgets.QAction as QAction
import Graphics.UI.Qtah.Widgets.QAction (triggeredSignal)
import qualified Graphics.UI.Qtah.Widgets.QFileDialog as QFileDialog
import qualified Graphics.UI.Qtah.Widgets.QMainWindow as QMainWindow
import Graphics.UI.Qtah.Widgets.QMainWindow (QMainWindow)
import qualified Graphics.UI.Qtah.Widgets.QMenu as QMenu
import qualified Graphics.UI.Qtah.Widgets.QMenuBar as QMenuBar
import qualified Graphics.UI.Qtah.Widgets.QMessageBox as QMessageBox
import qualified Graphics.UI.Qtah.Widgets.QTextEdit as QTextEdit
import Graphics.UI.Qtah.Widgets.QTextEdit (
QTextEdit,
copyAvailableSignal,
redoAvailableSignal,
undoAvailableSignal,
)
import qualified Graphics.UI.Qtah.Widgets.QWidget as QWidget
import System.FilePath (takeFileName)
data Notepad = Notepad
{ myWindow :: QMainWindow
, myText :: QTextEdit
, myFilePathRef :: IORef (Maybe FilePath)
}
run :: IO ()
run = do
mainWindow <- makeMainWindow
QWidget.show mainWindow
makeMainWindow :: IO QMainWindow
makeMainWindow = do
window <- QMainWindow.new
QWidget.resizeRaw window 640 480
menu <- QMenuBar.new
QMainWindow.setMenuBar window menu
menuFile <- QMenuBar.addNewMenu menu "&File"
menuFileNew <- QMenu.addNewAction menuFile "&New"
menuFileOpen <- QMenu.addNewAction menuFile "&Open..."
menuFileSave <- QMenu.addNewAction menuFile "&Save"
menuFileSaveAs <- QMenu.addNewAction menuFile "Sa&ve as..."
_ <- QMenu.addSeparator menuFile
menuFileClose <- QMenu.addNewAction menuFile "&Close"
menuFileQuit <- QMenu.addNewAction menuFile "&Quit Examples"
menuEdit <- QMenuBar.addNewMenu menu "&Edit"
menuEditUndo <- QMenu.addNewAction menuEdit "&Undo"
menuEditRedo <- QMenu.addNewAction menuEdit "&Redo"
_ <- QMenu.addSeparator menuEdit
menuEditCut <- QMenu.addNewAction menuEdit "Cu&t"
menuEditCopy <- QMenu.addNewAction menuEdit "&Copy"
menuEditPaste <- QMenu.addNewAction menuEdit "&Paste"
_ <- QMenu.addSeparator menuEdit
menuEditSelectAll <- QMenu.addNewAction menuEdit "Select all"
forM_ [menuEditUndo, menuEditRedo, menuEditCut, menuEditCopy] $ \action ->
QAction.setEnabled action False
menuHelp <- QMenuBar.addNewMenu menu "&Help"
menuHelpAboutQt <- QMenu.addNewAction menuHelp "About &Qt"
text <- QTextEdit.new
QMainWindow.setCentralWidget window text
QTextEdit.setUndoRedoEnabled text True
doc <- QTextEdit.document text
filePathRef <- newIORef Nothing
let me = Notepad
{ myWindow = window
, myText = text
, myFilePathRef = filePathRef
}
_ <- onEvent window $ \(event :: QCloseEvent) -> do
continue <- confirmSaveIfDirty me "Quit"
-- Qt delivers QCloseEvent to the window twice under certain circumstances,
-- for example on Linux when closing the window with the keyboard (reliably
-- with Alt+F4, and intermittently with Alt+Space C, on both Openbox and
-- KWin). To avoid getting prompted to save a second time after clicking No
-- when the document is dirty, we manually clear the dirty state if we know
-- we're going to close.
if continue
then do QTextDocument.setModified doc False
QEvent.accept event
else QEvent.ignore event
return $ not continue
connect_ menuFileNew triggeredSignal $ \_ -> fileNew me
connect_ menuFileOpen triggeredSignal $ \_ -> fileOpen me
connect_ menuFileSave triggeredSignal $ \_ -> void $ fileSave me
connect_ menuFileSaveAs triggeredSignal $ \_ -> void $ fileSaveAs me
connect_ menuFileClose triggeredSignal $ \_ -> void $ QWidget.close window
connect_ menuFileQuit triggeredSignal $ \_ -> do
closed <- QWidget.close window
when closed QCoreApplication.quit
connect_ menuEditUndo triggeredSignal $ \_ -> QTextEdit.undo text
connect_ menuEditRedo triggeredSignal $ \_ -> QTextEdit.redo text
connect_ menuEditCut triggeredSignal $ \_ -> QTextEdit.cut text
connect_ menuEditCopy triggeredSignal $ \_ -> QTextEdit.copy text
connect_ menuEditPaste triggeredSignal $ \_ -> QTextEdit.paste text
connect_ menuEditSelectAll triggeredSignal $ \_ -> QTextEdit.selectAll text
connect_ menuHelpAboutQt triggeredSignal $ \_ -> QMessageBox.aboutQt window "About Qt"
connect_ text undoAvailableSignal $ \b -> QAction.setEnabled menuEditUndo b
connect_ text redoAvailableSignal $ \b -> QAction.setEnabled menuEditRedo b
connect_ text copyAvailableSignal $ \b -> do
QAction.setEnabled menuEditCut b
QAction.setEnabled menuEditCopy b
connect_ doc modificationChangedSignal $ \_ -> updateTitle me
updateTitle me
return window
fileNew :: Notepad -> IO ()
fileNew me = do
continue <- confirmSaveIfDirty me "New file"
when continue $ do
QTextEdit.clear $ myText me
setFilePath me Nothing
setDirty me False
fileOpen :: Notepad -> IO ()
fileOpen me = do
continue <- confirmSaveIfDirty me "Open file"
when continue $ do
path <- QFileDialog.getOpenFileName (myWindow me) "Open file" "" fileDialogFilter
unless (null path) $ do
contents <- readFile path
QTextEdit.setText (myText me) contents
setFilePath me $ Just path
setDirty me False
-- | Returns true if the save was performed.
fileSave :: Notepad -> IO Bool
fileSave me = do
pathMaybe <- readIORef $ myFilePathRef me
case pathMaybe of
Nothing -> fileSaveAs me
Just path -> do
contents <- QTextEdit.toPlainText $ myText me
writeFile path contents
setDirty me False
return True
-- | Returns true if the save was performed.
fileSaveAs :: Notepad -> IO Bool
fileSaveAs me = do
path <- QFileDialog.getSaveFileName
(myWindow me)
"Save file"
""
fileDialogFilter
if null path
then return False
else do setFilePath me $ Just path
fileSave me
-- | Returns true if the surrounding action should continue: that is, if the
-- editor was not dirty, or if the editor was dirty and the save was performed.
confirmSaveIfDirty :: Notepad -> String -> IO Bool
confirmSaveIfDirty me title = do
dirty <- QTextDocument.isModified =<< QTextEdit.document (myText me)
if dirty
then do response <- QMessageBox.questionWithButtons
(myWindow me)
title
"There are unsaved changes. Would you like to save them?"
(QMessageBox.yes .|.
QMessageBox.no .|.
QMessageBox.cancel)
QMessageBox.Cancel
case response of
QMessageBox.Yes -> fileSave me
QMessageBox.No -> return True
_ -> return False
else return True
setDirty :: Notepad -> Bool -> IO ()
setDirty me dirty = do
doc <- QTextEdit.document $ myText me
QTextDocument.setModified doc dirty
setFilePath :: Notepad -> Maybe FilePath -> IO ()
setFilePath me path = do
writeIORef (myFilePathRef me) path
updateTitle me
updateTitle :: Notepad -> IO ()
updateTitle me = do
dirty <- QTextDocument.isModified =<< QTextEdit.document (myText me)
file <- fmap (maybe "(Untitled)" takeFileName) $ readIORef $ myFilePathRef me
QWidget.setWindowTitle (myWindow me) $
(if dirty then ('*':) else id) $
file ++ " - Notepad"
fileDialogFilter :: String
fileDialogFilter =
"Text Files (*.txt);;Haskell sources (*.hs *.hs-boot *.lhs *.chs);;All Files (*)"