owickstrom / gi-gtk-declarative

Declarative GTK+ programming in Haskell
https://owickstrom.github.io/gi-gtk-declarative/
284 stars 32 forks source link

Adding Buttons to/Using FileChooserDialog #87

Closed smolck closed 3 years ago

smolck commented 3 years ago

Hello! So, I've been trying to use FileChooserDialog a bit, but I'm having some trouble with it. By default, it seems as though a FileChooserDialog doesn't have any buttons at the bottom of the dialog, and I'm not sure how to add, say, a Cancel and a Save button. I was wondering if I'd have to create a new custom widget to do that, or is there an easier way?

My end goal is to have a MenuBar with a File > Save As button, but I'm not sure how to go about implementing the "Save As" part using FileChooserDialog; I'm not sure how to add the Cancel and Save buttons at the bottom or how to know when the user is done choosing the filename/location for the file to be saved to.

marhop commented 3 years ago

Hi,

Would you mind sharing the code you have so far? I'm trying to do the same but I'm stuck even before I get the FileChooserDialog to display properly.

I have an unfinished workaround consisting of a FileChooserWidget plus Buttons inside a Box inside a (regular) Dialog. That might perhaps work but it's quite ugly.

Thanks, Martin

smolck commented 3 years ago

@marhop Hi! So, I haven't used Haskell or worked on the project I was making with this library in a little while, but iirc what I ended up doing was write some imperative code to create the dialog which I then used in the declarative view. So I have this function (with a little bit of context included; also, please excuse any messiness, my Haskell skills are not the best :sweat_smile:):

data Event
  = Closed
    | Save (Maybe FilePath)

showFileChooserDialog :: IO Event
showFileChooserDialog =
  Gtk.new FileChooserDialog [#action Gtk.:= FileChooserActionSave] >>= run
 where
  run :: FileChooserDialog -> IO Event
  run fc = do
    configure fc
    event <- eventFromResponse fc (#run fc)
    #destroy fc
    pure event

  configure :: Gtk.FileChooserDialog -> IO ()
  configure fc = do
    #addButton fc "Cancel" 1
    #addButton fc "Save" 2
    #setDefaultResponse fc 2

  eventFromResponse :: Gtk.FileChooserDialog -> IO Int32 -> IO Event
  eventFromResponse fc resp = do
    resp' <- resp
    #getUri fc
      >>= pure
      .   fmap (Text.unpack . Text.replace "file://" "")
      >>= if resp' == 2 then pure . Save else const $ pure (Save Nothing)

Then, I use it in the view like so:

container
          MenuBar
          []
          [ subMenu
              "File"
              [ menuItem MenuItem [onM #activate (const showFileChooserDialog)]
                  $ widget Label [#label := "Save"]
              ]
          ]

If you need more information like with imports or language extensions or similar let me know and I'll see if I can give you those as well.

Note that I haven't tested this (started the compilation but it's still not finished :smile:) but hopefully it gives you enough to get an idea of what you can do.

Also, there may very well be a better way of doing this that is more declarative, I just didn't know it at the time (and still don't).

marhop commented 3 years ago

Wow, thank you very much for your quick and detailed answer! That's really helpful. I'll take a closer look at the weekend.

marhop commented 3 years ago

OK, I now played with your code and it works like a charm. Thanks, that helped a lot! I spent some rather confused evenings with this problem and don't know if I would have come up with the idea to solve it imperatively on my own.

If I may offer some hints in return:

  1. If you replace #getUri with #getFilename you can get rid of the Text.replace part because #getFilename seems to drop the file:// prefix anyway.
  2. ~I don't think you have to check the response ID from #run because you already get a proper Maybe value from #getUri (or #getFilename).~ EDIT: I was wrong here. If you don't check the response ID the Save event will contain a Just value if a file was selected, even if the dialog was cancelled afterwards. This is a great way to introduce some interesting bugs. ;-) A corrected version is in my comment below.

So I went with something like this:

showFileChooserDialog :: IO Event
showFileChooserDialog = do
  fc <- new FileChooserDialog [#action Gtk.:= FileChooserActionSave]
  _ <- #addButton fc "Cancel" 1
  _ <- #addButton fc "Save" 2
  #setDefaultResponse fc 2
  _ <- #run fc
  name <- #getFilename fc
  #destroy fc
  return $ Save name

Thanks again, Martin

owickstrom commented 3 years ago

Seems you're all set, but just for reference there is also FileChooserNative, which I'm using in Komposition here:

https://github.com/owickstrom/komposition/blob/edbf59c7812c9ab45c8b8b6177214ed4a1916eb5/src/Komposition/UserInterface/GtkInterface.hs#L237

Cheers!

marhop commented 3 years ago

Good point, that's even better! Not only does the FileChooserNative dialog look nicer, it also adds the appropriate buttons automatically. So the above code can be further reduced to this (edited to check the response type):

showFileChooserDialog :: IO Event
showFileChooserDialog = do
  fc <- new FileChooserNative [#action Gtk.:= FileChooserActionSave]
  response <- toEnum . fromIntegral <$> #run fc
  name <- #getFilename fc
  #destroy fc
  return . Save $ guard (response == ResponseTypeAccept) >> name

(FYI, ResponseType is defined in GI.Gtk.Enums.)

Cheers, Martin

owickstrom commented 3 years ago

Closing this as it seems there's an accepted solution to the problem? Otherwise let me know and I'll reopen.