File dialog improvements



While messing with beast lately, I've been trying to add "save changes?"
dialog when closing window. This turned out into quite an adventure of
studying the beast source... I guess now I understand the meaning behind
"Bedevilled" in the name :) Well, I finally implemented the
functionality (and updated to filechooser). Patches attached, but
there're still several points:

* The project item_name (retreived with bse_item_get_name) is set to
"Untitled.bse" on startup, but I think the directory path is stored in
it later as well. I did not find a clear way to tell whenever a file has
been saved to disk for the first time (for Save/Not-Save-As operation),
so I added a boolean "first_time_save_flag" in bstapp.h/BstApp and a
boolean "save_self_contained" to preserve self_contained ("fully include
wave files") flag when saving the file later. first_time_save_flag is
set to TRUE in bst_app_init, but to FALSE when opening project. I still
don't know how to check if waves were included with the newly opened
project...

* It seems the only way to clear the project "dirty" flag (which means
"unsaved" as stated in the code) is to clear the undo history with
bse_project_clear_undo (get_property returns value based on undo
history). This means, that undo is impossible after saving the project
(generally ok, but undo after saving is even better, though not a must).

* I used the native gtk file chooser dialog, because BstFileDialog
(GxkDialog) misses gtk_dialog_run (they inherit from GtkWindow, not
GtkDialog), so it seems impossible [I may be wrong here] to escape the
main loop for some time... bst_choice_modal failed to block the loop as
well :( Besides, the new GtkFileChooser looks cooler. The chooser
requires gtk 2.4, but I beleive it's ok. 2.4 has been around in many
distributions already.


Things not implemented (yet):

* Need to update merge dialog, etc.
* Need a way to split project filename and it's path for
messages/titles/etc.
* When quitting with more than one unsaved file, a new dialog should
popup asking which unsaved files to save (like the one in gedit).
Generally polish the quit procedure.
* Need to generate error strings from bse errors.
* Polish strings in the new dialogs and mark them as translatable.
* For the "save unsaved file?"-dialog to satisfy hig requirements 100%,
there must be label with time passed since last save/creation.
* The "warning: file changed" and "replace file?" dialogs need markup
(again for hig).


I also wonder what's the reason behind so much gxk/bst extra stuff? Why
not use pure gtk where available/appropriate?
--Artem
--- bstapp.c.orig	2004-08-24 08:50:24.000000000 +0700
+++ bstapp.c	2004-09-08 12:35:08.000000000 +0700
@@ -31,6 +31,7 @@
 #include "bstprojectctrl.h"
 #include "bstprofiler.h"
 
+#include <unistd.h>
 
 /* --- prototypes --- */
 static void           bst_app_destroy             (GtkObject   *object);
@@ -45,6 +46,10 @@
 static gboolean       app_action_check            (gpointer     data,
                                                    gulong       action);
 
+static gint bst_app_close_project (BstApp *app);
+static void bst_app_quit (BstApp *app);
+static void bst_app_open_project (BstApp *app);
+static gboolean bst_app_save_project (BstApp *app, gboolean save_as);
 
 /* --- menus --- */
 enum {
@@ -295,6 +300,9 @@
                     "swapped_signal_after::switch-page", gxk_widget_update_actions, self,
                     "signal_after::switch-page", gxk_widget_viewable_changed, NULL,
                     NULL);
+  
+  self->first_time_save_flag = TRUE; // if true, do "save as" on "save"
+  self->save_self_contained = FALSE;
 }
 
 static void
@@ -548,9 +556,7 @@
   g_return_val_if_fail (BST_IS_APP (widget), FALSE);
   
   app = BST_APP (widget);
-  
-  gtk_widget_destroy (widget);
-  
+  bst_app_close_project (app);
   return TRUE;
 }
 
@@ -747,14 +753,7 @@
       SfiProxy proxy;
       GtkWidget *any;
     case BST_ACTION_EXIT:
-      if (bst_app_class)
-        {
-          GSList *slist, *free_slist = g_slist_copy (bst_app_class->apps);
-          
-          for (slist = free_slist; slist; slist = slist->next)
-            gxk_toplevel_delete (slist->data);
-          g_slist_free (free_slist);
-        }
+      bst_app_quit (self); // moved to quit procedure for futher work
       break;
     case BST_ACTION_CLOSE_PROJECT:
       gxk_toplevel_delete (widget);
@@ -773,7 +772,7 @@
         }
       break;
     case BST_ACTION_OPEN_PROJECT:
-      bst_file_dialog_popup_open_project (self);
+      bst_app_open_project (self);
       break;
     case BST_ACTION_MERGE_PROJECT:
       bst_file_dialog_popup_merge_project (self, self->project);
@@ -782,8 +781,10 @@
       bst_file_dialog_popup_import_midi (self, self->project);
       break;
     case BST_ACTION_SAVE_PROJECT:
+      bst_app_save_project (self, FALSE);
+      break;
     case BST_ACTION_SAVE_PROJECT_AS:
-      bst_file_dialog_popup_save_project (self, self->project);
+      bst_app_save_project (self, TRUE);
       break;
     case BST_ACTION_MERGE_EFFECT:
       bst_file_dialog_popup_merge_effect (self, self->project);
@@ -1050,3 +1051,217 @@
       return FALSE;
     }
 }
+
+// file dialog love below
+
+// to reduce typing
+static void show_error_dialog (GtkWindow *parent, gchar *message_format, ...)
+{
+  va_list args;
+  va_start (args, message_format);
+  GtkWidget *error_dialog = gtk_message_dialog_new_with_markup (parent,
+    GTK_DIALOG_DESTROY_WITH_PARENT,
+    GTK_MESSAGE_ERROR,
+    GTK_BUTTONS_CLOSE,
+    message_format, args);
+  va_end (args);
+  
+  gtk_dialog_run (GTK_DIALOG (error_dialog));
+  gtk_widget_destroy (error_dialog);
+}
+
+static void bst_app_open_project (BstApp *app)
+{
+  GtkWidget *file_dialog;
+  BseErrorType error;
+  
+  file_dialog = gtk_file_chooser_dialog_new ("Open Project", GTK_WINDOW (app),
+    GTK_FILE_CHOOSER_ACTION_SAVE,
+    GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+    GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+    NULL);
+  
+  if (gtk_dialog_run (GTK_DIALOG (file_dialog)) == GTK_RESPONSE_ACCEPT) {
+    gchar *filename;
+    
+    filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (file_dialog));
+    
+    SfiProxy project = bse_server_use_new_project (BSE_SERVER, filename);
+    error = bse_project_restore_from_file (project, filename);
+    
+    if (error)
+      show_error_dialog (GTK_WINDOW (file_dialog), "Error opening project '%s'", filename);
+    else {
+      BstApp *app;
+      bse_project_get_wave_repo (project);
+      app = bst_app_new (project);
+      app->first_time_save_flag = FALSE;
+      // FIXME: need to check if waves are included with the project
+      //        and set save_self_contained flag appropriately
+      gxk_status_window_push (app);
+      bst_status_eprintf (error, _("Opening project '%s'"), filename);
+      gxk_status_window_pop ();
+      gxk_idle_show_widget (GTK_WIDGET (app));
+    }
+    
+    bse_item_unuse (project);
+    g_free (filename);
+  }
+  gtk_widget_destroy (file_dialog);
+}
+
+static gboolean bst_app_save_project (BstApp *app, gboolean save_as)
+{
+  gchar *filename;
+  gboolean self_contained = app->save_self_contained;
+  
+  GtkWidget *file_dialog;
+  GtkWidget *replace_dialog;
+  GtkWidget *toggle;
+  BseErrorType error;
+  
+  if (save_as || app->first_time_save_flag) {
+    file_dialog = gtk_file_chooser_dialog_new ("Save Project", GTK_WINDOW (app),
+      GTK_FILE_CHOOSER_ACTION_SAVE,
+      GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+      GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
+      NULL);
+    
+    // FIXME: sometimes (when loading demo in particular)
+    //        set_current_name sets full path
+    /*if (app->first_time_save_flag)
+      gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (file_dialog), bse_item_get_name (app->project));
+    else
+      gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (file_dialog), bse_item_get_name (app->project));*/
+    gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (file_dialog), bse_item_get_name (app->project));
+    
+    // FIXME: add tooltip about this
+    toggle = gtk_check_button_new_with_label ("Include wave files with the project");
+    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), app->save_self_contained);
+    gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (file_dialog), toggle);
+    
+    // use loop to handle file exists situation, etc.
+    do {
+      if (gtk_dialog_run (GTK_DIALOG (file_dialog)) == GTK_RESPONSE_ACCEPT) {
+        filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (file_dialog));
+      } else {
+        gtk_widget_destroy (file_dialog);
+        return FALSE;
+      }
+      
+      // overwrite directory fix
+      // if the file exists and is a directory, issue a error
+      // FIXME: there're also tests like IS_REGULAR and IS_SYMLINK.
+      //        Should they also be included?
+      if (g_file_test (filename, G_FILE_TEST_EXISTS) &&
+          g_file_test (filename, G_FILE_TEST_IS_DIR)) {
+	show_error_dialog (GTK_WINDOW (file_dialog), "File %s is a directory!", filename);
+	continue; // return to file selection loop
+      }
+      
+      // if the file exists, either overwrite it, choose another file or cancel
+      if (g_file_test (filename, G_FILE_TEST_EXISTS)) {
+        replace_dialog = gtk_message_dialog_new (GTK_WINDOW (file_dialog),
+	  GTK_DIALOG_DESTROY_WITH_PARENT,
+	  GTK_MESSAGE_QUESTION,
+	  GTK_BUTTONS_NONE,
+	  "File Exists. Overwrite?");
+	gtk_dialog_add_buttons (GTK_DIALOG (replace_dialog),
+	  GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+	  "Replace", GTK_RESPONSE_ACCEPT,
+	  NULL);
+	if (gtk_dialog_run (GTK_DIALOG (replace_dialog)) == GTK_RESPONSE_ACCEPT)
+	  gtk_widget_hide (file_dialog);
+	gtk_widget_destroy (replace_dialog);
+      } else {
+        gtk_widget_hide (file_dialog);
+      }
+    } while (GTK_WIDGET_VISIBLE (file_dialog));
+    
+    self_contained = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (toggle));
+    gtk_widget_destroy (file_dialog);
+  } else {
+    filename = bse_item_get_name (app->project);
+  }
+  
+  // now we can unlink the file, because this is what the user wants
+  if (g_file_test (filename, G_FILE_TEST_EXISTS)) {
+    if (unlink (filename) < 0) {
+      show_error_dialog (GTK_WINDOW (app), "Failed to delete %s!", filename);
+      return FALSE;
+    }
+  }
+  
+  error = bse_project_store_bse (app->project, 0, filename, self_contained);
+  
+  if (error != BSE_ERROR_NONE) {
+    show_error_dialog (GTK_WINDOW (app), "A bse error occured when saving %s!", filename);
+    return FALSE;
+  }
+  
+  bse_item_set_name (app->project, filename);
+  if (app->first_time_save_flag)
+    app->first_time_save_flag = FALSE;
+  app->save_self_contained = self_contained;
+  
+  //g_free (filename); // commented out because otherwise causes segfault
+  bse_project_clear_undo (app->project);
+  return TRUE;
+}
+
+// this is convenience method for CloseProject/DeleteWindow actions
+static gint bst_app_close_project (BstApp *app)
+{
+  gboolean dirty;
+  gint response;
+  GtkWidget *dialog;
+  
+  bse_proxy_get (app->project, "dirty", &dirty, NULL);
+  
+  if (dirty) {
+    dialog = gtk_message_dialog_new (GTK_WINDOW (&app->window),
+      GTK_DIALOG_DESTROY_WITH_PARENT,
+      GTK_MESSAGE_WARNING,
+      GTK_BUTTONS_NONE,
+      "Save changes to project before closing?");
+    
+    gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+      GTK_STOCK_NO, GTK_RESPONSE_NO,
+      GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+      GTK_STOCK_SAVE, GTK_RESPONSE_YES,
+      NULL);
+    gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES);
+    response = gtk_dialog_run (GTK_DIALOG (dialog));
+    gtk_widget_destroy (dialog);
+    
+    switch (response) {
+      case GTK_RESPONSE_NO:
+        gtk_widget_destroy (GTK_WIDGET (app));
+        return 0;
+	break;
+      case GTK_RESPONSE_CANCEL:
+        return -1;
+	break;
+      case GTK_RESPONSE_YES:
+        if (!bst_app_save_project (app, FALSE))
+	  return -1;
+        break;
+    }
+  }
+  gtk_widget_destroy (GTK_WIDGET (app));
+  return 1;
+}
+
+static void bst_app_quit (BstApp *app)
+{
+  if (bst_app_class) {
+    GSList *slist, *free_slist = g_slist_copy (bst_app_class->apps);
+    
+    for (slist = free_slist; slist; slist = slist->next) {
+      bst_app_close_project (BST_APP (slist->data));
+      //gxk_toplevel_delete (slist->data);
+    }
+    
+    g_slist_free (free_slist);
+  }
+}
--- bstapp.h.orig	2004-08-23 14:46:42.000000000 +0700
+++ bstapp.h	2004-09-08 12:40:07.932412992 +0700
@@ -53,6 +53,9 @@
   GtkWidget      *rack_dialog;
   GtkWidget      *rack_editor;
   GtkWidget      *pcontrols;
+  
+  gboolean	 first_time_save_flag;
+  gboolean	 save_self_contained;
 };
 struct _BstAppClass
 {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]